From fce8b0eb8948bab608af5b73ca2062bb1c0502ef Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Wed, 4 Sep 2019 16:59:45 +0200 Subject: [PATCH] (feat) - schema awareness (#58) * (chore) - document idea's * (docs) - further document thought process * (chore) - document implementation details * (chore) - remove schemaFetching * (docs) - add wall of text about offline and partials * (feat) - find the rootFields in the schema or use sensible defaults * (chore) - start adding a test and define schema in our iterator * (feat) - see if a field is nullable * (feat) - add first implementation to exchange.ts * (refactor) - move to separate schemaPredicate class * (refactor) - add second argument to our query and write methods * (feat) - trigger heuristic matching when we are handling the no schema case * (feat) - implement heuristic/schema fragment matching and nullability. This also includes some readability improvements to the schemaPredicates class and uses the store.schemaPredicates instead of an additional pass * (tests) - add tests for our new SchemaPredicates class * (chore) - update paths for new schema * (chore) - update simpleSchema to include a union (this also updates snapshots for this case) * (tests) - test reexecution of operation on partial queryResult * (chore) - resolve typing issue and convert some Query ref to the rootKey from schemaPredicates * (refactor) - move rootKey logic to store * (refactor) - use graphql-js internals * (chore) - remove comment blocks * (refactor) - cleanup some unreachable code paths * (chore) - remove redundant comments * Add improved warning and invariant to SchemaPredicates.isFieldNullable * Add improved invariant to SchemaPredicates.isInterfaceOfType * Fix case for null type condition * Fix some types in schemaPredicates * Add warning on heuristic fragment matching * Add new partial handling to exchange * Fix partial query behaviour - When Query has no fields then return null and deem it incomplete (EMPTY) - When partial fields are found they still need to be set to null * Fix exchange outcome marking and tests * Add missing hasFields setter for invalid entities * Add limit for partial Query result to partial results * (tests) - update assertions of query.test.ts --- src/ast/node.ts | 12 - src/ast/schemaPredicates.test.ts | 32 + src/ast/schemaPredicates.ts | 89 ++ src/exchange.test.ts | 133 +++ src/exchange.ts | 86 +- src/operations/invalidate.ts | 9 +- src/operations/query.test.ts | 69 ++ src/operations/query.ts | 51 +- src/operations/shared.ts | 55 +- src/operations/write.ts | 21 +- src/store.test.ts | 8 +- src/store.ts | 33 +- src/test-utils/examples-1.test.ts | 8 +- src/test-utils/simple_schema.json | 1328 +++++++++++++++++++++++++++++ 14 files changed, 1848 insertions(+), 86 deletions(-) create mode 100644 src/ast/schemaPredicates.test.ts create mode 100644 src/ast/schemaPredicates.ts create mode 100644 src/operations/query.test.ts create mode 100644 src/test-utils/simple_schema.json diff --git a/src/ast/node.ts b/src/ast/node.ts index 9c7446c..2eaa923 100644 --- a/src/ast/node.ts +++ b/src/ast/node.ts @@ -5,7 +5,6 @@ import { SelectionSetNode, InlineFragmentNode, FieldNode, - OperationDefinitionNode, FragmentDefinitionNode, Kind, } from 'graphql'; @@ -15,17 +14,6 @@ import { SelectionSet } from '../types'; /** Returns the name of a given node */ export const getName = (node: { name: NameNode }): string => node.name.value; -export const getOperationName = (node: OperationDefinitionNode) => { - switch (node.operation) { - case 'query': - return 'Query'; - case 'mutation': - return 'Mutation'; - case 'subscription': - return 'Subscription'; - } -}; - export const getFragmentTypeName = (node: FragmentDefinitionNode): string => node.typeCondition.name.value; diff --git a/src/ast/schemaPredicates.test.ts b/src/ast/schemaPredicates.test.ts new file mode 100644 index 0000000..f509181 --- /dev/null +++ b/src/ast/schemaPredicates.test.ts @@ -0,0 +1,32 @@ +import { SchemaPredicates } from './schemaPredicates'; + +describe('SchemaPredicates', () => { + let schemaPredicates; + + beforeAll(() => { + // eslint-disable-next-line + const schema = require('../test-utils/simple_schema.json'); + schemaPredicates = new SchemaPredicates(schema); + }); + + it('should match fragments by interface/union', () => { + expect(schemaPredicates.isInterfaceOfType('ITodo', 'BigTodo')).toBeTruthy(); + expect( + schemaPredicates.isInterfaceOfType('ITodo', 'SmallTodo') + ).toBeTruthy(); + expect( + schemaPredicates.isInterfaceOfType('Search', 'BigTodo') + ).toBeTruthy(); + expect( + schemaPredicates.isInterfaceOfType('Search', 'SmallTodo') + ).toBeTruthy(); + expect(schemaPredicates.isInterfaceOfType('ITodo', 'Todo')).toBeFalsy(); + expect(schemaPredicates.isInterfaceOfType('Search', 'Todo')).toBeFalsy(); + }); + + it('should indicate nullability', () => { + expect(schemaPredicates.isFieldNullable('Todo', 'text')).toBeFalsy(); + expect(schemaPredicates.isFieldNullable('Todo', 'complete')).toBeTruthy(); + expect(schemaPredicates.isFieldNullable('Todo', 'author')).toBeTruthy(); + }); +}); diff --git a/src/ast/schemaPredicates.ts b/src/ast/schemaPredicates.ts new file mode 100644 index 0000000..add9f9a --- /dev/null +++ b/src/ast/schemaPredicates.ts @@ -0,0 +1,89 @@ +import invariant from 'invariant'; +import warning from 'warning'; + +import { + buildClientSchema, + isNullableType, + GraphQLSchema, + GraphQLAbstractType, + GraphQLObjectType, + GraphQLInterfaceType, + GraphQLUnionType, +} from 'graphql'; + +export class SchemaPredicates { + schema: GraphQLSchema; + + constructor(schema) { + this.schema = buildClientSchema(schema); + } + + isFieldNullable(typename: string, fieldName: string): boolean { + const type = this.schema.getType(typename); + expectObjectType(type, typename); + + const object = type as GraphQLObjectType; + if (object === undefined) { + warning( + false, + 'Invalid type: The type `%s` is not a type in the defined schema, ' + + 'but the GraphQL document expects it to exist.\n' + + 'Traversal will continue, however this may lead to undefined behavior!', + typename + ); + + return false; + } + + const field = object.getFields()[fieldName]; + if (field === undefined) { + warning( + false, + 'Invalid field: The field `%s` does not exist on `%s`, ' + + 'but the GraphQL document expects it to exist.\n' + + 'Traversal will continue, however this may lead to undefined behavior!', + fieldName, + typename + ); + + return false; + } + + return isNullableType(field.type); + } + + isInterfaceOfType( + typeCondition: null | string, + typename: string | void + ): boolean { + if (!typename || !typeCondition) return false; + if (typename === typeCondition) return true; + + const abstractType = this.schema.getType(typeCondition); + expectAbstractType(abstractType, typeCondition); + const objectType = this.schema.getType(typename); + expectObjectType(objectType, typename); + + const abstractNode = abstractType as GraphQLAbstractType; + const concreteNode = objectType as GraphQLObjectType; + return this.schema.isPossibleType(abstractNode, concreteNode); + } +} + +const expectObjectType = (type: any, typename: string) => { + invariant( + type instanceof GraphQLObjectType, + 'Invalid type: The type `%s` is not an object in the defined schema, ' + + 'but the GraphQL document is traversing it.', + typename + ); +}; + +const expectAbstractType = (type: any, typename: string) => { + invariant( + type instanceof GraphQLInterfaceType || type instanceof GraphQLUnionType, + 'Invalid type: The type `%s` is not an Interface or Union type in the defined schema, ' + + 'but a fragment in the GraphQL document is using it as a type condition.', + typename + ); +}; diff --git a/src/exchange.test.ts b/src/exchange.test.ts index e7e240d..19d13f5 100644 --- a/src/exchange.test.ts +++ b/src/exchange.test.ts @@ -622,3 +622,136 @@ it('follows nested resolvers for mutations', () => { 'AwesomeGQL', ]); }); + +it.only('reexecutes query and returns data on partial result', () => { + jest.useFakeTimers(); + const client = createClient({ url: '' }); + const [ops$, next] = makeSubject(); + const reexec = jest + .spyOn(client, 'reexecuteOperation') + // Empty mock to avoid going in an endless loop, since we would again return + // partial data. + .mockImplementation(() => {}); + + const query = gql` + query { + todos { + id + text + complete + author { + id + name + __typename + } + __typename + } + } + `; + + const queryOperation = client.createRequestOperation('query', { + key: 1, + query, + }); + + const queryData = { + __typename: 'Query', + todos: [ + { + __typename: 'Todo', + id: '123', + text: 'Learn', + }, + { + __typename: 'Todo', + id: '456', + text: 'Teach', + }, + ], + }; + + const response = jest.fn( + (forwardOp: Operation): OperationResult => { + if (forwardOp.key === 1) { + return { operation: queryOperation, data: queryData }; + } + + return undefined as any; + } + ); + + const result = jest.fn(); + const forward: ExchangeIO = ops$ => + pipe( + ops$, + delay(1), + map(response) + ); + + pipe( + cacheExchange({ + // eslint-disable-next-line + schema: require('./test-utils/simple_schema.json'), + })({ forward, client })(ops$), + tap(result), + publish + ); + + next(queryOperation); + jest.runAllTimers(); + expect(response).toHaveBeenCalledTimes(1); + expect(reexec).toHaveBeenCalledTimes(0); + expect(result.mock.calls[0][0].data).toEqual({ + __typename: 'Query', + todos: [ + { + __typename: 'Todo', + author: null, + complete: null, + id: '123', + text: 'Learn', + }, + { + __typename: 'Todo', + author: null, + complete: null, + id: '456', + text: 'Teach', + }, + ], + }); + + expect(result.mock.calls[0][0]).toHaveProperty( + 'operation.context.meta', + undefined + ); + + next(queryOperation); + jest.runAllTimers(); + expect(result).toHaveBeenCalledTimes(2); + expect(reexec).toHaveBeenCalledTimes(1); + expect(result.mock.calls[1][0].data).toEqual({ + __typename: 'Query', + todos: [ + { + __typename: 'Todo', + author: null, + complete: null, + id: '123', + text: 'Learn', + }, + { + __typename: 'Todo', + author: null, + complete: null, + id: '456', + text: 'Teach', + }, + ], + }); + + expect(result.mock.calls[1][0]).toHaveProperty( + 'operation.context.meta.cacheOutcome', + 'partial' + ); +}); diff --git a/src/exchange.ts b/src/exchange.ts index 3243e1c..bc03902 100644 --- a/src/exchange.ts +++ b/src/exchange.ts @@ -12,15 +12,15 @@ import { query, write, writeOptimistic, readOperation } from './operations'; import { Store } from './store'; import { - Completeness, UpdatesConfig, ResolverConfig, OptimisticMutationConfig, KeyingConfig, } from './types'; +import { SchemaPredicates } from './ast/schemaPredicates'; type OperationResultWithMeta = OperationResult & { - completeness: Completeness; + outcome: CacheOutcome; }; type OperationMap = Map; @@ -30,18 +30,13 @@ interface DependentOperations { } // Returns the given operation result with added cacheOutcome meta field -const addCacheOutcome = (outcome: CacheOutcome) => (res: OperationResult) => ({ - data: res.data, - error: res.error, - extensions: res.extensions, - operation: { - ...res.operation, - context: { - ...res.operation.context, - meta: { - ...res.operation.context.meta, - cacheOutcome: outcome, - }, +const addCacheOutcome = (op: Operation, outcome: CacheOutcome): Operation => ({ + ...op, + context: { + ...op.context, + meta: { + ...op.context.meta, + cacheOutcome: outcome, }, }, }); @@ -92,6 +87,7 @@ export interface CacheExchangeOpts { resolvers?: ResolverConfig; optimistic?: OptimisticMutationConfig; keys?: KeyingConfig; + schema?: object; } export const cacheExchange = (opts?: CacheExchangeOpts): Exchange => ({ @@ -99,12 +95,20 @@ export const cacheExchange = (opts?: CacheExchangeOpts): Exchange => ({ client, }) => { if (!opts) opts = {}; + + let schemaPredicates; + if (opts.schema) { + schemaPredicates = new SchemaPredicates(opts.schema); + } + const store = new Store( + schemaPredicates, opts.resolvers, opts.updates, opts.optimistic, opts.keys ); + const optimisticKeys = new Set(); const ops: OperationMap = new Map(); const deps = Object.create(null) as DependentOperations; @@ -171,16 +175,23 @@ export const cacheExchange = (opts?: CacheExchangeOpts): Exchange => ({ operation: Operation ): OperationResultWithMeta => { const policy = getRequestPolicy(operation); - const res = query(store, operation); - const isComplete = policy === 'cache-only' || res.completeness === 'FULL'; - if (isComplete) { - updateDependencies(operation, res.dependencies); + const { data, dependencies, completeness } = query(store, operation); + let cacheOutcome: CacheOutcome; + + if (completeness === 'FULL' || policy === 'cache-only') { + updateDependencies(operation, dependencies); + cacheOutcome = 'hit'; + } else if (completeness === 'PARTIAL') { + updateDependencies(operation, dependencies); + cacheOutcome = 'partial'; + } else { + cacheOutcome = 'miss'; } return { operation, - completeness: isComplete ? 'FULL' : 'EMPTY', - data: res.data, + outcome: cacheOutcome, + data, }; }; @@ -242,23 +253,35 @@ export const cacheExchange = (opts?: CacheExchangeOpts): Exchange => ({ // Rebound operations that are incomplete, i.e. couldn't be queried just from the cache const cacheOps$ = pipe( cache$, - filter(res => res.completeness !== 'FULL'), - map(res => res.operation) + filter(res => res.outcome === 'miss'), + map(res => addCacheOutcome(res.operation, res.outcome)) ); // Resolve OperationResults that the cache was able to assemble completely and trigger // a network request if the current operation's policy is cache-and-network const cacheResult$ = pipe( cache$, - filter(res => res.completeness === 'FULL'), - tap(({ operation }) => { - const policy = getRequestPolicy(operation); - if (policy === 'cache-and-network') { - const networkOnly = toRequestPolicy(operation, 'network-only'); - client.reexecuteOperation(networkOnly); + filter(res => res.outcome !== 'miss'), + map( + (res: OperationResultWithMeta): OperationResult => { + const { operation, outcome } = res; + const policy = getRequestPolicy(operation); + if ( + policy === 'cache-and-network' || + (policy === 'cache-first' && outcome === 'partial') + ) { + const networkOnly = toRequestPolicy(operation, 'network-only'); + client.reexecuteOperation(networkOnly); + } + + return { + operation: addCacheOutcome(operation, outcome), + data: res.data, + error: res.error, + extensions: res.extensions, + }; } - }), - map(addCacheOutcome('hit')) + ) ); // Forward operations that aren't cacheable and rebound operations @@ -273,8 +296,7 @@ export const cacheExchange = (opts?: CacheExchangeOpts): Exchange => ({ cacheOps$, ]) ), - map(updateCacheWithResult), - map(addCacheOutcome('miss')) + map(updateCacheWithResult) ); return merge([result$, cacheResult$]); diff --git a/src/operations/invalidate.ts b/src/operations/invalidate.ts index 9392227..7eab81e 100644 --- a/src/operations/invalidate.ts +++ b/src/operations/invalidate.ts @@ -16,11 +16,13 @@ import { clearStoreState, } from '../store'; import { joinKeys, keyOfField } from '../helpers'; +import { SchemaPredicates } from '../ast/schemaPredicates'; interface Context { store: Store; variables: Variables; fragments: Fragments; + schemaPredicates?: SchemaPredicates; } export const invalidate = (store: Store, request: OperationRequest) => { @@ -31,9 +33,14 @@ export const invalidate = (store: Store, request: OperationRequest) => { variables: normalizeVariables(operation, request.variables), fragments: getFragments(request.query), store, + schemaPredicates: store.schemaPredicates, }; - invalidateSelection(ctx, 'Query', getSelectionSet(operation)); + invalidateSelection( + ctx, + ctx.store.getRootKey('query'), + getSelectionSet(operation) + ); clearStoreState(); }; diff --git a/src/operations/query.test.ts b/src/operations/query.test.ts new file mode 100644 index 0000000..30cd48d --- /dev/null +++ b/src/operations/query.test.ts @@ -0,0 +1,69 @@ +import { Store } from '../store'; +import gql from 'graphql-tag'; +import { write } from './write'; +import { query } from './query'; +import { SchemaPredicates } from '../ast/schemaPredicates'; + +const TODO_QUERY = gql` + query todos { + todos { + id + text + complete + author { + id + name + known + __typename + } + __typename + } + } +`; + +describe('Query', () => { + let schema, store; + + beforeAll(() => { + schema = require('../test-utils/simple_schema.json'); + }); + + beforeEach(() => { + store = new Store(new SchemaPredicates(schema)); + write( + store, + { query: TODO_QUERY }, + { + __typename: 'Query', + todos: [ + { id: '0', text: 'Teach', __typename: 'Todo' }, + { id: '1', text: 'Learn', __typename: 'Todo' }, + ], + } + ); + }); + + it('test partial results', () => { + const result = query(store, { query: TODO_QUERY }); + expect(result.completeness).toEqual('PARTIAL'); + expect(result.data).toEqual({ + __typename: 'Query', + todos: [ + { + id: '0', + text: 'Teach', + __typename: 'Todo', + author: null, + complete: null, + }, + { + id: '1', + text: 'Learn', + __typename: 'Todo', + author: null, + complete: null, + }, + ], + }); + }); +}); diff --git a/src/operations/query.ts b/src/operations/query.ts index f9c1c59..dcbd570 100644 --- a/src/operations/query.ts +++ b/src/operations/query.ts @@ -8,7 +8,6 @@ import { getName, getFieldArguments, getFieldAlias, - getOperationName, } from '../ast'; import { @@ -33,6 +32,7 @@ import { import { SelectionIterator, isScalar } from './shared'; import { joinKeys, keyOfField } from '../helpers'; +import { SchemaPredicates } from '../ast/schemaPredicates'; export interface QueryResult { completeness: Completeness; @@ -45,6 +45,7 @@ interface Context { store: Store; variables: Variables; fragments: Fragments; + schemaPredicates?: SchemaPredicates; } /** Reads a request entirely from the store */ @@ -70,9 +71,15 @@ export const startQuery = (store: Store, request: OperationRequest) => { fragments: getFragments(request.query), result, store, + schemaPredicates: store.schemaPredicates, }; - result.data = readSelection(ctx, 'Query', getSelectionSet(operation), root); + result.data = readSelection( + ctx, + ctx.store.getRootKey('query'), + getSelectionSet(operation), + root + ); return result; }; @@ -97,11 +104,12 @@ export const readOperation = ( fragments: getFragments(request.query), result, store, + schemaPredicates: store.schemaPredicates, }; result.data = readRoot( ctx, - getOperationName(operation), + ctx.store.getRootKey(operation.operation), getSelectionSet(operation), data ); @@ -175,23 +183,24 @@ const readSelection = ( select: SelectionSet, data: Data ): Data | null => { - const isQuery = entityKey === 'Query'; + const { store, variables, schemaPredicates } = ctx; + const isQuery = entityKey === store.getRootKey('query'); if (!isQuery) addDependency(entityKey); - const { store, variables } = ctx; - // Get the __typename field for a given entity to check that it exists - const typename = isQuery ? 'Query' : store.getField(entityKey, '__typename'); + const typename = isQuery + ? store.getRootKey('query') + : store.getField(entityKey, '__typename'); if (typeof typename !== 'string') { ctx.result.completeness = 'EMPTY'; return null; } data.__typename = typename; - const iter = new SelectionIterator(typename, entityKey, select, ctx); let node; + let hasFields = false; while ((node = iter.next()) !== undefined) { // Derive the needed data from our node. const fieldName = getName(node); @@ -205,9 +214,12 @@ const readSelection = ( const resolvers = store.resolvers[typename]; if (resolvers !== undefined && resolvers.hasOwnProperty(fieldName)) { // We have a resolver for this field. + hasFields = true; + // Prepare the actual fieldValue, so that the resolver can use it if (fieldValue !== undefined) { data[fieldAlias] = fieldValue; } + const resolverValue = resolvers[fieldName]( data, fieldArgs || {}, @@ -235,13 +247,21 @@ const readSelection = ( } } else if (node.selectionSet === undefined) { // The field is a scalar and can be retrieved directly - if (fieldValue === undefined) { + if ( + fieldValue === undefined && + schemaPredicates !== undefined && + schemaPredicates.isFieldNullable(typename, fieldName) + ) { // Cache Incomplete: A missing field means it wasn't cached + ctx.result.completeness = 'PARTIAL'; + data[fieldAlias] = null; + } else if (fieldValue === undefined) { ctx.result.completeness = 'EMPTY'; data[fieldAlias] = null; } else { // Not dealing with undefined means it's a cached field data[fieldAlias] = fieldValue; + hasFields = true; } } else { // null values mean that a field might be linked to other entities @@ -253,6 +273,13 @@ const readSelection = ( if (typeof fieldValue === 'object' && fieldValue !== null) { // The entity on the field was invalid and can still be recovered data[fieldAlias] = fieldValue; + hasFields = true; + } else if ( + schemaPredicates !== undefined && + schemaPredicates.isFieldNullable(typename, fieldName) + ) { + ctx.result.completeness = 'PARTIAL'; + data[fieldAlias] = null; } else { ctx.result.completeness = 'EMPTY'; data[fieldAlias] = null; @@ -260,10 +287,16 @@ const readSelection = ( } else { const prevData = data[fieldAlias] as Data; data[fieldAlias] = resolveLink(ctx, link, fieldSelect, prevData); + hasFields = true; } } } + if (isQuery && ctx.result.completeness === 'PARTIAL' && !hasFields) { + ctx.result.completeness = 'EMPTY'; + return null; + } + return data; }; diff --git a/src/operations/shared.ts b/src/operations/shared.ts index 1bc19ba..a065655 100644 --- a/src/operations/shared.ts +++ b/src/operations/shared.ts @@ -1,3 +1,4 @@ +import warning from 'warning'; import { FieldNode, InlineFragmentNode, FragmentDefinitionNode } from 'graphql'; import { Fragments, Variables, SelectionSet, Scalar } from '../types'; import { Store } from '../store'; @@ -12,26 +13,36 @@ import { getSelectionSet, getName, } from '../ast'; +import { SchemaPredicates } from '../ast/schemaPredicates'; interface Context { store: Store; variables: Variables; fragments: Fragments; + schemaPredicates?: SchemaPredicates; } -const isFragmentMatching = ( +const isFragmentHeuristicallyMatching = ( node: InlineFragmentNode | FragmentDefinitionNode, typename: void | string, entityKey: string, ctx: Context ) => { - if (!typename) { - return false; - } else if (typename === getTypeCondition(node)) { - return true; - } + if (!typename) return false; + const typeCondition = getTypeCondition(node); + if (typename === typeCondition) return true; + + warning( + false, + 'Heuristic Fragment Matching: A fragment is trying to match against the `%s` type, ' + + 'but the type condition is `%s`. Since GraphQL allows for interfaces `%s` may be an' + + 'interface.\nA schema needs to be defined for this match to be deterministic, ' + + 'otherwise the fragment will be matched heuristically!', + typename, + typeCondition, + typeCondition + ); - // This is a heuristic for now, but temporary until schema awareness becomes a thing return !getSelectionSet(node).some(node => { if (!isFieldNode(node)) return false; const fieldName = getName(node); @@ -78,17 +89,25 @@ export class SelectionIterator { const fragmentNode = !isInlineFragment(node) ? this.context.fragments[getName(node)] : node; - if ( - fragmentNode !== undefined && - isFragmentMatching( - fragmentNode, - this.typename, - this.entityKey, - this.context - ) - ) { - this.indexStack.push(0); - this.selectionStack.push(getSelectionSet(fragmentNode)); + + if (fragmentNode !== undefined) { + const isMatching = + this.context.schemaPredicates !== undefined + ? this.context.schemaPredicates.isInterfaceOfType( + getTypeCondition(fragmentNode), + this.typename + ) + : isFragmentHeuristicallyMatching( + fragmentNode, + this.typename, + this.entityKey, + this.context + ); + + if (isMatching) { + this.indexStack.push(0); + this.selectionStack.push(getSelectionSet(fragmentNode)); + } } continue; diff --git a/src/operations/write.ts b/src/operations/write.ts index b6208f8..0f0aca4 100644 --- a/src/operations/write.ts +++ b/src/operations/write.ts @@ -9,7 +9,6 @@ import { normalizeVariables, getFragmentTypeName, getName, - getOperationName, getFieldArguments, } from '../ast'; @@ -33,6 +32,7 @@ import { import { SelectionIterator, isScalar } from './shared'; import { joinKeys, keyOfField } from '../helpers'; +import { SchemaPredicates } from '../ast/schemaPredicates'; export interface WriteResult { dependencies: Set; @@ -43,6 +43,7 @@ interface Context { store: Store; variables: Variables; fragments: Fragments; + schemaPredicates?: SchemaPredicates; } /** Writes a request given its response to the store */ @@ -73,12 +74,13 @@ export const startWrite = ( fragments: getFragments(request.query), result, store, + schemaPredicates: store.schemaPredicates, }; const select = getSelectionSet(operation); - const operationName = getOperationName(operation); + const operationName = ctx.store.getRootKey(operation.operation); - if (operationName === 'Query') { + if (operationName === ctx.store.getRootKey('query')) { writeSelection(ctx, operationName, select, data); } else { writeRoot(ctx, operationName, select, data); @@ -102,10 +104,11 @@ export const writeOptimistic = ( fragments: getFragments(request.query), result, store, + schemaPredicates: store.schemaPredicates, }; - const operationName = getOperationName(operation); - if (operationName === 'Mutation') { + const operationName = ctx.store.getRootKey(operation.operation); + if (operationName === ctx.store.getRootKey('mutation')) { const select = getSelectionSet(operation); const iter = new SelectionIterator( operationName, @@ -170,6 +173,7 @@ export const writeFragment = ( fragments, result: { dependencies: getCurrentDependencies() }, store, + schemaPredicates: store.schemaPredicates, }; writeSelection(ctx, entityKey, select, writeData); @@ -182,7 +186,7 @@ const writeSelection = ( data: Data ) => { const { store, variables } = ctx; - const isQuery = entityKey === 'Query'; + const isQuery = entityKey === ctx.store.getRootKey('query'); const typename = data.__typename; if (!isQuery) addDependency(entityKey); @@ -292,7 +296,10 @@ const writeRoot = ( writeRootField(ctx, fieldValue, fieldSelect); } - if (typename === 'Mutation' || typename === 'Subscription') { + if ( + typename === ctx.store.getRootKey('mutation') || + typename === ctx.store.getRootKey('subscription') + ) { // We run side-effect updates after the default, normalized updates // so that the data is already available in-store if necessary const updater = ctx.store.updates[typename][fieldName]; diff --git a/src/store.test.ts b/src/store.test.ts index 7a3cc3f..79fd48f 100644 --- a/src/store.test.ts +++ b/src/store.test.ts @@ -39,7 +39,7 @@ const Todos = gql` describe('Store with KeyingConfig', () => { it('generates keys from custom keying function', () => { - const store = new Store(undefined, undefined, undefined, { + const store = new Store(undefined, undefined, undefined, undefined, { User: () => 'me', }); @@ -56,7 +56,7 @@ describe('Store with OptimisticMutationConfig', () => { let store, todosData; beforeEach(() => { - store = new Store(undefined, undefined, { + store = new Store(undefined, undefined, undefined, { addTodo: variables => { return { ...variables, @@ -236,7 +236,9 @@ describe('Store with OptimisticMutationConfig', () => { })); clearStoreState(); - const { data: result } = query(store, { query: Todos }); + const { data: result } = query(store, { + query: Todos, + }); expect(result).toEqual({ __typename: 'Query', todos: [ diff --git a/src/store.ts b/src/store.ts index ea5922e..6727e1e 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1,5 +1,5 @@ import invariant from 'invariant'; -import { DocumentNode } from 'graphql'; +import { DocumentNode, GraphQLSchema } from 'graphql'; import * as Pessimism from 'pessimism'; import { @@ -18,6 +18,7 @@ import { keyOfEntity, joinKeys, keyOfField } from './helpers'; import { startQuery } from './operations/query'; import { writeFragment, startWrite } from './operations/write'; import { invalidate } from './operations/invalidate'; +import { SchemaPredicates } from './ast/schemaPredicates'; interface Ref { current: null | T; @@ -70,6 +71,14 @@ const mapRemove = (map: Pessimism.Map, key: string) => { : Pessimism.remove(map, key); }; +const defaultRootFields = { + query: 'Query', + mutation: 'Mutation', + subscription: 'Subscription', +}; + +type RootField = 'query' | 'mutation' | 'subscription'; + export class Store { records: Pessimism.Map; links: Pessimism.Map; @@ -78,8 +87,11 @@ export class Store { updates: UpdatesConfig; optimisticMutations: OptimisticMutationConfig; keys: KeyingConfig; + schemaPredicates?: SchemaPredicates; + rootFields: { query: string; mutation: string; subscription: string }; constructor( + schemaPredicates?: SchemaPredicates, resolvers?: ResolverConfig, updates?: Partial, optimisticMutations?: OptimisticMutationConfig, @@ -94,6 +106,14 @@ export class Store { } as UpdatesConfig; this.optimisticMutations = optimisticMutations || {}; this.keys = keys || {}; + this.schemaPredicates = schemaPredicates; + this.rootFields = schemaPredicates + ? getRootTypes(schemaPredicates.schema) + : defaultRootFields; + } + + getRootKey(name: RootField) { + return this.rootFields[name]; } keyOfEntity(data: Data) { @@ -203,3 +223,14 @@ export class Store { writeFragment(this, dataFragment, data); } } + +const getRootTypes = (schema: GraphQLSchema) => { + const queryType = schema.getQueryType(); + const mutationType = schema.getMutationType(); + const subscriptionType = schema.getSubscriptionType(); + return { + query: queryType ? queryType.name : 'Query', + mutation: mutationType ? mutationType.name : 'Mutation', + subscription: subscriptionType ? subscriptionType.name : 'Subscription', + }; +}; diff --git a/src/test-utils/examples-1.test.ts b/src/test-utils/examples-1.test.ts index 0c51649..2863361 100644 --- a/src/test-utils/examples-1.test.ts +++ b/src/test-utils/examples-1.test.ts @@ -43,7 +43,7 @@ const NestedClearNameTodo = gql` `; it('passes the "getting-started" example', () => { - const store = new Store(); + const store = new Store(undefined); const todosData = { __typename: 'Query', todos: [ @@ -117,7 +117,9 @@ it('passes the "getting-started" example', () => { }); it('respects property-level resolvers when given', () => { - const store = new Store({ Todo: { text: () => 'hi' } }); + const store = new Store(undefined, { + Todo: { text: () => 'hi' }, + }); const todosData = { __typename: 'Query', todos: [ @@ -175,7 +177,7 @@ it('respects property-level resolvers when given', () => { }); it('Respects property-level resolvers when given', () => { - const store = new Store(undefined, { + const store = new Store(undefined, undefined, { Mutation: { toggleTodo: function toggleTodo(result, _, cache) { cache.updateQuery({ query: Todos }, data => { diff --git a/src/test-utils/simple_schema.json b/src/test-utils/simple_schema.json new file mode 100644 index 0000000..7172f3f --- /dev/null +++ b/src/test-utils/simple_schema.json @@ -0,0 +1,1328 @@ +{ + "__schema": { + "queryType": { + "name": "Query" + }, + "mutationType": { + "name": "Mutation" + }, + "subscriptionType": null, + "types": [ + { + "kind": "OBJECT", + "name": "Query", + "fields": [ + { + "name": "todos", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Todo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Todo", + "fields": [ + { + "name": "id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "text", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "complete", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "author", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Author", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "ID", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "String", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Boolean", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Author", + "fields": [ + { + "name": "id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "known", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Mutation", + "fields": [ + { + "name": "toggleTodo", + "args": [ + { + "name": "id", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "Todo", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Schema", + "fields": [ + { + "name": "types", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "queryType", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mutationType", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subscriptionType", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "directives", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Directive", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Type", + "fields": [ + { + "name": "kind", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__TypeKind", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fields", + "args": [ + { + "name": "includeDeprecated", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Field", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "interfaces", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "possibleTypes", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enumValues", + "args": [ + { + "name": "includeDeprecated", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__EnumValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inputFields", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ofType", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__TypeKind", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "SCALAR", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LIST", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NON_NULL", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Field", + "fields": [ + { + "name": "name", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__InputValue", + "fields": [ + { + "name": "name", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "defaultValue", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__EnumValue", + "fields": [ + { + "name": "name", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Directive", + "fields": [ + { + "name": "name", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "locations", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__DirectiveLocation", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__DirectiveLocation", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "QUERY", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MUTATION", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SUBSCRIPTION", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_DEFINITION", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_SPREAD", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INLINE_FRAGMENT", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "VARIABLE_DEFINITION", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCHEMA", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCALAR", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD_DEFINITION", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ARGUMENT_DEFINITION", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM_VALUE", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_FIELD_DEFINITION", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INTERFACE", + "name": "ITodo", + "fields": [ + { + "name": "id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "text", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "complete", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "author", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Author", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "BigTodo", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "SmallTodo", + "ofType": null + } + ] + }, + { + "kind": "OBJECT", + "name": "BigTodo", + "fields": [ + { + "name": "id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "text", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "complete", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "author", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Author", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "wallOfText", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "ITodo", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SmallTodo", + "fields": [ + { + "name": "id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "text", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "complete", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "author", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Author", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "maxLength", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "ITodo", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Int", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "Todos", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "SmallTodo", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "BigTodo", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "UNION", + "name": "Search", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "SmallTodo", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "BigTodo", + "ofType": null + } + ] + }, + { + "kind": "ENUM", + "name": "CacheControlScope", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "PUBLIC", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PRIVATE", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Upload", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + } + ], + "directives": [ + { + "name": "cacheControl", + "locations": ["FIELD_DEFINITION", "OBJECT", "INTERFACE"], + "args": [ + { + "name": "maxAge", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "scope", + "type": { + "kind": "ENUM", + "name": "CacheControlScope", + "ofType": null + }, + "defaultValue": null + } + ] + }, + { + "name": "skip", + "locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], + "args": [ + { + "name": "if", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + } + ] + }, + { + "name": "include", + "locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], + "args": [ + { + "name": "if", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + } + ] + }, + { + "name": "deprecated", + "locations": ["FIELD_DEFINITION", "ENUM_VALUE"], + "args": [ + { + "name": "reason", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": "\"No longer supported\"" + } + ] + } + ] + } +}