From 8c8d4fc09ddc63c306db16d7386865ac297794bd Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Mon, 26 Jul 2021 10:15:43 -0400 Subject: [PATCH] Some refactor (#3229) * Experiment LRU Cache No need for deepMerge use createDefaultExecutor Few improvements workflow_dispatch Not only master refactor(stitchingInfo): shift more calculation to build time (#3199) -- run collectFields on selectionSet hints at build time -- use a fieldNode cache at build time so that at run time we can collect unique fieldNodes simply by using a Set Use collectFields and ExecutionContext from graphql-js (#3200) refactor: use Set to avoid repetition (#3202) * use Set to deduplicate * further refactor refactor ExpandAbstractTypes as PrepareGatewayRequest will consolidate remaining pre-user supplied transforms into this transform to hopefully reduce repetition also adds memoization of schema metadata small refactors add visitorKeys argument small refactor import WrapConcreteFields into PrepareGatewayRequest this does not use the same visitors and does not visit beneath fields, so can just be inlined info should be optional in DelegationContext fix types Consolidate VisitSelectionSets rename var internalize PrepareGatewayDocument as transform as prepareGatewayDocument function move varName generator out of loop to prevent recurrent looping refactor AddArgumentsAsVariables rename AddArgumentsAsVariables to FinalizeGatewayRequest to consolidate post custom transforms arguments should be added to target final targetSchema not transformedSchema move FilterToSchema tests to delegate package starting refacotr of FilterToSchema add visitorArgs also -- unfortunately -- requires another visit pass to properly remove variables if an object or interface field does not have selections -- another approach could be to keep track of variable uses, and subtract at the end of the first pass integrate FilterToSchema into FinalizeGatewayRequest fold AddTypenameToAbstract into FinalizeGatewayRequest extract finalizeGatewayRequest function from the transform retire all delegation transforms fix build typo Experiment LRU Cache More promise LRU with Refactor Fix TS Build Try Print instead More cache Use JSON.stringify? Fix caching * Fix mergeExtensions * Fix benchmark * More * Fix tests * Remove schemaTransforms * More fixes * Fix * More improvements * More improvements * More fixes? * Refactor w/o caching * Try sth * Reduce number of iterations * Reduce number of iterations * Small * Fix build * Cleanup * Add changeset * Fix build * Fix tests --- .changeset/mighty-nails-bake.md | 5 + .github/workflows/benchmark.yml | 5 +- benchmark/federation/call.js | 6 +- benchmark/federation/monolith.js | 39 +- benchmark/federation/stitching.js | 14 +- packages/batch-execute/package.json | 4 +- .../src/createBatchingExecutor.ts | 24 +- packages/batch-execute/src/mergeRequests.ts | 33 +- packages/delegate/src/Transformer.ts | 43 +- .../delegate/src/applySchemaTransforms.ts | 4 +- packages/delegate/src/createRequest.ts | 32 +- .../delegate/src/defaultMergedResolver.ts | 2 +- packages/delegate/src/delegateToSchema.ts | 2 +- packages/delegate/src/externalObjects.ts | 94 -- .../delegate/src/finalizeGatewayRequest.ts | 53 +- .../delegate/src/getFieldsNotInSubschema.ts | 17 +- packages/delegate/src/index.ts | 2 +- packages/delegate/src/mergeFields.ts | 181 ++- .../delegate/src/prepareGatewayDocument.ts | 55 +- packages/delegate/src/resolveExternalValue.ts | 7 +- packages/delegate/src/types.ts | 1 + packages/delegate/tests/errors.test.ts | 2 +- packages/schema/src/makeExecutableSchema.ts | 60 +- packages/schema/src/types.ts | 8 - packages/stitch/src/stitchSchemas.ts | 23 +- packages/stitch/tests/fixtures/schemas.ts | 54 +- packages/stitch/tests/mergeFailures.test.ts | 2 +- packages/stitch/tests/stitchSchemas.test.ts | 5 +- .../tests/typeMergingWithDirectives.test.ts | 6 +- .../tests/typeMergingWithInterfaces.test.ts | 6 +- .../stitching-directives/src/properties.ts | 4 +- .../src/stitchingDirectivesTransformer.ts | 12 +- .../stitchingDirectivesValidator.test.ts | 20 +- packages/utils/src/Interfaces.ts | 2 +- packages/utils/src/clone.ts | 60 - packages/utils/src/getArgumentValues.ts | 14 +- packages/utils/src/index.ts | 1 - packages/utils/tests/schemaTransforms.test.ts | 1235 ----------------- .../transforms/TransformCompositeFields.ts | 20 +- packages/wrap/src/transforms/WrapFields.ts | 18 +- packages/wrap/tests/fixtures/schemas.ts | 40 +- website/docs/generate-schema.md | 2 - website/docs/schema-directives.md | 52 +- website/docs/stitch-directives-sdl.md | 9 +- 44 files changed, 417 insertions(+), 1861 deletions(-) create mode 100644 .changeset/mighty-nails-bake.md delete mode 100644 packages/delegate/src/externalObjects.ts delete mode 100644 packages/utils/src/clone.ts delete mode 100644 packages/utils/tests/schemaTransforms.test.ts diff --git a/.changeset/mighty-nails-bake.md b/.changeset/mighty-nails-bake.md new file mode 100644 index 00000000000..955451aa54f --- /dev/null +++ b/.changeset/mighty-nails-bake.md @@ -0,0 +1,5 @@ +--- +'@graphql-tools/utils': major +--- + +BREAKING CHANGE: remove cloneSchema diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index c20a5db8460..9baf12a5e82 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -1,9 +1,8 @@ name: Benchmark on: - pull_request: - branches: - - master + pull_request: {} + workflow_dispatch: {} jobs: federation-benchmark: diff --git a/benchmark/federation/call.js b/benchmark/federation/call.js index 7e9fe6e32e9..eed34473e8c 100644 --- a/benchmark/federation/call.js +++ b/benchmark/federation/call.js @@ -3,7 +3,7 @@ const fetch = require('cross-fetch'); fetch('http://localhost:3000/stitching', { method: 'POST', headers: { - 'content-type': 'application/json' + 'content-type': 'application/json', }, body: JSON.stringify({ query: /* GraphQL */ ` @@ -49,4 +49,6 @@ fetch('http://localhost:3000/stitching', { } `, }), -}).then(res => res.json()).then(data => console.log(JSON.stringify(data, null, 2))); +}) + .then(res => res.json()) + .then(data => console.log(JSON.stringify(data, null, 2))); diff --git a/benchmark/federation/monolith.js b/benchmark/federation/monolith.js index 4298d8c9000..48c77cd1558 100644 --- a/benchmark/federation/monolith.js +++ b/benchmark/federation/monolith.js @@ -59,7 +59,7 @@ const resolvers = { return object.weight * 0.5; }, reviews(product) { - return reviews.filter((review) => review.product.upc === product.upc); + return reviews.filter((review) => review.productUpc === product.upc); }, inStock(product) { return inventory.find((inv) => inv.upc === product.upc)?.inStock; @@ -69,6 +69,9 @@ const resolvers = { author(review) { return users.find((user) => user.id === review.authorID); }, + product(review) { + return products.find(product => review.productUpc === product.upc) + } }, }; @@ -122,7 +125,7 @@ const definedProducts = [ weight: 50, }, ]; -const products = [...Array(listSize)].map((_, index) => definedProducts[index % 3]); +const products = Array(listSize).fill({}).map((_, index) => definedProducts[index % 3]); const usernames = [ { id: "1", username: "@ada" }, @@ -131,27 +134,27 @@ const usernames = [ const reviews = [ { - id: "1", - authorID: "1", - product: { upc: "1" }, - body: "Love it!", + id: '1', + authorID: '1', + productUpc: '1', + body: 'Love it!', }, { - id: "2", - authorID: "1", - product: { upc: "2" }, - body: "Too expensive.", + id: '2', + authorID: '1', + productUpc: '2', + body: 'Too expensive.', }, { - id: "3", - authorID: "2", - product: { upc: "3" }, - body: "Could be better.", + id: '3', + authorID: '2', + productUpc: '3', + body: 'Could be better.', }, { - id: "4", - authorID: "2", - product: { upc: "1" }, - body: "Prefer something else.", + id: '4', + authorID: '2', + productUpc: '1', + body: 'Prefer something else.', }, ]; diff --git a/benchmark/federation/stitching.js b/benchmark/federation/stitching.js index 4d8a5f55eb4..fa43ffc1473 100644 --- a/benchmark/federation/stitching.js +++ b/benchmark/federation/stitching.js @@ -2,6 +2,7 @@ const { stitchSchemas } = require('@graphql-tools/stitch'); const { federationToStitchingSDL, stitchingDirectives } = require('@graphql-tools/stitching-directives'); const { stitchingDirectivesTransformer } = stitchingDirectives(); const { buildSchema, execute, print } = require('graphql'); +const { createDefaultExecutor } = require('@graphql-tools/delegate'); const services = [ require('./services/accounts'), @@ -10,17 +11,6 @@ const services = [ require('./services/reviews'), ]; -function createExecutor(schema) { - return function serviceExecutor({ document, variables, context }) { - return execute({ - schema, - document, - variableValues: variables, - contextValue: context, - }); - }; -} - async function makeGatewaySchema() { return stitchSchemas({ subschemaConfigTransforms: [stitchingDirectivesTransformer], @@ -32,7 +22,7 @@ function fetchFederationSubschema({ schema, typeDefs }) { const sdl = federationToStitchingSDL(print(typeDefs)); return { schema: buildSchema(sdl), - executor: createExecutor(schema), + executor: createDefaultExecutor(schema), batch: true }; } diff --git a/packages/batch-execute/package.json b/packages/batch-execute/package.json index e06cc9a1aa7..2ebf2c80605 100644 --- a/packages/batch-execute/package.json +++ b/packages/batch-execute/package.json @@ -10,8 +10,8 @@ "license": "MIT", "sideEffects": false, "main": "dist/index.js", - "module": "dist/index.mjs", - "exports": { + "module": "dist/index.mjs", + "exports": { ".": { "require": "./dist/index.js", "import": "./dist/index.mjs" diff --git a/packages/batch-execute/src/createBatchingExecutor.ts b/packages/batch-execute/src/createBatchingExecutor.ts index 2645230bc8e..6897612cc39 100644 --- a/packages/batch-execute/src/createBatchingExecutor.ts +++ b/packages/batch-execute/src/createBatchingExecutor.ts @@ -1,7 +1,5 @@ import DataLoader from 'dataloader'; -import { ValueOrPromise } from 'value-or-promise'; - import { ExecutionRequest, Executor, ExecutionResult } from '@graphql-tools/utils'; import { mergeRequests } from './mergeRequests'; @@ -25,7 +23,7 @@ function createLoadFn( executor: Executor, extensionsReducer: (mergedExtensions: Record, request: ExecutionRequest) => Record ) { - return async (requests: ReadonlyArray): Promise> => { + return async function batchExecuteLoadFn(requests: ReadonlyArray): Promise> { const execBatches: Array> = []; let index = 0; const request = requests[index]; @@ -48,19 +46,15 @@ function createLoadFn( } } - const executionResults: Array> = execBatches.map(execBatch => { - const mergedRequests = mergeRequests(execBatch, extensionsReducer); - return new ValueOrPromise(() => executor(mergedRequests) as ExecutionResult); - }); + const results = await Promise.all( + execBatches.map(async execBatch => { + const mergedRequests = mergeRequests(execBatch, extensionsReducer); + const resultBatches = (await executor(mergedRequests)) as ExecutionResult; + return splitResult(resultBatches, execBatch.length); + }) + ); - return ValueOrPromise.all(executionResults) - .then(resultBatches => - resultBatches.reduce( - (results, resultBatch, index) => results.concat(splitResult(resultBatch, execBatches[index].length)), - new Array>>() - ) - ) - .resolve(); + return results.flat(); }; } diff --git a/packages/batch-execute/src/mergeRequests.ts b/packages/batch-execute/src/mergeRequests.ts index d40f35f3442..6e52a9fa5fc 100644 --- a/packages/batch-execute/src/mergeRequests.ts +++ b/packages/batch-execute/src/mergeRequests.ts @@ -108,27 +108,32 @@ export function mergeRequests( } function prefixRequest(prefix: string, request: ExecutionRequest): ExecutionRequest { - let document = aliasTopLevelFields(prefix, request.document); const executionVariables = request.variables ?? {}; - const variableNames = Object.keys(executionVariables); - if (variableNames.length === 0) { - return { ...request, document }; + function prefixNode(node: VariableNode | FragmentDefinitionNode | FragmentSpreadNode) { + return prefixNodeName(node, prefix); } - document = visit(document, { - [Kind.VARIABLE]: (node: VariableNode) => prefixNodeName(node, prefix), - [Kind.FRAGMENT_DEFINITION]: (node: FragmentDefinitionNode) => prefixNodeName(node, prefix), - [Kind.FRAGMENT_SPREAD]: (node: FragmentSpreadNode) => prefixNodeName(node, prefix), - }); + let prefixedDocument = aliasTopLevelFields(prefix, request.document); + + const executionVariableNames = Object.keys(executionVariables); + + if (executionVariableNames.length > 0) { + prefixedDocument = visit(prefixedDocument, { + [Kind.VARIABLE]: prefixNode, + [Kind.FRAGMENT_DEFINITION]: prefixNode, + [Kind.FRAGMENT_SPREAD]: prefixNode, + }) as DocumentNode; + } + + const prefixedVariables = {}; - const prefixedVariables = variableNames.reduce((acc, name) => { - acc[prefix + name] = executionVariables[name]; - return acc; - }, Object.create(null)); + for (const variableName of executionVariableNames) { + prefixedVariables[prefix + variableName] = executionVariables[variableName]; + } return { - document, + document: prefixedDocument, variables: prefixedVariables, operationType: request.operationType, }; diff --git a/packages/delegate/src/Transformer.ts b/packages/delegate/src/Transformer.ts index c214b26d097..e637b51e042 100644 --- a/packages/delegate/src/Transformer.ts +++ b/packages/delegate/src/Transformer.ts @@ -29,31 +29,36 @@ export class Transformer> { } public transformRequest(originalRequest: ExecutionRequest): ExecutionRequest { - const preparedRequest = { + let request = { ...originalRequest, - document: prepareGatewayDocument(originalRequest.document, this.delegationContext), + document: prepareGatewayDocument( + originalRequest.document, + this.delegationContext.transformedSchema, + this.delegationContext.returnType, + this.delegationContext.info?.schema + ), }; - const transformedRequest = this.transformations.reduce( - (request: ExecutionRequest, transformation: Transformation) => - transformation.transform.transformRequest != null - ? transformation.transform.transformRequest(request, this.delegationContext, transformation.context) - : request, - preparedRequest - ); + for (const transformation of this.transformations) { + if (transformation.transform.transformRequest) { + request = transformation.transform.transformRequest(request, this.delegationContext, transformation.context); + } + } - return finalizeGatewayRequest(transformedRequest, this.delegationContext); + return finalizeGatewayRequest(request, this.delegationContext); } - public transformResult(originalResult: ExecutionResult): any { - const transformedResult = this.transformations.reduceRight( - (result: ExecutionResult, transformation: Transformation) => - transformation.transform.transformResult != null - ? transformation.transform.transformResult(result, this.delegationContext, transformation.context) - : result, - originalResult - ); + public transformResult(originalResult: ExecutionResult) { + let result = originalResult; + + // from rigth to left + for (let i = this.transformations.length - 1; i >= 0; i--) { + const transformation = this.transformations[i]; + if (transformation.transform.transformResult) { + result = transformation.transform.transformResult(result, this.delegationContext, transformation.context); + } + } - return checkResultAndHandleErrors(transformedResult, this.delegationContext); + return checkResultAndHandleErrors(result, this.delegationContext); } } diff --git a/packages/delegate/src/applySchemaTransforms.ts b/packages/delegate/src/applySchemaTransforms.ts index 225cb59920b..6a14f48c968 100644 --- a/packages/delegate/src/applySchemaTransforms.ts +++ b/packages/delegate/src/applySchemaTransforms.ts @@ -1,7 +1,5 @@ import { GraphQLSchema } from 'graphql'; -import { cloneSchema } from '@graphql-tools/utils'; - import { SubschemaConfig } from './types'; export function applySchemaTransforms( @@ -18,7 +16,7 @@ export function applySchemaTransforms( return schemaTransforms.reduce( (schema: GraphQLSchema, transform) => transform.transformSchema != null - ? transform.transformSchema(cloneSchema(schema), subschemaConfig, transformedSchema) + ? transform.transformSchema(schema, subschemaConfig, transformedSchema) : schema, originalWrappingSchema ); diff --git a/packages/delegate/src/createRequest.ts b/packages/delegate/src/createRequest.ts index 175aa0ca7f7..6c28857d5df 100644 --- a/packages/delegate/src/createRequest.ts +++ b/packages/delegate/src/createRequest.ts @@ -79,16 +79,19 @@ export function createRequest({ info, }: ICreateRequest): ExecutionRequest { let newSelectionSet: SelectionSetNode | undefined; - let argumentNodeMap: Record; + const argumentNodeMap: Record = Object.create(null); if (selectionSet != null) { newSelectionSet = selectionSet; - argumentNodeMap = Object.create(null); } else { - const selections: Array = (fieldNodes ?? []).reduce( - (acc, fieldNode) => (fieldNode.selectionSet != null ? acc.concat(fieldNode.selectionSet.selections) : acc), - [] as Array - ); + const selections: Array = []; + for (const fieldNode of fieldNodes || []) { + if (fieldNode.selectionSet) { + for (const selection of fieldNode.selectionSet.selections) { + selections.push(selection); + } + } + } newSelectionSet = selections.length ? { @@ -97,17 +100,11 @@ export function createRequest({ } : undefined; - argumentNodeMap = {}; - const args = fieldNodes?.[0]?.arguments; if (args) { - argumentNodeMap = args.reduce( - (prev, curr) => ({ - ...prev, - [curr.name.value]: curr, - }), - argumentNodeMap - ); + for (const argNode of args) { + argumentNodeMap[argNode.name.value] = argNode; + } } } @@ -173,7 +170,10 @@ export function createRequest({ const definitions: Array = [operationDefinition]; if (fragments != null) { - definitions.push(...Object.values(fragments)); + for (const fragmentName in fragments) { + const fragment = fragments[fragmentName]; + definitions.push(fragment); + } } const document: DocumentNode = { diff --git a/packages/delegate/src/defaultMergedResolver.ts b/packages/delegate/src/defaultMergedResolver.ts index 8ae4e9ce24d..da5559440d0 100644 --- a/packages/delegate/src/defaultMergedResolver.ts +++ b/packages/delegate/src/defaultMergedResolver.ts @@ -3,7 +3,7 @@ import { defaultFieldResolver, GraphQLResolveInfo } from 'graphql'; import { getResponseKeyFromInfo } from '@graphql-tools/utils'; import { resolveExternalValue } from './resolveExternalValue'; -import { getSubschema, getUnpathedErrors, isExternalObject } from './externalObjects'; +import { getSubschema, getUnpathedErrors, isExternalObject } from './mergeFields'; import { ExternalObject } from './types'; /** diff --git a/packages/delegate/src/delegateToSchema.ts b/packages/delegate/src/delegateToSchema.ts index 953078e9131..5f58e4a030b 100644 --- a/packages/delegate/src/delegateToSchema.ts +++ b/packages/delegate/src/delegateToSchema.ts @@ -208,7 +208,7 @@ function getExecutor(delegationContext: DelegationContext): const defaultExecutorCache = new WeakMap(); -function createDefaultExecutor(schema: GraphQLSchema): Executor { +export function createDefaultExecutor(schema: GraphQLSchema): Executor { let defaultExecutor = defaultExecutorCache.get(schema); if (!defaultExecutor) { defaultExecutor = function defaultExecutor({ diff --git a/packages/delegate/src/externalObjects.ts b/packages/delegate/src/externalObjects.ts deleted file mode 100644 index 58887eca119..00000000000 --- a/packages/delegate/src/externalObjects.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { GraphQLSchema, GraphQLError, GraphQLObjectType, SelectionSetNode, locatedError } from 'graphql'; - -import { relocatedError } from '@graphql-tools/utils'; - -import { SubschemaConfig, ExternalObject } from './types'; -import { OBJECT_SUBSCHEMA_SYMBOL, FIELD_SUBSCHEMA_MAP_SYMBOL, UNPATHED_ERRORS_SYMBOL } from './symbols'; -import { collectFields, ExecutionContext } from 'graphql/execution/execute.js'; - -export function isExternalObject(data: any): data is ExternalObject { - return data[UNPATHED_ERRORS_SYMBOL] !== undefined; -} - -export function annotateExternalObject( - object: any, - errors: Array, - subschema: GraphQLSchema | SubschemaConfig | undefined -): ExternalObject { - Object.defineProperties(object, { - [OBJECT_SUBSCHEMA_SYMBOL]: { value: subschema }, - [FIELD_SUBSCHEMA_MAP_SYMBOL]: { value: Object.create(null) }, - [UNPATHED_ERRORS_SYMBOL]: { value: errors }, - }); - return object; -} - -export function getSubschema(object: ExternalObject, responseKey: string): GraphQLSchema | SubschemaConfig { - return object[FIELD_SUBSCHEMA_MAP_SYMBOL][responseKey] ?? object[OBJECT_SUBSCHEMA_SYMBOL]; -} - -export function getUnpathedErrors(object: ExternalObject): Array { - return object[UNPATHED_ERRORS_SYMBOL]; -} - -export function mergeExternalObjects( - schema: GraphQLSchema, - path: Array, - typeName: string, - target: ExternalObject, - sources: Array, - selectionSets: Array -): ExternalObject { - const results: Array = []; - let errors: Array = []; - - for (const index in sources) { - const source = sources[index]; - if (source instanceof Error || source === null) { - const selectionSet = selectionSets[index]; - const fieldNodes = collectFields( - { - schema, - variableValues: {}, - fragments: {}, - } as ExecutionContext, - schema.getType(typeName) as GraphQLObjectType, - selectionSet, - Object.create(null), - Object.create(null) - ); - const nullResult = {}; - for (const responseKey in fieldNodes) { - if (source instanceof GraphQLError) { - nullResult[responseKey] = relocatedError(source, path.concat([responseKey])); - } else if (source instanceof Error) { - nullResult[responseKey] = locatedError(source, fieldNodes[responseKey], path.concat([responseKey])); - } else { - nullResult[responseKey] = null; - } - } - results.push(nullResult); - } else { - errors = errors.concat(source[UNPATHED_ERRORS_SYMBOL]); - results.push(source); - } - } - - const combinedResult: ExternalObject = Object.assign({}, target, ...results); - - const newFieldSubschemaMap = target[FIELD_SUBSCHEMA_MAP_SYMBOL] ?? Object.create(null); - - for (const source of results) { - const objectSubschema = source[OBJECT_SUBSCHEMA_SYMBOL]; - const fieldSubschemaMap = source[FIELD_SUBSCHEMA_MAP_SYMBOL]; - for (const responseKey in source) { - newFieldSubschemaMap[responseKey] = fieldSubschemaMap?.[responseKey] ?? objectSubschema; - } - } - - combinedResult[FIELD_SUBSCHEMA_MAP_SYMBOL] = newFieldSubschemaMap; - combinedResult[OBJECT_SUBSCHEMA_SYMBOL] = target[OBJECT_SUBSCHEMA_SYMBOL]; - combinedResult[UNPATHED_ERRORS_SYMBOL] = target[UNPATHED_ERRORS_SYMBOL].concat(errors); - - return combinedResult; -} diff --git a/packages/delegate/src/finalizeGatewayRequest.ts b/packages/delegate/src/finalizeGatewayRequest.ts index 6f88eb71ba7..909e5499927 100644 --- a/packages/delegate/src/finalizeGatewayRequest.ts +++ b/packages/delegate/src/finalizeGatewayRequest.ts @@ -30,23 +30,13 @@ import { import { DelegationContext } from './types'; import { getDocumentMetadata } from './getDocumentMetadata'; -import { TypeMap } from 'graphql/type/schema'; - -export function finalizeGatewayRequest( - originalRequest: ExecutionRequest, - delegationContext: DelegationContext -): ExecutionRequest { - let { document, variables } = originalRequest; - - let { operations, fragments } = getDocumentMetadata(document); - const { targetSchema, args } = delegationContext; - - if (args) { - const requestWithNewVariables = addVariablesToRootFields(targetSchema, operations, args); - operations = requestWithNewVariables.newOperations; - variables = Object.assign({}, variables ?? {}, requestWithNewVariables.newVariables); - } +import type { TypeMap } from 'graphql/type/schema'; +function finalizeGatewayDocument( + targetSchema: GraphQLSchema, + fragments: FragmentDefinitionNode[], + operations: OperationDefinitionNode[] +) { let usedVariables: Array = []; let usedFragments: Array = []; const newOperations: Array = []; @@ -100,6 +90,32 @@ export function finalizeGatewayRequest( }); } + return { + usedVariables, + newDocument: { + kind: Kind.DOCUMENT, + definitions: [...newOperations, ...newFragments], + }, + }; +} + +export function finalizeGatewayRequest( + originalRequest: ExecutionRequest, + delegationContext: DelegationContext +): ExecutionRequest { + let { document, variables } = originalRequest; + + let { operations, fragments } = getDocumentMetadata(document); + const { targetSchema, args } = delegationContext; + + if (args) { + const requestWithNewVariables = addVariablesToRootFields(targetSchema, operations, args); + operations = requestWithNewVariables.newOperations; + variables = Object.assign({}, variables ?? {}, requestWithNewVariables.newVariables); + } + + const { usedVariables, newDocument } = finalizeGatewayDocument(targetSchema, fragments, operations); + const newVariables = {}; if (variables != null) { for (const variableName of usedVariables) { @@ -110,11 +126,6 @@ export function finalizeGatewayRequest( } } - const newDocument = { - kind: Kind.DOCUMENT, - definitions: [...newOperations, ...newFragments], - }; - return { ...originalRequest, document: newDocument, diff --git a/packages/delegate/src/getFieldsNotInSubschema.ts b/packages/delegate/src/getFieldsNotInSubschema.ts index a4192daff05..965ca067efc 100644 --- a/packages/delegate/src/getFieldsNotInSubschema.ts +++ b/packages/delegate/src/getFieldsNotInSubschema.ts @@ -33,7 +33,7 @@ function collectSubFields(info: GraphQLResolveInfo, typeName: string): Record, mergedTypeInfo: MergedTypeInfo @@ -51,23 +51,24 @@ export const getFieldsNotInSubschema = memoizeInfoAnd2Objects(function ( const stitchingInfo: Maybe = info.schema.extensions?.['stitchingInfo']; const fieldNodesByField = stitchingInfo?.fieldNodesByField; - let fieldsNotInSchema: Array = []; - const additionalFieldNodes: Set = new Set(); + const fieldsNotInSchema = new Set(); for (const responseKey in subFieldNodes) { const subFieldNodesForResponseKey = subFieldNodes[responseKey]; const fieldName = subFieldNodesForResponseKey[0].name.value; - if (!(fieldName in fields)) { - fieldsNotInSchema = fieldsNotInSchema.concat(subFieldNodesForResponseKey); + if (!fields[fieldName]) { + for (const subFieldNodeForResponseKey of subFieldNodesForResponseKey) { + fieldsNotInSchema.add(subFieldNodeForResponseKey); + } } const fieldNodesForField = fieldNodesByField?.[typeName]?.[fieldName]; if (fieldNodesForField) { for (const fieldNode of fieldNodesForField) { - if (!(fieldNode.name.value in fields)) { - additionalFieldNodes.add(fieldNode); + if (!fields[fieldNode.name.value]) { + fieldsNotInSchema.add(fieldNode); } } } } - return fieldsNotInSchema.concat(Array.from(additionalFieldNodes)); + return Array.from(fieldsNotInSchema); }); diff --git a/packages/delegate/src/index.ts b/packages/delegate/src/index.ts index 2087973cfd9..e8f39de2bbc 100644 --- a/packages/delegate/src/index.ts +++ b/packages/delegate/src/index.ts @@ -4,7 +4,7 @@ export * from './applySchemaTransforms'; export * from './createRequest'; export * from './defaultMergedResolver'; export * from './delegateToSchema'; -export * from './externalObjects'; +export * from './mergeFields'; export * from './resolveExternalValue'; export * from './subschemaConfig'; export * from './types'; diff --git a/packages/delegate/src/mergeFields.ts b/packages/delegate/src/mergeFields.ts index e15ae559756..9aa1a157608 100644 --- a/packages/delegate/src/mergeFields.ts +++ b/packages/delegate/src/mergeFields.ts @@ -7,16 +7,19 @@ import { GraphQLObjectType, responsePathAsArray, getNamedType, + GraphQLError, + locatedError, + GraphQLSchema, } from 'graphql'; -import { ValueOrPromise } from 'value-or-promise'; - -import { MergedTypeInfo } from './types'; +import { ExternalObject, MergedTypeInfo, SubschemaConfig } from './types'; import { memoize4, memoize3, memoize2 } from './memoize'; -import { mergeExternalObjects } from './externalObjects'; import { Subschema } from './Subschema'; +import { collectFields, ExecutionContext } from 'graphql/execution/execute'; +import { relocatedError } from '@graphql-tools/utils'; +import { FIELD_SUBSCHEMA_MAP_SYMBOL, OBJECT_SUBSCHEMA_SYMBOL, UNPATHED_ERRORS_SYMBOL } from './symbols'; -const sortSubschemasByProxiability = memoize4(function ( +const sortSubschemasByProxiability = memoize4(function sortSubschemasByProxiability( mergedTypeInfo: MergedTypeInfo, sourceSubschemaOrSourceSubschemas: Subschema | Array, targetSubschemas: Array, @@ -63,7 +66,7 @@ const sortSubschemasByProxiability = memoize4(function ( }; }); -const buildDelegationPlan = memoize3(function ( +const buildDelegationPlan = memoize3(function buildDelegationPlan( mergedTypeInfo: MergedTypeInfo, fieldNodes: Array, proxiableSubschemas: Array @@ -76,7 +79,7 @@ const buildDelegationPlan = memoize3(function ( // 2. for each selection: - const delegationMap: Map> = new Map(); + const delegationMap: Map = new Map(); for (const fieldNode of fieldNodes) { if (fieldNode.name.value === '__typename') { continue; @@ -91,11 +94,14 @@ const buildDelegationPlan = memoize3(function ( continue; } - const existingSubschema = delegationMap.get(uniqueSubschema); + const existingSubschema = delegationMap.get(uniqueSubschema)?.selections as SelectionNode[]; if (existingSubschema != null) { existingSubschema.push(fieldNode); } else { - delegationMap.set(uniqueSubschema, [fieldNode]); + delegationMap.set(uniqueSubschema, { + kind: Kind.SELECTION_SET, + selections: [fieldNode], + }); } continue; @@ -119,28 +125,22 @@ const buildDelegationPlan = memoize3(function ( const existingSubschema = nonUniqueSubschemas.find(s => delegationMap.has(s)); if (existingSubschema != null) { // It is okay we previously explicitly check whether the map has the element. - delegationMap.get(existingSubschema)!.push(fieldNode); + (delegationMap.get(existingSubschema)!.selections as SelectionNode[]).push(fieldNode); } else { - delegationMap.set(nonUniqueSubschemas[0], [fieldNode]); + delegationMap.set(nonUniqueSubschemas[0], { + kind: Kind.SELECTION_SET, + selections: [fieldNode], + }); } } - const finalDelegationMap: Map = new Map(); - - for (const [subschema, selections] of delegationMap) { - finalDelegationMap.set(subschema, { - kind: Kind.SELECTION_SET, - selections, - }); - } - return { - delegationMap: finalDelegationMap, + delegationMap, unproxiableFieldNodes, }; }); -const combineSubschemas = memoize2(function ( +const combineSubschemas = memoize2(function combineSubschemas( subschemaOrSubschemas: Subschema | Array, additionalSubschemas: Array ): Array { @@ -149,7 +149,32 @@ const combineSubschemas = memoize2(function ( : [subschemaOrSubschemas].concat(additionalSubschemas); }); -export function mergeFields( +export function isExternalObject(data: any): data is ExternalObject { + return data[UNPATHED_ERRORS_SYMBOL] !== undefined; +} + +export function annotateExternalObject( + object: any, + errors: Array, + subschema: GraphQLSchema | SubschemaConfig | undefined +): ExternalObject { + Object.defineProperties(object, { + [OBJECT_SUBSCHEMA_SYMBOL]: { value: subschema }, + [FIELD_SUBSCHEMA_MAP_SYMBOL]: { value: Object.create(null) }, + [UNPATHED_ERRORS_SYMBOL]: { value: errors }, + }); + return object; +} + +export function getSubschema(object: ExternalObject, responseKey: string): GraphQLSchema | SubschemaConfig { + return object[FIELD_SUBSCHEMA_MAP_SYMBOL][responseKey] ?? object[OBJECT_SUBSCHEMA_SYMBOL]; +} + +export function getUnpathedErrors(object: ExternalObject): Array { + return object[UNPATHED_ERRORS_SYMBOL]; +} + +export async function mergeFields( mergedTypeInfo: MergedTypeInfo, typeName: string, object: any, @@ -158,7 +183,7 @@ export function mergeFields( targetSubschemas: Array>, context: any, info: GraphQLResolveInfo -): any { +): Promise { if (!fieldNodes.length) { return object; } @@ -176,41 +201,85 @@ export function mergeFields( return object; } - const resultMap: Map, SelectionSetNode> = new Map(); - for (const [s, selectionSet] of delegationMap) { - const resolver = mergedTypeInfo.resolvers.get(s); - if (resolver) { - const valueOrPromise = new ValueOrPromise(() => resolver(object, context, info, s, selectionSet)).catch( - error => error - ); - resultMap.set(valueOrPromise, selectionSet); - } - } + const combinedErrors = object[UNPATHED_ERRORS_SYMBOL] || []; + + const path = responsePathAsArray(info.path); + + const newFieldSubschemaMap = object[FIELD_SUBSCHEMA_MAP_SYMBOL] ?? Object.create(null); + + const type = info.schema.getType(object.__typename) as GraphQLObjectType; + + const results = await Promise.all( + [...delegationMap.entries()].map(async ([s, selectionSet]) => { + const resolver = mergedTypeInfo.resolvers.get(s); + if (resolver) { + let source: any; + try { + source = await resolver(object, context, info, s, selectionSet); + } catch (error) { + source = error; + } + if (source instanceof Error || source === null) { + const fieldNodes = collectFields( + { + schema: info.schema, + variableValues: {}, + fragments: {}, + } as ExecutionContext, + type, + selectionSet, + Object.create(null), + Object.create(null) + ); + const nullResult = {}; + for (const responseKey in fieldNodes) { + const combinedPath = [...path, responseKey]; + if (source instanceof GraphQLError) { + nullResult[responseKey] = relocatedError(source, combinedPath); + } else if (source instanceof Error) { + nullResult[responseKey] = locatedError(source, fieldNodes[responseKey], combinedPath); + } else { + nullResult[responseKey] = null; + } + } + source = nullResult; + } else { + if (source[UNPATHED_ERRORS_SYMBOL]) { + combinedErrors.push(...source[UNPATHED_ERRORS_SYMBOL]); + } + } + + const objectSubschema = source[OBJECT_SUBSCHEMA_SYMBOL]; + const fieldSubschemaMap = source[FIELD_SUBSCHEMA_MAP_SYMBOL]; + for (const responseKey in source) { + newFieldSubschemaMap[responseKey] = fieldSubschemaMap?.[responseKey] ?? objectSubschema; + } + + return source; + } + }) + ); + + const combinedResult: ExternalObject = Object.assign({}, object, ...results); - return ValueOrPromise.all(Array.from(resultMap.keys())) - .then(results => - mergeFields( - mergedTypeInfo, - typeName, - mergeExternalObjects( - info.schema, - responsePathAsArray(info.path), - object.__typename, - object, - results, - Array.from(resultMap.values()) - ), - unproxiableFieldNodes, - combineSubschemas(sourceSubschemaOrSourceSubschemas, proxiableSubschemas), - nonProxiableSubschemas, - context, - info - ) - ) - .resolve(); + combinedResult[FIELD_SUBSCHEMA_MAP_SYMBOL] = newFieldSubschemaMap; + combinedResult[OBJECT_SUBSCHEMA_SYMBOL] = object[OBJECT_SUBSCHEMA_SYMBOL]; + + combinedResult[UNPATHED_ERRORS_SYMBOL] = combinedErrors; + + return mergeFields( + mergedTypeInfo, + typeName, + combinedResult, + unproxiableFieldNodes, + combineSubschemas(sourceSubschemaOrSourceSubschemas, proxiableSubschemas), + nonProxiableSubschemas, + context, + info + ); } -const subschemaTypesContainSelectionSet = memoize3(function ( +const subschemaTypesContainSelectionSet = memoize3(function subschemaTypesContainSelectionSetMemoized( mergedTypeInfo: MergedTypeInfo, sourceSubschemaOrSourceSubschemas: Subschema | Array, selectionSet: SelectionSetNode diff --git a/packages/delegate/src/prepareGatewayDocument.ts b/packages/delegate/src/prepareGatewayDocument.ts index 14c9437a51f..72a35ec66cb 100644 --- a/packages/delegate/src/prepareGatewayDocument.ts +++ b/packages/delegate/src/prepareGatewayDocument.ts @@ -20,18 +20,18 @@ import { import { implementsAbstractType, getRootTypeNames } from '@graphql-tools/utils'; -import { DelegationContext } from './types'; import { memoize2 } from './memoize'; import { getDocumentMetadata } from './getDocumentMetadata'; export function prepareGatewayDocument( originalDocument: DocumentNode, - delegationContext: DelegationContext + transformedSchema: GraphQLSchema, + returnType: GraphQLOutputType, + infoSchema?: GraphQLSchema ): DocumentNode { - const { info, transformedSchema, returnType } = delegationContext; const wrappedConcreteTypesDocument = wrapConcreteTypes(returnType, transformedSchema, originalDocument); - if (info == null) { + if (infoSchema == null) { return wrappedConcreteTypesDocument; } @@ -42,7 +42,7 @@ export function prepareGatewayDocument( fieldNodesByType, fieldNodesByField, dynamicSelectionSetsByField, - } = getSchemaMetaData(info.schema, transformedSchema); + } = getSchemaMetaData(infoSchema, transformedSchema); const { operations, fragments, fragmentNames } = getDocumentMetadata(wrappedConcreteTypesDocument); @@ -145,17 +145,18 @@ function visitSelectionSet( fieldNodesByField: Record>>, dynamicSelectionSetsByField: Record SelectionSetNode>>> ): SelectionSetNode { - const newSelections: Array = []; + const newSelections = new Set(); const maybeType = typeInfo.getParentType(); if (maybeType != null) { const parentType: GraphQLNamedType = getNamedType(maybeType); - const uniqueSelections: Set = new Set(); const parentTypeName = parentType.name; const fieldNodes = fieldNodesByType[parentTypeName]; if (fieldNodes) { - addSelectionsToSet(uniqueSelections, fieldNodes); + for (const fieldNode of fieldNodes) { + newSelections.add(fieldNode); + } } const interfaceExtensions = interfaceExtensionsMap[parentType.name]; @@ -167,22 +168,22 @@ function visitSelectionSet( const possibleTypes = possibleTypesMap[selection.typeCondition.name.value]; if (possibleTypes == null) { - newSelections.push(selection); + newSelections.add(selection); continue; } for (const possibleTypeName of possibleTypes) { const maybePossibleType = schema.getType(possibleTypeName); if (maybePossibleType != null && implementsAbstractType(schema, parentType, maybePossibleType)) { - newSelections.push(generateInlineFragment(possibleTypeName, selection.selectionSet)); + newSelections.add(generateInlineFragment(possibleTypeName, selection.selectionSet)); } } } } else if (selection.kind === Kind.FRAGMENT_SPREAD) { const fragmentName = selection.name.value; - if (!(fragmentName in fragmentReplacements)) { - newSelections.push(selection); + if (!fragmentReplacements[fragmentName]) { + newSelections.add(selection); continue; } @@ -191,7 +192,7 @@ function visitSelectionSet( const maybeReplacementType = schema.getType(typeName); if (maybeReplacementType != null && implementsAbstractType(schema, parentType, maybeType)) { - newSelections.push({ + newSelections.add({ kind: Kind.FRAGMENT_SPREAD, name: { kind: Kind.NAME, @@ -205,7 +206,9 @@ function visitSelectionSet( const fieldNodes = fieldNodesByField[parentTypeName]?.[fieldName]; if (fieldNodes != null) { - addSelectionsToSet(uniqueSelections, fieldNodes); + for (const fieldNode of fieldNodes) { + newSelections.add(fieldNode); + } } const dynamicSelectionSets = dynamicSelectionSetsByField[parentTypeName]?.[fieldName]; @@ -213,7 +216,9 @@ function visitSelectionSet( for (const selectionSetFn of dynamicSelectionSets) { const selectionSet = selectionSetFn(selection); if (selectionSet != null) { - addSelectionsToSet(uniqueSelections, selectionSet.selections); + for (const selection of selectionSet.selections) { + newSelections.add(selection); + } } } } @@ -221,13 +226,13 @@ function visitSelectionSet( if (interfaceExtensions?.[fieldName]) { interfaceExtensionFields.push(selection); } else { - newSelections.push(selection); + newSelections.add(selection); } } } - if (parentType.name in reversePossibleTypesMap) { - newSelections.push({ + if (reversePossibleTypesMap[parentType.name]) { + newSelections.add({ kind: Kind.FIELD, name: { kind: Kind.NAME, @@ -240,7 +245,7 @@ function visitSelectionSet( const possibleTypes = possibleTypesMap[parentType.name]; if (possibleTypes != null) { for (const possibleType of possibleTypes) { - newSelections.push( + newSelections.add( generateInlineFragment(possibleType, { kind: Kind.SELECTION_SET, selections: interfaceExtensionFields, @@ -252,7 +257,7 @@ function visitSelectionSet( return { ...node, - selections: newSelections.concat(Array.from(uniqueSelections)), + selections: Array.from(newSelections), }; } @@ -314,7 +319,7 @@ const getSchemaMetaData = memoize2( } } - if (!isAbstractType(targetType) || typeName in interfaceExtensionsMap) { + if (interfaceExtensionsMap[typeName] || !isAbstractType(targetType)) { const implementations = sourceSchema.getPossibleTypes(type); possibleTypesMap[typeName] = []; @@ -343,7 +348,7 @@ function reversePossibleTypesMap(possibleTypesMap: Record> for (const typeName in possibleTypesMap) { const toTypeNames = possibleTypesMap[typeName]; for (const toTypeName of toTypeNames) { - if (!(toTypeName in result)) { + if (!result[toTypeName]) { result[toTypeName] = []; } result[toTypeName].push(typeName); @@ -522,9 +527,3 @@ function wrapConcreteTypes( } ); } - -function addSelectionsToSet(set: Set, selections: ReadonlyArray): void { - for (const selection of selections) { - set.add(selection); - } -} diff --git a/packages/delegate/src/resolveExternalValue.ts b/packages/delegate/src/resolveExternalValue.ts index 8a6fc60972f..232ce3faf02 100644 --- a/packages/delegate/src/resolveExternalValue.ts +++ b/packages/delegate/src/resolveExternalValue.ts @@ -17,9 +17,8 @@ import { import { AggregateError, Maybe } from '@graphql-tools/utils'; import { StitchingInfo, SubschemaConfig } from './types'; -import { annotateExternalObject, isExternalObject } from './externalObjects'; +import { annotateExternalObject, isExternalObject, mergeFields } from './mergeFields'; import { getFieldsNotInSubschema } from './getFieldsNotInSubschema'; -import { mergeFields } from './mergeFields'; import { Subschema } from './Subschema'; export function resolveExternalValue( @@ -41,7 +40,7 @@ export function resolveExternalValue( return reportUnpathedErrorsViaNull(unpathedErrors); } - if (isLeafType(type)) { + if ('parseValue' in type) { return type.parseValue(result); } else if (isCompositeType(type)) { return resolveExternalObject(type, result, unpathedErrors, subschema, context, info, skipTypeMerging); @@ -80,7 +79,7 @@ function resolveExternalObject( let typeName: string; if (isAbstractType(type)) { - const resolvedType = info.schema.getTypeMap()[object.__typename]; + const resolvedType = info.schema.getType(object.__typename); if (resolvedType == null) { throw new Error( `Unable to resolve type '${object.__typename}'. Did you forget to include a transform that renames types? Did you delegate to the original subschema rather that the subschema config object containing the transform?` diff --git a/packages/delegate/src/types.ts b/packages/delegate/src/types.ts index 0df784f044a..695b572400c 100644 --- a/packages/delegate/src/types.ts +++ b/packages/delegate/src/types.ts @@ -196,6 +196,7 @@ export interface StitchingInfo> { } export interface ExternalObject> { + __typename: string; key: any; [OBJECT_SUBSCHEMA_SYMBOL]: GraphQLSchema | SubschemaConfig; [FIELD_SUBSCHEMA_MAP_SYMBOL]: Record>; diff --git a/packages/delegate/tests/errors.test.ts b/packages/delegate/tests/errors.test.ts index e06e0c482d1..8177f0dd428 100644 --- a/packages/delegate/tests/errors.test.ts +++ b/packages/delegate/tests/errors.test.ts @@ -6,7 +6,7 @@ import { stitchSchemas } from '@graphql-tools/stitch'; import { checkResultAndHandleErrors } from '../src/checkResultAndHandleErrors'; import { UNPATHED_ERRORS_SYMBOL } from '../src/symbols'; -import { getUnpathedErrors } from '../src/externalObjects'; +import { getUnpathedErrors } from '../src/mergeFields'; import { delegateToSchema, defaultMergedResolver, DelegationContext } from '../src'; class ErrorWithExtensions extends GraphQLError { diff --git a/packages/schema/src/makeExecutableSchema.ts b/packages/schema/src/makeExecutableSchema.ts index 8dc6c0de675..0afb7b4a916 100644 --- a/packages/schema/src/makeExecutableSchema.ts +++ b/packages/schema/src/makeExecutableSchema.ts @@ -4,7 +4,7 @@ import { pruneSchema } from '@graphql-tools/utils'; import { addResolversToSchema } from './addResolversToSchema'; import { assertResolversPresent } from './assertResolversPresent'; -import { ExecutableSchemaTransformation, IExecutableSchemaDefinition } from './types'; +import { IExecutableSchemaDefinition } from './types'; import { mergeResolvers, mergeTypeDefs } from '@graphql-tools/merge'; /** @@ -55,7 +55,6 @@ export function makeExecutableSchema({ typeDefs, resolvers = {}, resolverValidationOptions = {}, - schemaTransforms: userProvidedSchemaTransforms, parseOptions = {}, inheritResolversFromInterfaces = false, pruningOptions, @@ -70,49 +69,36 @@ export function makeExecutableSchema({ throw new Error('Must provide typeDefs'); } - // Arguments are now validated and cleaned up - const schemaTransforms: ExecutableSchemaTransformation[] = [ - schema => { - // We allow passing in an array of resolver maps, in which case we merge them - - const schemaWithResolvers = addResolversToSchema({ - schema, - resolvers: mergeResolvers(resolvers), - resolverValidationOptions, - inheritResolversFromInterfaces, - updateResolversInPlace, - }); - - if (Object.keys(resolverValidationOptions).length > 0) { - assertResolversPresent(schemaWithResolvers, resolverValidationOptions); - } - - return schemaWithResolvers; - }, - ]; - - if (userProvidedSchemaTransforms) { - schemaTransforms.push(schema => - userProvidedSchemaTransforms.reduce((s, schemaTransform) => schemaTransform(s), schema) - ); - } - - if (pruningOptions) { - schemaTransforms.push(pruneSchema); - } - - let schemaFromTypeDefs: GraphQLSchema; + let schema: GraphQLSchema; if (parseOptions?.commentDescriptions) { const mergedTypeDefs = mergeTypeDefs(typeDefs, { ...parseOptions, commentDescriptions: true, }); - schemaFromTypeDefs = buildSchema(mergedTypeDefs, parseOptions); + schema = buildSchema(mergedTypeDefs, parseOptions); } else { const mergedTypeDefs = mergeTypeDefs(typeDefs, parseOptions); - schemaFromTypeDefs = buildASTSchema(mergedTypeDefs, parseOptions); + schema = buildASTSchema(mergedTypeDefs, parseOptions); + } + + if (pruningOptions) { + schema = pruneSchema(schema); + } + + // We allow passing in an array of resolver maps, in which case we merge them + + schema = addResolversToSchema({ + schema, + resolvers: mergeResolvers(resolvers), + resolverValidationOptions, + inheritResolversFromInterfaces, + updateResolversInPlace, + }); + + if (Object.keys(resolverValidationOptions).length > 0) { + assertResolversPresent(schema, resolverValidationOptions); } - return schemaTransforms.reduce((schema, schemaTransform) => schemaTransform(schema), schemaFromTypeDefs); + return schema; } diff --git a/packages/schema/src/types.ts b/packages/schema/src/types.ts index ed28779d686..47b9f882cb2 100644 --- a/packages/schema/src/types.ts +++ b/packages/schema/src/types.ts @@ -1,5 +1,3 @@ -import { GraphQLSchema } from 'graphql'; - import { TypeSource, IResolvers, @@ -24,10 +22,6 @@ export interface IExecutableSchemaDefinition { * Additional options for validating the provided resolvers */ resolverValidationOptions?: IResolverValidationOptions; - /** - * An array of schema transformation functions - */ - schemaTransforms?: ExecutableSchemaTransformation[]; /** * Additional options for parsing the type definitions if they are provided * as a string @@ -47,5 +41,3 @@ export interface IExecutableSchemaDefinition { */ updateResolversInPlace?: boolean; } - -export type ExecutableSchemaTransformation = (schema: GraphQLSchema) => GraphQLSchema; diff --git a/packages/stitch/src/stitchSchemas.ts b/packages/stitch/src/stitchSchemas.ts index 3678c7fe53e..bc318d5af2f 100644 --- a/packages/stitch/src/stitchSchemas.ts +++ b/packages/stitch/src/stitchSchemas.ts @@ -36,7 +36,6 @@ export function stitchSchemas>({ resolvers = {}, inheritResolversFromInterfaces = false, resolverValidationOptions = {}, - schemaTransforms = [], parseOptions = {}, pruningOptions, updateResolversInPlace, @@ -80,11 +79,11 @@ export function stitchSchemas>({ } const extensions: Array = []; - const directives: Array = []; - const directiveMap: Record = specifiedDirectives.reduce((acc, directive) => { - acc[directive.name] = directive; - return acc; - }, Object.create(null)); + + const directiveMap: Record = Object.create(null); + for (const directive of specifiedDirectives) { + directiveMap[directive.name] = directive; + } const schemaDefs = Object.create(null); const [typeCandidates, rootTypeNameMap] = buildTypeCandidates({ @@ -99,15 +98,11 @@ export function stitchSchemas>({ mergeDirectives, }); - for (const directiveName in directiveMap) { - directives.push(directiveMap[directiveName]); - } - let stitchingInfo = createStitchingInfo(subschemaMap, typeCandidates, mergeTypes); const { typeMap: newTypeMap, directives: newDirectives } = buildTypes({ typeCandidates, - directives, + directives: Object.values(directiveMap), stitchingInfo, rootTypeNames: Object.values(rootTypeNameMap), onTypeConflict, @@ -119,7 +114,7 @@ export function stitchSchemas>({ query: newTypeMap[rootTypeNameMap.query] as GraphQLObjectType, mutation: newTypeMap[rootTypeNameMap.mutation] as GraphQLObjectType, subscription: newTypeMap[rootTypeNameMap.subscription] as GraphQLObjectType, - types: Object.keys(newTypeMap).map(key => newTypeMap[key]), + types: Object.values(newTypeMap), directives: newDirectives, astNode: schemaDefs.schemaDef, extensionASTNodes: schemaDefs.schemaExtensions, @@ -156,10 +151,6 @@ export function stitchSchemas>({ schema = addStitchingInfo(schema, stitchingInfo); - for (const schemaTransform of schemaTransforms) { - schema = schemaTransform(schema); - } - if (pruningOptions) { schema = pruneSchema(schema, pruningOptions); } diff --git a/packages/stitch/tests/fixtures/schemas.ts b/packages/stitch/tests/fixtures/schemas.ts index 7b3e7555346..a2e081c191e 100644 --- a/packages/stitch/tests/fixtures/schemas.ts +++ b/packages/stitch/tests/fixtures/schemas.ts @@ -1,9 +1,6 @@ import { PubSub } from 'graphql-subscriptions'; import { GraphQLSchema, - graphql, - print, - subscribe, Kind, GraphQLScalarType, ValueNode, @@ -12,21 +9,14 @@ import { GraphQLInterfaceType, } from 'graphql'; -import isPromise from 'is-promise'; - -import { ValueOrPromise } from 'value-or-promise'; - import { introspectSchema } from '@graphql-tools/wrap'; import { + AsyncExecutor, IResolvers, - ExecutionResult, - mapAsyncIterator, - isAsyncIterable, - ExecutionRequest, } from '@graphql-tools/utils'; import { makeExecutableSchema } from '@graphql-tools/schema'; -import { SubschemaConfig } from '@graphql-tools/delegate'; +import { SubschemaConfig, createDefaultExecutor } from '@graphql-tools/delegate'; export class CustomError extends GraphQLError { constructor(message: string, extensions: Record) { @@ -682,47 +672,11 @@ export const subscriptionSchema: GraphQLSchema = makeExecutableSchema({ resolvers: subscriptionResolvers, }); -function makeExecutorFromSchema(schema: GraphQLSchema) { - return async ({ document, variables, context, operationType }: ExecutionRequest) => { - if (operationType === 'subscription') { - const result = subscribe( - schema, - document, - null, - context, - variables, - ) as Promise> | ExecutionResult>; - if (isPromise(result)) { - return result.then(asyncIterator => { - assertAsyncIterable(asyncIterator) - return mapAsyncIterator(asyncIterator, (originalResult: ExecutionResult) => JSON.parse(JSON.stringify(originalResult))) - }); - } - return JSON.parse(JSON.stringify(result)); - } - return (new ValueOrPromise(() => graphql( - schema, - print(document), - null, - context, - variables, - )) - .then(originalResult => JSON.parse(JSON.stringify(originalResult))) - .resolve()) as Promise> | ExecutionResult; - }; -} - -function assertAsyncIterable(input: unknown): asserts input is AsyncIterableIterator { - if (isAsyncIterable(input) === false) { - throw new Error("Expected AsyncIterable.") - } -} - export async function makeSchemaRemote( schema: GraphQLSchema, ): Promise { - const executor = makeExecutorFromSchema(schema); - const clientSchema = await introspectSchema(executor); + const executor = createDefaultExecutor(schema); + const clientSchema = await introspectSchema(executor as AsyncExecutor); return { schema: clientSchema, executor, diff --git a/packages/stitch/tests/mergeFailures.test.ts b/packages/stitch/tests/mergeFailures.test.ts index d7a1055d309..5dd3988c912 100644 --- a/packages/stitch/tests/mergeFailures.test.ts +++ b/packages/stitch/tests/mergeFailures.test.ts @@ -213,7 +213,7 @@ describe('merge failures', () => { const expectedResult: ExecutionResult = { data: { thing: null }, - errors: [new GraphQLError('Cannot return null for non-nullable field Thing.description.')], + errors: [new GraphQLError('Cannot return null for non-nullable field Thing.id.')], } expect(result).toEqual(expectedResult); diff --git a/packages/stitch/tests/stitchSchemas.test.ts b/packages/stitch/tests/stitchSchemas.test.ts index 91fec6016e9..4bf73342a3f 100644 --- a/packages/stitch/tests/stitchSchemas.test.ts +++ b/packages/stitch/tests/stitchSchemas.test.ts @@ -14,7 +14,6 @@ import { delegateToSchema, SubschemaConfig } from '@graphql-tools/delegate'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { stitchSchemas } from '../src/stitchSchemas'; import { - cloneSchema, getResolversFromSchema, IResolvers, ExecutionResult, @@ -59,12 +58,12 @@ const testCombinations = [ }, { name: 'recreated', - booking: cloneSchema(localBookingSchema), + booking: localBookingSchema, property: makeExecutableSchema({ typeDefs: printSchema(localPropertySchema), resolvers: getResolversFromSchema(localPropertySchema), }), - product: cloneSchema(localProductSchema), + product: localProductSchema, }, ]; diff --git a/packages/stitch/tests/typeMergingWithDirectives.test.ts b/packages/stitch/tests/typeMergingWithDirectives.test.ts index de32835db5d..1b6b5b9a075 100644 --- a/packages/stitch/tests/typeMergingWithDirectives.test.ts +++ b/packages/stitch/tests/typeMergingWithDirectives.test.ts @@ -15,7 +15,7 @@ import { stitchingDirectives } from '@graphql-tools/stitching-directives'; import { ValidationLevel } from '../src/types'; describe('merging using type merging with directives', () => { - const { allStitchingDirectivesTypeDefs, stitchingDirectivesValidator, stitchingDirectivesTransformer } = stitchingDirectives(); + const { allStitchingDirectivesTypeDefs, stitchingDirectivesTransformer } = stitchingDirectives(); const users = [ { @@ -59,7 +59,6 @@ describe('merging using type merging with directives', () => { _users: (_root, { keys }) => keys.map((key: Record) => users.find(u => u.id === key['id'])), }, }, - schemaTransforms: [stitchingDirectivesValidator], }); const inventory = [ @@ -126,7 +125,6 @@ describe('merging using type merging with directives', () => { }, }, }, - schemaTransforms: [stitchingDirectivesValidator], }); const products = [ @@ -191,7 +189,6 @@ describe('merging using type merging with directives', () => { _productsByUpc: (_root, { upcs }) => upcs.map((upc: any) => products.find(product => product.upc === upc)), } }, - schemaTransforms: [stitchingDirectivesValidator], }); const usernames = [ @@ -295,7 +292,6 @@ describe('merging using type merging with directives', () => { _products: (_root, { input }) => input.keys, }, }, - schemaTransforms: [stitchingDirectivesValidator], }); const stitchedSchema = stitchSchemas({ diff --git a/packages/stitch/tests/typeMergingWithInterfaces.test.ts b/packages/stitch/tests/typeMergingWithInterfaces.test.ts index 5b0fa9511a2..a413ab4aeb1 100644 --- a/packages/stitch/tests/typeMergingWithInterfaces.test.ts +++ b/packages/stitch/tests/typeMergingWithInterfaces.test.ts @@ -15,7 +15,7 @@ import { stitchingDirectives } from '@graphql-tools/stitching-directives'; import { ValidationLevel } from '../src/types'; describe('merging using type merging', () => { - const { allStitchingDirectivesTypeDefs, stitchingDirectivesValidator, stitchingDirectivesTransformer } = stitchingDirectives(); + const { allStitchingDirectivesTypeDefs, stitchingDirectivesTransformer } = stitchingDirectives(); const users = [ { @@ -62,7 +62,6 @@ describe('merging using type merging', () => { }, }, }, - schemaTransforms: [stitchingDirectivesValidator], }); const inventory = [ @@ -126,7 +125,6 @@ describe('merging using type merging', () => { }, }, }, - schemaTransforms: [stitchingDirectivesValidator], }); const products = [ @@ -191,7 +189,6 @@ describe('merging using type merging', () => { } } }, - schemaTransforms: [stitchingDirectivesValidator], }); const usernames = [ @@ -290,7 +287,6 @@ describe('merging using type merging', () => { }, }, }, - schemaTransforms: [stitchingDirectivesValidator], }); const stitchedSchema = stitchSchemas({ diff --git a/packages/stitching-directives/src/properties.ts b/packages/stitching-directives/src/properties.ts index deca3896bef..f33aaec9066 100644 --- a/packages/stitching-directives/src/properties.ts +++ b/packages/stitching-directives/src/properties.ts @@ -54,7 +54,9 @@ export function getProperties(object: Record, propertyTree: Propert const prop = object[key]; - newObject[key] = deepMap(prop, item => getProperties(item, subKey)); + newObject[key] = deepMap(prop, function deepMapFn(item) { + return getProperties(item, subKey); + }); } return newObject; diff --git a/packages/stitching-directives/src/stitchingDirectivesTransformer.ts b/packages/stitching-directives/src/stitchingDirectivesTransformer.ts index 178a2cf9424..570aed10ac2 100644 --- a/packages/stitching-directives/src/stitchingDirectivesTransformer.ts +++ b/packages/stitching-directives/src/stitchingDirectivesTransformer.ts @@ -291,7 +291,7 @@ export function stitchingDirectivesTransformer( } mapSchema(schema, { - [MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName) => { + [MapperKind.OBJECT_FIELD]: function objectFieldMapper(fieldConfig, fieldName) { const mergeDirective = getDirective(schema, fieldConfig, mergeDirectiveName, pathToDirectivesInExtensions)?.[0]; if (mergeDirective != null) { @@ -319,7 +319,7 @@ export function stitchingDirectivesTransformer( const typeNames: Array = mergeDirective['types']; - forEachConcreteTypeName(namedType, schema, typeNames, typeName => { + forEachConcreteTypeName(namedType, schema, typeNames, function generateResolveInfo(typeName) { const parsedMergeArgsExpr = parseMergeArgsExpr( mergeArgsExpr, allSelectionSetsByType[typeName] == null @@ -465,14 +465,16 @@ function forEachConcreteType( } function generateKeyFn(mergedTypeResolverInfo: MergedTypeResolverInfo): (originalResult: any) => any { - return (originalResult: any): any => getProperties(originalResult, mergedTypeResolverInfo.usedProperties); + return function keyFn(originalResult: any) { + return getProperties(originalResult, mergedTypeResolverInfo.usedProperties); + }; } function generateArgsFromKeysFn( mergedTypeResolverInfo: MergedTypeResolverInfo ): (keys: ReadonlyArray) => Record { const { expansions, args } = mergedTypeResolverInfo; - return (keys: ReadonlyArray): Record => { + return function generateArgsFromKeys(keys: ReadonlyArray): Record { const newArgs = mergeDeep([{}, args]); if (expansions) { for (const expansion of expansions) { @@ -499,7 +501,7 @@ function generateArgsFromKeysFn( function generateArgsFn(mergedTypeResolverInfo: MergedTypeResolverInfo): (originalResult: any) => Record { const { mappingInstructions, args, usedProperties } = mergedTypeResolverInfo; - return (originalResult: any): Record => { + return function generateArgs(originalResult: any): Record { const newArgs = mergeDeep([{}, args]); const filteredResult = getProperties(originalResult, usedProperties); if (mappingInstructions) { diff --git a/packages/stitching-directives/tests/stitchingDirectivesValidator.test.ts b/packages/stitching-directives/tests/stitchingDirectivesValidator.test.ts index 708d2918e6a..03988182170 100644 --- a/packages/stitching-directives/tests/stitchingDirectivesValidator.test.ts +++ b/packages/stitching-directives/tests/stitchingDirectivesValidator.test.ts @@ -11,7 +11,7 @@ describe('type merging directives', () => { `; expect(() => makeExecutableSchema({ typeDefs })).not.toThrow(); - expect(() => makeExecutableSchema({ typeDefs, schemaTransforms: [stitchingDirectivesValidator] })).not.toThrow(); + expect(() => stitchingDirectivesValidator(makeExecutableSchema({ typeDefs }))).not.toThrow(); }); test('throws an error if type selectionSet invalid', () => { @@ -28,7 +28,7 @@ describe('type merging directives', () => { `; expect(() => makeExecutableSchema({ typeDefs })).not.toThrow(); - expect(() => makeExecutableSchema({ typeDefs, schemaTransforms: [stitchingDirectivesValidator] })).toThrow(); + expect(() => stitchingDirectivesValidator(makeExecutableSchema({ typeDefs }))).toThrow(); }); test('does not throw an error if type selectionSet valid', () => { @@ -45,7 +45,7 @@ describe('type merging directives', () => { `; expect(() => makeExecutableSchema({ typeDefs })).not.toThrow(); - expect(() => makeExecutableSchema({ typeDefs, schemaTransforms: [stitchingDirectivesValidator] })).not.toThrow(); + expect(() => stitchingDirectivesValidator(makeExecutableSchema({ typeDefs }))).not.toThrow(); }); test('throws an error if computed selectionSet invalid', () => { @@ -62,7 +62,7 @@ describe('type merging directives', () => { `; expect(() => makeExecutableSchema({ typeDefs })).not.toThrow(); - expect(() => makeExecutableSchema({ typeDefs, schemaTransforms: [stitchingDirectivesValidator] })).toThrow(); + expect(() => stitchingDirectivesValidator(makeExecutableSchema({ typeDefs }))).toThrow(); }); test('does not throw an error if computed selectionSet valid', () => { @@ -79,7 +79,7 @@ describe('type merging directives', () => { `; expect(() => makeExecutableSchema({ typeDefs })).not.toThrow(); - expect(() => makeExecutableSchema({ typeDefs, schemaTransforms: [stitchingDirectivesValidator] })).not.toThrow(); + expect(() => stitchingDirectivesValidator(makeExecutableSchema({ typeDefs }))).not.toThrow(); }); test('throws an error if merge argsExpr invalid', () => { @@ -98,7 +98,7 @@ describe('type merging directives', () => { `; expect(() => makeExecutableSchema({ typeDefs })).not.toThrow(); - expect(() => makeExecutableSchema({ typeDefs, schemaTransforms: [stitchingDirectivesValidator] })).toThrow(); + expect(() => stitchingDirectivesValidator(makeExecutableSchema({ typeDefs }))).toThrow(); }); test('does not throw an error if merge argsExpr valid', () => { @@ -117,13 +117,13 @@ describe('type merging directives', () => { `; expect(() => makeExecutableSchema({ typeDefs })).not.toThrow(); - expect(() => makeExecutableSchema({ typeDefs, schemaTransforms: [stitchingDirectivesValidator] })).not.toThrow(); + expect(() => stitchingDirectivesValidator(makeExecutableSchema({ typeDefs }))).not.toThrow(); }); test('does not throw an error when using merge with argsExpr on a multiple args endpoint', () => { const typeDefs = ` ${allStitchingDirectivesTypeDefs} - + type Query { _user(id: ID, name: String): User @merge(argsExpr: "id: $key.id, name: $key.name") } @@ -135,7 +135,7 @@ describe('type merging directives', () => { `; expect(() => makeExecutableSchema({ typeDefs })).not.toThrow(); - expect(() => makeExecutableSchema({ typeDefs, schemaTransforms: [stitchingDirectivesValidator] })).not.toThrow(); + expect(() => stitchingDirectivesValidator(makeExecutableSchema({ typeDefs }))).not.toThrow(); }); test('does not throw an error if merge used without arguments', () => { @@ -154,6 +154,6 @@ describe('type merging directives', () => { `; expect(() => makeExecutableSchema({ typeDefs })).not.toThrow(); - expect(() => makeExecutableSchema({ typeDefs, schemaTransforms: [stitchingDirectivesValidator] })).not.toThrow(); + expect(() => stitchingDirectivesValidator(makeExecutableSchema({ typeDefs }))).not.toThrow(); }); }); diff --git a/packages/utils/src/Interfaces.ts b/packages/utils/src/Interfaces.ts index 147a6716b86..5442b0419e7 100644 --- a/packages/utils/src/Interfaces.ts +++ b/packages/utils/src/Interfaces.ts @@ -65,8 +65,8 @@ export interface ExecutionRequest< document: DocumentNode; operationType: OperationTypeNode; variables?: TArgs; - extensions?: TExtensions; operationName?: string; + extensions?: TExtensions; // If the request will be executed locally, it may contain a rootValue rootValue?: TRootValue; // If the request originates within execution of a parent request, it may contain the parent context and info diff --git a/packages/utils/src/clone.ts b/packages/utils/src/clone.ts deleted file mode 100644 index 358875ab8ad..00000000000 --- a/packages/utils/src/clone.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { - GraphQLDirective, - GraphQLEnumType, - GraphQLInputObjectType, - GraphQLInterfaceType, - GraphQLObjectType, - GraphQLNamedType, - GraphQLScalarType, - GraphQLSchema, - GraphQLUnionType, - isObjectType, - isInterfaceType, - isUnionType, - isInputObjectType, - isEnumType, - isScalarType, - isSpecifiedScalarType, - isSpecifiedDirective, -} from 'graphql'; - -import { mapSchema } from './mapSchema'; - -export function cloneDirective(directive: GraphQLDirective): GraphQLDirective { - return isSpecifiedDirective(directive) ? directive : new GraphQLDirective(directive.toConfig()); -} - -export function cloneType(type: GraphQLNamedType): GraphQLNamedType { - if (isObjectType(type)) { - const config = type.toConfig(); - return new GraphQLObjectType({ - ...config, - interfaces: typeof config.interfaces === 'function' ? config.interfaces : config.interfaces.slice(), - }); - } else if (isInterfaceType(type)) { - const config = type.toConfig() as any; - const newConfig = { - ...config, - interfaces: [...((typeof config.interfaces === 'function' ? config.interfaces() : config.interfaces) || [])], - }; - return new GraphQLInterfaceType(newConfig); - } else if (isUnionType(type)) { - const config = type.toConfig(); - return new GraphQLUnionType({ - ...config, - types: config.types.slice(), - }); - } else if (isInputObjectType(type)) { - return new GraphQLInputObjectType(type.toConfig()); - } else if (isEnumType(type)) { - return new GraphQLEnumType(type.toConfig()); - } else if (isScalarType(type)) { - return isSpecifiedScalarType(type) ? type : new GraphQLScalarType(type.toConfig()); - } - - throw new Error(`Invalid type ${type as string}`); -} - -export function cloneSchema(schema: GraphQLSchema): GraphQLSchema { - return mapSchema(schema); -} diff --git a/packages/utils/src/getArgumentValues.ts b/packages/utils/src/getArgumentValues.ts index 1ad1003b6e6..e1ddf78920c 100644 --- a/packages/utils/src/getArgumentValues.ts +++ b/packages/utils/src/getArgumentValues.ts @@ -46,14 +46,12 @@ export function getArgumentValues( {} ); - for (const argDef of def.args) { - const name = argDef.name; - const argType = argDef.type; + for (const { name, type: argType, defaultValue } of def.args) { const argumentNode = argNodeMap[name]; if (!argumentNode) { - if (argDef.defaultValue !== undefined) { - coercedValues[name] = argDef.defaultValue; + if (defaultValue !== undefined) { + coercedValues[name] = defaultValue; } else if (isNonNullType(argType)) { throw new GraphQLError( `Argument "${name}" of required type "${inspect(argType)}" ` + 'was not provided.', @@ -68,9 +66,9 @@ export function getArgumentValues( if (valueNode.kind === Kind.VARIABLE) { const variableName = valueNode.name.value; - if (variableValues == null || !(variableName in variableMap)) { - if (argDef.defaultValue !== undefined) { - coercedValues[name] = argDef.defaultValue; + if (variableValues == null || !variableMap[variableName]) { + if (defaultValue !== undefined) { + coercedValues[name] = defaultValue; } else if (isNonNullType(argType)) { throw new GraphQLError( `Argument "${name}" of required type "${inspect(argType)}" ` + diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index ca51324521c..74d64e39aea 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -14,7 +14,6 @@ export * from './create-schema-definition'; export * from './build-operation-for-field'; export * from './types'; export * from './filterSchema'; -export * from './clone'; export * from './heal'; export * from './getResolversFromSchema'; export * from './forEachField'; diff --git a/packages/utils/tests/schemaTransforms.test.ts b/packages/utils/tests/schemaTransforms.test.ts deleted file mode 100644 index 12643ddafeb..00000000000 --- a/packages/utils/tests/schemaTransforms.test.ts +++ /dev/null @@ -1,1235 +0,0 @@ -import { createHash } from 'crypto'; - -import { - GraphQLObjectType, - GraphQLSchema, - printSchema, - defaultFieldResolver, - graphql, - GraphQLString, - GraphQLScalarType, - GraphQLInputFieldConfig, - GraphQLFieldConfig, - isNonNullType, - isScalarType, - GraphQLID, - isListType, - GraphQLOutputType, - GraphQLUnionType, - GraphQLEnumType, - GraphQLInt, - GraphQLList, - getNamedType, - GraphQLNonNull, -} from 'graphql'; - -import formatDate from 'dateformat'; - -import { makeExecutableSchema } from '@graphql-tools/schema'; -import { - mapSchema, - MapperKind, - getDirectives, - ExecutionResult, - getDirective, -} from '@graphql-tools/utils'; - -import { addMocksToSchema } from '@graphql-tools/mock'; - -const typeDefs = ` - directive @schemaDirective(role: String) on SCHEMA - directive @schemaExtensionDirective(role: String) on SCHEMA - directive @queryTypeDirective on OBJECT - directive @queryTypeExtensionDirective on OBJECT - directive @queryFieldDirective on FIELD_DEFINITION - directive @enumTypeDirective on ENUM - directive @enumTypeExtensionDirective on ENUM - directive @enumValueDirective on ENUM_VALUE - directive @dateDirective(tz: String) on SCALAR - directive @dateExtensionDirective(tz: String) on SCALAR - directive @interfaceDirective on INTERFACE - directive @interfaceExtensionDirective on INTERFACE - directive @interfaceFieldDirective on FIELD_DEFINITION - directive @inputTypeDirective on INPUT_OBJECT - directive @inputTypeExtensionDirective on INPUT_OBJECT - directive @inputFieldDirective on INPUT_FIELD_DEFINITION - directive @mutationTypeDirective on OBJECT - directive @mutationTypeExtensionDirective on OBJECT - directive @mutationArgumentDirective on ARGUMENT_DEFINITION - directive @mutationMethodDirective on FIELD_DEFINITION - directive @objectTypeDirective on OBJECT - directive @objectTypeExtensionDirective on OBJECT - directive @objectFieldDirective on FIELD_DEFINITION - directive @unionDirective on UNION - directive @unionExtensionDirective on UNION - - schema @schemaDirective(role: "admin") { - query: Query - mutation: Mutation - } - - extend schema @schemaExtensionDirective(role: "admin") - - type Query @queryTypeDirective { - people: [Person] @queryFieldDirective - } - - extend type Query @queryTypeExtensionDirective - - enum Gender @enumTypeDirective { - NONBINARY @enumValueDirective - FEMALE - MALE - } - - extend enum Gender @enumTypeExtensionDirective - scalar Date @dateDirective(tz: "utc") - - extend scalar Date @dateExtensionDirective(tz: "utc") - interface Named @interfaceDirective { - name: String! @interfaceFieldDirective - } - - extend interface Named @interfaceExtensionDirective - input PersonInput @inputTypeDirective { - name: String! @inputFieldDirective - gender: Gender - } - - extend input PersonInput @inputTypeExtensionDirective - type Mutation @mutationTypeDirective { - addPerson( - input: PersonInput @mutationArgumentDirective - ): Person @mutationMethodDirective - } - - extend type Mutation @mutationTypeExtensionDirective - type Person implements Named @objectTypeDirective { - id: ID! @objectFieldDirective - name: String! - } - - extend type Person @objectTypeExtensionDirective - union WhateverUnion @unionDirective = Person | Query | Mutation - - extend union WhateverUnion @unionExtensionDirective -`; - -describe('@directives', () => { - test('can be iterated with mapSchema', () => { - const visited: Set = new Set(); - - function addObjectTypeToSetDirective(directiveNames: Array): (schema: GraphQLSchema) => GraphQLSchema { - return schema => mapSchema(schema, { - [MapperKind.OBJECT_TYPE]: type => { - const directives = getDirectives(schema, type); - for (const directive of directives) { - if (directiveNames.includes(directive.name)) { - expect(type.name).toBe(schema.getQueryType()?.name); - visited.add(type); - } - } - return undefined; - } - }) - } - - makeExecutableSchema({ - typeDefs, - schemaTransforms: [ - addObjectTypeToSetDirective(['queryTypeDirective', 'queryTypeExtensionDirective']) - ] - }); - - expect(visited.size).toBe(1); - }); - - test('can visit the schema directly', () => { - const visited: Array = []; - - function recordSchemaDirectiveUses(directiveNames: Array): (schema: GraphQLSchema) => GraphQLSchema { - return schema => { - const directives = getDirectives(schema, schema); - for (const directive of directives) { - if (directiveNames.includes(directive.name)) { - visited.push(schema); - } - } - return schema; - } - } - - const schema = makeExecutableSchema({ - typeDefs, - schemaTransforms: [ - recordSchemaDirectiveUses(['schemaDirective', 'schemaExtensionDirective']) - ] - }); - - const printedSchema = printSchema(makeExecutableSchema({ typeDefs })); - expect(printSchema(schema)).toEqual(printedSchema); - - expect(visited.length).toBe(2); - expect(printSchema(visited[0])).toEqual(printedSchema); - expect(printSchema(visited[1])).toEqual(printedSchema); - }); - - test('can be used to implement the @upper example', () => { - function upperDirective(directiveName: string) { - return { - upperDirectiveTypeDefs: `directive @${directiveName} on FIELD_DEFINITION`, - upperDirectiveTransformer: (schema: GraphQLSchema) => mapSchema(schema, { - [MapperKind.OBJECT_FIELD]: (fieldConfig) => { - const upperDirective = getDirective(schema, fieldConfig, directiveName)?.[0]; - if (upperDirective) { - const { resolve = defaultFieldResolver } = fieldConfig; - fieldConfig.resolve = async function (source, args, context, info) { - const result = await resolve(source, args, context, info); - if (typeof result === 'string') { - return result.toUpperCase(); - } - return result; - } - return fieldConfig; - } - } - }) - }; - } - - const { upperDirectiveTypeDefs, upperDirectiveTransformer } = upperDirective('upper'); - - const schema = makeExecutableSchema({ - typeDefs: [upperDirectiveTypeDefs, ` - type Query { - hello: String @upper - } - `], - resolvers: { - Query: { - hello() { - return 'hello world'; - }, - }, - }, - schemaTransforms: [upperDirectiveTransformer], - }); - - return graphql( - schema, - ` - query { - hello - } - `, - ).then(({ data }) => { - expect(data).toEqual({ - hello: 'HELLO WORLD', - }); - }); - }); - - test('can be used to implement the @deprecated example', () => { - function deprecatedDirective(directiveName: string) { - return { - deprecatedDirectiveTypeDefs: `directive @${directiveName}(reason: String) on FIELD_DEFINITION | ENUM_VALUE`, - deprecatedDirectiveTransformer: (schema: GraphQLSchema) => mapSchema(schema, { - [MapperKind.OBJECT_FIELD]: (fieldConfig) => { - const deprecatedDirective = getDirective(schema, fieldConfig, directiveName)?.[0]; - if (deprecatedDirective) { - fieldConfig.deprecationReason = deprecatedDirective['reason']; - return fieldConfig; - } - }, - [MapperKind.ENUM_VALUE]: (enumValueConfig) => { - const deprecatedDirective = getDirective(schema, enumValueConfig, directiveName)?.[0]; - if (deprecatedDirective) { - enumValueConfig.deprecationReason = deprecatedDirective['reason']; - return enumValueConfig; - } - } - }), - }; - } - - const { deprecatedDirectiveTypeDefs, deprecatedDirectiveTransformer } = deprecatedDirective('deprecated'); - - const schema = makeExecutableSchema({ - typeDefs: [deprecatedDirectiveTypeDefs, ` - type ExampleType { - newField: String - oldField: String @deprecated(reason: "Use \`newField\`.") - } - - type Query { - rootField: ExampleType - } - `], - schemaTransforms: [deprecatedDirectiveTransformer], - }); - - expect((schema.getType('ExampleType') as GraphQLObjectType).getFields()['oldField'].deprecationReason).toBe('Use \`newField\`.') - }); - - test('can be used to implement the @date example', () => { - function dateDirective(directiveName: string) { - return { - dateDirectiveTypeDefs: `directive @${directiveName}(format: String) on FIELD_DEFINITION`, - dateDirectiveTransformer: (schema: GraphQLSchema) => mapSchema(schema, { - [MapperKind.OBJECT_FIELD]: (fieldConfig) => { - const dateDirective = getDirective(schema, fieldConfig, directiveName)?.[0]; - if (dateDirective) { - const { resolve = defaultFieldResolver } = fieldConfig; - const { format } = dateDirective; - fieldConfig.resolve = async (source, args, context, info) => { - const date = await resolve(source, args, context, info); - return formatDate(date, format, true); - }; - return fieldConfig; - } - } - }), - }; - } - - const { dateDirectiveTypeDefs, dateDirectiveTransformer } = dateDirective('date'); - - const schema = makeExecutableSchema({ - typeDefs: [dateDirectiveTypeDefs, ` - scalar Date - - type Query { - today: Date @date(format: "mmmm d, yyyy") - } - `], - resolvers: { - Query: { - today() { - return new Date(1519688273858).toUTCString(); - }, - }, - }, - schemaTransforms: [dateDirectiveTransformer], - }); - - return graphql( - schema, - ` - query { - today - } - `, - ).then(({ data }) => { - expect(data).toEqual({ - today: 'February 26, 2018', - }); - }); - }); - - test('can be used to implement the @date by adding an argument', async () => { - function formattableDateDirective(directiveName: string) { - return { - formattableDateDirectiveTypeDefs: `directive @${directiveName}( - defaultFormat: String = "mmmm d, yyyy" - ) on FIELD_DEFINITION - `, - formattableDateDirectiveTransformer: (schema: GraphQLSchema) => mapSchema(schema, { - [MapperKind.OBJECT_FIELD]: (fieldConfig) => { - const dateDirective = getDirective(schema, fieldConfig, directiveName)?.[0]; - if (dateDirective) { - const { resolve = defaultFieldResolver } = fieldConfig; - const { defaultFormat } = dateDirective; - - if (!fieldConfig.args) { - throw new Error("Unexpected Error. args should be defined.") - } - - fieldConfig.args['format'] = { - type: GraphQLString, - }; - - fieldConfig.type = GraphQLString; - fieldConfig.resolve = async ( - source, - { format, ...args }, - context, - info, - ) => { - const newFormat = format || defaultFormat; - const date = await resolve(source, args, context, info); - return formatDate(date, newFormat, true); - }; - return fieldConfig; - } - } - }), - }; - } - - const { formattableDateDirectiveTypeDefs, formattableDateDirectiveTransformer } = formattableDateDirective('date'); - - const schema = makeExecutableSchema({ - typeDefs: [formattableDateDirectiveTypeDefs, ` - scalar Date - - type Query { - today: Date @date - } - `], - resolvers: { - Query: { - today() { - return new Date(1521131357195); - }, - }, - }, - schemaTransforms: [formattableDateDirectiveTransformer], - }); - - const resultNoArg = await graphql(schema, 'query { today }'); - - if (resultNoArg.errors != null) { - expect(resultNoArg.errors).toEqual([]); - } - - expect(resultNoArg.data).toEqual({ today: 'March 15, 2018' }); - - const resultWithArg = await graphql( - schema, - ` - query { - today(format: "dd mmm yyyy") - } - `, - ); - - if (resultWithArg.errors != null) { - expect(resultWithArg.errors).toEqual([]); - } - - expect(resultWithArg.data).toEqual({ today: '15 Mar 2018' }); - }); - - test('can be used to implement the @auth example', async () => { - function authDirective(directiveName: string, getUserFn: (token: string) => { hasRole: (role: string) => boolean} ) { - const typeDirectiveArgumentMaps: Record = {}; - return { - authDirectiveTypeDefs: `directive @${directiveName}( - requires: Role = ADMIN, - ) on OBJECT | FIELD_DEFINITION - - enum Role { - ADMIN - REVIEWER - USER - UNKNOWN - }`, - authDirectiveTransformer: (schema: GraphQLSchema) => mapSchema(schema, { - [MapperKind.TYPE]: (type) => { - const authDirective = getDirective(schema, type, directiveName)?.[0]; - if (authDirective) { - typeDirectiveArgumentMaps[type.name] = authDirective; - } - return undefined; - }, - [MapperKind.OBJECT_FIELD]: (fieldConfig, _fieldName, typeName) => { - const authDirective = getDirective(schema, fieldConfig, directiveName)?.[0] ?? typeDirectiveArgumentMaps[typeName]; - if (authDirective) { - const { requires } = authDirective; - if (requires) { - const { resolve = defaultFieldResolver } = fieldConfig; - fieldConfig.resolve = function (source, args, context, info) { - const user = getUserFn(context.headers.authToken); - if (!user.hasRole(requires)) { - throw new Error('not authorized'); - } - return resolve(source, args, context, info); - } - return fieldConfig; - } - } - } - }) - }; - } - - function getUser(token: string) { - const roles = ['UNKNOWN', 'USER', 'REVIEWER', 'ADMIN']; - return { - hasRole: (role: string) => { - const tokenIndex = roles.indexOf(token); - const roleIndex = roles.indexOf(role); - return roleIndex >= 0 && tokenIndex >= roleIndex; - }, - }; - } - - const { authDirectiveTypeDefs, authDirectiveTransformer } = authDirective('auth', getUser); - - const schema = makeExecutableSchema({ - typeDefs: [authDirectiveTypeDefs, ` - type User @auth(requires: USER) { - name: String - banned: Boolean @auth(requires: ADMIN) - canPost: Boolean @auth(requires: REVIEWER) - } - - type Query { - users: [User] - } - `], - resolvers: { - Query: { - users() { - return [ - { - banned: true, - canPost: false, - name: 'Ben', - }, - ]; - }, - }, - }, - schemaTransforms: [authDirectiveTransformer], - }); - - function execWithRole(role: string): Promise { - return graphql( - schema, - ` - query { - users { - name - banned - canPost - } - } - `, - null, - { - headers: { - authToken: role, - }, - }, - ); - } - - function assertStringArray(input: Array): asserts input is Array { - if (input.some(item => typeof item !== "string")) { - throw new Error("All items in array should be strings.") - } - } - - function checkErrors( - expectedCount: number, - ...expectedNames: Array - ) { - return function ({ - errors = [], - data, - }: ExecutionResult) { - expect(errors.length).toBe(expectedCount); - expect( - errors.every((error) => error.message === 'not authorized'), - ).toBeTruthy(); - const actualNames = errors.map((error) => error.path!.slice(-1)[0]); - assertStringArray(actualNames) - expect(expectedNames.sort((a, b) => a.localeCompare(b))).toEqual( - actualNames.sort((a, b) => a.localeCompare(b)), - ); - return data; - }; - } - - return Promise.all([ - execWithRole('UNKNOWN').then(checkErrors(3, 'banned', 'canPost', 'name')), - execWithRole('USER').then(checkErrors(2, 'banned', 'canPost')), - execWithRole('REVIEWER').then(checkErrors(1, 'banned')), - execWithRole('ADMIN') - .then(checkErrors(0)) - .then((data) => { - expect(data?.['users'].length).toBe(1); - expect(data?.['users'][0].banned).toBe(true); - expect(data?.['users'][0].canPost).toBe(false); - expect(data?.['users'][0].name).toBe('Ben'); - }), - ]); - }); - - test('can be used to implement the @length example', async () => { - function lengthDirective(directiveName: string) { - class LimitedLengthType extends GraphQLScalarType { - constructor(type: GraphQLScalarType, maxLength: number) { - super({ - name: `${type.name}WithLengthAtMost${maxLength.toString()}`, - - serialize(value: string) { - const newValue: string = type.serialize(value); - expect(typeof newValue.length).toBe('number'); - if (newValue.length > maxLength) { - throw new Error( - `expected ${newValue.length.toString( - 10, - )} to be at most ${maxLength.toString(10)}`, - ); - } - return newValue; - }, - - parseValue(value: string) { - return type.parseValue(value); - }, - - parseLiteral(ast) { - return type.parseLiteral(ast, {}); - }, - }); - } - } - - const limitedLengthTypes: Record> = {}; - - function getLimitedLengthType(type: GraphQLScalarType, maxLength: number): GraphQLScalarType { - const limitedLengthTypesByTypeName = limitedLengthTypes[type.name] - if (!limitedLengthTypesByTypeName) { - const newType = new LimitedLengthType(type, maxLength); - limitedLengthTypes[type.name] = {} - limitedLengthTypes[type.name][maxLength] = newType; - return newType; - } - - const limitedLengthType = limitedLengthTypesByTypeName[maxLength]; - if (!limitedLengthType) { - const newType = new LimitedLengthType(type, maxLength); - limitedLengthTypesByTypeName[maxLength] = newType; - return newType; - } - - return limitedLengthType; - } - - function wrapType | GraphQLInputFieldConfig>(fieldConfig: F, directiveArgumentMap: Record): void { - if (isNonNullType(fieldConfig.type) && isScalarType(fieldConfig.type.ofType)) { - fieldConfig.type = getLimitedLengthType(fieldConfig.type.ofType, directiveArgumentMap['max']); - } else if (isScalarType(fieldConfig.type)) { - fieldConfig.type = getLimitedLengthType(fieldConfig.type, directiveArgumentMap['max']); - } else { - throw new Error(`Not a scalar type: ${fieldConfig.type.toString()}`); - } - } - - return { - lengthDirectiveTypeDefs: `directive @${directiveName}(max: Int) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION`, - lengthDirectiveTransformer: (schema: GraphQLSchema) => mapSchema(schema, { - [MapperKind.FIELD]: (fieldConfig) => { - const lengthDirective = getDirective(schema, fieldConfig, directiveName)?.[0]; - if (lengthDirective) { - wrapType(fieldConfig, lengthDirective); - return fieldConfig; - } - } - }), - }; - } - - const { lengthDirectiveTypeDefs, lengthDirectiveTransformer } = lengthDirective('length'); - - const schema = makeExecutableSchema({ - typeDefs: [lengthDirectiveTypeDefs, ` - type Query { - books: [Book] - } - - type Book { - title: String @length(max: 10) - } - - type Mutation { - createBook(book: BookInput): Book - } - - input BookInput { - title: String! @length(max: 10) - }`] - , - resolvers: { - Query: { - books() { - return [ - { - title: 'abcdefghijklmnopqrstuvwxyz', - }, - ]; - }, - }, - Mutation: { - createBook(_parent, args) { - return args.book; - }, - }, - }, - schemaTransforms: [lengthDirectiveTransformer], - }); - - const { errors } = await graphql( - schema, - ` - query { - books { - title - } - } - `, - ); - expect(errors?.length).toBe(1); - expect(errors?.[0].message).toBe('expected 26 to be at most 10'); - - const result = await graphql( - schema, - ` - mutation { - createBook(book: { title: "safe title" }) { - title - } - } - `, - ); - - if (result.errors != null) { - expect(result.errors).toEqual([]); - } - - expect(result.data).toEqual({ - createBook: { - title: 'safe title', - }, - }); - }); - - test('can be used to implement the @uniqueID example', () => { - function uniqueIDDirective(directiveName: string) { - return { - uniqueIDDirectiveTypeDefs: `directive @${directiveName}(name: String, from: [String]) on OBJECT`, - uniqueIDDirectiveTransformer: (schema: GraphQLSchema) => mapSchema(schema, { - [MapperKind.OBJECT_TYPE]: (type) => { - const uniqueIDDirective = getDirective(schema, type, directiveName)?.[0]; - if (uniqueIDDirective) { - const { name, from } = uniqueIDDirective; - const config = type.toConfig(); - config.fields[name] = { - type: GraphQLID, - description: 'Unique ID', - args: {}, - resolve(object: any) { - const hash = createHash('sha1'); - hash.update(type.name); - for (const fieldName of from ){ - hash.update(String(object[fieldName])); - } - return hash.digest('hex'); - }, - }; - return new GraphQLObjectType(config); - } - } - }), - }; - } - - const { uniqueIDDirectiveTypeDefs, uniqueIDDirectiveTransformer } = uniqueIDDirective('uniqueID'); - - const schema = makeExecutableSchema({ - typeDefs: [uniqueIDDirectiveTypeDefs, ` - type Query { - people: [Person] - locations: [Location] - } - - type Person @uniqueID(name: "uid", from: ["personID"]) { - personID: Int - name: String - } - - type Location @uniqueID(name: "uid", from: ["locationID"]) { - locationID: Int - address: String - } - `], - resolvers: { - Query: { - people() { - return [ - { - personID: 1, - name: 'Ben', - }, - ]; - }, - locations() { - return [ - { - locationID: 1, - address: '140 10th St', - }, - ]; - }, - }, - }, - schemaTransforms: [uniqueIDDirectiveTransformer], - }); - - return graphql( - schema, - ` - query { - people { - uid - personID - name - } - locations { - uid - locationID - address - } - } - `, - null, - {}, - ).then((result) => { - const { data } = result; - - expect(data?.['people']).toEqual([ - { - uid: '580a207c8e94f03b93a2b01217c3cc218490571a', - personID: 1, - name: 'Ben', - }, - ]); - - expect(data?.['locations']).toEqual([ - { - uid: 'c31b71e6e23a7ae527f94341da333590dd7cba96', - locationID: 1, - address: '140 10th St', - }, - ]); - }); - }); - - test('automatically updates references to changed types', () => { - function renameObjectTypeToHumanDirective(directiveName: string): (schema: GraphQLSchema) => GraphQLSchema { - return schema => mapSchema(schema, { - [MapperKind.OBJECT_TYPE]: (type) => { - const directive = getDirective(schema, type, directiveName)?.[0]; - if (directive) { - const config = type.toConfig(); - config.name = 'Human'; - return new GraphQLObjectType(config); - } - } - }); - } - - const schema = makeExecutableSchema({ - typeDefs, - schemaTransforms: [renameObjectTypeToHumanDirective('objectTypeDirective')] - }); - - const Query = schema.getType('Query') as GraphQLObjectType; - const peopleType = Query.getFields()['people'].type; - if (isListType(peopleType)) { - expect(peopleType.ofType).toBe(schema.getType('Human')); - } else { - throw new Error('Query.people not a GraphQLList type'); - } - - const Mutation = schema.getType('Mutation') as GraphQLObjectType; - const addPersonResultType = Mutation.getFields()['addPerson'].type; - expect(addPersonResultType).toBe( - schema.getType('Human') as GraphQLOutputType, - ); - - const WhateverUnion = schema.getType('WhateverUnion') as GraphQLUnionType; - const found = WhateverUnion.getTypes().some((type) => { - if (type.name === 'Human') { - expect(type).toBe(schema.getType('Human')); - return true; - } - return false; - }); - expect(found).toBe(true); - - // Make sure that the Person type was actually removed. - expect(typeof schema.getType('Person')).toBe('undefined'); - }); - - test('can remove enum values', () => { - function removeEnumValueDirective(directiveName: string): (schema: GraphQLSchema) => GraphQLSchema { - return schema => mapSchema(schema, { - [MapperKind.ENUM_VALUE]: (enumValueConfig) => { - const directive = getDirective(schema, enumValueConfig, directiveName)?.[0]; - if (directive?.['if']) { - return null; - } - } - }); - } - - const schema = makeExecutableSchema({ - typeDefs: ` - directive @remove(if: Boolean) on ENUM_VALUE - - type Query { - age(unit: AgeUnit): Int - } - - enum AgeUnit { - DOG_YEARS - TURTLE_YEARS @remove(if: true) - PERSON_YEARS @remove(if: false) - }`, - - schemaTransforms: [removeEnumValueDirective('remove')] - }); - - const AgeUnit = schema.getType('AgeUnit') as GraphQLEnumType; - expect(AgeUnit.getValues().map((value) => value.name)).toEqual([ - 'DOG_YEARS', - 'PERSON_YEARS', - ]); - }); - - test("can modify enum value's external value", () => { - function modifyExternalEnumValueDirective(directiveName: string): (schema: GraphQLSchema) => GraphQLSchema { - return schema => mapSchema(schema, { - [MapperKind.ENUM_VALUE]: (enumValueConfig) => { - const directive = getDirective(schema, enumValueConfig, directiveName)?.[0]; - if (directive) { - return [directive['new'], enumValueConfig]; - } - } - }); - } - - const schema = makeExecutableSchema({ - typeDefs: ` - directive @value(new: String!) on ENUM_VALUE - - type Query { - device: Device - } - - enum Device { - PHONE - TABLET - LAPTOP @value(new: "COMPUTER") - } - `, - schemaTransforms: [modifyExternalEnumValueDirective('value')] - }); - - const Device = schema.getType('Device') as GraphQLEnumType; - expect(Device.getValues().map((value) => value.name)).toEqual([ - 'PHONE', - 'TABLET', - 'COMPUTER', - ]); - }); - - test("can modify enum value's internal value", () => { - function modifyInternalEnumValueDirective(directiveName: string): (schema: GraphQLSchema) => GraphQLSchema { - return schema => mapSchema(schema, { - [MapperKind.ENUM_VALUE]: (enumValueConfig) => { - const directive = getDirective(schema, enumValueConfig, directiveName)?.[0]; - if (directive) { - enumValueConfig.value = directive['new']; - return enumValueConfig; - } - } - }); - } - - const schema = makeExecutableSchema({ - typeDefs: ` - directive @value(new: String!) on ENUM_VALUE - - type Query { - device: Device - } - - enum Device { - PHONE - TABLET - LAPTOP @value(new: "COMPUTER") - } - `, - schemaTransforms: [modifyInternalEnumValueDirective('value')] - }); - - const Device = schema.getType('Device') as GraphQLEnumType; - expect(Device.getValues().map((value) => value.value)).toEqual([ - 'PHONE', - 'TABLET', - 'COMPUTER', - ]); - }); - - test('can swap names of GraphQLNamedType objects', () => { - function renameObjectTypeDirective(directiveName: string): (schema: GraphQLSchema) => GraphQLSchema { - return schema => mapSchema(schema, { - [MapperKind.OBJECT_TYPE]: (type) => { - const directive = getDirective(schema, type, directiveName)?.[0]; - if (directive) { - const config = type.toConfig(); - config.name = directive['to']; - return new GraphQLObjectType(config); - } - } - }); - } - - const schema = makeExecutableSchema({ - typeDefs: ` - directive @rename(to: String) on OBJECT - - type Query { - people: [Person] - } - - type Person @rename(to: "Human") { - heightInInches: Int - } - - scalar Date - - type Human @rename(to: "Person") { - born: Date - } - `, - schemaTransforms: [renameObjectTypeDirective('rename')] - }); - - const Human = schema.getType('Human') as GraphQLObjectType; - expect(Human.name).toBe('Human'); - expect(Human.getFields()['heightInInches'].type).toBe(GraphQLInt); - - const Person = schema.getType('Person') as GraphQLObjectType; - expect(Person.name).toBe('Person'); - expect(Person.getFields()['born'].type).toBe( - schema.getType('Date') as GraphQLScalarType, - ); - - const Query = schema.getType('Query') as GraphQLObjectType; - const peopleType = Query.getFields()['people'].type as GraphQLList< - GraphQLObjectType - >; - expect(peopleType.ofType).toBe(Human); - }); - - test('does not enforce query directive locations (issue #680)', () => { - function addObjectTypeToSetDirective(directiveName: string): (schema: GraphQLSchema) => GraphQLSchema { - return schema => mapSchema(schema, { - [MapperKind.OBJECT_TYPE]: type => { - const directive = getDirective(schema, type, directiveName)?.[0]; - if (directive) { - expect(type.name).toBe(schema.getQueryType()?.name); - visited.add(type); - } - return undefined; - } - }) - } - - const visited = new Set(); - makeExecutableSchema({ - typeDefs: ` - directive @hasScope(scope: [String]) on QUERY | FIELD | OBJECT - - type Query @hasScope { - oyez: String - } - `, - schemaTransforms: [addObjectTypeToSetDirective('hasScope')] - }); - - expect(visited.size).toBe(1); - }); - - test('allows multiple directives when first replaces type (issue #851)', () => { - function upperDirective(directiveName: string): (schema: GraphQLSchema) => GraphQLSchema { - return schema => mapSchema(schema, { - [MapperKind.OBJECT_FIELD]: (fieldConfig) => { - const upperDirective = getDirective(schema, fieldConfig, directiveName)?.[0]; - if (upperDirective) { - const { resolve = defaultFieldResolver } = fieldConfig; - fieldConfig.resolve = async function (source, args, context, info) { - const result = await resolve(source, args, context, info); - if (typeof result === 'string') { - return result.toUpperCase(); - } - return result; - } - return fieldConfig; - } - } - }); - } - - function reverseDirective(directiveName: string): (schema: GraphQLSchema) => GraphQLSchema { - return schema => mapSchema(schema, { - [MapperKind.OBJECT_FIELD]: (fieldConfig) => { - const reverseDirective = getDirective(schema, fieldConfig, directiveName)?.[0]; - if (reverseDirective) { - const { resolve = defaultFieldResolver } = fieldConfig; - fieldConfig.resolve = async function (source, args, context, info) { - const result = await resolve(source, args, context, info); - if (typeof result === 'string') { - return result.split('').reverse().join(''); - } - return result; - } - return fieldConfig; - } - } - }); - } - - const schema = makeExecutableSchema({ - typeDefs: ` - directive @upper on FIELD_DEFINITION - directive @reverse on FIELD_DEFINITION - - type Query { - hello: String @upper @reverse - } - `, - resolvers: { - Query: { - hello() { - return 'hello world'; - }, - }, - }, - schemaTransforms: [upperDirective('upper'), reverseDirective('reverse')] - }); - - return graphql( - schema, - ` - query { - hello - } - `, - ).then(({ data }) => { - expect(data).toEqual({ - hello: 'DLROW OLLEH', - }); - }); - }); - - test('allows creation of types that reference other types (issue #1877)', async () => { - function listWrapperTransformer(schema: GraphQLSchema) { - const listWrapperTypes = new Map(); - return mapSchema(schema, { - [MapperKind.COMPOSITE_FIELD]: (fieldConfig, fieldName) => { - const directive = getDirective(schema, fieldConfig, 'addListWrapper')?.[0]; - - // Leave the field untouched if it does not have the directive annotation - if (!directive) { - return undefined; - } - - const itemTypeInList = getNamedType(fieldConfig.type); - const itemTypeNameInList = itemTypeInList.name; - - // 1. Creating the XListWrapper type and replace the type of the field with that - if (!listWrapperTypes.has(itemTypeNameInList)) { - listWrapperTypes.set(itemTypeNameInList, new GraphQLObjectType({ - name: `${itemTypeNameInList}ListWrapper`, - fields: { - // Adding `size` field - size: { - type: new GraphQLNonNull(GraphQLInt), - description: 'The number of items in the `items` field', - }, - // Creating a new List which contains the same type than the original List - items: { - type: new GraphQLNonNull(new GraphQLList(itemTypeInList)) - } - } - })); - } - - fieldConfig.type = listWrapperTypes.get(itemTypeNameInList); - - // 2. Replacing resolver to return `{ size, items }` - const originalResolver = fieldConfig.resolve; - - fieldConfig.resolve = (parent, args, ctx, info) => { - const value = originalResolver ? originalResolver(parent, args, ctx, info) : parent[fieldName]; - const items = value || []; - - return { - size: items.length, - items - }; - }; - - // 3. Returning the updated `fieldConfig` - return fieldConfig; - }, - }); - } - - let schema = makeExecutableSchema({ - typeDefs: ` - directive @addListWrapper on FIELD_DEFINITION - - type Query { - me: Person - } - type Person { - name: String! - friends: [Person] @addListWrapper - } - `, - schemaTransforms: [listWrapperTransformer] - }); - - schema = addMocksToSchema({ schema }); - - const result = await graphql( - schema, - ` - query { - me { - friends { - items { - name - } - } - } - } - `, - ); - - const expectedResult: any = { - me: { - friends: { - items: [ - { - name: 'Hello World', - }, - { - name: 'Hello World', - }, - ] - }, - } - }; - - expect(result.data).toEqual(expectedResult); - }); -}); diff --git a/packages/wrap/src/transforms/TransformCompositeFields.ts b/packages/wrap/src/transforms/TransformCompositeFields.ts index 99be7cf41e8..10b2fc06f41 100644 --- a/packages/wrap/src/transforms/TransformCompositeFields.ts +++ b/packages/wrap/src/transforms/TransformCompositeFields.ts @@ -83,15 +83,9 @@ export default class TransformCompositeFields> im transformationContext: Record ): ExecutionRequest { const document = originalRequest.document; - const fragments = Object.create(null); - for (const def of document.definitions) { - if (def.kind === Kind.FRAGMENT_DEFINITION) { - fragments[def.name.value] = def; - } - } return { ...originalRequest, - document: this.transformDocument(document, fragments, transformationContext), + document: this.transformDocument(document, transformationContext), }; } @@ -110,11 +104,13 @@ export default class TransformCompositeFields> im return result; } - private transformDocument( - document: DocumentNode, - fragments: Record, - transformationContext: Record - ): DocumentNode { + private transformDocument(document: DocumentNode, transformationContext: Record): DocumentNode { + const fragments = Object.create(null); + for (const def of document.definitions) { + if (def.kind === Kind.FRAGMENT_DEFINITION) { + fragments[def.name.value] = def; + } + } return visit( document, visitWithTypeInfo(this._getTypeInfo(), { diff --git a/packages/wrap/src/transforms/WrapFields.ts b/packages/wrap/src/transforms/WrapFields.ts index 395fe81549a..a6766ff128e 100644 --- a/packages/wrap/src/transforms/WrapFields.ts +++ b/packages/wrap/src/transforms/WrapFields.ts @@ -142,18 +142,12 @@ export default class WrapFields implements Transform selectedFieldNames.includes(fieldName), - { - [wrappingFieldName]: { - type: newSchema.getType(wrappingTypeName) as GraphQLObjectType, - resolve, - }, - } - ); + [newSchema] = modifyObjectFields(newSchema, this.outerTypeName, fieldName => !!newTargetFieldConfigMap[fieldName], { + [wrappingFieldName]: { + type: newSchema.getType(wrappingTypeName) as GraphQLObjectType, + resolve, + }, + }); return this.transformer.transformSchema(newSchema, subschemaConfig, transformedSchema); } diff --git a/packages/wrap/tests/fixtures/schemas.ts b/packages/wrap/tests/fixtures/schemas.ts index d413c9ec9f1..66b536676a2 100644 --- a/packages/wrap/tests/fixtures/schemas.ts +++ b/packages/wrap/tests/fixtures/schemas.ts @@ -1,9 +1,6 @@ import { PubSub } from 'graphql-subscriptions'; import { GraphQLSchema, - graphql, - print, - subscribe, Kind, GraphQLScalarType, ValueNode, @@ -12,18 +9,14 @@ import { GraphQLInterfaceType, } from 'graphql'; -import { ValueOrPromise } from 'value-or-promise'; import { introspectSchema } from '../../src/introspect'; import { IResolvers, - ExecutionResult, - isAsyncIterable, AsyncExecutor, - ExecutionRequest, } from '@graphql-tools/utils'; import { makeExecutableSchema } from '@graphql-tools/schema'; -import { SubschemaConfig } from '@graphql-tools/delegate'; +import { createDefaultExecutor, SubschemaConfig } from '@graphql-tools/delegate'; export class CustomError extends GraphQLError { constructor(message: string, extensions: Record) { @@ -678,38 +671,11 @@ export const subscriptionSchema: GraphQLSchema = makeExecutableSchema({ resolvers: subscriptionResolvers, }); -function makeExecutorFromSchema(schema: GraphQLSchema): AsyncExecutor { - return async ({ document, variables, context, operationType }: ExecutionRequest) => { - if (operationType === 'subscription') { - const result = await subscribe( - schema, - document, - null, - context, - variables, - ); - if (isAsyncIterable>(result)) { - return result; - } - return result; - } - return (new ValueOrPromise(() => graphql( - schema, - print(document), - null, - context, - variables, - )) - .then(originalResult => JSON.parse(JSON.stringify(originalResult))) - .resolve()); - }; -} - export async function makeSchemaRemote( schema: GraphQLSchema, ): Promise { - const executor = makeExecutorFromSchema(schema); - const clientSchema = await introspectSchema(executor); + const executor = createDefaultExecutor(schema); + const clientSchema = await introspectSchema(executor as AsyncExecutor); return { schema: clientSchema, executor, diff --git a/website/docs/generate-schema.md b/website/docs/generate-schema.md index 426cbf5d222..a6f46f19a46 100644 --- a/website/docs/generate-schema.md +++ b/website/docs/generate-schema.md @@ -220,7 +220,6 @@ const jsSchema = makeExecutableSchema({ resolvers, // optional logger, // optional resolverValidationOptions: {}, // optional - schemaTransforms: [], // optional parseOptions: {}, // optional inheritResolversFromInterfaces: false // optional }); @@ -245,5 +244,4 @@ const jsSchema = makeExecutableSchema({ - `inheritResolversFromInterfaces` GraphQL Objects that implement interfaces will inherit missing resolvers from their interface types defined in the `resolvers` object. -- `schemaTransforms` is an optional argument _(empty array by default)_ and should be an array of schema transformation functions, essentially designed to enable the use of [directive-based functional schema transformation](/docs/schema-directives/) diff --git a/website/docs/schema-directives.md b/website/docs/schema-directives.md index 5b1709bc54c..3a02b01f377 100644 --- a/website/docs/schema-directives.md +++ b/website/docs/schema-directives.md @@ -27,7 +27,7 @@ This document focuses on directives that appear in GraphQL _schemas_ (as opposed Most of this document is concerned with _implementing_ schema directives, and some of the examples may seem quite complicated. No matter how many tools and best practices you have at your disposal, it can be difficult to implement a non-trivial schema directive in a reliable, reusable way. Exhaustive testing is essential, and using a typed language like TypeScript is recommended, because there are so many different schema types to worry about. -However, the API we provide for _using_ a schema directive is extremely simple. Just import the implementation of the directive, then pass it to `makeExecutableSchema` via the `schemaTransforms` argument, which is an array of schema transformation functions: +However, the API we provide for _using_ a schema directive is extremely simple. Just import the implementation of the directive, then pass the schema generated by `makeExecutableSchema`: ```js import { makeExecutableSchema } from '@graphql-tools/schema'; @@ -39,10 +39,11 @@ type Person @rename(to: "Human") { currentDateMinusDateOfBirth: Int @rename(to: "age") }`; -const schema = makeExecutableSchema({ +let schema = makeExecutableSchema({ typeDefs, - schemaTransforms: [renameDirective('rename')], }); + +schema = renameDirective('rename')(schema) ``` That's it. The implementation of `renameDirective` takes care of everything else. If you understand what the directive is supposed to do to your schema, then you do not have to worry about how it works. @@ -92,7 +93,7 @@ import { makeExecutableSchema } from '@graphql-tools/schema'; const { deprecatedDirectiveTypeDefs, deprecatedDirectiveTransformer } = deprecatedDirective('deprecated'); -const schema = makeExecutableSchema({ +let schema = makeExecutableSchema({ typeDefs: [ deprecatedDirectiveTypeDefs, ` @@ -106,15 +107,8 @@ const schema = makeExecutableSchema({ } `, ], - schemaTransforms: [deprecatedDirectiveTransformer], }); -``` - -Alternatively, if you want to modify an existing schema object, you can use the function interface directly: - -```typescript -const schemaTransform = deprecatedDirective('deprecated'); -const newSchema = schemaTransform(originalSchema); +schema = deprecatedDirectiveTransformer(schema); ``` We suggest that creators of directive-based schema modification functions allow users to customize the names of the relevant directives, to help users avoid collision of directive names with existing directives within their schema or other external schema modification functions. Of course, you could hard-code the name of the directive into the function, further simplifying the above examples. @@ -148,9 +142,9 @@ function upperDirective(directiveName: string): (schema: GraphQLSchema) => Graph } const { upperDirectiveTypeDefs, upperDirectiveTransformer } = upperDirective('upper'); -const { upperDirectiveTypeDefs: upperCaseDirectiveTypeDefs, upperDirectiveTransformer:upperCaseDirectiveTransformer } = upperDirective('upperCase'); +const { upperDirectiveTypeDefs: upperCaseDirectiveTypeDefs, upperDirectiveTransformer: upperCaseDirectiveTransformer } = upperDirective('upperCase'); -const schema = makeExecutableSchema({ +let schema = makeExecutableSchema({ typeDefs: [upperDirectiveTypeDefs, upperCaseDirectiveTypeDefs, ` type Query { hello: String @upper @@ -167,9 +161,9 @@ const schema = makeExecutableSchema({ }, }, }, - schemaTransforms: [upperDirectiveTransformer, upperCaseDirectiveTransformer], -}); }); + +schema = upperDirectiveTransformer(schema); ``` Notice how easy it is to handle both `@upper` and `@upperCase` with the same `upperDirective` implementation. @@ -197,14 +191,15 @@ function restDirective(directiveName: string) { const { restDirectiveTypeDefs, restDirectiveTransformer } = restDirective('rest'); -const schema = makeExecutableSchema({ +let schema = makeExecutableSchema({ typeDefs: [restDirectiveTypeDefs, ` type Query { people: [Person] @rest(url: "/api/v1/people") } `], - schemaTransforms: [restDirectiveTransformer], }); + +schema = restDirectiveTransformer(schema); ``` There are many more issues to consider when implementing a real GraphQL wrapper over a REST endpoint (such as how to do caching or pagination), but this example demonstrates the basic structure. @@ -236,7 +231,7 @@ function dateDirective(directiveName: string) { const { dateDirectiveTypeDefs, dateDirectiveTransformer } = dateDirective('date'); -const schema = makeExecutableSchema({ +let schema = makeExecutableSchema({ typeDefs: [ dateDirectiveTypeDefs, ` @@ -254,8 +249,8 @@ const schema = makeExecutableSchema({ }, }, }, - schemaTransforms: [dateDirectiveTransformer], }); +schema = dateDirectiveTransformer(schema); ``` Of course, it would be even better if the schema author did not have to decide on a specific `Date` format, but could instead leave that decision to the client. To make this work, the directive just needs to add an additional argument to the field: @@ -304,7 +299,7 @@ function formattableDateDirective(directiveName: string) { const { formattableDateDirectiveTypeDefs, formattableDateDirectiveTransformer } = formattableDateDirective('date'); -const schema = makeExecutableSchema({ +let schema = makeExecutableSchema({ typeDefs: [ formattableDateDirectiveTypeDefs, ` @@ -322,8 +317,8 @@ const schema = makeExecutableSchema({ }, }, }, - schemaTransforms: [formattableDateDirectiveTransformer], }); +schema = formattableDateDirectiveTransformer(schema); ``` Now the client can specify a desired `format` argument when requesting the `Query.today` field, or omit the argument to use the `defaultFormat` string specified in the schema: @@ -433,7 +428,7 @@ function getUser(token: string) { const { authDirectiveTypeDefs, authDirectiveTransformer } = authDirective('auth', getUser); -const schema = makeExecutableSchema({ +let schema = makeExecutableSchema({ typeDefs: [authDirectiveTypeDefs, ` type User @auth(requires: USER) { name: String @@ -458,9 +453,8 @@ const schema = makeExecutableSchema({ }, }, }, - schemaTransforms: [authDirectiveTransformer], -}); }); +schema = authDirectiveTransformer(schema); ``` One drawback of this approach is that it does not guarantee fields will be wrapped if they are added to the schema after `AuthDirective` is applied, and the whole `getUser(context.headers.authToken)` is a made-up API that would need to be fleshed out. In other words, we’ve glossed over some of the details that would be required for a production-ready implementation of this directive, though we hope the basic structure shown here inspires you to find clever solutions to the remaining problems. @@ -547,7 +541,7 @@ function lengthDirective(directiveName: string) { const { lengthDirectiveTypeDefs, lengthDirectiveTransformer } = lengthDirective('length'); -const schema = makeExecutableSchema({ +let schema = makeExecutableSchema({ typeDefs: [lengthDirectiveTypeDefs, ` type Query { books: [Book] @@ -581,8 +575,8 @@ const schema = makeExecutableSchema({ }, }, }, - schemaTransforms: [lengthDirectiveTransformer], }); +schema = lengthDirectiveTransformer(schema); ``` Note that new types can be added to the schema with ease, but that each type must be uniquely named. @@ -626,7 +620,7 @@ function uniqueIDDirective(directiveName: string) { const { uniqueIDDirectiveTypeDefs, uniqueIDDirectiveTransformer } = uniqueIDDirective('uniqueID'); -const schema = makeExecutableSchema({ +let schema = makeExecutableSchema({ typeDefs: [ uniqueIDDirectiveTypeDefs, ` @@ -666,8 +660,8 @@ const schema = makeExecutableSchema({ }, }, }, - schemaTransforms: [uniqueIDDirectiveTransformer], }); +schema = uniqueIDDirectiveTransformer(schema); ``` ## Declaring schema directives diff --git a/website/docs/stitch-directives-sdl.md b/website/docs/stitch-directives-sdl.md index 236a57791c3..c06d5f3730b 100644 --- a/website/docs/stitch-directives-sdl.md +++ b/website/docs/stitch-directives-sdl.md @@ -114,17 +114,18 @@ const typeDefs = ` } `; -module.exports = makeExecutableSchema({ - // 2. include the stitching directives validator... - schemaTransforms: [stitchingDirectivesValidator], +const schema = makeExecutableSchema({ typeDefs, resolvers: { Query: { - // 3. setup a query that exposes the raw SDL... + // 2. setup a query that exposes the raw SDL... _sdl: () => typeDefs } } }); + +// 3. include the stitching directives validator... +module.exports = stitchingDirectivesValidator(schema) ``` 1. Include `allStitchingDirectivesTypeDefs` in your schema's type definitions string (these define the schema of the directives themselves).