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

@ttoss/graphql-api

v0.8.1

Published

A library for building GraphQL APIs using ttoss ecosystem.

Downloads

319

Readme

@ttoss/graphql-api

This package offers an opinionated approach to building a GraphQL API using the ttoss ecosystem modules. It is designed to provide a resilient and scalable solution for creating complex GraphQL APIs while focusing on the following goals:

  1. Modular Design:
    Build your GraphQL API using modules to simplify and organize the development of large, complex APIs.

  2. Relay Compatibility:
    As Relay is the primary GraphQL client in the ttoss ecosystem, this package implements the Relay Server Specification for seamless client-server interaction.

  3. Schema Building:
    Generate the GraphQL schema required for Relay's introspection queries with @ttoss/graphql-api-cli.

  4. TypeScript Types Generation: Automatically generate TypeScript types for your GraphQL schema with @ttoss/graphql-api-cli.

  5. AWS AppSync Support: Create GraphQL APIs compatible with AWS AppSync. Additionally, this package includes support for running a local GraphQL API server for development and testing purposes.

Installation

pnpm add @ttoss/graphql-api graphql

Usage

This library uses graphql-compose to create the GraphQL schema. It re-export all the graphql-compose types and methods, so you can use it directly from this package.

Type Creation

For more examples about how to create types, check the graphql-compose documentation.

import { schemaComposer } from '@ttoss/graphql-api';

const UserTC = schemaComposer.createObjectTC({
  name: 'User',
  fields: {
    id: 'ID!',
    name: 'String!',
  },
});

This library uses the tsconfig.json file from the target package it is being applied on. If you are using relative imports in your package you can skip this section, but, if you use path aliases in your typescript code by leveraging the paths property, the baseUrl must be filled accordingly.This is needed because in order to interpret the path aliases, ts-node uses tsconfig-paths to resolve the modules that uses this config, and tsconfig-paths needs both baseUrl and paths values to be non-null. A tsconfig.json example that follows such recommendations is given below:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "src/*": ["src/*"]
    }
  }
}

Resolvers

TODO

Integrate All Modules

Once you've created all your types and resolvers, you can integrate all the modules to create the GraphQL schema.

// scr/schemaComposer.ts
import { schemaComposer } from '@ttoss/graphql-api';
import './modules/Module1/composer';
import './modules/Module3/composer';
import './modules/User/composer';

export { schemaComposer };

Relay Server Specification

As ttoss uses Relay as the main GraphQL client, this library implements the Relay Server Specification.

Object Identification

Method composeWithRelay will handle the object identification for your ObjectTypeComposer, it will return a globally unique ID among all types in the following format base64(TypeName + ':' + recordId).

Method composeWithRelay only works if ObjectTypeComposer meets the following requirements:

  1. Has defined recordIdFn: returns the id for the globalId construction. For example, if you use DynamoDB, you could create id from hash and range keys:

    UserTC.setRecordIdFn((source) => {
      return `${source.hashKey}:${source.rangeKey}`;
    });
  2. Have findById resolver: this resolver will be used by RootQuery.node to resolve the object by globalId. Also, add the __typename field is required by Relay to know the type of the object to the node field works. For example:

    UserTC.addResolver({
      name: 'findById',
      type: UserTC,
      args: {
        id: 'String!',
      },
      resolve: ({ args }) => {
        const { type, recordId } = fromGlobalId(args.id);
        // find object
      },
    });
  3. Call composeWithRelay method: this will add the id field and the node query. For example:

    composeWithRelay(UserTC);

Example

import {
  composeWithRelay,
  schemaComposer,
  fromGlobalId,
} from '@ttoss/graphql-api';

const UserTC = schemaComposer.createObjectTC({
  name: 'User',
  fields: {
    id: 'ID!',
    name: 'String!',
  },
});

/**
 * 1. Returns you id for the globalId construction.
 */
UserTC.setRecordIdFn((source) => {
  /**
   * If you use DynamoDB, you could create id from hash and range keys:
   * return `${source.hashKey}:${source.rangeKey}`;
   */
  return source.id;
});

/**
 * 2. Define `findById` resolver (that will be used by `RootQuery.node`).
 */
UserTC.addResolver({
  name: 'findById',
  type: UserTC,
  args: {
    id: 'String!',
  },
  resolve: async ({ args }) => {
    const { type, recordId } = fromGlobalId(args.id);
    const user = await query({ id: recordId });
    return {
      ...user,
      __typename: UserTC.getTypeName(), // or 'User';
    };
  },
});

/**
 * 3. This will add the `id` field and the `node` query.
 */
composeWithRelay(UserTC);

We inspired ourselves on graphql-compose-relay to create composeWithRelay.

Connections

This packages provides the method composeWithConnection to create a connection type and queries for a given type, based on graphql-compose-connection plugin and following the Relay Connection Specification.

import { composeWithConnection } from '@ttoss/graphql-api';

AuthorTC.addResolver({
  name: 'findMany',
  type: AuthorTC,
  resolve: async ({ args }) => {
    // find many
  },
});

composeWithConnection(AuthorTC, {
  findManyResolver: AuthorTC.getResolver('findMany'),
  countResolver: AuthorTC.getResolver('count'),
  sort: {
    ASC: {
      value: {
        scanIndexForward: true,
      },
      cursorFields: ['id'],
      beforeCursorQuery: (rawQuery, cursorData, resolveParams) => {
        if (!rawQuery.id) rawQuery.id = {};
        rawQuery.id.$lt = cursorData.id;
      },
      afterCursorQuery: (rawQuery, cursorData, resolveParams) => {
        if (!rawQuery.id) rawQuery.id = {};
        rawQuery.id.$gt = cursorData.id;
      },
    },
    DESC: {
      value: {
        scanIndexForward: false,
      },
      cursorFields: ['id'],
      beforeCursorQuery: (rawQuery, cursorData, resolveParams) => {
        if (!rawQuery.id) rawQuery.id = {};
        rawQuery.id.$gt = cursorData.id;
      },
      afterCursorQuery: (rawQuery, cursorData, resolveParams) => {
        if (!rawQuery.id) rawQuery.id = {};
        rawQuery.id.$lt = cursorData.id;
      },
    },
  },
});

schemaComposer.Query.addFields({
  authors: Authors.getResolver('connection'),
});

When you composeWithConnection a type composer, it will add the resolver connection to the type composer, so you can add to Query or any other type composer. For example:

schemaComposer.Query.addFields({
  authors: Authors.getResolver('connection'),
});

The resolver connection has the following arguments based on the Relay Connection Specification:

  • first: the number of nodes to return.

  • after: the cursor to start the query.

  • last: the number of nodes to return.

  • before: the cursor to start the query.

  • limit: the limit of nodes to return. It's the first or last argument plus one. It's used to know if there are more nodes to return to set hasNextPage or hasPreviousPage PageInfo fields. For example, if first is 10, limit will be 11. If the resolver returns 11 nodes, the resolver will return 10 but it knows there are more nodes to return, so hasNextPage will be true.

  • skip: it's the count minus last. It only works on backward pagination.

  • sort: the sort option to use. It's the value of the sort object. In our example, it's { scanIndexForward: true } for ASC and { scanIndexForward: false }, for DESC.

  • filter: the filter to use. It'll exist if you add the filter to findManyResolver for example, the implementation below will add the filter argument with the name and book fields:

    AuthorTC.addResolver({
      name: 'findMany',
      type: AuthorTC,
      args: {
        filter: {
          name: 'String',
          book: 'String',
        },
      },
      resolve: async ({ args }) => {
        // find many
      },
    });

To configure composeWithConnection, you need to provide the following options:

findManyResolver

The resolver that will be used to find the nodes. It receives the following arguments:

  • args: the args object from the resolver. Example:

    AuthorTC.addResolver({
      name: 'findMany',
      type: AuthorTC,
      args: {
        filter: {
          name: 'String',
          book: 'String',
        },
      },
      resolve: async ({
        args,
      }: {
        args: {
          first?: number;
          after?: string;
          last?: number;
          before?: string;
          /**
           * It's the `first` or `last` argument plus one.
           */
          limit: number;
          /**
           * The `filter` argument, if provided on the query.
           */
          filter?: {
            name: string;
            book: string;
          };
          /**
           * The `sort` argument, if provided on the query as
           * they keys of the `sort` object. In our example
           * above, it's `ASC` and `DESC`. `scanIndexForward`
           * is the value of the `value` property on the sort
           * object. In our example above, it's `true` for
           * `ASC` and `false` for `DESC`.
           */
          sort: {
            scanIndexForward: boolean;
          };
        };
      }) => {
        //
      },
    });
  • rawQuery: an object created by beforeCursorQuery or afterCursorQuery methods from sort option.

countResolver

The resolver that will be used to count the nodes.

sort

It's an object that defines the sort options. Each key is the sort name and the value is an object with the following properties:

  • value: and object that the args resolver will receive as the sort argument. It'll also be the values of the sort enum composer created (check the implementation details here.)

  • cursorFields: an array of fields that will be used to create the cursor.

  • beforeCursorQuery and afterCursorQuery: methods that will be used to create the rawQuery object for the findManyResolver. They receive the following arguments:

    • rawQuery: the rawQuery object that will be used to find the nodes.
    • cursorData: the data from the cursor define on cursorFields. For example, if you define cursorFields as ['id', 'name'], the cursorData will an object with the id and name properties.
    • resolveParams: the resolveParams object from the resolver. You can access args, context and info and other GraphQL properties from this object.

    Example:

    composeWithConnection(AuthorTC, {
      // ...
      sort: {
        ASC: {
          // ...
          cursorFields: ['id', 'name'],
          // Called when `before` cursor is provided.
          beforeCursorQuery: (rawQuery, cursorData, resolveParams) => {
            if (!rawQuery.id) rawQuery.id = {};
            rawQuery.id.$lt = cursorData.id;
            rawQuery.name.$lt = cursorData.name;
          },
          // Called when `after` cursor is provided.
          afterCursorQuery: (rawQuery, cursorData, resolveParams) => {
            if (!rawQuery.id) rawQuery.id = {};
            rawQuery.id.$gt = cursorData.id;
            rawQuery.name.$gt = cursorData.name;
          },
        },
      },
    });

    In the example above, the findManyResolver will receive the following rawQuery object when before cursor is provided:

    {
      "id": {
        "$lt": "id-from-cursor"
      },
      "name": {
        "$lt": "name-from-cursor"
      }
    }

Middlewares

This package provides a way to add middlewares to your final schema. You can add middlewares compatible with graphql-middleware by passing them to the middlewares option on buildSchema method. For example, you can use GraphQL Shield to add authorization to your API:

import { buildSchema } from '@ttoss/graphql-api';
import { allow, deny, shield } from '@ttoss/graphql-api/shield';
import { schemaComposer } from './schemaComposer';

const NotAuthorizedError = new Error('Not authorized!');
/**
 * The error name is the same value `errorType` on GraphQL errors response.
 */
NotAuthorizedError.name = 'NotAuthorizedError';

const permissions = shield(
  {
    Query: {
      '*': deny,
      author: allow,
    },
    Author: {
      id: allow,
      name: allow,
    },
  },
  {
    fallbackRule: deny,
    fallbackError: NotAuthorizedError,
  }
);

/**
 * Apply middlewares to all resolvers.
 */
const logInput = async (resolve, source, args, context, info) => {
  console.log(`1. logInput: ${JSON.stringify(args)}`)
  const result = await resolve(source, args, context, info)
  console.log(`5. logInput`)
  return result
}

/**
 * Apply middlewares only to a specific resolver.
 */
const logOnQueryMe = {
  Query: {
    me: logInput
  }
}

const schema = buildSchema({
  schemaComposer,
  middlewares; [permissions, logInput, logOnQueryMe],
})

Shield

This package re-exports the all methods from GraphQL Shield.

import { allow, deny, shield } from '@ttoss/graphql-api/shield';

Building Schema and Types

Check @ttoss/graphql-api-cli for more information about how to build the schema and types.

How to Create Tests

We recommend testing the whole GraphQL API using the graphql object and the schema composer to provide the schema. For example:

import { graphql } from 'graphql';
import { schemaComposer } from './schemaComposer';

test('testing my query', () => {
  const author = {
    id: '1',
    name: 'John Doe',
  };

  const response = await graphql({
    schema: schemaComposer.buildSchema(),
    source: /* GraphQL */ `
      query ($id: ID!) {
        node(id: $id) {
          id
          ... on Author {
            name
          }
        }
      }
    `,
    variableValues: {
      id: author.id,
    },
  });

  expect(response).toEqual({
    data: {
      node: {
        id: author.id,
        name: author.name,
      },
    },
  });
});