π₯ The fastest way to build type safe web apps. IHP is a new batteries-included web framework optimized for longterm productivity and programmer happiness
A new IHP release with new features and many bug fixes π
IHP is a modern batteries-included haskell web framework, built on top of Haskell and Nix. Blazing fast, secure, easy to refactor and the best developer experience with everything you need - from prototype to production.
π½ Improved Migration Workflow: Since v0.16.0 IHP is already automatically generating a migration prefilled with the recommended changes that need to be applied to your database. Previously IHP was tracking all changes you've made in the Schema Designer and re-applied them to the migration sql file.
With this release the approach has been changed to a database diffing approach: IHP will now diff the Schema.sql against your local database and generate a efficient patch to bring both in sync again.
This also affects the local dev tools. The Update DB
button has been removed entirely. When there are unmigrated changes locally, the Schema Designer will suggest you to generate and run a migration, like this:
π¨βπ» Optional HSX Attributes:
HSX gained special handling for Maybe
values to make it easy to deal with optional attributes.
You can write
let
target :: Maybe Text
target = Just "_blank"
in
[hsx|<a target={target} />|]
and it will render to:
<a target="blank" />
Using Nothing
results in the target
attribute not being in the output HTML:
let
target :: Maybe Text
target = Nothing
in
[hsx|<a target={target} />|]
This will render to:
<a />
:atom: DataSync Updates: IHP DataSync now uses React Functional Components instead of Class based approach used in previous versions::
function TodoList() {
const todos = useQuery(query('todos'));
if (todos === null) {
return <div className="spinner-border text-primary" role="status">
<span className="sr-only">Loading...</span>
</div>;
}
return <div>
{todos.map(todo => <div>{todo.title}</div>)}
</div>
}
Additionally this release contains many bug fixes for DataSync and large performance and usability improvements.
π§° Haskell Language Server: Performance Improvements We've updated the Haskell Language Server from 1.4.0.0 to the latest version 1.5.1.0.
With HLS 1.5.1.0 the performance should be much better. If you're curious, here's a nice presentation of what has changed in HLS.
If you still have performance issues, please submit issues directly here https://github.com/haskell/haskell-language-server/issues/2340
π« Encrypted SMTP
When configuring the IHP Mailer to use SMTP, you can now specify the encryption by setting the encryption
field:
import IHP.Mail
config :: ConfigBuilder
config = do
option $ SMTP
{ host = "smtp.myisp.com"
, port = 2525
, credentials = Nothing
, encryption = STARTTLS -- <-- NEW
}
Full Changelog: https://github.com/digitallyinduced/ihp/compare/v0.16.0...v0.17.0
Help decide what's coming next to IHP by using the Feature Voting!
β See the UPGRADE.md for upgrade instructions.
If you have any problems with updating, let us know on the IHP forum.
π§ To stay in the loop, subscribe to the IHP release emails. Or follow digitally induced on twitter.
A new IHP release with new features and many bug fixes π
IHP is a modern batteries-included haskell web framework, built on top of Haskell and Nix. Blazing fast, secure, easy to refactor and the best developer experience with everything you need - from prototype to production.
π Breadcrumbs: @amitaibu added a new API for defining breadcrumb navigations inside your views. This new API makes it easier to customize the breadcrumb rendering across your app, e.g. to use a tailwind css based breadcrumb instead of the default bootstrap based one.
instance View ShowView where
html ShowView { .. } = [hsx|
{breadcrumb}
<h1>Show Post</h1>
<p>{post}</p>
|]
where
breadcrumb = renderBreadcrumb
[ breadcrumbLink "Posts" PostsAction
, breadcrumbText "Show Post"
]
The new breadcrumb functions are automatically used when you generate new controllers or views using the code generators.
Check out the docs: https://ihp.digitallyinduced.com/Guide/view.html#breadcrumbs
π Tailwind + Pagination: The CSS classes and HTML code of IHP's pagination can be customized now. This makes it possible to now use the pagination with tailwindcss.
π Build Improvements:
You can now run nix-build
inside your IHP project and it will build the whole project, including background workers and IHP scripts:
# Optional, if you skip this the binary will not be optimized by GHC
make prepare-optimized-nix-build
# The actual build process
nix-build
This will build a nix package that contains the following binaries:
RunProdServer
, the binary to start web serverRunJobs
, if youβre using the IHP job queue, this binary will be the entrypoint for the workers a binary for each script in Application/Script
, e.g. Welcome
for Application/Script/Welcome.hs
IHP_ASSET_VERSION
env variable, so cache busting should work out of the box.This will make it significant easier to deploy IHP apps. If you wondered: These build improvements are also what powers the new Experimental Deployment Process in IHP Cloud.
π» Helpers to Load Env Vars: Along with the recent deployment changes, we're also making it easier to define custom config parameters based on environment variables to your app.
Inside Config.hs you can now use the env
and envOrNothing
functions to read env variables:
module Config where
config :: ConfigBuilder
config = do
maxRetryCount <- env @Int "MAX_RETRY_COUNT"
appName <- env @Text "APP_NAME"
redisUrl :: Maybe Text <- envOrNothing "REDIS_URL"
SchemaDesigner: Support for Postgres Policies You can now create and manage Postgres Policies inside the Schema Designer:
This is still an early version and we'll extend the policy editor in future IHP versions. It's going to be used together with the new DataSync APIs soon.
π¨ New 404 Page We designed a new nice looking standard 404 for IHP apps :)
Check out the docs if you want to have your own custom 404 page.
Full Changelog: https://github.com/digitallyinduced/ihp/compare/v0.15.0...v0.16.0
Help decide what's coming next to IHP by using the Feature Voting!
If you have any problems with updating, let us know on the IHP forum.
π§ To stay in the loop, subscribe to the IHP release emails. Or follow digitally induced on twitter.
A new IHP release with new features and many bug fixes. This release also includes the Stripe Integration and Docker support for IHP Pro and IHP Business users π
IHP is a modern batteries-included haskell web framework, built on top of Haskell and Nix. Blazing fast, secure, easy to refactor and the best developer experience with everything you need - from prototype to production.
π° Payments with Stripe: This version finally ships one of the most requested features, the Stripe Integration. With the new Stripe Integration you can easily deal with payments and subscriptions in your projects. If you ever wanted to build as SaaS with Haskell, today is the best day to start! π»
module Web.Controller.CheckoutSessions where
import Web.Controller.Prelude
import qualified IHP.Stripe.Types as Stripe
import qualified IHP.Stripe.Actions as Stripe
instance Controller CheckoutSessionsController where
beforeAction = ensureIsUser
action CreateCheckoutSessionAction = do
plan <- query @Plan |> fetchOne
stripeCheckoutSession <- Stripe.send Stripe.CreateCheckoutSession
{ successUrl = urlTo CheckoutSuccessAction
, cancelUrl = urlTo CheckoutCancelAction
, mode = "subscription"
, paymentMethodTypes = ["card"]
, customer = get #stripeCustomerId currentUser
, lineItem = Stripe.LineItem
{ price = get #stripePriceId plan
, quantity = 1
, taxRate = Nothing
, adjustableQuantity = Nothing
}
, metadata =
[ ("userId", tshow currentUserId)
, ("planId", tshow planId)
]
}
redirectToUrl (get #url stripeCheckoutSession)
action CheckoutSuccessAction = do
plan <- fetchOne (get #planId currentUser)
setSuccessMessage ("You're on the " <> get #name plan <> " plan now!")
redirectTo SwitchToProAction
action CheckoutCancelAction = redirectTo PricingAction
π¦ Docker Support: Thanks to the new docker integration it's now easier than ever to ship your IHP apps into production!
$ ihp-app-to-docker-image
...
β
The docker image is at 'docker.tar.gz'
πΈοΈ SEO Improvements:
You can now dynamically manage meta tags like <meta property="og:description" content="dynamic content"/>
in your IHP apps.
instance View MyView where
beforeRender MyView { post } = do
setOGTitle (get #title post)
setOGDescription (get #summary post)
setOGUrl (urlTo ShowPostAction { .. })
case get #imageUrl post of
Just url -> setOGImage url
Nothing -> pure () -- When setOGImage is not called, the og:image tag will not be rendered
-- ...
π HTML in Validation Error Messages:
You can now use attachFailureHtml
if you want to write a custom validation logic that uses HTML in it's error message:
post
|> attachFailureHtml #title [hsx|Invalid value. <a href="https://example.com/docs">Check the documentation</a>|]
-- Link will work as expected, as it's HSX
Learn more about HTML validation errors in the documentation
π Documentation Search: There's now a new Agolia-based Search in the IHP Documentation Search for Something
π¨ New Design for the API Reference: Our haddock-based API reference now has a custom CSS that gives it a stunning new look! Check out the API Reference
π½ Auto-generated Migrations:
The Schema Designer now keeps track of your changes. Whenever you generate a new migration from the Web-based Code Generator or the new-migration
CLI command, it will prefill the .sql
file with the steps needed to migrate.
π§° Tooling Updates: We've updated several packages available in your IHP development environment:
GHC: 8.10.5 -> 8.10.7
Haskell Language Server: 1.1.0.0 -> 1.4.0.0
π§ͺ Testing Improvements:
Test modules now run in their own separate database. On test start IHP will create a new database and import the Application/Schema.sql
. After test completion the database is deleted.
In previous IHP versions tests used the development database by default.
Here's an example of how controllers tests can now look like:
module Test.Controller.PostsSpec where
import Network.HTTP.Types.Status
import IHP.Prelude
import IHP.QueryBuilder (query)
import IHP.Test.Mocking
import IHP.Fetch
import IHP.FrameworkConfig
import IHP.HaskellSupport
import Test.Hspec
import Config
import Generated.Types
import Web.Routes
import Web.Types
import Web.Controller.Posts ()
import Web.FrontController ()
import Network.Wai
import IHP.ControllerPrelude
tests :: Spec
tests = aroundAll (withIHPApp WebApplication config) do
describe "PostsController" $ do
it "has no existing posts" $ withContext do
count <- query @Post
|> fetchCount
count `shouldBe` 0
it "calling NewPostAction will render a new form" $ withContext do
mockActionStatus NewPostAction `shouldReturn` status200
it "creates a new post" $ withParams [("title", "Post title"), ("body", "Body of post")] do
response <- callAction CreatePostAction
let (Just location) = (lookup "Location" (responseHeaders response))
location `shouldBe` "http://localhost:8000/Posts"
-- Only one post should exist.
count <- query @Post |> fetchCount
count `shouldBe` 1
-- Fetch the new post.
post <- query @Post |> fetchOne
get #title post `shouldBe` "Post title"
get #body post `shouldBe` "Body of post"
it "can show posts" $ withContext do
post <- newRecord @Post
|> set #title "Lorem Ipsum"
|> set #body "**Mark down**"
|> createRecord
response <- callAction ShowPostAction { postId = get #id post }
response `responseStatusShouldBe` status200
response `responseBodyShouldContain` "Lorem Ipsum"
-- For debugging purposes you could do the following, to
-- see the HTML printed out on the terminal.
body <- responseBody response
putStrLn (cs body)
Inside the test you can use withUser
to call an action as a logged in user:
-- Create a user for our test case
user <- newRecord @User
|> set #email "[email protected]"
|> createRecord
-- Log into the user and then call CreatePostAction
response <- withUser user do
callAction CreatePostAction
πͺ Experimental: New DataSync API Building Hybrid & Single-Page Apps with Realtime Functionality is about to get much easier with IHP. This release includes an early version of the new DataSync API that allows you to query your database from within JS code on the frontend:
class TodoList extends React.Component {
constructor(props) {
super(props);
this.state = { tasks: null };
}
async componentDidMount() {
initIHPBackend({
host: 'https://ojomabrabrdiuzxydbgbebztjlejwcey.ihpapp.com'
})
await ensureIsUser();
await query('tasks')
.orderBy('createdAt')
.fetchAndRefresh(tasks => this.setState({ tasks }))
}
render() {
const { tasks } = this.state;
if (tasks === null) {
return <div className="spinner-border text-primary" role="status">
<span className="sr-only">Loading...</span>
</div>;
}
return <div>
<AppNavbar/>
{tasks.map(task => <TaskItem task={task} key={task.id}/>)}
<NewTodo/>
</div>
}
}
In the react component above you can see that our JS is able to use IHP's query builder using query(..).fetch()
, just like you would do it in your haskell code. You can also update and delete records:
function TaskItem({ task }) {
const taskIdAttr = "task-" + task.id;
return <div className="form-group form-check">
<input
id={taskIdAttr}
type="checkbox"
checked={task.isCompleted}
onChange={() => updateRecordById('tasks', task.id, { isCompleted: !task.isCompleted })}
className="mr-2"
/>
<label className="form-check-label" htmlFor={taskIdAttr}>{task.title}</label>
<button className="btn btn-link text-danger" onClick={() => deleteRecordById('tasks', task.id) }>Delete</button>
</div>
}
Authentication is handled using Postgres Row-level-Security and Policies (defined in the Schema.sql
) like this:
CREATE POLICY "Users can manage their tasks" ON tasks USING (user_id = ihp_user_id()) WITH CHECK (user_id = ihp_user_id());
You can find a demo app using this API here:
https://wlulygxcdknebrolshgolktblgapwnxn.ihpapp.com/ (Login: [email protected]
Password: demo
).
The app is real-time, try to open it in two browsers and change something :)
Config/client_session_key.aes
can now be set via the IHP_SESSION_SECRET
env var
SELECT *
: This avoids issues when the database schema is being migrated
run-script
fromLabel @"companyId"
bug by @dharmatech in https://github.com/digitallyinduced/ihp/pull/1060
matchesRegex
validator by @hendi in https://github.com/digitallyinduced/ihp/pull/1074
src
on <script>
tag in docs by @amitaibu in https://github.com/digitallyinduced/ihp/pull/1105
build/Generated/Types.hs
by @amitaibu in https://github.com/digitallyinduced/ihp/pull/1132
Full Changelog: https://github.com/digitallyinduced/ihp/compare/v0.14.0...v0.15.0
Help decide what's coming next to IHP by using the Feature Voting!
If you have any problems with updating, let us know on the IHP forum.
π§ To stay in the loop, subscribe to the IHP release emails. Or follow digitally induced on twitter.
A new IHP release with new features and many bug fixes. This release is also the first release that comes with an IHP Pro release π
π IHP Pro Available now: The new IHP Pro and IHP Business subscriptions are available now π
Check them out here! If you're curious about the details, check out this forum thread!
Allow scheduling jobs at a specific time with runAt
:
The job queue table's now have a run_at
column. Jobs are only executed after the specified time.
The job runner is now also polling the job queue at regular intervals as otherwise scheduled jobs will not be picked up when there's no activity on the job queue table.
Here's an example of a job scheduled to run in ten minutes from now:
inTenMinutes <- addUTCTime (10 * 60) <$> getCurrentTime
newRecord @DeleteContainerJob
|> set #containerId (get #containerId containerBuild)
|> set #runAt inTenMinutes
|> createRecord
New IHP Logo: The dev server now displays our new beautiful IHP logo π
Customize File Upload Limits:
By default IHP has a max file size limit when uploading files to the IHP server. You can now customize these limits inside your Config/Config.hs
.
π« Email Confirmation: One of the first IHP Pro features now made it :) There's now a built-in standard way to deal with email confirmation.
OAuth: Login with Google and Google are now finally available in IHP.
This new feature is also part of IHP Pro. Once you're app is running IHP Pro, follow this Guide on how to set it up.
New Form Helper: fileField
:
Previously you had to manually type the HTML code for <input type="file"/>
fields in your forms. We've finally added the missing helper π
renderForm :: Company -> Html
renderForm company = formFor company [hsx|
{(textField #name)}
{(fileField #logoUrl)}
{submitButton}
|]
New Function: createTemporaryDownloadUrlFromPathWithExpiredAt
: Get signed S3 download urls with a custom expiration time
This function is similiar to createTemporaryDownloadUrlFromPath
, but you can pass a custom expiration time. The createTemporaryDownloadUrlFromPath
function always generates urls that are valid for 7 days.
E.g. this example generates a presigned S3 url to a logo that expires in 5 minutes:
let validInSeconds = 5 * 60
signedUrl <- createTemporaryDownloadUrlFromPathWithExpiredAt validInSeconds "logos/8ed22caa-11ea-4c45-a05e-91a51e72558d"
let url :: Text = get #url signedUrl
let expiredAt :: UTCTime = get #expiredAt signedUrl
Improved Support for GitPod & GitHub Codespaces:
Previously IHP's dev tooling was only running when it was called at localhost:8000
. This is a problem in services like GitPod or GitHub spaces, where the the host might not be localhost
.
There are now two env variables IHP_BASEURL
and IHP_IDE_BASEURL
to override the BaseUrl
configured in Config/Config.hs
at runtime:
export IHP_BASEURL=http://dev.myhost.lo:1337
export IHP_IDE_BASEURL=http://dev.myhost.lo:1338
./start
We've already integrated this into the IHP GitPod template. If you're curious, follow this link to start a GitPod development session (it's free) with the IHP Template.
These new env vars might also be useful if you run your IHP dev tooling on a different device than your editor inside your local network.
error
call inside a view.
Help decide what's coming next to IHP by using the Feature Voting!
If you have any problems with updating, let us know on the IHP forum.
π§ To stay in the loop, subscribe to the IHP release emails. Or follow digitally induced on twitter.
A small out-of-line release that fixes two issues introduced in v0.13.0
.
Specifically it fixes:
putStrLn
are in scopeA new IHP release with new features and bug fixes. Over 120 commits have been merged since the last release π
New Function: storeFileFromUrl
Quickly upload something from an existing URL into your S3 bucket:
let externalUrl = "http://example/picture.jpg"
let options :: StoreFileOptions = def
{ directory = "pictures"
}
storedFile <- storeFileFromUrl externalUrl options
let newUrl = get #url storedFile
New Function: storeFileFromPath
Quickly upload something from the current app server into your S3 bucket:
let options :: StoreFileOptions = def
{ directory = "pictures"
}
storedFile <- storeFileFromPath "picture.jpg" options
let newUrl = get #url storedFile
New Storage Backend: Minio You can now store files on a minio server if you don't want to use S3. Learn more on how to set up minio in the documentation
New Error Messages when an Action is called with the wrong HTTP Method
Send Email Attachments with IHP Mail This version adds support to have mail attachments when sending mails with your IHP app.
module Web.Mail.Users.Confirmation where
import Web.View.Prelude
import IHP.MailPrelude
data ConfirmationMail = ConfirmationMail { user :: User }
instance BuildMail ConfirmationMail where
subject = "Subject"
to ConfirmationMail { .. } = Address { addressName = Just "F L", addressEmail = "[email protected]" }
from = "[email protected]"
html ConfirmationMail { .. } = [hsx|
Hello World
|]
attachments ConfirmationMail { .. } = [
MailAttachment { name = "attachment.xml", contentType = "application/xml", content = "<xml>...</xml>" }
]
New View Helper: time
Similiar to timeAgo
, but always showing an absolute time.
Learn more.
New icons in the Schema Designer
Package Updates We're updating our nixpkgs version to update several dependencies of IHP: - Haskell Language Server is updated from v0.7 to v1.2 - GHC is updated from v8.10.3 to v8.10.4
nix-build
fails by default in IHP projects
collectionFetchRelatedOrNothing
If you have any problems with updating, let us know on the IHP forum.
π§ To stay in the loop, subscribe to the IHP release emails. Or follow digitally induced on twitter.
A new IHP release with new features and bug fixes. Over 100 commits have been merged since the last release π
Pagination: IHP has a new built-in pagination module.
Given an action like this:
action PostsAction = do
posts <- query @Post
|> orderBy #createdAt
|> fetch
render IndexView { .. }
We can paginate our results by adding a call to paginate
:
action PostsAction = do
(postsQuery, pagination) <- query @Post
|> orderBy #createdAt
|> paginate
posts <- postsQuery |> fetch
render IndexView { .. }
We also need to change the view:
module Web.View.Posts.Index where
import Web.View.Prelude
data IndexView = IndexView
{ posts :: [Post]
, pagination :: Pagination -- <---- Pass pagination variable to the view
}
instance View IndexView where
html IndexView { .. } = [hsx|
<div>
{forEach posts renderPost}
</div>
{renderPagination pagination} -- <---- CALL renderPagination
|]
Here's how the pagination looks like in the view:
When you're adding a new Controller to your app, you can use the new pagination checkbox to automatically generate the needed code:
Job Timeouts: You can now configure a timeout for Job Workers:
instance Job EmailCustomersJob where
perform EmailCustomersJob { .. } = do
customers <- query @Customer |> fetch
forEach customers sendToCustomer
where
sendToCustomer customer = sendMail (MarketingMail customer)
timeoutInMicroseconds = Just $ 1000000 * 60 -- 60 seconds
Added function to delete files from the cloud storage
You can now use removeFileFromStorage
to remove uploaded files from S3 or any other configured cloud storage:
action DeleteUploadedFileAction { uploadedFileId } = do
uploadedFile <- fetch uploadedFile
let storedFile = StoredFile
{ path = get #objectPath uploadedFile
, url = get #url uploadedFile
}
removeFileFromStorage storedFile
deleteRecord uploadedFile
redirectTo UploadedFilesAction
Custom CORS policies If you're building APIs with IHP you can now specify a custom CORS policy:
-- Config.hs
import qualified Network.Wai.Middleware.Cors as Cors
config :: ConfigBuilder
config = do
option Development
option (AppHostname "localhost")
-- The boolean True specifies if credentials are allowed for the request. You still need to set withCredentials on your XmlHttpRequest
option Cors.simpleCorsResourcePolicy { Cors.corsOrigins = Just (["localhost"], True) }
New Helper Function: allEnumValues
Given a enum defined in the Schema.sql
like this:
CREATE TYPE colors AS ENUM ('yellow', 'red', 'blue');
you can call allEnumValues
to get a list of all the colors:
let allColors = allEnumValues @Color
-- allColors = [ Yellow, Red, Blue ]
This also works if you define custom type in Web/Types.hs
that is deriving Enum
:
data Color = Yellow | Red | Blue deriving (Enum)
let allColors = allEnumValues @Color
-- allColors = [ Yellow, Red, Blue ]
Respond with XML in your action:
We added a renderXml
function:
action MyVeryEnterprisyAction = do
renderXml "<xml></xml>"
Added support for Unique Indices
You can now use CREATE UNIQUE INDEX
statements inside your Schema.sql
:
CREATE UNIQUE INDEX users_index ON users (user_name);
If you have any problems with updating, let us know on the IHP forum.
π§ To stay in the loop, subscribe to the IHP release emails. Or follow digitally induced on twitter.
A new IHP release with some new features (spoiler alert: finally table joins) and bug fixes. Around 150 commits have been merged since the last release 23 days ago π
Joins: This has been requested for quite some time already. Finally @hllizi solved this: You can now do joins with the IHP query builder π
tomPosts <- query @Post
|> innerJoin @User (#authorId, #id)
|> filterWhereJoinedTable @User (#name, "Tom")
|> fetch
You can also join multiple tables:
query @Posts
|> innerJoin @Tagging (#id, #postId)
|> innerJoinThirdTable @Tag @Tagging (#id, #tagId)
|> filterWhereJoinedTable @Tag (#tagText, "haskell")
|> fetch
Type safety is maintained by adding all joined types to a type-level list and checking that the table has been joined where necessary.
E.g. uses of filterWhereJoinedTable @Model
will only compile if @Model
had been joined to the input before.
Server-Side Components We've mixed the ideas of react.js with HSX. IHP Server-Side Components provide a toolkit for building interactive client-side functionality without needing to write too much javascript.
Here's how a Counter with a Plus One
button looks like:
module Web.Component.Counter where
import IHP.ViewPrelude
import IHP.ServerSideComponent.Types
import IHP.ServerSideComponent.ControllerFunctions
-- The state object
data Counter = Counter { value :: !Int }
-- The set of actions
data CounterController
= IncrementCounterAction
deriving (Eq, Show, Data)
$(deriveSSC ''CounterController)
-- The render function and action handlers
instance Component Counter CounterController where
initialState = Counter { value = 0 }
render Counter { value } = [hsx|
Current: {value} <br />
<button onclick="callServerAction('IncrementCounterAction')">Plus One</button>
|]
action state IncrementCounterAction = do
state
|> incrementField #value
|> pure
instance SetField "value" Counter Int where setField value' counter = counter { value = value' }
Here's a demo of using Server-Side Components for a interactive data table:
Learn more about Server-Side Components in the documentation.
New IHP.FileStorage Module: For storing files in AWS S3 and other Cloud Storage Services
A common task when building web applications is to save and manage uploaded files like custom logos, profile pictures or .csv
files provided by the user. IHP now provides a simple file storage system to upload files to Amazon S3 or any S3 compatible cloud service.
Here's how the new API looks, once configured:
action UpdateCompanyAction { companyId } = do
company <- fetch companyId
company
|> fill @'["name"]
|> uploadToStorage #logoUrl
>>= ifValid \case
Left company -> render EditView { .. }
Right company -> do
company <- company |> updateRecord
redirectTo EditCompanyAction { .. }
Some of the cool features:
static/
directory instead of uploading to S3 in developmentCase Insensitive Operations:
You can now use filterWhereCaseInsensitive
to query for strings in a case insensitive way:
userByEmail :: Text -> IO (Maybe User)
userByEmail email = do
user <- query @User
|> filterWhereCaseInsensitive (#email, email)
|> fetchOneOrNothing
-- Query: `SELECT * FROM users WHERE LOWER(email) = <email>`
pure user
There's also a new validateIsUniqueCaseInsensitive
for checking uniqueness in a case insensitive way:
action CreateUserAction = do
let user = newRecord @User
user
|> fill @'["email"]
|> validateIsUniqueCaseInsensitive #email
>>= ifValid \case
Left user -> render NewView { .. }
Right user -> do
createRecord user
redirectTo UsersAction
Important if you use IHP's Login: IHP's built-in sessions controller used for the built-in login now uses case-insensitive lookup for the email addresses at login. This will improve user experience for users that create their account with [email protected]
and then try to log in using [email protected]
.
isTrue
and isFalse
Validation Constraints
make ghci
to make console
If you have any problems with updating, let us know on the IHP forum.
π§ To stay in the loop, subscribe to the IHP release emails. Or follow digitally induced on twitter.
This is another release before our major version 1 :) It comes packed with a lot of small improvements and bug fixes π
Auto Index Creation: The Schema Designer will now automatically create an index when creating a column that has a foreign key constraint.
E.g. when adding user_id
column to a projects
table, the schema designer will also automatically create an index on the user_id
column. This speeds up queries with conditions on the user_id
column, typically like query @Project |> filterWhere (#userId, currentUserId) |> fetch
. In case you don't want an index for specific reasons, you can always manually remove the index again from your Schema.sql
.
Debounce File Changes: Switching git branches triggers a lot of file changes on the IHP dev server. To avoid long reload times the dev server is debouncing file changes now.
New Functions: currentAdmin
and currentAdminOrNothing
:
Like currentUser
, but works when you also have a admins
table next to your normal users
table.
Cache Busting: Sometimes problems are caused when your users still have an old version of your JS or CSS files inside their browsers cache. To avoid this you typically append a hash to the url of your JS or CSS file.
IHP now provides an assetPath
view helper to deal with this:
[hsx|
<script={assetPath "/app.js"}/>
<link rel="stylesheet" href={assetPath "/app.css"}/>
|]
The paths will look like this "/app.js?v=9be8995c-7055-43d9-a1b2-43e05c210271"
.
IHP will set the asset version from the IHP_ASSET_VERSION
env variable. This should be set to e.g. your git commit hash in production. In development you don't need to specify this environment variable. If you run on IHP Cloud, it works out of the box.
Built-in Background Jobs Dashboard: @zacwood9 added a new very cool dashboard to schedule and manage your IHP background jobs without manually writing lots of CRUD code. Check the new Guide section to get started
Code of Conduct: The IHP community now follows the Guidelines for Respectful Communication by the Haskell Foundation.
Improved Form Customization:
We added a new formForWithOptions
and also a shortcut formForWithoutJavascript
to allow advanced form configuration. This specifically addresses the issue that previously there was no way to disable the javascript form submission for a specific form. Check out the new guide section to learn more.
New Function: filterWhereNot
Like filterWhere
but negated:
projectsByUser :: UserId -> IO [Project]
projectsByUser userId = do
otherProjects <- query @Project
|> filterWhereNot (#userId, userId)
|> fetch
-- Query: `SELECT * FROM projects WHERE user_id != <userId>`
return otherProjects
New Function: modifyJust
Like modify
, but only modifies the value if it's not Nothing
.
let pauseDuration = now `diffUTCTime` pausedAt
floorTimer <- floorTimer
|> modifyJust #startedAt (addUTCTime pauseDuration)
|> updateRecord
default.nix
it's now possible to request haddock docs for your haskell packages
Integer
ID types in AutoRoute
deleteSession
and getSessionRecordId
functions
If you have any problems with updating, let us know on the IHP forum.
π§ To stay in the loop, subscribe to the IHP release emails. Or follow digitally induced on twitter..
A lot has been done since the last IHP release in january. With v0.9 we're now getting closer to the first major version of IHP π
Use the new GHC Garbage Collector: We now use the new non-moving GC which has been added in recent GHC versions. This avoids long GC pauses when it's time to free some memory. This will improve the production performance.
New Logging module:
IHP was previously using putStrLn
to log something. This has been replaced with a new IHP.Log
modules that provides a unified logging solution for the full framework. The logger supports custom formatters and also custom log destinations (e.g. logging to a file instead of stdout/stderr).
Check the full documentation here: https://ihp.digitallyinduced.com/Guide/logging.html
New Plugins: ihp-sentry and ihp-zip
Add error reporting to your IHP apps with ihp-sentry Provides easy zip file downloads with ihp-zip Check the plugin's README for details on how to install them.
If you're thinking about making your own IHP plugin, consider using these as the starting point :)
Custom WebSocket Servers: We now opened up the IHP WebSocket interface so that you can write your own WebSocket servers.
Check out the new Guide on WebSockets: https://ihp.digitallyinduced.com/Guide/websockets.html
AutoRoute Improvements:
In previous IHP versions AutoRoute by default only worked with UUID
arguments (and Id types like Id Post
). If you have a controller like data HelloWorldController = HelloAction { name :: Text }
where you have a text parameter, you would have to configure AutoRoute manually using parseArgument
. Also when we you had an action with two different types like HelloAction { userId :: Id User, name :: Maybe Text }
this was not supported by AutoRoute.
The type-magic has been reengineered and now it works with all types especially different types. If you have used parseArgument
before, you need to remove that as these functions have been removed because this now works without manually configuring it.
Database Transactions:
There's a new withTransaction
function to run sql statements inside a single database transaction:
withTransaction do
company <- newRecord @Company |> createRecord
user <- newRecord @User
|> set #companyId (get #id company)
|> createRecord
company <- company
|> setJust #ownerId (get #id user)
|> updateRecord
Check out the Guide for details: https://ihp.digitallyinduced.com/Guide/database.html#transactions
New Function: setJust
:
Like set
but wraps the value in a Just
. Across a lot of IHP code bases we spotted a pattern like |> set #field (Just(value))
. The (Just ..)
is basically just syntactical noise and makes the value expression kind of more complicated:
-- BEFORE
project |> set #userId (Just (get #id user))
-- AFTER
project |> setJust #userId (get #id user)
New Query Function: distinct
: Use distinct
to remove all duplicate rows from the result
query @Book
|> distinct
|> fetch
-- SELECT DISTINCT * FROM books
New Query Function: distinctOn
: Use distinctOn
to add a DISTINCT ON
to your sql query.
query @Book
|> distinctOn #categoryId
|> fetch
-- SELECT DISTINCT ON (category_id) * FROM books
New Query Functions: filterWhereLike
and friends:
query @Book
|> filterWhereLike (#title, "%haskell%")
|> fetch
-- SELECT * FROM books WHERE title LIKE '%haskell%'
Additonally there's now also:
- filterWhereILike
(like filterWhereLike
but case-insensitive)
- filterWhereMatches
filter with a postgres regex
- filterWhereIMatches
case-insensitive variant of filterWhereMatches
smallint
and brought existing support for bigint
and smallint
to the schema designer
make postgres
command to start only the postgres server
forceRedirectToPath
to work around JS issues
ihp:load
Javascript Event
getRequestBody
now works for JSON requests as well
If you have any problems with updating, let us know on the IHP forum.
π§ To stay in the loop, subscribe to the IHP release emails. Or follow digitally induced on twitter..
π The next release is expected to be available in march.