houdini
    TypeScript icon, indicating that this package has built-in type declarations

    0.12.1 • Public • Published
    houdini

    The disappearing GraphQL client for Sapper and SvelteKit.

    If you are interested in helping out, the contributing guide should provide some guidance. If you need something more specific, feel free to reach out to @AlecAivazis on the Svelte discord. There's lots to do regardless of how deep you want to dive 🙂

      Features

    • Composable and colocated data requirements for your components
    • Normalized cache with declarative updates
    • Generated types
    • Subscriptions
    • Support for SvelteKit and Sapper
    • Pagination (cursors and offsets)

    At its core, houdini seeks to enable a high quality developer experience without compromising bundle size. Like Svelte, houdini shifts what is traditionally handled by a bloated runtime into a compile step that allows for the generation of an incredibly lean GraphQL abstraction for your application.

    📚  Table of Contents

    1. Example
    2. Installation
    3. Configuring Your Application
      1. SvelteKit
      2. Sapper
      3. Svelte
    4. Config File
    5. Running the Compiler
    6. Fetching Data
      1. Query variables and page data
      2. Loading State
      3. Hooks
      4. Refetching Data
      5. Cache policy
        1. Data Retention
        2. Changing default cache policy
      6. What about load?
    7. Fragments
      1. Fragment Arguments
    8. Mutations
      1. Updating fields
      2. Lists
        1. Insert
        2. Remove
        3. Delete
        4. Conditionals
    9. Subscriptions
      1. Configuring the WebSocket client
      2. Using graphql-ws
      3. Using subscriptions-transport-ws
    10. Pagination
      1. Paginated Fragments
      2. Mutation Operations
    11. Custom Scalars
    12. Authentication
    13. Persisted Queries
    14. Notes, Constraints, and Conventions

    🕹  Example

    A demo can be found in the example directory.

    Please note that the examples in that directory and this readme showcase the typescript definitions generated by the compiler. While it is highly recommended, Typescript is NOT required in order to use houdini.

      Installation

    houdini is available on npm.

    yarn add -D houdini houdini-preprocess
    # or
    npm install --save-dev houdini houdini-preprocess

    🔧  Configuring Your Application

    Adding houdini to an existing project can easily be done with the provided command-line tool. If you don't already have an existing app, visit this link for help setting one up. Once you have a project and want to add houdini, execute the following command which will create a few necessary files, as well as pull down a json representation of your API's schema.

    npx houdini init

    This will send a request to your API to download your schema definition. If you need headers to authenticate this request, you can pass them in with the --pull-header flag (abbreviated -ph). For example, npx houdini init -ph Authorization="Bearer MyToken". You will also need to provide the same flag to generate when using the --pull-schema flag.

    Finally, follow the steps appropriate for your framework.

    SvelteKit

    We need to define an alias so that your codebase can import the generated runtime. Add the following values to svelte.config.js:

    import houdini from 'houdini-preprocess'
    
    {
        preprocess: [houdini()],
    
        kit: {
            vite: {
                resolve: {
                    alias: {
                        $houdini: path.resolve('.', '$houdini')
                    }
                }
            }
        }
    }

    And finally, we need to configure our application to use the generated network layer. To do this, add the following block of code to src/routes/__layout.svelte:

    <script context="module">
    	import env from '../environment';
    	import { setEnvironment } from '$houdini';
    
    	setEnvironment(env);
    </script>

    You might need to generate your runtime in order to fix typescript errors.

    Note: If you are building your application with adapter-static (or any other adapter that turns your application into a static site), you will need to set the static value in your config file to true.

    Sapper

    You'll need to add the preprocessor to both your client and your server configuration:

    import houdini from 'houdini-preprocess'
    
    // add to both server and client configurations
    {
        plugins: [
            svelte({
                preprocess: [houdini()],
            }),
        ]
    }

    With that in place, the only thing left to configure your Sapper application is to connect your client and server to the generate network layer:

    // in both src/client.js and src/server.js
    
    import { setEnvironment } from '$houdini'
    import env from './environment'
    
    setEnvironment(env)

    Svelte

    If you are working on an application that isn't using SvelteKit or Sapper, you have to configure the compiler and preprocessor to generate the correct logic by setting the framework field in your config file to "svelte".

    Please keep in mind that returning the response from a query, you should not rely on this.redirect to handle the redirect as it will update your browsers location attribute, causing a hard transition to that url. Instead, you should use this.error to return an error and handle the redirect in a way that's appropriate for your application.

      Running the Compiler

    The compiler is responsible for a number of things, ranging from generating the actual runtime to creating types for your documents. Running the compiler can be done with npx or via a script in package.json and needs to be run every time a GraphQL document in your source code changes:

    npx houdini generate

    The generated runtime can be accessed by importing $houdini anywhere in your application.

    If you have updated your schema on the server, you can pull down the most recent schema before generating your runtime by using --pull-schema or -p:

    npx houdini generate --pull-schema

    📄 Config File

    All configuration for your houdini application is defined in a single file that is imported by both the runtime and the command-line tool. Because of this, you must make sure that any imports and logic are resolvable in both environments. This means that if you rely on process.env or other node-specifics you will have to use a plugin to replace the expression with something that can run in the browser.

    🚀  Fetching Data

    Grabbing data from your API is done with the query function:

    <script lang="ts">
        import { query, graphql, AllItems } from '$houdini'
    
        // load the items
        const { data } = query<AllItems>(graphql`
            query AllItems {
                items {
                    id
                    text
                }
            }
        `)
    </script>
    
    {#each $data.items as item}
        <div>{item.text}</div>
    {/each}

    Query variables and page data

    At the moment, query variables are declared as a function in the module context of your component. This function must be named after your query and in a sapper application, it takes the same arguments that are passed to the preload function described in the Sapper documentation. In a SvelteKit project, this function takes the same arguments passed to the load function described in the SvelteKit docs. Regardless of the framework, you can return the value from this.error and this.redirect in order to change the behavior of the response. Here is a modified example from the demo:

    // src/routes/[filter].svelte
    
    <script lang="ts">
        import { query, graphql, AllItems } from '$houdini'
    
        // load the items
        const { data } = query<AllItems>(graphql`
            query AllItems($completed: Boolean) {
                items(completed: $completed) {
                    id
                    text
                }
            }
        `)
    </script>
    
    <script context="module" lang="ts">
        // This is the function for the AllItems query.
        // Query variable functions must be named <QueryName>Variables.
        export function AllItemsVariables(page): AllItems$input {
            // make sure we recognize the value
            if (!['active', 'completed'].includes(page.params.filter)) {
                return this.error(400, "filter must be one of 'active' or 'completed'")
            }
    
            return {
                completed: page.params.filter === 'completed',
            }
        }
    </script>
    
    {#each $data.items as item}
        <div>{item.text}</div>
    {/each}

    Loading State

    The methods used for tracking the loading state of your queries changes depending on the context of your component. For queries that live in routes (ie, in /src/routes/...), the actual query happens in a load function as described in What about load?. Because of this, the best way to track if your query is loading is to use the navigating store exported from $app/stores:

    // src/routes/index.svelte
    
    <script>
        import { query } from '$houdini'
        import { navigating } from '$app/stores'
    
        const { data } = query(...)
    </script>
    
    {#if $navigating}
        loading...
    {:else}
        data is loaded!
    {/if}

    However, since queries inside of non-route components (ie, ones that are not defined in /src/routes/...) do not get hoisted to a load function, the recommended practice to is use the store returned from the result of query:

    // src/components/MyComponent.svelte
    
    <script>
        import { query } from '$houdini'
    
        const { data, loading } = query(...)
    </script>
    
    {#if $loading}
        loading...
    {:else}
        data is loaded!
    {/if}

    Load Hooks

    Sometimes you will need to add additional logic to a component's query. For example, you might want to check if the current session is valid before a query is sent to the server. In order to support this, houdini will look for hook functions defined in the module context which can be used to perform any logic you need.

    beforeLoad

    Called before Houdini executes load queries against the server. You can expect the same arguments as SvelteKit's load hook.

    If you return a value from this function, it will be passed as props to your component.

    <script context="module">
        // It has access to the same arguments and this.error this.redirect as the variable functions
        export function beforeLoad({page, session}){
            if(!session.authenticated){
                return this.redirect(302, '/login')
            }
    
        	return {
        	    message: "There are this many items"
            }
        }
    </script>
    
    <script>
        import { query, graphql } from '$houdini'
    
        export let message
    
        // load the items
        const { data } = query(graphql`
            query AllItems {
                items {
                    id
                }
            }
        `)
    </script>
    
    {message}: {$data.items.length}

    afterLoad

    Called after Houdini executes load queries against the server. You can expect the same arguments as SvelteKit's load hook, plus an additional data property referencing query result data.

    If you return a value from this function, it will be passed as props to your component.

    <script context="module">
        export function MyProfileVariables({ page: { params: { id } } }) {
            return { id }
        }
        export function afterLoad({ data }){
            if(!data.MyProfile){
                return this.error(404)
            }
        }
    </script>
    
    <script>
        import { query, graphql } from '$houdini'
    
        // load the items
        const { data } = query(graphql`
            query MyProfile {
                profile(id) {
                    name
                }
            }
        `)
    </script>
    
    Hello I'm {$data.profile.name}

    Refetching Data

    Refetching data is done with the refetch function provided from the result of a query:

    <script lang="ts">
        import { query, graphql, AllItems } from '$houdini'
    
        // load the items
        const { refetch } = query<AllItems>(graphql`
            query AllItems($completed: Boolean) {
                items(completed: $completed) {
                    id
                    text
                }
            }
        `)
    
        let completed = true
    
        $: refetch({ completed })
    </script>
    
    <input type=checkbox bind:checked={completed}>

    Cache policy

    By default, houdini will only try to load queries against its local cache when you indicate it is safe to do so. This can be done with the @cache directive:

    query AllItems @cache(policy: CacheOrNetwork) {
        items {
            id
            text
        }
    }

    There are 3 different policies that can be specified:

    • CacheOrNetwork will first check if a query can be resolved from the cache. If it can, it will return the cached value and only send a network request if data was missing.
    • CacheAndNetwork will use cached data if it exists and always send a network request after the component has mounted to retrieve the latest data from the server
    • NetworkOnly will never check if the data exists in the cache and always send a network request

    Data Retention

    Houdini will retain a query's data for a configurable number of queries (default 10). For a concrete example, consider an example app that has 3 routes. If you load one of the routes and then click between the other two 5 times, the first route's data will still be resolvable (and the counter will reset if you visit it). If you then toggle between the other routes 10 times and then try to load the first route, a network request will be sent. This number is configurable with the cacheBufferSize value in your config file:

    // houdini.config.js
    
    export default {
        // ...
        cacheBufferSize: 5,
    }

    Changing default cache policy

    As previously mentioned, the default cache policy is CacheOrNetwork. This can be changed by setting the defaultCachePolicy config value:

    // houdini.config.js
    
    import { CachePolicy } from '$houdini'
    
    export default {
        // ...
    
        // note: if you are upgrading from a previous version of houdini, you might
        // have to generate your runtime for this type to be defined.
        defaultCachePolicy: CachePolicy.NetworkOnly,
    }

    What about load?

    Don't worry - that's where the preprocessor comes in. One of its responsibilities is moving the actual fetch into a load. You can think of the block at the top of this section as equivalent to:

    <script context="module">
        export async function load() {
                return {
                    _data: await this.fetch({
                        text: `
                            query AllItems {
                                items {
                                    id
                                    text
                                }
                            }
                        `
                    }),
                }
        }
    </script>
    
    <script>
        export let _data
    
        const data = readable(_data, ...)
    </script>
    
    {#each $data.items as item}
        <div>{item.text}</div>
    {/each}

    🧩  Fragments

    Your components will want to make assumptions about which attributes are available in your queries. To support this, Houdini uses GraphQL fragments embedded within your component. Take, for example, a UserAvatar component that requires the profilePicture field of a User:

    // components/UserAvatar.svelte
    
    <script lang="ts">
        import { fragment, graphql, UserAvatar } from '$houdini'
    
        // the reference will get passed as a prop
        export let user: UserAvatar
    
        const data = fragment(graphql`
            fragment UserAvatar on User {
                profilePicture
            }
        `, user)
    </script>
    
    <img src={$data.profilePicture} />

    This component can be rendered anywhere we want to query for a user, with a guarantee that all necessary data has been asked for:

    // src/routes/users.svelte
    
    <script>
        import { query, graphql, AllUsers } from '$houdini'
        import { UserAvatar } from 'components'
    
        const { data } = query<AllUsers>(graphql`
            query AllUsers {
                users {
                    id
                    ...UserAvatar
                }
            }
        `)
    </script>
    
    {#each $data.users as user}
        <UserAvatar user={user} />
    {/each}

    It's worth mentioning explicitly that a component can rely on multiple fragments at the same time so long as the fragment names are unique and prop names are different.

    Fragment Arguments

    In some situations it's necessary to configure the documents inside of a fragment. For example, you might want to extend the UserAvatar component to allow for different sized profile pictures. To support this, houdini provides two directives @arguments and @with which declare arguments for a fragment and provide values, respectively.

    Default values can be provided to fragment arguments with the default key:

    fragment UserAvatar on User @arguments(width: {type:"Int", default: 50}) {
        profilePicture(width: $width)
    }

    In order to mark an argument as required, pass the type with a ! at the end. If no value is provided, an error will be thrown when generating your runtime.

    fragment UserAvatar on User @arguments(width: {type:"Int!"}) {
        profilePicture(width: $width)
    }

    Providing values for fragments is done with the @with decorator:

    query AllUsers {
        users {
            ...UserAvatar @with(width: 100)
        }
    }

    Keep in mind, if you are using fragment variables inside of a field flagged for list operations, you'll have to pass a value for the variable when performing the operation

    📝  Mutations

    Mutations are defined in your component like the rest of the documents but instead of triggering a network request when called, you get a function which can be invoked to execute the mutation. Here's another modified example from the demo:

    <script lang="ts">
        import { mutation, graphql, UncheckItem } from '$houdini'
    
        let itemID: string
    
        const uncheckItem = mutation<UncheckItem>(graphql`
            mutation UncheckItem($id: ID!) {
                uncheckItem(item: $id) {
                    item {
                        id
                        completed
                    }
                }
            }
        `)
    </script>
    
    <button on:click={() => uncheckItem({ id: itemID })}>
        Uncheck Item
    </button>

    Note: mutations usually do best when combined with at least one fragment grabbing the information needed for the mutation (for an example of this pattern, see below.)

    Updating fields

    When a mutation is responsible for updating fields of entities, houdini should take care of the details for you as long as you request the updated data alongside the record's id. Take for example, an TodoItemRow component:

    <script lang="ts">
        import { fragment, mutation, graphql, TodoItemRow } from '$houdini'
    
        export let item: TodoItemRow
    
        // the resulting store will stay up to date whenever `checkItem`
        // is triggered
        const data = fragment(
            graphql`
                fragment TodoItemRow on TodoItem {
                    id
                    text
                    completed
                }
            `,
            item
        )
    
        const checkItem = mutation<CompleteItem>(graphql`
            mutation CompleteItem($id: ID!) {
                checkItem(item: $id) {
                    item {
                        id
                        completed
                    }
                }
            }
        `)
    </script>
    
    <li class:completed={$data.completed}>
        <input
            name={$data.text}
            class="toggle"
            type="checkbox"
            checked={$data.completed}
            on:click={handleClick}
        />
        <label for={$data.text}>{$data.text}</label>
        <button class="destroy" on:click={() => deleteItem({ id: $data.id })} />
    </li>

    Lists

    Adding and removing records from a list is done by mixing together a few different generated fragments and directives. In order to tell the compiler which lists are targets for these operations, you have to mark them with the @list directive and provide a unique name:

    query AllItems {
        items @list(name: "All_Items") {
            id
        }
    }

    It's recommended to name these lists with a different casing convention than the rest of your application to distinguish the generated fragments from those in your codebase.

    Inserting a record

    With this field tagged, any mutation that returns an Item can be used to insert items in this list:

    mutation NewItem($input: AddItemInput!) {
        addItem(input: $input) {
            ...All_Items_insert
        }
    }

    Removing a record

    Any mutation that returns an Item can also be used to remove an item from the list:

    mutation RemoveItem($input: RemoveItemInput!) {
        removeItem(input: $input) {
            ...All_Items_remove
        }
    }

    Deleting a record

    Sometimes it can be tedious to remove a record from every single list that mentions it. For these situations, Houdini provides a directive that can be used to mark a field in the mutation response holding the ID of a record to delete from all lists.

    mutation DeleteItem($id: ID!) {
        deleteItem(id: $id) {
            itemID @Item_delete
        }
    }

    Conditionals

    Sometimes you only want to add or remove a record from a list when an argument has a particular value. For example, in a todo list you might only want to add the result to the list if there is no filter being applied. To support this, houdini provides the @when and @when_not directives:

    mutation NewItem($input: AddItemInput!) {
        addItem(input: $input) {
            ...All_Items_insert @when_not(completed: true)
        }
    }

    🧾  Subscriptions

    Subscriptions in houdini are handled with the subscription function exported by your runtime. This function takes a tagged document, and returns a store with the most recent value returned by the server. Keep in mind that houdini will keep the cache (and any subscribing components) up to date as new data is encountered.

    It's worth mentioning that you can use the same fragments described in the mutation section in order to update houdini's cache with the response from a subscription.

    Here is an example of a simple subscription from the example application included in this repo:

    <script lang="ts">
        import {
            fragment,
            mutation,
            graphql,
            subscription,
            ItemEntry_item,
        } from '$houdini'
    
        // the reference we're passed from our parents
        export let item: ItemEntry_item
    
        // get the information we need about the item
        const data = fragment(/* ... */)
    
        // since we're just using subscriptions to stay up to date, we don't care about the return value
        subscription(
            graphql`
                subscription ItemUpdate($id: ID!) {
                    itemUpdate(id: $id) {
                        item {
                            id
                            completed
                            text
                        }
                    }
                }
            `,
            {
                id: $data.id,
            }
        )
    </script>
    
    <li class:completed={$data.completed}>
        <div class="view">
            <input
                name={$data.text}
                class="toggle"
                type="checkbox"
                checked={$data.completed}
                on:click={handleClick}
            />
            <label for={$data.text}>{$data.text}</label>
            <button class="destroy" on:click={() => deleteItem({ id: $data.id })} />
        </div>
    </li>

    Configuring the WebSocket client

    Houdini can work with any websocket client as long as you can provide an object that satisfies the SubscriptionHandler interface as the second argument to the Environment's constructor. Keep in mind that WebSocket connections only exist between the browser and your API, therefor you must remember to pass null when configuring your environment on the rendering server.

    Using graphql-ws

    If your API supports the graphql-ws protocol, you can create a client and pass it directly:

    // environment.ts
    
    import { createClient } from 'graphql-ws'
    import { browser } from '$app/env'
    
    // in sapper, this would be something like `(process as any).browser`
    let socketClient = browser
        ? new createClient({
                url: 'ws://api.url',
          })
        : null
    
    export default new Environment(fetchQuery, socketClient)

    Using subscriptions-transport-ws

    If you are using the deprecated subscriptions-transport-ws library and associated protocol, you will have to slightly modify the above block:

    // environment.ts
    
    import { SubscriptionClient } from 'subscriptions-transport-ws'
    import { browser } from '$app/env'
    
    let socketClient: SubscriptionHandler | null = null
    if (browser) {
        // instantiate the transport client
        const client = new SubscriptionClient('ws://api.url', {
            reconnect: true,
        })
    
        // wrap the client in something houdini can use
        socketClient = {
            subscribe(payload, handlers) {
                // send the request
                const { unsubscribe } = client.request(payload).subscribe(handlers)
    
                // return the function to unsubscribe
                return unsubscribe
            },
        }
    }
    
    export default new Environment(fetchQuery, socketClient)

    ♻️ Pagination

    It's often the case that you want to avoid querying an entire list from your API in order to minimize the amount of data transfers over the network. To support this, GraphQL APIs will "paginate" a field, allowing users to query a slice of the list. The strategy used to access slices of a list fall into two categories. Offset-based pagination relies offset and limit arguments and mimics the mechanisms provided by most database engines. Cursor-based pagination is a bi-directional strategy that relies on first/after or last/before arguments and is designed to handle modern pagination features such a infinite scrolling.

    Regardless of the strategy used, houdini follows a simple pattern: wrap your document in a "paginated" function (ie, paginatedQuery or paginatedFragment), mark the field with @paginate, and provide the "page size" via the first, last or limit arguments to the field. paginatedQuery and paginatedFragment behave identically: they return a data field containing a svelte store with your full dataset, functions you can call to load the next or previous page, as well as a readable store with a boolean loading state. For example, a field supporting offset-based pagination would look something like:

    const { data, loadNextPage, loading } = paginatedQuery(graphql`
        query UserList {
            friends(limit: 10) @paginate {
                id
            }
        }
    `)

    and a field that supports cursor-based pagination starting at the end of the list would look something like:

    const { data, loadPreviousPage } = paginatedQuery(graphql`
        query UserList {
            friends(last: 10) @paginate {
                edges {
                    node {
                        id
                    }
                }
            }
        }
    `)

    If you are paginating a field with a cursor-based strategy (forward or backwards), the current page info can be looked up with the pageInfo store returned from the paginated function:

    <script>
        const { data, loadNextPage, pageInfo } = paginatedQuery(graphql`
            query UserList {
                friends(first: 10) @paginate {
                    edges {
                        node {
                            id
                        }
                    }
                }
            }
        `)
    </script>
    
    {#if $pageInfo.hasNextPage}
        <button onClick={() => loadNextPage()}> load more </button>
    {/if}

    Paginated Fragments

    paginatedFragment functions very similarly to paginatedQuery with a few caveats. Consider the following:

    const { loadNextPage, data, pageInfo } = paginatedFragment(graphql`
        fragment UserWithFriends on User {
            friends(first: 10) @paginate {
                edges {
                    node {
                        id
                    }
                }
            }
        }
    `)

    In order to look up the next page for the user's friend. We need a way to query the specific user that this fragment has been spread into. In order to pull this off, houdini relies on the generic Node interface and corresponding query:

    interface Node {
        id: ID!
    }
    
    type Query {
        node(id: ID!): Node
    }

    In short, this means that any paginated fragment must be of a type that implements the Node interface (so it can be looked up in the api). You can read more information about the Node interface in this section of the graphql community website. This is only a requirement for paginated fragments. If your application only uses paginated queries, you do not need to implement the Node interface and resolver.

    Mutation Operations

    A paginated field can be marked as a potential target for a mutation operation by passing a name argument to the @paginate directive:

    const { loadNextPage, data, pageInfo } = paginatedFragment(graphql`
        fragment UserWithFriends on User {
            friends(first: 10) @paginate(name: "User_Friends") {
                edges {
                    node {
                        id
                    }
                }
            }
        }
    `)

    ⚖️ Custom Scalars

    Configuring your runtime to handle custom scalars is done under the scalars key in your config:

    // houdini.config.js
    
    export default {
    	// ...
    
    	scalars: {
    		// the name of the scalar we are configuring
    		DateTime: {
    			// the corresponding typescript type
    			type: 'Date',
    			// turn the api's response into that type
    			unmarshal(val) {
    				return new Date(val)
    			},
    			// turn the value into something the API can use
    			marshal(date) {
    				return date.getTime()
    			},
    		},
    	},
    }

    🔐  Authentication

    houdini defers to SvelteKit's sessions for authentication. Assuming that the session has been populated somehow, you can access it through the second argument in the environment definition:

    //src/environment.ts
    
    import { Environment } from '$houdini'
    
    // this function can take a second argument that will contain the session
    // data during a request or mutation
    export default new Environment(async function ({ text, variables = {} }, session) {
        const result = await this.fetch('http://localhost:4000', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': session.token ? `Bearer ${session.token}` : null,
            },
            body: JSON.stringify({
                query: text,
                variables,
            }),
        })
    
        // parse the result as json
        return await result.json()
    })

    🚦  Persisted Queries

    Sometimes you want to confine an API to only fire a set of pre-defined queries. This can be useful to not only reduce the amount of information transferred over the write but also act as a list of approved queries, providing additional security. Regardless of your motivation, the approach involves associating a known string with a particular query and sending that string to the server instead of the full query body. To support this, houdini passes a queries hash to the fetch function for you to use.

    Automatic Persisted Queries

    An approach to Persisted Queries, popularized by Apollo, is known as Automatic Persisted Queries. This involves first sending a queries hash and if its unrecognized, sending the full query string. This might look something like:

    /// src/environment.ts
    
    // This sends the actual fetch request to the server
    async function sendFetch({ text, variables, hash }) {
    	const result = await this.fetch('localhost:4000/graphql', {
    		method: 'POST',
    		headers: {
    			'Content-Type': 'application/json',
    		},
    		body: JSON.stringify({
    			query: text ? text : undefined,
    			variables,
    			extensions: {
    				persistedQuery: {
    					version: 1,
    					sha256Hash: hash,
    				},
    			},
    		}),
    	})
    
    	return result.json();
    }
    
    export default new Environment(async function({ text, variables = {}, hash }){
    	// first send the request without the text, only the hash
    	const response = await sendFetch.call(this, { variables, hash, text: null })
    
    	// if there were no errors, we're good to go
    	if (!response.errors) {
    		return response
    	}
    
    	// there were errors, send the hash and the query to associate the two for
    	// future requests
    	return await sendFetch.call(this, { variables, hash, text })
    })

    Fixed List of Persisted Queries

    If you don't want the flexibility of Automatic Persisted Queries, you will need a fixed association of hash to query for every document that your client will send. To support this, you can pass the --persist-output flag to the generate command and provide a path to save the map:

    npx houdini generate --persist-output ./path/to/persisted-queries.json
    # or
    npx houdini generate -po ./path/to/persisted-queries.json

    Once this map has been created, you will have to make it available to your server.

    Now, instead of sending the full operation text with every request, you can now simply pass the hash under whatever field name you prefer:

    /// src/environment.ts
    
    export default new Environment(async function({ text, variables = {}, hash }){
        const result = await this.fetch('http://localhost:4000', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                doc_id: hash,
                variables,
            }),
        })
    
        // parse the result as json
        return await result.json()
    })

    ⚠️  Notes, Constraints, and Conventions

    • The compiler must be run every time the contents of a graphql tagged string changes
    • Every GraphQL Document must have a name that is unique
    • Variable functions must be named after their query
    • Documents with a query must have only one operation in them
    • Documents without an operation must have only one fragment in them

    Install

    npm i houdini

    DownloadsWeekly Downloads

    354

    Version

    0.12.1

    License

    MIT

    Unpacked Size

    5.22 MB

    Total Files

    274

    Last publish

    Collaborators

    • aaivazis