GithubHelp home page GithubHelp logo

georgecrawford / houdini Goto Github PK

View Code? Open in Web Editor NEW

This project forked from houdinigraphql/houdini

0.0 1.0 0.0 2.28 MB

The "disappearing" Svelte GraphQL client with support for Sapper and Sveltekit

TypeScript 94.36% JavaScript 3.43% HTML 0.09% Svelte 2.05% Shell 0.07%

houdini's Introduction

houdini

The disappearing GraphQL client for Sapper and SvelteKit.

NOTE: Houdini is in the early phases of development. Please create an issue or start a discussion if you run into problems. For more information on what's coming for this project, you can visit the roadmap.

If you are interested in helping out, please 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 Sapper and SvelteKit's public beta

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. Sapper
    2. SvelteKit
    3. Svelte
  4. Running the Compiler
  5. Fetching Data
    1. Query variables and page data
    2. Loading State
    3. Refetching Data
    4. What about load?
  6. Fragments
  7. Mutations
    1. Updating fields
    2. Connections
      1. Insert
      2. Remove
      3. Delete
      4. Conditionals
  8. Subscriptions
    1. Configuring the WebSocket client
    2. Using graphql-ws
    3. Using subscriptions-transport-ws
  9. Authentication
  10. 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:

npx houdini init

This will create a few necessary files, as well as pull down a json representation of your API's schema. Next, add the preprocessor to your sapper setup. Don't forget to add it to both the client and the server configurations if you're using sapper.

import houdini from 'houdini-preprocess'

// somewhere in your config file
{
    plugins: [
        svelte({
            preprocess: [houdini()],
        }),
    ]
}

Sapper

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)

SvelteKit

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

{
    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.

  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

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". You should also use this setting if you are building a SvelteKit application in SPA mode.

🚀  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}

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}>

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.

📝  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>

Connections

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 @connection directive and provide a unique name:

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

It's recommended to name these connections 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 connection:

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 connection 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 connections.

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

Conditionals

Sometimes you only want to add or remove a record from a connection 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(argument: "completed", value: "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)

🔐  Authentication

houdini defers to Sapper'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()
})

⚠️  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

houdini's People

Contributors

alecaivazis avatar georgecrawford avatar github-actions[bot] avatar pixelmund avatar sorenholsthansen avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.