npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@quilted/graphql

v3.3.8

Published

Tiny, type-safe helpers for using GraphQL

Downloads

333

Readme

@quilted/graphql

Tiny, type-safe helpers for using GraphQL. This includes helpers for fetching GraphQL queries and mutations, functions to help you create GraphQL resolvers for your server, and utilities for testing projects that depend on GraphQL results.

To provide better integration for GraphQL in your build tools, combine this with @quilted/graphql-tools, or use Quilt as a framework.

Installation

# npm
npm install @quilted/graphql --save
# pnpm
pnpm install @quilted/graphql --save
# yarn
yarn add @quilted/graphql

Usage

Fetching GraphQL queries and mutations

GraphQL is only useful if you can fetch results. This library provides a few helpers for fetching GraphQL results over the most common transport: HTTP. These utilities are focused on being as small as possible — basic GraphQL fetches require only about 1Kb of compressed code, and streaming fetches require only about 2Kb.

If you’re getting started with GraphQL, you probably have a GraphQL server being served over HTTP. The Quilt GraphQL application template, for example, serves its GraphQL endpoint on /graphql of the app’s domain. To create a function that lets you fetch data from an HTTP endpoint like this, use the createGraphQLFetch() function:

import {createGraphQLFetch} from '@quilted/graphql';

const fetchGraphQL = createGraphQLFetch({url: '/graphql'});

The createGraphQLFetch() function accepts options to customize the GraphQL request before it is performed. The only required option is url, which specifies the URL to send the GraphQL request to.

The resulting function can be called with a GraphQL query or mutation, and returns a promise that resolves to the data (or errors) returned by the GraphQL server:

import {graphql} from '@quilted/graphql';

// `graphql` is optional, it just provides nice syntax highlighting
// in some editors.
const query = graphql`
  query MyQuery {
    # ...
  }
`;

const {data, errors} = await fetchGraphQL(query);

You can also provide variables to the fetch function, as well as a signal to abort the request:

try {
  const controller = new AbortController();

  const timeout = window.setTimeout(() => {
    controller.abort();
  }, 1_000);

  const {data, errors} = await fetchGraphQL(query, {
    variables: {name: 'Winston'},
    signal: controller.signal,
  });

  window.clearTimeout(timeout);
} catch (error) {
  // handle abort
}

This library also provides GraphQLQuery and GraphQLMutation classes that can be used to create observable GraphQL operations. These classes are useful when you want to fetch a GraphQL result multiple times, and when you want to be able to observe each GraphQL call as it completes.

import {createGraphQLFetch, GraphQLQuery} from '@quilted/graphql';

const fetchGraphQL = createGraphQLFetch({url: '/graphql'});

const query = new GraphQLQuery('query MyQuery { ... }', {fetch: fetchGraphQL});
const firstResult = await query.fetch({name: 'Winston'}); // {data: ...}
const secondResult = await query.fetch({name: 'Molly'}); // {data: ...}

query.value; // The most recent GraphQL result

Customizing GraphQL HTTP requests

By default, operations are sent to the specified url using a POST request, according to the GraphQL over HTTP specification. You can force operations to be made using GET requests instead by setting the method option:

import {createGraphQLFetch} from '@quilted/graphql';

const fetch = createGraphQLFetch({
  url: 'https://my-app.com/query',
  method: 'GET',
});

You can add additional headers by specifying a headers option:

import {createGraphQLFetch} from '@quilted/graphql';

const fetch = createGraphQLFetch({
  url: 'https://my-app.com/query',
  // You can pass anything accepted by `new Headers()` here
  headers: {
    'X-Client': 'web',
  },
});

In addition to being able to the method, headers, and url as global, statically-defined options, each can also be set as a function that is called with the operation being fetched, and returns the value to use:

import {createGraphQLFetch} from '@quilted/graphql';

const fetch = createGraphQLFetch({
  url: 'https://my-app.com/query',
  // POST for mutations, GET for queries
  method: (operation) =>
    /^mutation\s/.test(operation.source) ? 'POST' : 'GET',
});

Alternatively, each option can be set per-fetch, as part of the second argument to the fetch function:

import {createGraphQLFetch} from '@quilted/graphql';

const fetch = createGraphQLFetch({
  url: 'https://my-app.com/query',
});

const {data} = await fetch(`query { me { name } }`, {
  method: 'GET',
});

The operation source is sent in all HTTP requests: as the query parameter for GET requests, and as the query body field for POST requests. To accomplish techniques like "persisted" GraphQL queries, you may want to exclude the operation source, and send only only a hashed identifier of each GraphQL operation. You can disable sending the source for all GraphQL fetches by setting source: false when creating your fetch() function:

import {createGraphQLFetch} from '@quilted/graphql';

// Importing `.graphql` files automatically generates hashed
// identifiers for your operations. If you don’t use this feature,
// you must pass the identifier yourself.
import myQuery from './MyQuery.graphql';

const fetch = createGraphQLFetch({
  source: false,
  url: 'https://my-app.com/query',
});

const {data} = await fetch(myQuery);

This isn’t typically useful unless you also communicate the operation’s hash identifier. Here’s an example showing how you could pass the identifier as an additional URL parameter:

import {createGraphQLFetch} from '@quilted/graphql';
import myQuery from './MyQuery.graphql';

const fetch = createGraphQLFetch({
  source: false,
  url(operation) {
    const url = new URL('https://my-app.com/query');
    url.searchParams.set('id', operation.id);
    return url;
  },
});

const {data} = await fetch(myQuery);

Here’s an alternative approach, which sends the operation using a GraphQL extensions field, according to Apollo’s automatic persisted queries protocol:

import {createGraphQLFetch} from '@quilted/graphql';
import myQuery from './MyQuery.graphql';

const fetch = createGraphQLFetch({
  source: false,
  url: 'https://my-app.com/query',
  extensions(operation) {
    return {
      persistedQuery: {version: 1, sha256Hash: operation.id},
    };
  },
});

const {data} = await fetch(myQuery);

These source and extension options can be set globally, as shown above, or per-fetch:

import {createGraphQLFetch} from '@quilted/graphql';
import myQuery from './MyQuery.graphql';

const fetch = createGraphQLFetch({
  url: 'https://my-app.com/query',
});

const {data} = await fetch(myQuery, {
  source: false,
  method: 'GET',
  extensions: {
    persistedQuery: {version: 1, sha256Hash: myQuery.id},
  },
});

If you want to take more control over the HTTP request, this library also provides a helpful subclass of the built-in Request class that will automatically serialize GraphQL operations into the body of the request. You can use instances of this object with the global fetch() API, but remember that you will need to parse the response yourself.

import {GraphQLFetchRequest} from '@quilted/graphql';

const request = new GraphQLFetchRequest(
  '/graphql',
  'query MyQuery($name: String!) { ... }',
  {
    variables: {name: 'Winston'},
  },
);

const response = await fetch(request);
const result = await response.json();

Streaming GraphQL results with @stream and @defer

Some GraphQL servers support streaming results for the @defer and @stream directives. When an operation contains these directives, partial results are streamed to the client as they are available, and must be combined together to form a final result. To create a function that lets you fetch data from an HTTP endpoint like this, use the createGraphQLStreamingFetch() function:

import {createGraphQLStreamingFetch} from '@quilted/graphql';

const fetchGraphQL = createGraphQLStreamingFetch({url: '/graphql'});

This function accepts the same options as createGraphQLFetch(). Instead of returning just a promise for the final result, this function returns an object that is both a promise (resolves when the final result has been received and combined) and an async iterable (yields partial results as they are received):

import {graphql} from '@quilted/graphql';

// `graphql` is optional, it just provides nice syntax highlighting
// in some editors.
const query = graphql`
  query MyQuery {
    # ...
  }
`;

for await (const {data, errors, incremental} of fetchGraphQL(query)) {
  // ...
}

Building type-safe GraphQL resolvers

GraphQL resolvers are functions that return the data for a particular type in a GraphQL schema. Writing these resolvers with the benefits of type-safe can be tricky, so this library provides a set of “resolver builders” to make this process easier.

To start, you need a type that describes your GraphQL schema. Quilt expects a schema to be represented as a single layer of nested objects, matching the types as named in your GraphQL schema. Fields on these types are expected to be functions that take the variable type for that field, and return the data for that field. For example, given the following GraphQL schema:

type Query {
  me: Person!
}

type Mutation {
  greet(name: String!): String!
}

type Person {
  name: String!
}

schema {
  query: Query
  mutation: Mutation
}

Quilt expects a type like this:

interface Schema {
  Query: {
    me(variables: Record<string, never>): Person;
  };
  Mutation: {
    greet(variables: {readonly name: string}): string;
  };
  Person: {
    name(variables: Record<string, never>): string;
  };
}

If you use @quilted/graphql-tools to generate type definitions, you can have Quilt create this type for you automatically by importing from a .graphql file:

import type {Schema} from './schema.graphql';

Once you have this type, you can use the createGraphQLResolverBuilder() helper provided by this library. This helper returns a collection of functions that are used to create GraphQL resolver objects matching the schema. In the example above, the resolvers for the schema could be written like this:

import {createGraphQLResolverBuilder} from '@quilted/graphql/server';

import type {Schema} from './schema.graphql';

const {createQueryResolver, createMutationResolver} =
  createGraphQLResolverBuilder<Schema>();

const Query = createQueryResolver({
  me() {
    return {name: 'Winston'};
  },
});

const Mutation = createMutationResolver({
  greet(_, {name}) {
    return `Hello, ${name}!`;
  },
});

Commonly, you will want to have all fields in your schema that return a particular type to return some base object. That base object is then used by the resolver for that type to construct the final return value. You can indicate these type mappings by providing a second type argument to createGraphQLResolverBuilder(). For example, if we wanted all fields returning a Person to return an object that the Person resolver will use to construct its GraphQL fields, we could write the following:

import {createGraphQLResolverBuilder} from '@quilted/graphql/server';

import type {Schema} from './schema.graphql';

interface GraphQLResolverValues {
  Person: {firstName: string; lastName?: string};
}

const {createResolver, createQueryResolver} = createGraphQLResolverBuilder<
  Schema,
  GraphQLResolverValues
>();

const Query = createQueryResolver({
  me() {
    return {firstName: 'Winston'};
  },
});

const Person = createResolver('Person', {
  name({firstName, lastName}) {
    return lastName ? `${firstName} ${lastName}` : firstName;
  },
});

GraphQL servers also commonly provide “context”, values shared throughout all resolvers in the schema. You can indicate the type of the context argument by providing a third type argument to createGraphQLResolverBuilder(). For example, if we will provide a database value through context, we could expose it to our resolvers like this:

import {createGraphQLResolverBuilder} from '@quilted/graphql/server';

import type {Schema} from './schema.graphql';

interface GraphQLContext {
  database: Database;
}

const {createQueryResolver} = createGraphQLResolverBuilder<
  Schema,
  {},
  GraphQLContext
>();

const Query = createQueryResolver({
  // First argument is the "base" object, which is usually ignored for query fields
  // Second argument are the variables, which we don’t have for this field
  async me(_, __, {database}) {
    const me = await database.user.findFirst();
    return me;
  },
});

To actually run a GraphQL query, you need to include the resolvers created with these helpers in a GraphQL server. Most GraphQL servers, including the reference JavaScript implementation and GraphQL Yoga, need a GraphQL schema to execute a query or mutation. For convenience, this library provides a createGraphQLSchema() helper that can create a GraphQL schema object from your resolvers:

import {graphql} from 'graphql';
import {
  createGraphQLSchema,
  createGraphQLResolverBuilder,
} from '@quilted/graphql/server';

// Assumes we are using `@quilted/graphql-tools`, which gives us both
// the schema source and type definitions as exports from the schema file
import schemaSource, {type Schema} from './schema.graphql';

const {createQueryResolver, createMutationResolver} =
  createGraphQLResolverBuilder<Schema>();

const Query = createQueryResolver({
  me() {
    return {name: 'Winston'};
  },
});

const Mutation = createMutationResolver({
  greet(_, {name}) {
    return `Hello, ${name}!`;
  },
});

const schema = createGraphQLSchema(schemaSource, {Query, Mutation});

const result = await graphql({
  schema,
  source: 'query { me { name } }',
});

Testing GraphQL-dependent code

During testing, it can be useful to have a GraphQL fetcher that always returns specific results. Having this tool at your disposal lets you simulate a GraphQL-dependent UI in various states. This library helps you implement this pattern, while taking advantage of the type safety of GraphQL to ensure test results are always valid.

There are two main parts to these GraphQL testing utilities: a GraphQL “controller”, which can fetch mock GraphQL results, and GraphQL “fillers”, which can provide type-safe mocked results for individual GraphQL queries and mutations.

To create a controller, use the GraphQLController class. This function accepts one or more GraphQL “mocks”: objects that contain an operation key, detailing the GraphQL operation this mock should be used for, and a result key. The result can either be an object, or a function that returns an object, or a function that returns a promise for an object. This result will be used to fulfill a GraphQL operation matching the operation key.

To demonstrate, we’ll assume you have a GraphQL schema that looks like this:

type Person {
  name: String!
  age: Int!
}

type Query {
  me: Person!
}

schema {
  query: Query
}

We could create a GraphQL controller with a hand-written mock:

import {graphql, GraphQLController} from '@quilted/graphql/testing';

// `graphql` is optional, but it can provide better syntax
// highlighting in some editors.
const query = graphql`
  query Me {
    me {
      name
      age
    }
  }
`;

const controller = new GraphQLController();

controller.mock({
  operation: query,
  result() {
    return {
      me: {name: 'Winston', age: 9},
    };
  },
});

And you can then use this controller to fetch results by using its fetch() method:

const result = await controller.fetch(query);
// {data: {me: {name: 'Winston', age: 9}}}

We have a controller, but we haven’t done anything particularly useful yet — we had to know the exact shape of our GraphQL queries, and mock all the fields manually. This is where GraphQL “fillers” come in: they let you create mocks for GraphQL queries that will automatically fill in the correct shape of the query.

To create GraphQL fillers, we need a GraphQL schema to describe the available types. If you use @quilted/graphql-tools, you can import this schema’s TypeScript type and schema source, which can be used to create a GraphQL schema (using the createGraphQLSchema() helper) and filler function (using the createGraphQLFiller() helper):

import {
  createGraphQLSchema,
  createGraphQLFiller,
} from '@quilted/graphql/testing';

import schemaSource from './schema.graphql';

const schema = createGraphQLSchema(schemaSource);
const fillGraphQL = createGraphQLFiller(schema);

Now, when we create GraphQL controllers, we can use the fillGraphQL function to create fillers for our queries and mutations. If we provide just a GraphQL operation to this function, it will create a GraphQL mock that fills in data for the query, respecting the nullability of your GraphQL schema and the types of your fields:

import {GraphQLController} from '@quilted/graphql/testing';

const controller = new GraphQLController();

const query = `
  query Me {
    me {
      name
      age
    }
  }
`;

controller.mock(fillGraphQL(query));

const result = await controller.fetch(query);
// {data: {me: {name: 'random string', age: 123}}}

When writing tests, it’s common to want to set a specific subset of fields, but allow other fields outside of the area under the test to be random values. You can do this by providing a subset of the GraphQL operation as the second argument to fillGraphQL():

import {GraphQLController} from '@quilted/graphql/testing';

const controller = new GraphQLController();

const query = `
  query Me {
    me {
      name
      age
    }
  }
`;

controller.mock(fillGraphQL(query, {me: {name: 'Winston'}}));

const result = await controller.fetch(query);
// {data: {me: {name: 'Winston', age: 123}}}

When you use @quilted/graphql-tools to import GraphQL queries and mutations, TypeScript will ensure you only provide matching fields in your mock data:

import {GraphQLController} from '@quilted/graphql/testing';

import meQuery from './MeQuery.graphql';

const controller = new GraphQLController();

controller.mock(fillGraphQL(meQuery, {me: {age: '123'}}));
// Type error: `me.age` must be a number

The automatically filled data will match the shape of your operation, but otherwise will be randomly generated using the chance library. If you have specific types that always return data in a particular shape (such as custom scalars), you can provide default value creators for those types when calling createGraphQLFiller():

import {
  createGraphQLSchema,
  createGraphQLFiller,
} from '@quilted/graphql/testing';

import schemaSource from './schema.graphql';

const schema = createGraphQLSchema(schemaSource);
const fillGraphQL = createGraphQLFiller(schema, {
  resolvers: {
    // For convenience, the Chance object is provided for you to generate
    // random values matching your custom data shape
    Date: ({random}) => random.date().toISOString(),

    // This overrides the default `ID` mock to provide ids in a consistent shape,
    // here using a gid pattern.
    ID: ({random, parent}) => `gid://my-app/${parent.name}/${random.integer()}`,
  },
});