graphql-lookahead v1.3.0
GraphQL Lookahead in Javascript
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
🇨🇦 Online tax declaration service 🇨🇦 |
Table of contents
Highlights
- ⚡️ Performant - Avoid querying nested database relationships if they are not requested.
- 🎯 Accurate - Check for the
field
ortype
name. Check for a specific hierarchy of fields. - 🧘 Flexible - Works with any ORM, query builder, GraphQL servers.
- 💪 Reliable - Covered by 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: ({ field }) => field === 'product' })) {
// include product in the query
}
// ...
},
},
}
Types
import type { GraphQLResolveInfo, SelectionNode } from 'graphql'
function lookahead<TState, RError extends boolean | undefined>(options: {
depth?: number | null
info: Pick<
GraphQLResolveInfo,
'operation' | 'schema' | 'fragments' | 'returnType' | 'fieldNodes' | 'fieldName'
>
next?: (details: NextHandlerDetails<TState>) => TState
onError?: (err: unknown) => RError
state?: TState
until?: (details: UntilHandlerDetails<TState>) => boolean
}): boolean
type HandlerDetails<TState> = {
field: string
selection: SelectionNode
state: TState
type: string
}
type UntilHandlerDetails<TState> = HandlerDetails<TState> & {
nextSelectionSet?: SelectionSetNode
}
type NextHandlerDetails<TState> = HandlerDetails<TState> & {
nextSelectionSet: SelectionSetNode
}
Options
Name | Description |
---|---|
depth | ❔ Optional - Specify how deep it should look in the selectionSet (i.e. depth: 1 is the initial selectionSet , depth: null is no limit). Default: depth: null . |
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. |
onError | ❔ Optional - Hook called from a try..catch when an error is caught. Default: (err: unknown) => { console.error(ERROR_PREFIX, err); return true } . |
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 documentation: Multiple eager loading
- Stackoverflow: Nested include in sequelize?
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, type }) {
const nextState: QueryFilter = { model: type }
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
- See graphql-yoga directory
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 🚀
Contribution
Develop using the playground
pnpm dev
Run ESLint
pnpm lint
Run Vitest
pnpm test
Run Vitest in watch mode
pnpm test:watch
</details>
<!-- Badges -->
[typescript-src]: https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg
[typescript-href]: http://www.typescriptlang.org/
[npm-version-src]: https://img.shields.io/npm/v/graphql-lookahead/latest.svg?style=flat&colorA=020420&colorB=00DC82
[npm-version-href]: https://npmjs.com/package/graphql-lookahead
[npm-downloads-src]: https://img.shields.io/npm/dm/graphql-lookahead.svg?style=flat&colorA=020420&colorB=00DC82
[npm-downloads-href]: https://npmjs.com/package/graphql-lookahead
[license-src]: https://img.shields.io/npm/l/graphql-lookahead.svg?style=flat&colorA=020420&colorB=00DC82
[license-href]: https://npmjs.com/package/graphql-lookahead
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
10 months ago
10 months ago
10 months ago
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago