v1.1.0
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.
This release brings some large improvements to the dev environment by integrating devenv.sh, adds native GPT4 support through ihp-openai and much more.
Major Changes
-
devenv.sh x IHP:
IHP projects now use devenv.sh. devenv is a wrapper around nix flakes that provides fast, declarative, reproducable and composable development environments. It supercedes the previous.envrc
approach. Especially projects with lots of dependencies are much faster to open with devenv. -
ihp-openai:
The newihp-openai
package adds an easy way to integrate GPT3 and GPT4 to your Haskell web apps. The library is extracted from a production app at digitally induced. Compared to existing haskell libs this library is a streaming API (so works great with IHP AutoRefresh and IHP DataSync), works with the latest Chat API, and has smart retry on error without throwing away tokens. Also it's battle tested in real world production use cases.The package can be found in the IHP repo and a demo project can be found on GitHub as well.
Example:
module Web.Controller.Questions where import Web.Controller.Prelude import Web.View.Questions.Index import Web.View.Questions.New import Web.View.Questions.Edit import Web.View.Questions.Show import qualified IHP.OpenAI as GPT instance Controller QuestionsController where action QuestionsAction = autoRefresh do questions <- query @Question |> orderByDesc #createdAt |> fetch render IndexView { .. } action NewQuestionAction = do let question = newRecord |> set #question "What makes haskell so great?" render NewView { .. } action CreateQuestionAction = do let question = newRecord @Question question |> fill @'["question"] |> validateField #question nonEmpty |> ifValid \case Left question -> render NewView { .. } Right question -> do question <- question |> createRecord setSuccessMessage "Question created" fillAnswer question redirectTo QuestionsAction action DeleteQuestionAction { questionId } = do question <- fetch questionId deleteRecord question setSuccessMessage "Question deleted" redirectTo QuestionsAction fillAnswer :: (?modelContext :: ModelContext) => Question -> IO (Async ()) fillAnswer question = do -- Put your OpenAI secret key below: let secretKey = "sk-XXXXXXXX" -- This should be done with an IHP job worker instead of async async do GPT.streamCompletion secretKey (buildCompletionRequest question) (clearAnswer question) (appendToken question) pure () buildCompletionRequest :: Question -> GPT.CompletionRequest buildCompletionRequest Question { question } = -- Here you can adjust the parameters of the request GPT.newCompletionRequest { GPT.maxTokens = 512 , GPT.prompt = [trimming| Question: ${question} Answer: |] } -- | Sets the answer field back to an empty string clearAnswer :: (?modelContext :: ModelContext) => Question -> IO () clearAnswer question = do sqlExec "UPDATE questions SET answer = '' WHERE id = ?" (Only question.id) pure () -- | Stores a couple of newly received characters to the database appendToken :: (?modelContext :: ModelContext) => Question -> Text -> IO () appendToken question token = do sqlExec "UPDATE questions SET answer = answer || ? WHERE id = ?" (token, question.id) pure ()
Bildschirmaufnahme.2023-04-17.um.23.48.41.mov
-
onlyWhere
,onlyWhereReferences
andonlyWhereReferencesMaybe
:
In IHP code bases you often write filter functions such as these:getUserPosts user posts = filter (\p -> p.userId == user.id) posts
This can be written in a shorter way using
onlyWhere
:getUserPosts user posts = posts |> onlyWhere #userId user.id
Because the
userId
field is an Id, we can useonlyWhereReferences
to make it even shorter:getUserPosts user posts = posts |> onlyWhereReferences #userId user
If the Id field is nullable, we need to use
onlyWhereReferencesMaybe
:getUserTasks user tasks = tasks |> onlyWhereReferences #optionalUserId user
-
GHC 9.2.4 -> 9.4.4
We've moved to a newer GHC version 👍 -
Initalizers
You can now run code on the start up of your IHP app using an initializer. For that you can calladdInitializer
from your project'sConfig.hs
.The following example will print a hello world message on startup:
config = do addInitializer (putStrLn "Hello World!")
This is especially useful when using IHP's Row level security helpers. If your app is calling
ensureAuthenticatedRoleExists
from theFrontController
, you can now move that to the app startup to reduce latency of your application:config :: ConfigBuilder config = do -- ... addInitializer Role.ensureAuthenticatedRoleExists
-
Multiple Record Forms
You can now use
nestedFormFor
to make nested forms with the IHP form helpers. This helps solve more complex form use cases.Here's a code example:
renderForm :: Include "tags" Task -> Html renderForm task = formFor task [hsx| {textField #description} <fieldset> <legend>Tags</legend> {nestedFormFor #tags renderTagForm} </fieldset> <button type="button" class="btn btn-light" data-prototype={prototypeFor #tags (newRecord @Tag)} onclick="this.insertAdjacentHTML('beforebegin', this.dataset.prototype)">Add Tag</button> {submitButton} |] renderTagForm :: (?formContext :: FormContext Tag) => Html renderTagForm = [hsx| {(textField #name) { disableLabel = True, placeholder = "Tag name" } } |]
-
Faster Initial Startup for large IHP Apps:
TheGenerated.Types
module is a module generated by IHP based on your project'sSchema.sql
. The module is now splitted into multiple sub modules, one for each table in yourSchema.sql
. This makes the initial startup of your app faster, as the individual sub modules can now be loaded in parallel by the compiler. -
Static Files Changes:
IHP is now using the more actively maintainedwai-app-static
instead ofwai-middleware-static
for serving files from thestatic/
directory.The old
wai-middleware-static
had some issues, particular related to leaking file handles. Alsowai-app-static
has better cache handling for our dev mode.You might see some changes related to caching of your app's static files:
- files in
static/vendor/
previously had more aggressive caching rules, this is not supported anymore. - files in dev mode are now cached with
maxage=0
instead ofCache-Control: nocache
- application assets are now cached forever. As long as you're using IHP's
asssetPath
helper, this will not cause any issues.
Additionally the routing priority has changed to save some syscall overhead for every request:
Previously:
GET /test.txt Does file exists static/test.txt? => If yes: return file => If no: run IHP router to check for an action matching /test.txt
Now:
GET /test.txt Run IHP router to check for an action matching /test.txt Is there an action matching this? => If yes: Run IHP action => If no: Try to serve file static/test.txt?
- files in
-
.env
Files:
Next to the.envrc
, you can now save secrets outside your project repository by putting them into the.env
file.
The.env
is not committed to the repo, so all secrets are safe against being accidentally leaked. -
HSX Comments:
You can now write Haskell comments inside HSX expressions:render MyView { .. } = [hsx| <div> {- This is a comment and will not render to the output -} </div> |]
-
HSX Doctype:
HTML Doctypes are now supported in HSX. Previously you had to write them by using the underlying blaze-html library:render MyView { .. } = [hsx| <!DOCTYPE html> <html lang="en"> <body> hello </body> </html> |]
Minor Changes
- Fixed Google OAuth docs use outdated google JS library
- added interval type to parser and compiler.
- adding default and FromField instance for interval.
- Moving Interval into its own module.
- ported parts of PostgresSimple parser.
- interval implemented for default postgres.
- added InputValue instance for PGInterval.
- Ignore large_pg_notifications table in migrations
- fixed replacing text nodes bug
- Fix modal close button
- Add backoff strategy when a PGListener fails
- Fixed race condition in DataSync causing errors like "Failed to delete DataSubscription, could not find DataSubscription with id 8239e77c-23ae-4b08-85ce-2e60d9ebc10c"
- Fixed hiddenField renders a group instead of just the input field
- Implemented missing 'DROP TRIGGER' operation
- Fixed trigger function for updated_at always recreated
- Updated popper.js to the version that works with bootstrap 5
- Adjusted ghc options for better inlining behaviour
- Fixed JS helpers causing wrong url to be shown in the url bar when a form is submitted
- Fixed migration generator suggesting IHP.PGListener's triggers in the migration
- Extracted IHP.Postgres.* into it's own ihp-postgresql-simple-extra module
- Extracted ihp-graphql into it's own package
- Fixed code gen for multiple has many relationships to the same table
- helpers.js: Support formAction attribute on
- Allow passing fileName in the StoreFileOptions
- Expose the storage function
- Fixed unhandled pattern match case in dev server
- Fixed app loaded twice on first start
- Add
accessDeniedWhen
- Add refreshTemporaryDownloadUrlFromFile
- Add responseBodyShouldNotContain function
- do not watch .direnv and .devenv subdirectories
- Replace the redundant Data controller constraint on parseRoute with Typeable controller
- Added IHP_LIB env var to devenv
- Removed ensureSymlink
- findLibDirectory uses IHP_LIB now instead of build/ihp-lib
- Removed build/ihp-lib from the Makefile and nix config
- Flake ihp schema package
- Added a migrate output to the IHP flake
Notable Documentation Changes
- Add info about Codespaces support under Installation Guide
- Add info for VSCode Devcontainers
- Documentation for GHCRTS
- Updated notes on bare metal deployment
- Added env var reference
- Use nix-build instead of nix-shell for production builds
- Docs for htmx and hyperscript (https://github.com/digitallyinduced/ihp/pull/1648[)](https://github.com/digitallyinduced/ihp/commit/41ab76e496665072009e165e5e65c3891e41a920)
- File upload doc improvmements
- Add example for fetching a single column with sqlQuery
- recommend direnv plugin instead of env-selector
Full Changelog: v1.0.1...v1.1.0
Feature Voting
Help decide what's coming next to IHP by using the Feature Voting!
Updating
→ 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 (right at the bottom of the page). Or follow digitally induced on twitter.