Skip to content

v0.15.0 (Beta 18.10.2021)

Compare
Choose a tag to compare
@mpscholten mpscholten released this 18 Oct 16:49
· 1877 commits to master since this release

IHP v0.15.0 is out now

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.

Major Changes

  • 馃挵 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

    Learn how to integration Stripe in the Documentation

  • 馃摝 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'

    Learn how to deploy with Docker

  • 馃暩锔 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
    
        -- ...

    Learn how to manage OG meta tags

  • 馃寪 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
    Bildschirmfoto 2021-10-18 um 15 39 01

  • 馃帹 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
    image

  • 馃捊 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 "marc@digitallyinduced.com"
        |> 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: demo@digitallyinduced.com Password: demo).
    The app is real-time, try to open it in two browsers and change something :)

    The source code is available in the ihp-datasync-demo repo.

Other Changes

Pull Requests

New Contributors

Full Changelog: v0.14.0...v0.15.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. Or follow digitally induced on twitter.