Skip to content

v1.1.0

Compare
Choose a tag to compare
@mpscholten mpscholten released this 21 Jul 14:32
· 2 commits to v1.1 since this release
e6c6eaf

IHP v1.1.0 is out now

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 new ihp-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 and onlyWhereReferencesMaybe:
    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 use onlyWhereReferences 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 call addInitializer from your project's Config.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 the FrontController, 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" } }
    |]

    You can find a demo app here.

  • Faster Initial Startup for large IHP Apps:
    The Generated.Types module is a module generated by IHP based on your project's Schema.sql. The module is now splitted into multiple sub modules, one for each table in your Schema.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 maintained wai-app-static instead of wai-middleware-static for serving files from the static/ directory.

    The old wai-middleware-static had some issues, particular related to leaking file handles. Also wai-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 of Cache-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?
    
  • .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

Notable Documentation Changes

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.