JSPM

  • Created
  • Published
  • Downloads 344
  • Score
    100M100P100Q114137F
  • License MIT

GraphQL Lookahead in Typescript to check if some fields are present in the operation

Package Exports

    Readme

    GraphQL Lookahead in Javascript

    TypeScript npm version npm downloads License

    Use graphql-lookahead to check within the resolver function if particular fields are part of the operation (query or mutation).

    ❤️ Provided by Accès Impôt's engineering team

    Accès Impôt
    🇨🇦 Online tax declaration service 🇨🇦

    Table of contents

    Highlights

    • ⚡️ Performant - Avoid querying nested database relationships if they are not requested.
    • 🎯 Accurate - Check for fieldName, typeName. Check for a specific hierarchy of fields.
    • 🧘 Flexible - Works with any ORM, query builder, GraphQL servers.
    • 💪 Reliable - Fully covered by both unit and integration tests.
    • 🏀 Accessible - Clone this repository and try it out locally using the playground.

    Quick Setup

    Install the module:

    # pnpm
    pnpm add graphql-lookahead
    
    # yarn
    yarn add graphql-lookahead
    
    # npm
    npm i graphql-lookahead

    Basic usage

    You can add a condition using the until option which will be called for every nested field within the operation starting from the resolver field.

    import type { createSchema } from 'graphql-yoga'
    import { lookahead } from 'graphql-lookahead'
    
    type Resolver = NonNullable<Parameters<typeof createSchema>[0]['resolvers']>
    
    export const resolvers: Resolver = {
      Query: {
        order: async (_parent, args, _context, info) => {
          //
          // add your condition
    
          if (lookahead({ info, until: ({ fieldName }) => fieldName === 'product' })) {
            // include product in the query
          }
          // ...
        },
      },
    }

    Types

    import type { GraphQLResolveInfo, SelectionSetNode } from 'graphql'
    
    function lookahead<TState>(options: {
      info: Pick<GraphQLResolveInfo, 'operation' | 'schema' | 'returnType' | 'path'>
      next?: (details: HandlerDetails<TState>) => TState
      state?: TState
      until?: (details: HandlerDetails<TState>) => boolean
    }): boolean
    
    type HandlerDetails<TState> = {
      fieldName: string
      selectionSet: SelectionSetNode
      state: TState
      typeName: string
    }

    Options

    Name Description
    info ❗️ Required - GraphQLResolveInfo object which is usually the fourth argument of the resolver function.
    next Optional - Handler called for every nested field within the operation. It can return a state that will be passed to each next call of its direct child fields. See Advanced usage.
    state Optional - Initial state used in next handler. See Advanced usage.
    until Optional - Handler called for every nested field within the operation. Returning true will stop the iteration and make lookahead return true as well.

    Advanced usage

    You can pass a state and use the next option that will be called for every nested field within the operation. It is similar to until, but next can mutate the parent state and return the next state that will be passed to its child fields. You will still need the until option if you want to stop the iteration at some point (optional).

    If your schema matches your database models, you could build the query filters like this:

    Example: Sequelize with nested query filters

    📚 Sequelize ORM

    import type { createSchema } from 'graphql-yoga'
    import { lookahead } from 'graphql-lookahead'
    
    type Resolver = NonNullable<Parameters<typeof createSchema>[0]['resolvers']>
    
    interface QueryFilter {
      model?: string
      include?: (QueryFilter | string)[]
    }
    
    export const resolvers: Resolver = {
      Query: {
        order: async (_parent, args, _context, info) => {
          const sequelizeQueryFilters: QueryFilter = {}
    
          lookahead({
            info,
            state: sequelizeQueryFilters,
    
            next({ state, typeName }) {
              const nextState: QueryFilter = { model: typeName }
    
              state.include = state.include || []
              state.include.push(nextState)
    
              return nextState
            },
          })
    
          /**
           * `sequelizeQueryFilters` now equals to
           * {
           *   include: [
           *     {
           *       model: 'OrderItem',
           *       include: [
           *         { model: 'Product', include: [{ model: 'Inventory' }] },
           *         { model: 'ProductGroup' },
           *       ],
           *     },
           *   ],
           * }
           *
           * or would be without the `ProductGroup` filter if the operation didn't include it
           */
    
          return await Order.findOne(sequelizeQueryFilters)
        },
      },
    }

    More examples in integration tests

    Playground

    You can play around with lookahead and our mock schema by cloning this repository and running the dev script locally (requires pnpm).

    pnpm install
    pnpm dev

    Visit the playground at http://localhost:4455/graphql 🚀

    image

    Contribution

    Local development
    # Install dependencies
    pnpm install
    
    # Develop using the playground
    pnpm dev
    
    # Run ESLint
    pnpm lint
    
    # Run Vitest
    pnpm test
    
    # Release new version
    pnpm release