From f0cb92e1361d10294b12a20188c2e1d57b92a543 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Thu, 17 Jun 2021 00:48:01 +0300 Subject: [PATCH 1/3] Breaking cleanup before major release --- .changeset/cuddly-horses-prove.md | 5 + .changeset/forty-ducks-drum.md | 7 + .changeset/heavy-vans-whisper.md | 5 + .changeset/long-rings-happen.md | 5 + .changeset/mean-news-return.md | 8 + .changeset/new-balloons-reply.md | 5 + .changeset/ninety-shirts-crash.md | 5 + .changeset/polite-yaks-drive.md | 5 + .changeset/silent-comics-tell.md | 5 + .changeset/smooth-tips-know.md | 5 + .changeset/twelve-suns-run.md | 5 + .github/workflows/tests.yml | 7 +- packages/delegate/package.json | 1 - packages/delegate/src/delegateToSchema.ts | 3 +- packages/delegate/src/resolveExternalValue.ts | 2 +- .../transforms/CheckResultAndHandleErrors.ts | 4 +- packages/graphql-tools/package.json | 23 - packages/graphql-tools/src/index.ts | 28 +- packages/load/src/filter-document-kind.ts | 6 +- packages/load/src/load-typedefs/load-file.ts | 11 +- packages/loaders/apollo-engine/package.json | 3 +- packages/loaders/apollo-engine/src/index.ts | 7 +- packages/loaders/code-file/src/index.ts | 11 +- .../prisma/src/prisma-yml/Variables.ts | 18 +- .../prisma/src/prisma-yml/constants.ts | 4 +- packages/merge/src/merge-resolvers.ts | 14 +- packages/merge/src/merge-schemas.ts | 32 +- .../src/typedefs-mergers/merge-typedefs.ts | 74 +- .../extract-extensions-from-schema.spec.ts | 72 +- packages/mock/package.json | 1 + packages/mock/src/mockServer.ts | 14 +- .../mock/tests/mocking-compatibility.spec.ts | 82 +- .../src/resolvers-composition.ts | 30 +- packages/schema/package.json | 1 + .../schema/src/addCatchUndefinedToSchema.ts | 25 - .../schema/src/addErrorLoggingToSchema.ts | 19 - packages/schema/src/addSchemaLevelResolver.ts | 70 - .../schema/src/attachDirectiveResolvers.ts | 49 - .../src/buildSchemaFromTypeDefinitions.ts | 48 - packages/schema/src/concatenateTypeDefs.ts | 26 - packages/schema/src/decorateWithLogger.ts | 50 - packages/schema/src/extensionDefinitions.ts | 43 - packages/schema/src/index.ts | 8 - packages/schema/src/makeExecutableSchema.ts | 50 +- packages/schema/src/types.ts | 33 +- packages/schema/tests/Logger.ts | 36 - .../schema/tests/extensionExtraction.test.ts | 65 - packages/schema/tests/logger.test.ts | 409 ----- packages/schema/tests/resolution.test.ts | 148 -- packages/schema/tests/schemaGenerator.test.ts | 523 +----- packages/stitch/src/mergeCandidates.ts | 15 +- packages/stitch/src/stitchSchemas.ts | 41 +- packages/stitch/src/typeCandidates.ts | 8 +- packages/stitch/src/types.ts | 4 +- packages/stitch/tests/stitchSchemas.test.ts | 42 - packages/utils/package.json | 6 +- packages/utils/src/AggregateError.ts | 20 + packages/utils/src/Interfaces.ts | 100 +- packages/utils/src/SchemaDirectiveVisitor.ts | 324 ---- packages/utils/src/SchemaVisitor.ts | 119 -- .../utils/src/build-operation-for-field.ts | 9 +- packages/utils/src/debug-log.ts | 6 - packages/utils/src/fix-windows-path.ts | 1 - packages/utils/src/flatten-array.ts | 2 - packages/utils/src/getArgumentValues.ts | 2 +- packages/utils/src/index.ts | 8 +- packages/utils/src/inspect.ts | 113 -- packages/utils/src/toConfig.ts | 51 - packages/utils/src/validate-documents.ts | 4 +- packages/utils/src/visitSchema.ts | 320 ---- .../build-operation-node-for-field.spec.ts | 28 +- packages/utils/tests/directives.test.ts | 1601 ----------------- packages/utils/tests/get-directives.spec.ts | 38 +- .../utils/tests/validate-documents.spec.ts | 7 +- packages/wrap/src/introspect.ts | 17 +- website/docs/directive-resolvers.md | 178 -- website/docs/generate-schema.md | 8 - website/docs/legacy-schema-directives.md | 714 -------- website/docs/schema-directives.md | 6 +- website/sidebars.js | 1 - yarn.lock | 23 +- 81 files changed, 367 insertions(+), 5559 deletions(-) create mode 100644 .changeset/cuddly-horses-prove.md create mode 100644 .changeset/forty-ducks-drum.md create mode 100644 .changeset/heavy-vans-whisper.md create mode 100644 .changeset/long-rings-happen.md create mode 100644 .changeset/mean-news-return.md create mode 100644 .changeset/new-balloons-reply.md create mode 100644 .changeset/ninety-shirts-crash.md create mode 100644 .changeset/polite-yaks-drive.md create mode 100644 .changeset/silent-comics-tell.md create mode 100644 .changeset/smooth-tips-know.md create mode 100644 .changeset/twelve-suns-run.md delete mode 100644 packages/schema/src/addCatchUndefinedToSchema.ts delete mode 100644 packages/schema/src/addErrorLoggingToSchema.ts delete mode 100644 packages/schema/src/addSchemaLevelResolver.ts delete mode 100644 packages/schema/src/attachDirectiveResolvers.ts delete mode 100644 packages/schema/src/buildSchemaFromTypeDefinitions.ts delete mode 100644 packages/schema/src/concatenateTypeDefs.ts delete mode 100644 packages/schema/src/decorateWithLogger.ts delete mode 100644 packages/schema/src/extensionDefinitions.ts delete mode 100644 packages/schema/tests/Logger.ts delete mode 100644 packages/schema/tests/extensionExtraction.test.ts delete mode 100644 packages/schema/tests/logger.test.ts delete mode 100644 packages/schema/tests/resolution.test.ts create mode 100644 packages/utils/src/AggregateError.ts delete mode 100644 packages/utils/src/SchemaDirectiveVisitor.ts delete mode 100644 packages/utils/src/SchemaVisitor.ts delete mode 100644 packages/utils/src/debug-log.ts delete mode 100644 packages/utils/src/fix-windows-path.ts delete mode 100644 packages/utils/src/flatten-array.ts delete mode 100644 packages/utils/src/inspect.ts delete mode 100644 packages/utils/src/toConfig.ts delete mode 100644 packages/utils/src/visitSchema.ts delete mode 100644 packages/utils/tests/directives.test.ts delete mode 100644 website/docs/directive-resolvers.md delete mode 100644 website/docs/legacy-schema-directives.md diff --git a/.changeset/cuddly-horses-prove.md b/.changeset/cuddly-horses-prove.md new file mode 100644 index 00000000000..69d10c9951d --- /dev/null +++ b/.changeset/cuddly-horses-prove.md @@ -0,0 +1,5 @@ +--- +'@graphql-tools/utils': major +--- + +BREAKING - Remove fieldToFieldConfig, fieldToFieldConfig, argsToFieldConfigArgument and argumentToArgumentConfig diff --git a/.changeset/forty-ducks-drum.md b/.changeset/forty-ducks-drum.md new file mode 100644 index 00000000000..b1726137568 --- /dev/null +++ b/.changeset/forty-ducks-drum.md @@ -0,0 +1,7 @@ +--- +'@graphql-tools/schema': major +'@graphql-tools/stitch': major +'@graphql-tools/utils': major +--- + +BREAKING - deprecate legacy schema directives and directive resolvers diff --git a/.changeset/heavy-vans-whisper.md b/.changeset/heavy-vans-whisper.md new file mode 100644 index 00000000000..99d1fd03470 --- /dev/null +++ b/.changeset/heavy-vans-whisper.md @@ -0,0 +1,5 @@ +--- +'@graphql-tools/schema': major +--- + +breaking - remove logger and addErrorLoggingToSchema - use envelop instead diff --git a/.changeset/long-rings-happen.md b/.changeset/long-rings-happen.md new file mode 100644 index 00000000000..60494b41d0f --- /dev/null +++ b/.changeset/long-rings-happen.md @@ -0,0 +1,5 @@ +--- +'graphql-tools': major +--- + +Add deprecation notice and export makeExecutableSchema only diff --git a/.changeset/mean-news-return.md b/.changeset/mean-news-return.md new file mode 100644 index 00000000000..57f962b5bf1 --- /dev/null +++ b/.changeset/mean-news-return.md @@ -0,0 +1,8 @@ +--- +'@graphql-tools/delegate': major +'@graphql-tools/apollo-engine-loader': major +'@graphql-tools/utils': major +'@graphql-tools/wrap': major +--- + +BREAKING - Use native AggregateError if possible. Native AggregateError doesn't have iterator but errors prop diff --git a/.changeset/new-balloons-reply.md b/.changeset/new-balloons-reply.md new file mode 100644 index 00000000000..9ea808648e4 --- /dev/null +++ b/.changeset/new-balloons-reply.md @@ -0,0 +1,5 @@ +--- +'@graphql-tools/schema': major +--- + +BREAKING - enhance(schema): remove schema level resolvers and addSchemaLevelResolver diff --git a/.changeset/ninety-shirts-crash.md b/.changeset/ninety-shirts-crash.md new file mode 100644 index 00000000000..9767056e056 --- /dev/null +++ b/.changeset/ninety-shirts-crash.md @@ -0,0 +1,5 @@ +--- +'@graphql-tools/utils': major +--- + +BREAKING - remove debugLog diff --git a/.changeset/polite-yaks-drive.md b/.changeset/polite-yaks-drive.md new file mode 100644 index 00000000000..e4d486c2dd7 --- /dev/null +++ b/.changeset/polite-yaks-drive.md @@ -0,0 +1,5 @@ +--- +'@graphql-tools/utils': major +--- + +BREAKING - do not apply camelCase naming convention in buildOperationNodeForField diff --git a/.changeset/silent-comics-tell.md b/.changeset/silent-comics-tell.md new file mode 100644 index 00000000000..b015049e75b --- /dev/null +++ b/.changeset/silent-comics-tell.md @@ -0,0 +1,5 @@ +--- +'@graphql-tools/schema': major +--- + +BREAKING(schema) - remove allowUndefinedResolve option, buildSchemaFromTypeDefinitions and use buildSchema instead diff --git a/.changeset/smooth-tips-know.md b/.changeset/smooth-tips-know.md new file mode 100644 index 00000000000..6f8013bc4da --- /dev/null +++ b/.changeset/smooth-tips-know.md @@ -0,0 +1,5 @@ +--- +'@graphql-tools/utils': major +--- + +BREAKING - Remove SchemaVisitor, visitSchema and VisitSchemaKind diff --git a/.changeset/twelve-suns-run.md b/.changeset/twelve-suns-run.md new file mode 100644 index 00000000000..7c816992aa7 --- /dev/null +++ b/.changeset/twelve-suns-run.md @@ -0,0 +1,5 @@ +--- +'@graphql-tools/schema': patch +--- + +enhance(schema): use merge package to handle typeDefs and resolvers merging diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bcd0e515c36..f4e0a414fe9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -57,12 +57,11 @@ jobs: - name: Build run: yarn ts:transpile test: - name: Test on ${{matrix.os}}, Node ${{matrix.node_version}} and GraphQL v${{matrix.graphql_version}} - runs-on: ${{matrix.os}} + name: Test, Node ${{matrix.node_version}} and GraphQL v${{matrix.graphql_version}} + runs-on: ubuntu-latest strategy: matrix: - os: [ubuntu-latest] # remove windows to speed up the tests - node_version: [10, 16] + node_version: [12, 16] graphql_version: [14, 15] steps: - name: Checkout Master diff --git a/packages/delegate/package.json b/packages/delegate/package.json index b9dcaa0a76a..663562b2253 100644 --- a/packages/delegate/package.json +++ b/packages/delegate/package.json @@ -35,7 +35,6 @@ "@graphql-tools/batch-execute": "^7.1.2", "@graphql-tools/schema": "^7.1.5", "@graphql-tools/utils": "^7.7.1", - "@ardatan/aggregate-error": "0.0.6", "dataloader": "2.0.0", "tslib": "~2.3.0", "value-or-promise": "1.0.10" diff --git a/packages/delegate/src/delegateToSchema.ts b/packages/delegate/src/delegateToSchema.ts index 079bd44107f..1f229c2de11 100644 --- a/packages/delegate/src/delegateToSchema.ts +++ b/packages/delegate/src/delegateToSchema.ts @@ -14,8 +14,6 @@ import { import { ValueOrPromise } from 'value-or-promise'; -import AggregateError from '@ardatan/aggregate-error'; - import { getBatchingExecutor } from '@graphql-tools/batch-execute'; import { @@ -26,6 +24,7 @@ import { Subscriber, Maybe, assertSome, + AggregateError, } from '@graphql-tools/utils'; import { diff --git a/packages/delegate/src/resolveExternalValue.ts b/packages/delegate/src/resolveExternalValue.ts index 954a024a30c..5c7885ad39d 100644 --- a/packages/delegate/src/resolveExternalValue.ts +++ b/packages/delegate/src/resolveExternalValue.ts @@ -13,7 +13,7 @@ import { locatedError, } from 'graphql'; -import AggregateError from '@ardatan/aggregate-error'; +import { AggregateError } from '@graphql-tools/utils'; import { StitchingInfo, SubschemaConfig } from './types'; import { annotateExternalObject, isExternalObject } from './externalObjects'; diff --git a/packages/delegate/src/transforms/CheckResultAndHandleErrors.ts b/packages/delegate/src/transforms/CheckResultAndHandleErrors.ts index 912da07bc46..81552bafb8f 100644 --- a/packages/delegate/src/transforms/CheckResultAndHandleErrors.ts +++ b/packages/delegate/src/transforms/CheckResultAndHandleErrors.ts @@ -7,9 +7,7 @@ import { locatedError, } from 'graphql'; -import AggregateError from '@ardatan/aggregate-error'; - -import { getResponseKeyFromInfo, ExecutionResult, relocatedError } from '@graphql-tools/utils'; +import { AggregateError, getResponseKeyFromInfo, ExecutionResult, relocatedError } from '@graphql-tools/utils'; import { SubschemaConfig, Transform, DelegationContext } from '../types'; import { resolveExternalValue } from '../resolveExternalValue'; diff --git a/packages/graphql-tools/package.json b/packages/graphql-tools/package.json index bce47c57ac0..431ff3299be 100644 --- a/packages/graphql-tools/package.json +++ b/packages/graphql-tools/package.json @@ -36,30 +36,7 @@ "directory": "dist" }, "dependencies": { - "@graphql-tools/optimize": "1.0.1", - "@graphql-tools/batch-delegate": "^7.0.0", - "@graphql-tools/batch-execute": "^7.0.0", - "@graphql-tools/delegate": "^7.0.10", - "@graphql-tools/graphql-tag-pluck": "^6.2.6", - "@graphql-tools/import": "^6.2.4", - "@graphql-tools/links": "^7.0.4", - "@graphql-tools/load": "^6.2.5", - "@graphql-tools/code-file-loader": "^6.2.5", - "@graphql-tools/git-loader": "^6.2.5", - "@graphql-tools/github-loader": "^6.2.5", - "@graphql-tools/graphql-file-loader": "^6.2.5", - "@graphql-tools/json-file-loader": "^6.2.5", - "@graphql-tools/module-loader": "^6.2.5", - "@graphql-tools/url-loader": "^6.3.2", - "@graphql-tools/load-files": "^6.2.4", - "@graphql-tools/merge": "^6.2.14", - "@graphql-tools/mock": "^7.0.0", - "@graphql-tools/relay-operation-optimizer": "^6.2.5", - "@graphql-tools/resolvers-composition": "^6.2.5", "@graphql-tools/schema": "^7.0.0", - "@graphql-tools/stitch": "^7.3.0", - "@graphql-tools/utils": "^7.0.1", - "@graphql-tools/wrap": "^7.0.0", "tslib": "~2.3.0" } } diff --git a/packages/graphql-tools/src/index.ts b/packages/graphql-tools/src/index.ts index dbd653e5d01..b214057bd3d 100644 --- a/packages/graphql-tools/src/index.ts +++ b/packages/graphql-tools/src/index.ts @@ -1,23 +1,5 @@ -export * from '@graphql-tools/batch-delegate'; -export * from '@graphql-tools/batch-execute'; -export * from '@graphql-tools/delegate'; -export * from '@graphql-tools/graphql-tag-pluck'; -export * from '@graphql-tools/import'; -export * from '@graphql-tools/links'; -export * from '@graphql-tools/load'; -export * from '@graphql-tools/code-file-loader'; -export * from '@graphql-tools/git-loader'; -export * from '@graphql-tools/github-loader'; -export * from '@graphql-tools/graphql-file-loader'; -export * from '@graphql-tools/module-loader'; -export * from '@graphql-tools/url-loader'; -export * from '@graphql-tools/load-files'; -export * from '@graphql-tools/merge'; -export * from '@graphql-tools/mock'; -export * from '@graphql-tools/relay-operation-optimizer'; -export * from '@graphql-tools/resolvers-composition'; -export * from '@graphql-tools/schema'; -export * from '@graphql-tools/stitch'; -export * from '@graphql-tools/utils'; -export * from '@graphql-tools/wrap'; -export * from '@graphql-tools/optimize'; +export { makeExecutableSchema } from '@graphql-tools/schema'; + +console.warn(`This package has been deprecated and now it only exports makeExecutableSchema. +We recommend you to migrate to scoped packages. +Check out https://www.graphql-tools.com for more information!`); diff --git a/packages/load/src/filter-document-kind.ts b/packages/load/src/filter-document-kind.ts index 0798ff35254..5f63ab4b9a1 100644 --- a/packages/load/src/filter-document-kind.ts +++ b/packages/load/src/filter-document-kind.ts @@ -1,5 +1,5 @@ -import { debugLog } from '@graphql-tools/utils'; import { DocumentNode, DefinitionNode, Kind } from 'graphql'; +import { env } from 'process'; /** * @internal @@ -18,7 +18,9 @@ export const filterKind = (content: DocumentNode | undefined, filterKinds: null if (invalidDefinitions.length > 0) { invalidDefinitions.forEach(d => { - debugLog(`Filtered document of kind ${d.kind} due to filter policy (${filterKinds.join(', ')})`); + if (env.DEBUG) { + console.error(`Filtered document of kind ${d.kind} due to filter policy (${filterKinds.join(', ')})`); + } }); } diff --git a/packages/load/src/load-typedefs/load-file.ts b/packages/load/src/load-typedefs/load-file.ts index ef8efadfbc9..7f6acf067e8 100644 --- a/packages/load/src/load-typedefs/load-file.ts +++ b/packages/load/src/load-typedefs/load-file.ts @@ -1,4 +1,5 @@ -import { Source, debugLog, Maybe } from '@graphql-tools/utils'; +import { Source, Maybe } from '@graphql-tools/utils'; +import { env } from 'process'; import { LoadTypedefsOptions } from '../load-typedefs'; export async function loadFile(pointer: string, options: LoadTypedefsOptions): Promise> { @@ -17,7 +18,9 @@ export async function loadFile(pointer: string, options: LoadTypedefsOptions): P return loadedValue; } } catch (error) { - debugLog(`Failed to find any GraphQL type definitions in: ${pointer} - ${error.message}`); + if (env['DEBUG']) { + console.error(`Failed to find any GraphQL type definitions in: ${pointer} - ${error.message}`); + } throw error; } } @@ -41,7 +44,9 @@ export function loadFileSync(pointer: string, options: LoadTypedefsOptions): May return loader.loadSync!(pointer, options); } } catch (error) { - debugLog(`Failed to find any GraphQL type definitions in: ${pointer} - ${error.message}`); + if (env['DEBUG']) { + console.error(`Failed to find any GraphQL type definitions in: ${pointer} - ${error.message}`); + } throw error; } } diff --git a/packages/loaders/apollo-engine/package.json b/packages/loaders/apollo-engine/package.json index 4e99982fe17..3382d69b11d 100644 --- a/packages/loaders/apollo-engine/package.json +++ b/packages/loaders/apollo-engine/package.json @@ -30,8 +30,7 @@ "graphql": "^14.0.0 || ^15.0.0" }, "dependencies": { - "@ardatan/aggregate-error": "0.0.6", - "@graphql-tools/utils": "^7.0.0", + "@graphql-tools/utils": "^7.10.0", "cross-fetch": "3.1.4", "tslib": "~2.3.0", "sync-fetch": "0.3.0" diff --git a/packages/loaders/apollo-engine/src/index.ts b/packages/loaders/apollo-engine/src/index.ts index 3015ce125b1..2c1c4b47ba5 100644 --- a/packages/loaders/apollo-engine/src/index.ts +++ b/packages/loaders/apollo-engine/src/index.ts @@ -1,6 +1,5 @@ -import { SchemaLoader, Source, SingleFileOptions, parseGraphQLSDL } from '@graphql-tools/utils'; +import { SchemaLoader, Source, SingleFileOptions, parseGraphQLSDL, AggregateError } from '@graphql-tools/utils'; import { fetch } from 'cross-fetch'; -import AggregateError from '@ardatan/aggregate-error'; import syncFetch from 'sync-fetch'; /** @@ -66,7 +65,7 @@ export class ApolloEngineLoader implements SchemaLoader { const { data, errors } = await response.json(); if (errors) { - throw new AggregateError(errors); + throw new AggregateError(errors, 'Introspection from Apollo Engine failed'); } return parseGraphQLSDL(pointer, data.service.schema.document, options); @@ -79,7 +78,7 @@ export class ApolloEngineLoader implements SchemaLoader { const { data, errors } = response.json(); if (errors) { - throw new AggregateError(errors); + throw new AggregateError(errors, 'Introspection from Apollo Engine failed'); } return parseGraphQLSDL(pointer, data.service.schema.document, options); diff --git a/packages/loaders/code-file/src/index.ts b/packages/loaders/code-file/src/index.ts index 37f042e916e..cc74ac21d40 100644 --- a/packages/loaders/code-file/src/index.ts +++ b/packages/loaders/code-file/src/index.ts @@ -4,7 +4,6 @@ import { isSchema, GraphQLSchema, DocumentNode } from 'graphql'; import { SchemaPointerSingle, DocumentPointerSingle, - debugLog, SingleFileOptions, Source, UniversalLoader, @@ -24,7 +23,7 @@ import isGlob from 'is-glob'; import unixify from 'unixify'; import { tryToLoadFromExport, tryToLoadFromExportSync } from './load-from-module'; import { isAbsolute, resolve } from 'path'; -import { cwd } from 'process'; +import { cwd, env } from 'process'; import { readFileSync, promises as fsPromises, existsSync } from 'fs'; const { readFile, access } = fsPromises; @@ -135,7 +134,9 @@ export class CodeFileLoader implements UniversalLoader { return parseGraphQLSDL(pointer, sdl, options); } } catch (e) { - debugLog(`Failed to load schema from code file "${normalizedFilePath}": ${e.message}`); + if (env.DEBUG) { + console.error(`Failed to load schema from code file "${normalizedFilePath}": ${e.message}`); + } errors.push(e); } } @@ -178,7 +179,9 @@ export class CodeFileLoader implements UniversalLoader { return parseGraphQLSDL(pointer, sdl, options); } } catch (e) { - debugLog(`Failed to load schema from code file "${normalizedFilePath}": ${e.message}`); + if (env.DEBUG) { + console.error(`Failed to load schema from code file "${normalizedFilePath}": ${e.message}`); + } errors.push(e); } } diff --git a/packages/loaders/prisma/src/prisma-yml/Variables.ts b/packages/loaders/prisma/src/prisma-yml/Variables.ts index d7ed25325c7..8af48d59272 100644 --- a/packages/loaders/prisma/src/prisma-yml/Variables.ts +++ b/packages/loaders/prisma/src/prisma-yml/Variables.ts @@ -1,4 +1,4 @@ -import * as lodash from 'lodash'; +import _ from 'lodash'; // eslint-disable-next-line // @ts-ignore import replaceall from 'replaceall'; @@ -43,10 +43,10 @@ export class Variables { const deepMapValues = (object: any, callback: any, propertyPath?: string[]): any => { const deepMapValuesIteratee = (value: any, key: any) => deepMapValues(value, callback, propertyPath ? propertyPath.concat(key) : [key]); - if (lodash.isArray(object)) { - return lodash.map(object, deepMapValuesIteratee); - } else if (lodash.isObject(object) && !lodash.isDate(object) && !lodash.isFunction(object)) { - return lodash.extend({}, object, lodash.mapValues(object, deepMapValuesIteratee)); + if (_.isArray(object)) { + return _.map(object, deepMapValuesIteratee); + } else if (_.isObject(object) && !_.isDate(object) && !_.isFunction(object)) { + return _.extend({}, object, _.mapValues(object, deepMapValuesIteratee)); } return callback(object, propertyPath); }; @@ -54,7 +54,7 @@ export class Variables { deepMapValues(objectToPopulate, (property: any, propertyPath: any) => { if (typeof property === 'string') { const populateSingleProperty = this.populateProperty(property, true).then((newProperty: any) => - lodash.set(objectToPopulate, propertyPath, newProperty) + _.set(objectToPopulate, propertyPath, newProperty) ); populateAll.push(populateSingleProperty); } @@ -64,7 +64,7 @@ export class Variables { } populateProperty(propertyParam: any, populateInPlace?: boolean): any { - let property = populateInPlace ? propertyParam : lodash.cloneDeep(propertyParam); + let property = populateInPlace ? propertyParam : _.cloneDeep(propertyParam); const allValuesToPopulate: any[] = []; let warned = false; @@ -143,7 +143,7 @@ export class Variables { return ( finalValue !== null && typeof finalValue !== 'undefined' && - !(typeof finalValue === 'object' && lodash.isEmpty(finalValue)) + !(typeof finalValue === 'object' && _.isEmpty(finalValue)) ); }); return Promise.resolve(finalValue); @@ -215,7 +215,7 @@ export class Variables { if ( valueToPopulate === null || typeof valueToPopulate === 'undefined' || - (typeof valueToPopulate === 'object' && lodash.isEmpty(valueToPopulate)) + (typeof valueToPopulate === 'object' && _.isEmpty(valueToPopulate)) ) { let varType; if (variableString.match(this.envRefSyntax)) { diff --git a/packages/loaders/prisma/src/prisma-yml/constants.ts b/packages/loaders/prisma/src/prisma-yml/constants.ts index 161a00f0683..93832914b50 100644 --- a/packages/loaders/prisma/src/prisma-yml/constants.ts +++ b/packages/loaders/prisma/src/prisma-yml/constants.ts @@ -1,4 +1,4 @@ -import { invert } from 'lodash'; +import _ from 'lodash'; export const cloudApiEndpoint = process.env['CLOUD_API_ENDPOINT'] || 'https://api.cloud.prisma.sh'; @@ -7,4 +7,4 @@ export const clusterEndpointMap: { [key: string]: string } = { 'prisma-us1': 'https://us1.prisma.sh', }; -export const clusterEndpointMapReverse: { [key: string]: string } = invert(clusterEndpointMap); +export const clusterEndpointMapReverse: { [key: string]: string } = _.invert(clusterEndpointMap); diff --git a/packages/merge/src/merge-resolvers.ts b/packages/merge/src/merge-resolvers.ts index 2173eba5911..87e88db18c2 100644 --- a/packages/merge/src/merge-resolvers.ts +++ b/packages/merge/src/merge-resolvers.ts @@ -40,19 +40,19 @@ export interface MergeResolversOptions { * ``` */ export function mergeResolvers>( - resolversDefinitions: Maybe, + resolversDefinitions: Maybe, options?: MergeResolversOptions ): T { - if (!resolversDefinitions || resolversDefinitions.length === 0) { + if (!resolversDefinitions || (Array.isArray(resolversDefinitions) && resolversDefinitions.length === 0)) { return {} as T; } + if (!Array.isArray(resolversDefinitions)) { + return resolversDefinitions; + } + if (resolversDefinitions.length === 1) { - const singleDefinition = resolversDefinitions[0]; - if (Array.isArray(singleDefinition)) { - return mergeResolvers(singleDefinition); - } - return singleDefinition; + return resolversDefinitions[0]; } type TFactory = (...args: any[]) => T; diff --git a/packages/merge/src/merge-schemas.ts b/packages/merge/src/merge-schemas.ts index 5fd180946b6..b841019c929 100644 --- a/packages/merge/src/merge-schemas.ts +++ b/packages/merge/src/merge-schemas.ts @@ -1,13 +1,13 @@ import { GraphQLSchema, DocumentNode, buildASTSchema, BuildSchemaOptions, buildSchema } from 'graphql'; -import { addResolversToSchema, addErrorLoggingToSchema, ILogger } from '@graphql-tools/schema'; +import { addResolversToSchema } from '@graphql-tools/schema'; import { mergeTypeDefs, Config } from './typedefs-mergers/merge-typedefs'; import { mergeResolvers } from './merge-resolvers'; import { IResolvers, - SchemaDirectiveVisitor, IResolverValidationOptions, asArray, getResolversFromSchema, + TypeSource, } from '@graphql-tools/utils'; import { mergeExtensions, extractExtensionsFromSchema, applyExtensions, SchemaExtensions } from './extensions'; @@ -22,23 +22,15 @@ export interface MergeSchemasConfig e /** * Additional type definitions to also merge */ - typeDefs?: (DocumentNode | string)[] | DocumentNode | string; + typeDefs?: TypeSource; /** * Additional resolvers to also merge */ resolvers?: Resolvers | Resolvers[]; - /** - * Schema directives to apply to the type definitions being merged, if provided - */ - schemaDirectives?: { [directiveName: string]: typeof SchemaDirectiveVisitor }; /** * Options to validate the resolvers being merged, if provided */ resolverValidationOptions?: IResolverValidationOptions; - /** - * Custom logger instance - */ - logger?: ILogger; } const defaultResolverValidationOptions: Partial = { @@ -54,7 +46,7 @@ const defaultResolverValidationOptions: Partial = { * @param config Configuration object */ export function mergeSchemas(config: MergeSchemasConfig) { - const typeDefs = mergeTypes(config); + const typeDefs = mergeTypeDefs([config.schemas, config.typeDefs], config); const extractedResolvers: IResolvers[] = []; const extractedExtensions: SchemaExtensions[] = []; for (const schema of config.schemas) { @@ -75,7 +67,7 @@ export function mergeSchemas(config: MergeSchemasConfig) { */ export async function mergeSchemasAsync(config: MergeSchemasConfig) { const [typeDefs, resolvers, extensions] = await Promise.all([ - mergeTypes(config), + mergeTypeDefs([config.schemas, config.typeDefs], config), Promise.all(config.schemas.map(async schema => getResolversFromSchema(schema))).then(extractedResolvers => mergeResolvers([...extractedResolvers, ...ensureResolvers(config)], config) ), @@ -87,10 +79,6 @@ export async function mergeSchemasAsync(config: MergeSchemasConfig) { return makeSchema({ resolvers, typeDefs, extensions }, config); } -function mergeTypes({ schemas, typeDefs, ...config }: MergeSchemasConfig) { - return mergeTypeDefs([...schemas, ...(typeDefs ? asArray(typeDefs) : [])], config); -} - function ensureResolvers(config: MergeSchemasConfig) { return config.resolvers ? asArray(config.resolvers) : []; } @@ -117,16 +105,6 @@ function makeSchema( }); } - // use logger - if (config.logger) { - schema = addErrorLoggingToSchema(schema, config.logger); - } - - // use schema directives - if (config.schemaDirectives) { - SchemaDirectiveVisitor.visitSchemaDirectives(schema, config.schemaDirectives); - } - // extensions applyExtensions(schema, extensions); diff --git a/packages/merge/src/typedefs-mergers/merge-typedefs.ts b/packages/merge/src/typedefs-mergers/merge-typedefs.ts index 2b4a6707ee6..02057f10f37 100644 --- a/packages/merge/src/typedefs-mergers/merge-typedefs.ts +++ b/packages/merge/src/typedefs-mergers/merge-typedefs.ts @@ -1,24 +1,28 @@ import { DefinitionNode, DocumentNode, - GraphQLSchema, parse, - Source, Kind, isSchema, OperationTypeDefinitionNode, OperationTypeNode, isDefinitionNode, + ParseOptions, } from 'graphql'; import { CompareFn, defaultStringComparator, isSourceTypes, isStringTypes } from './utils'; import { MergedResultMap, mergeGraphQLNodes, schemaDefSymbol } from './merge-nodes'; import { resetComments, printWithComments } from './comments'; -import { getDocumentNodeFromSchema } from '@graphql-tools/utils'; +import { + getDocumentNodeFromSchema, + GetDocumentNodeFromSchemaOptions, + isDocumentNode, + TypeSource, +} from '@graphql-tools/utils'; import { DEFAULT_OPERATION_TYPE_NAME_MAP } from './schema-def'; type Omit = Pick>; -export interface Config { +export interface Config extends ParseOptions, GetDocumentNodeFromSchemaOptions { /** * Produces `schema { query: ..., mutation: ..., subscription: ... }` * @@ -76,24 +80,18 @@ export interface Config { * Merges multiple type definitions into a single `DocumentNode` * @param types The type definitions to be merged */ -export function mergeTypeDefs(types: Array): DocumentNode; -export function mergeTypeDefs( - types: Array, - config?: Partial & { commentDescriptions: true } -): string; +export function mergeTypeDefs(typeSource: TypeSource): DocumentNode; +export function mergeTypeDefs(typeSource: TypeSource, config?: Partial & { commentDescriptions: true }): string; export function mergeTypeDefs( - types: Array, + typeSource: TypeSource, config?: Omit, 'commentDescriptions'> ): DocumentNode; -export function mergeTypeDefs( - types: Array, - config?: Partial -): DocumentNode | string { +export function mergeTypeDefs(typeSource: TypeSource, config?: Partial): DocumentNode | string { resetComments(); const doc = { kind: Kind.DOCUMENT, - definitions: mergeGraphQLTypes(types, { + definitions: mergeGraphQLTypes(typeSource, { useSchemaDefinition: true, forceSchemaDefinition: false, throwOnConflict: false, @@ -116,36 +114,38 @@ export function mergeTypeDefs( } function visitTypeSources( - types: Array, - allNodes: DefinitionNode[] = [] + typeSource: TypeSource, + options: ParseOptions & GetDocumentNodeFromSchemaOptions, + allNodes: DefinitionNode[] = [], + visitedTypeSources = new Set() ) { - for (const type of types) { - if (type) { - if (Array.isArray(type)) { - visitTypeSources(type, allNodes); - } else if (isSchema(type)) { - const documentNode = getDocumentNodeFromSchema(type); - visitTypeSources(documentNode.definitions as DefinitionNode[], allNodes); - } else if (isStringTypes(type) || isSourceTypes(type)) { - const documentNode = parse(type); - visitTypeSources(documentNode.definitions as DefinitionNode[], allNodes); - } else if (isDefinitionNode(type)) { - allNodes.push(type); - } else { - visitTypeSources(type.definitions as DefinitionNode[], allNodes); - } + if (typeSource && !visitedTypeSources.has(typeSource)) { + visitedTypeSources.add(typeSource); + if (typeof typeSource === 'function') { + visitTypeSources(typeSource(), options, allNodes, visitedTypeSources); + } else if (Array.isArray(typeSource)) { + typeSource.forEach(type => visitTypeSources(type, options, allNodes, visitedTypeSources)); + } else if (isSchema(typeSource)) { + const documentNode = getDocumentNodeFromSchema(typeSource, options); + visitTypeSources(documentNode.definitions as DefinitionNode[], options, allNodes, visitedTypeSources); + } else if (isStringTypes(typeSource) || isSourceTypes(typeSource)) { + const documentNode = parse(typeSource, options); + visitTypeSources(documentNode.definitions as DefinitionNode[], options, allNodes, visitedTypeSources); + } else if (typeof typeSource === 'object' && isDefinitionNode(typeSource)) { + allNodes.push(typeSource); + } else if (isDocumentNode(typeSource)) { + visitTypeSources(typeSource.definitions as DefinitionNode[], options, allNodes, visitedTypeSources); + } else { + throw new Error(`typeDefs must contain only strings, documents, schemas, or functions, got ${typeof typeSource}`); } } return allNodes; } -export function mergeGraphQLTypes( - types: Array, - config: Config -): DefinitionNode[] { +export function mergeGraphQLTypes(typeSource: TypeSource, config: Config): DefinitionNode[] { resetComments(); - const allNodes = visitTypeSources(types); + const allNodes = visitTypeSources(typeSource, config); const mergedNodes: MergedResultMap = mergeGraphQLNodes(allNodes, config); diff --git a/packages/merge/tests/extract-extensions-from-schema.spec.ts b/packages/merge/tests/extract-extensions-from-schema.spec.ts index 1823fec4d7b..6e2cae3fe1a 100644 --- a/packages/merge/tests/extract-extensions-from-schema.spec.ts +++ b/packages/merge/tests/extract-extensions-from-schema.spec.ts @@ -1,7 +1,7 @@ -import { buildSchema, GraphQLEnumType, GraphQLSchema, printSchema, buildClientSchema, buildASTSchema, parse } from 'graphql'; -import { assertSome } from '@graphql-tools/utils'; +import { buildSchema, GraphQLSchema, printSchema, buildASTSchema, parse } from 'graphql'; +import { assertGraphQLEnumType, assertGraphQLInputObjectType, assertGraphQLObjectType, assertGraphQLInterfaceType, assertGraphQLUnionType, assertGraphQLScalerType } from '../../testing/assertion'; +import { assertSome } from 'packages/utils/src/helpers'; import { extractExtensionsFromSchema, mergeExtensions, applyExtensions } from '../src/extensions' -import { assertGraphQLEnumType, assertGraphQLInputObjectType, assertGraphQLInterfaceType, assertGraphQLObjectType, assertGraphQLScalerType, assertGraphQLUnionType } from '../../testing/assertion'; describe('extensions', () => { let schema: GraphQLSchema; @@ -69,25 +69,25 @@ describe('extensions', () => { MyScalar.extensions = { scalar: true }; const { types: extensions } = extractExtensionsFromSchema(schema); - expect(extensions.MyInput.extensions).toEqual({ input: true }) - expect(extensions.MyType.extensions).toEqual({ type: true }) - expect(extensions.Node.extensions).toEqual({ interface: true }) - expect(extensions.MyEnum.extensions).toEqual({ enum: true }) - expect(extensions.MyUnion.extensions).toEqual({ union: true }) - expect(extensions.MyScalar.extensions).toEqual({ scalar: true }) + expect(extensions['MyInput'].extensions).toEqual({ input: true }) + expect(extensions['MyType'].extensions).toEqual({ type: true }) + expect(extensions['Node'].extensions).toEqual({ interface: true }) + expect(extensions['MyEnum'].extensions).toEqual({ enum: true }) + expect(extensions['MyUnion'].extensions).toEqual({ union: true }) + expect(extensions['MyScalar'].extensions).toEqual({ scalar: true }) }); it('Should extract extensions correctly for fields arguments', () => { const queryType = schema.getQueryType() assertSome(queryType) - queryType.getFields().t.args[0].extensions = { fieldArg: true }; + queryType.getFields()['t'].args[0].extensions = { fieldArg: true }; const { types: extensions } = extractExtensionsFromSchema(schema); - if (extensions.Query.type !== "object") { + if (extensions['Query'].type !== "object") { throw new Error("Unexpected type.") } - expect(extensions.Query.fields.t.arguments.i).toEqual({ fieldArg: true }) + expect(extensions['Query'].fields['t'].arguments['i']).toEqual({ fieldArg: true }) }); it('Should extract extensions correctly for enum values', () => { @@ -95,37 +95,37 @@ describe('extensions', () => { assertGraphQLEnumType(MyEnum) MyEnum.getValues()[0].extensions = { enumValue: true }; - const { types: extensions } = extractExtensionsFromSchema(schema); - if (extensions.MyEnum.type !== "enum") { - throw new Error("Unexpected type.") - } - expect(extensions.MyEnum.values.A).toEqual({ enumValue: true }); - expect(extensions.MyEnum.values.B).toEqual({}); - expect(extensions.MyEnum.values.C).toEqual({}); + const { types: extensions } = extractExtensionsFromSchema(schema); + if (extensions['MyEnum'].type !== "enum") { + throw new Error("Unexpected type.") + } + expect(extensions['MyEnum'].values['A']).toEqual({ enumValue: true }); + expect(extensions['MyEnum'].values['B']).toEqual({}); + expect(extensions['MyEnum'].values['C']).toEqual({}); }); it('Should extract extensions correctly for fields', () => { const queryType = schema.getQueryType() assertSome(queryType) - queryType.getFields().t.extensions = { field: true }; + queryType.getFields()['t'].extensions = { field: true }; const { types: extensions } = extractExtensionsFromSchema(schema); - if (extensions.Query.type !== "object") { + if (extensions['Query'].type !== "object") { throw new Error("Unexpected type.") } - expect(extensions.Query.fields.t.extensions).toEqual({ field: true }) + expect(extensions['Query'].fields['t'].extensions).toEqual({ field: true }) }); it('Should extract extensions correctly for input fields', () => { const MyInput = schema.getType('MyInput') -assertGraphQLInputObjectType(MyInput) - MyInput.getFields().foo.extensions = { inputField: true }; + assertGraphQLInputObjectType(MyInput) + MyInput.getFields()['foo'].extensions = { inputField: true }; - const { types: extensions } = extractExtensionsFromSchema(schema); - if (extensions.MyInput.type !== "input") { - throw new Error("Unexpected type.") - } - expect(extensions.MyInput.fields.foo.extensions).toEqual({ inputField: true }) + const { types: extensions } = extractExtensionsFromSchema(schema); + if (extensions['MyInput'].type !== "input") { + throw new Error("Unexpected type.") + } + expect(extensions['MyInput'].fields['foo'].extensions).toEqual({ inputField: true }) }); }); @@ -146,7 +146,7 @@ assertGraphQLInputObjectType(MyInput) const extensions = extractExtensionsFromSchema(schema); const secondExtensions = extractExtensionsFromSchema(secondSchema); const mergedExtensions = mergeExtensions([extensions, secondExtensions]); - expect(mergedExtensions.types.Query.extensions).toEqual({ queryTest: true, querySecondTest: true }) + expect(mergedExtensions.types['Query'].extensions).toEqual({ queryTest: true, querySecondTest: true }) }) }); @@ -171,12 +171,12 @@ assertGraphQLInputObjectType(MyInput) let MyScalar = schema.getType('MyScalar') assertGraphQLScalerType(MyScalar) MyScalar.extensions = { scalar: true }; - MyInput.getFields().foo.extensions = { inputField: true }; + MyInput.getFields()['foo'].extensions = { inputField: true }; let QueryType = schema.getQueryType(); assertSome(QueryType) - QueryType.getFields().t.extensions = { field: true }; + QueryType.getFields()['t'].extensions = { field: true }; MyEnum.getValues()[0].extensions = { enumValue: true }; - QueryType.getFields().t.args[0].extensions = { fieldArg: true }; + QueryType.getFields()['t'].args[0].extensions = { fieldArg: true }; const result = extractExtensionsFromSchema(schema); const cleanSchema = buildASTSchema(parse(printSchema(schema))); @@ -207,12 +207,12 @@ assertGraphQLInputObjectType(MyInput) MyScalar = modifiedSchema.getType('MyScalar') assertGraphQLScalerType(MyScalar) expect(MyScalar.extensions).toEqual({ scalar: true }); - expect(MyInput.getFields().foo.extensions).toEqual({ inputField: true }); + expect(MyInput.getFields()['foo'].extensions).toEqual({ inputField: true }); QueryType = modifiedSchema.getQueryType(); assertSome(QueryType) - expect(QueryType.getFields().t.extensions).toEqual({ field: true }); + expect(QueryType.getFields()['t'].extensions).toEqual({ field: true }); expect(MyEnum.getValues()[0].extensions).toEqual({ enumValue: true }); - expect(QueryType.getFields().t.args[0].extensions).toEqual({ fieldArg: true }); + expect(QueryType.getFields()['t'].args[0].extensions).toEqual({ fieldArg: true }); }); }) }); diff --git a/packages/mock/package.json b/packages/mock/package.json index 33bb65ad49e..fa16db31983 100644 --- a/packages/mock/package.json +++ b/packages/mock/package.json @@ -33,6 +33,7 @@ }, "dependencies": { "@graphql-tools/schema": "^7.0.0", + "@graphql-tools/merge": "6.2.14", "@graphql-tools/utils": "^7.0.0", "fast-json-stable-stringify": "^2.1.0", "ts-is-defined": "^1.0.0", diff --git a/packages/mock/src/mockServer.ts b/packages/mock/src/mockServer.ts index 301e9ebcf55..bb3bc2a9123 100644 --- a/packages/mock/src/mockServer.ts +++ b/packages/mock/src/mockServer.ts @@ -1,6 +1,6 @@ -import { ITypeDefinitions } from '@graphql-tools/utils'; -import { GraphQLSchema, isSchema, graphql } from 'graphql'; -import { buildSchemaFromTypeDefinitions } from '@graphql-tools/schema'; +import { TypeSource } from '@graphql-tools/utils'; +import { GraphQLSchema, isSchema, graphql, buildASTSchema } from 'graphql'; +import { mergeTypeDefs } from '@graphql-tools/merge'; import { addMocksToSchema } from './addMocksToSchema'; import { IMockServer, IMocks } from './types'; @@ -16,15 +16,11 @@ import { IMockServer, IMocks } from './types'; * overwritten to provide mock data. This can be used to mock some parts of the * server and not others. */ -export function mockServer( - schema: GraphQLSchema | ITypeDefinitions, - mocks: IMocks, - preserveResolvers = false -): IMockServer { +export function mockServer(schema: TypeSource, mocks: IMocks, preserveResolvers = false): IMockServer { let mySchema: GraphQLSchema; if (!isSchema(schema)) { // TODO: provide useful error messages here if this fails - mySchema = buildSchemaFromTypeDefinitions(schema); + mySchema = buildASTSchema(mergeTypeDefs(schema)); } else { mySchema = schema; } diff --git a/packages/mock/tests/mocking-compatibility.spec.ts b/packages/mock/tests/mocking-compatibility.spec.ts index c2c85745137..4c2ffc15ee3 100644 --- a/packages/mock/tests/mocking-compatibility.spec.ts +++ b/packages/mock/tests/mocking-compatibility.spec.ts @@ -11,7 +11,6 @@ import { sentence, first_name } from 'casual'; import { addMocksToSchema, MockList, mockServer, IMocks, IMockStore } from '../src'; import { addResolversToSchema, - buildSchemaFromTypeDefinitions, makeExecutableSchema, } from '@graphql-tools/schema'; @@ -100,7 +99,7 @@ describe('Mock retro-compatibility', () => { }); test('throws an error if second argument is not a Map', () => { - const jsSchema = buildSchemaFromTypeDefinitions(shorthand); + const jsSchema = buildSchema(shorthand); expect(() => addMocksToSchema({ schema: jsSchema, @@ -110,7 +109,7 @@ describe('Mock retro-compatibility', () => { }); test('mocks the default types for you', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const mockMap = {}; jsSchema = addMocksToSchema({ schema: jsSchema, mocks: mockMap }); const testQuery = `{ @@ -171,7 +170,7 @@ describe('Mock retro-compatibility', () => { }); test('mockServer is able to preserveResolvers of a prebuilt schema', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const resolvers = { RootQuery: { returnString: () => 'someString', @@ -208,7 +207,7 @@ describe('Mock retro-compatibility', () => { }); test('lets you use mockServer with prebuilt schema', () => { - const jsSchema = buildSchemaFromTypeDefinitions(shorthand); + const jsSchema = buildSchema(shorthand); const testQuery = `{ returnInt returnFloat @@ -248,7 +247,7 @@ describe('Mock retro-compatibility', () => { }); test('does not mask resolveType functions if you tell it not to', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); let spy = 0; const resolvers = { BirdsAndBees: { @@ -284,7 +283,7 @@ describe('Mock retro-compatibility', () => { // TODO test mockServer with precompiled schema test('can mock Enum', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const mockMap = {}; jsSchema = addMocksToSchema({ schema: jsSchema, mocks: mockMap }); const testQuery = `{ @@ -296,7 +295,7 @@ describe('Mock retro-compatibility', () => { }); test('can mock Enum with a certain value', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const mockMap = { SomeEnum: () => 'C', }; @@ -310,7 +309,7 @@ describe('Mock retro-compatibility', () => { }); test('can mock Unions', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const mockMap = { Int: () => 10, String: () => 'aha', @@ -350,7 +349,7 @@ describe('Mock retro-compatibility', () => { }); test('can mock Interfaces by default', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const mockMap = { Int: () => 10, String: () => 'aha', @@ -392,7 +391,7 @@ describe('Mock retro-compatibility', () => { }); it('can mock nullable Interfaces', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const mockMap = { Bird: (): null => null, @@ -458,7 +457,7 @@ describe('Mock retro-compatibility', () => { }); test('can support explicit Interface mock with resolver', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); let spy = 0; const mockMap = { Bird: () => ({ @@ -508,7 +507,7 @@ describe('Mock retro-compatibility', () => { }); test('can support explicit UnionType mock', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); let spy = 0; const mockMap = { Bird: () => ({ @@ -549,7 +548,7 @@ describe('Mock retro-compatibility', () => { }); test('throws an error when __typename is not returned within an explicit interface mock', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const mockMap = { Bird: (_root: any, args: any) => ({ id: args.id, @@ -575,7 +574,7 @@ describe('Mock retro-compatibility', () => { }); test('throws an error in resolve if mock type is not defined', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const mockMap = {}; jsSchema = addMocksToSchema({ schema: jsSchema, mocks: mockMap }); const testQuery = `{ @@ -588,7 +587,7 @@ describe('Mock retro-compatibility', () => { }); test('throws an error in resolve if mock type is not defined and resolver failed', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const resolvers = { MissingMockType: { __serialize: (val: string) => val, @@ -617,7 +616,7 @@ describe('Mock retro-compatibility', () => { }); test('can preserve scalar resolvers', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const resolvers = { MissingMockType: { __serialize: (val: string) => val, @@ -649,7 +648,7 @@ describe('Mock retro-compatibility', () => { }); test('can mock an Int', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const mockMap = { Int: () => 55 }; jsSchema = addMocksToSchema({ schema: jsSchema, mocks: mockMap }); const testQuery = `{ @@ -661,7 +660,7 @@ describe('Mock retro-compatibility', () => { }); test('can mock a Float', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const mockMap = { Float: () => 55.5 }; jsSchema = addMocksToSchema({ schema: jsSchema, mocks: mockMap }); const testQuery = `{ @@ -672,7 +671,7 @@ describe('Mock retro-compatibility', () => { }); }); test('can mock a String', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const mockMap = { String: () => 'a string' }; jsSchema = addMocksToSchema({ schema: jsSchema, mocks: mockMap }); const testQuery = `{ @@ -683,7 +682,7 @@ describe('Mock retro-compatibility', () => { }); }); test('can mock a Boolean', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const mockMap = { Boolean: () => true }; jsSchema = addMocksToSchema({ schema: jsSchema, mocks: mockMap }); const testQuery = `{ @@ -694,7 +693,7 @@ describe('Mock retro-compatibility', () => { }); }); test('can mock an ID', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const mockMap = { ID: () => 'ea5bdc19' }; jsSchema = addMocksToSchema({ schema: jsSchema, mocks: mockMap }); const testQuery = `{ @@ -705,7 +704,7 @@ describe('Mock retro-compatibility', () => { }); }); test('nullable type is nullable', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const mockMap = { String: (): null => null }; jsSchema = addMocksToSchema({ schema: jsSchema, mocks: mockMap }); const testQuery = `{ @@ -716,7 +715,7 @@ describe('Mock retro-compatibility', () => { }); }); test('can mock a nonNull type', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const mockMap = { String: () => 'nonnull' }; jsSchema = addMocksToSchema({ schema: jsSchema, mocks: mockMap }); const testQuery = `{ @@ -727,7 +726,7 @@ describe('Mock retro-compatibility', () => { }); }); test('nonNull type is not nullable', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const mockMap = { String: (): null => null }; jsSchema = addMocksToSchema({ schema: jsSchema, mocks: mockMap }); const testQuery = `{ @@ -739,7 +738,7 @@ describe('Mock retro-compatibility', () => { }); }); test('can mock object types', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const mockMap = { String: () => 'abc', Int: () => 123, @@ -757,7 +756,7 @@ describe('Mock retro-compatibility', () => { }); test('can mock a list of ints', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const mockMap = { Int: () => 123 }; jsSchema = addMocksToSchema({ schema: jsSchema, mocks: mockMap }); const testQuery = `{ @@ -772,7 +771,7 @@ describe('Mock retro-compatibility', () => { }); test('can mock a list of lists of objects', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const mockMap = { String: () => 'a', Int: () => 1, @@ -799,7 +798,7 @@ describe('Mock retro-compatibility', () => { }); test('does not mask resolvers if you tell it not to', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const mockMap = { RootQuery: () => ({ returnInt: (_root: any, _args: Record) => 42, // a) in resolvers, will not be used @@ -836,7 +835,7 @@ describe('Mock retro-compatibility', () => { }); test('lets you mock non-leaf types conveniently', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const mockMap = { Bird: () => ({ returnInt: 12, @@ -865,7 +864,7 @@ describe('Mock retro-compatibility', () => { }); test('lets you mock and resolve non-leaf types concurrently', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const resolvers = { RootQuery: { returnListOfInt: () => [1, 2, 3], @@ -908,7 +907,7 @@ describe('Mock retro-compatibility', () => { }); test('lets you mock and resolve non-leaf types concurrently, support promises', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const resolvers = { RootQuery: { returnObject: () => @@ -948,7 +947,7 @@ describe('Mock retro-compatibility', () => { }); test('lets you mock and resolve non-leaf types concurrently, support defineProperty', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const objProxy = {}; Object.defineProperty( objProxy, @@ -989,7 +988,7 @@ describe('Mock retro-compatibility', () => { }); }); - test('let you mock with preserving resolvers, also when using logger', () => { + test('let you mock with preserving resolvers', () => { const resolvers = { RootQuery: { returnString: () => 'woot!?', // a) resolve of a string @@ -998,7 +997,6 @@ describe('Mock retro-compatibility', () => { let jsSchema = makeExecutableSchema({ typeDefs: [shorthand], resolvers, - logger: console, }); const mockMap = { Int: () => 123, // b) mock of Int. @@ -1028,7 +1026,7 @@ describe('Mock retro-compatibility', () => { }); test('let you resolve null with mocking and preserving resolvers', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const resolvers = { RootQuery: { returnString: (): string => null, // a) resolve of a string @@ -1063,7 +1061,7 @@ describe('Mock retro-compatibility', () => { }); test('lets you mock root query fields', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const resolvers = { RootQuery: { returnStringArgument: (_: void, a: Record) => a.s, @@ -1082,7 +1080,7 @@ describe('Mock retro-compatibility', () => { }); test('lets you mock root mutation fields', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const resolvers = { RootMutation: { returnStringArgument: (_: void, a: Record) => a.s, @@ -1101,7 +1099,7 @@ describe('Mock retro-compatibility', () => { }); test('lets you mock a list of a certain length', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const mockMap = { RootQuery: () => ({ returnListOfInt: () => new MockList(3) }), Int: () => 12, @@ -1119,7 +1117,7 @@ describe('Mock retro-compatibility', () => { }); test('lets you mock a list of a random length', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const mockMap = { RootQuery: () => ({ returnListOfInt: () => new MockList([10, 20]) }), Int: () => 12, @@ -1136,7 +1134,7 @@ describe('Mock retro-compatibility', () => { }); test('lets you provide a function for your MockList', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const mockMap = { RootQuery: () => ({ returnListOfInt: () => new MockList(2, () => 33), @@ -1165,7 +1163,7 @@ describe('Mock retro-compatibility', () => { }); test('lets you nest MockList in MockList', () => { - let jsSchema = buildSchemaFromTypeDefinitions(shorthand); + let jsSchema = buildSchema(shorthand); const mockMap = { RootQuery: () => ({ returnListOfListOfInt: () => new MockList(2, () => new MockList(3)), diff --git a/packages/resolvers-composition/src/resolvers-composition.ts b/packages/resolvers-composition/src/resolvers-composition.ts index 927c4b2875d..6b8cb197885 100644 --- a/packages/resolvers-composition/src/resolvers-composition.ts +++ b/packages/resolvers-composition/src/resolvers-composition.ts @@ -1,5 +1,5 @@ import { chainFunctions } from './chain-functions'; -import { get, set, flatten } from 'lodash'; +import _ from 'lodash'; import { GraphQLFieldResolver, GraphQLScalarTypeConfig } from 'graphql'; import { asArray } from '@graphql-tools/utils'; @@ -45,11 +45,9 @@ function resolveRelevantMappings = Record< if (!resolvers) { return []; } - return flatten( - Object.keys(resolvers).map(typeName => + return Object.keys(resolvers).map(typeName => resolveRelevantMappings(resolvers, `${typeName}.${fieldName}`, allMappings) - ) - ); + ).flat(); } if (fieldName === '*') { @@ -57,9 +55,7 @@ function resolveRelevantMappings = Record< if (!fieldMap) { return []; } - return flatten( - Object.keys(fieldMap).map(field => resolveRelevantMappings(resolvers, `${typeName}.${field}`, allMappings)) - ).filter(mapItem => !allMappings[mapItem]); + return Object.keys(fieldMap).map(field => resolveRelevantMappings(resolvers, `${typeName}.${field}`, allMappings)).flat().filter(mapItem => !allMappings[mapItem]); } else { const paths = []; @@ -87,11 +83,9 @@ function resolveRelevantMappings = Record< return []; } - return flatten( - Object.keys(fieldMap).map(fieldName => + return Object.keys(fieldMap).map(fieldName => resolveRelevantMappings(resolvers, `${typeName}.${fieldName}`, allMappings) - ) - ); + ).flat(); } return []; @@ -125,18 +119,18 @@ export function composeResolvers>( const composeFns = resolverPathMapping[fieldName]; const relevantFields = resolveRelevantMappings(resolvers, resolverPath + '.' + fieldName, mapping); - relevantFields.forEach((path: string) => { + for (const path of relevantFields) { mappingResult[path] = asArray(composeFns); - }); + } }); } }); - Object.keys(mappingResult).forEach(path => { - const fns = chainFunctions([...asArray(mappingResult[path]), () => get(resolvers, path)]); + for (const path in mappingResult) { + const fns = chainFunctions([...asArray(mappingResult[path]), () => _.get(resolvers, path)]); - set(resolvers, path, fns()); - }); + _.set(resolvers, path, fns()); + } return resolvers; } diff --git a/packages/schema/package.json b/packages/schema/package.json index 95052175ff1..85efbcc0a70 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -35,6 +35,7 @@ "input": "./src/index.ts" }, "dependencies": { + "@graphql-tools/merge": "6.2.14", "@graphql-tools/utils": "^7.1.2", "tslib": "~2.3.0", "value-or-promise": "1.0.10" diff --git a/packages/schema/src/addCatchUndefinedToSchema.ts b/packages/schema/src/addCatchUndefinedToSchema.ts deleted file mode 100644 index 806f81e4a1f..00000000000 --- a/packages/schema/src/addCatchUndefinedToSchema.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { GraphQLFieldResolver, defaultFieldResolver, GraphQLSchema } from 'graphql'; -import { mapSchema, MapperKind, Maybe } from '@graphql-tools/utils'; - -function decorateToCatchUndefined( - fn: Maybe>, - hint: string -): GraphQLFieldResolver { - const resolve = fn == null ? defaultFieldResolver : fn; - return (root, args, ctx, info) => { - const result = resolve(root, args, ctx, info); - if (typeof result === 'undefined') { - throw new Error(`Resolver for "${hint}" returned undefined`); - } - return result; - }; -} - -export function addCatchUndefinedToSchema(schema: GraphQLSchema): GraphQLSchema { - return mapSchema(schema, { - [MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName, typeName) => ({ - ...fieldConfig, - resolve: decorateToCatchUndefined(fieldConfig.resolve, `${typeName}.${fieldName}`), - }), - }); -} diff --git a/packages/schema/src/addErrorLoggingToSchema.ts b/packages/schema/src/addErrorLoggingToSchema.ts deleted file mode 100644 index ac42a375b48..00000000000 --- a/packages/schema/src/addErrorLoggingToSchema.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { GraphQLSchema } from 'graphql'; -import { mapSchema, MapperKind } from '@graphql-tools/utils'; -import { decorateWithLogger } from './decorateWithLogger'; -import { ILogger } from './types'; - -export function addErrorLoggingToSchema(schema: GraphQLSchema, logger?: ILogger): GraphQLSchema { - if (!logger) { - throw new Error('Must provide a logger'); - } - if (typeof logger.log !== 'function') { - throw new Error('Logger.log must be a function'); - } - return mapSchema(schema, { - [MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName, typeName) => ({ - ...fieldConfig, - resolve: decorateWithLogger(fieldConfig.resolve, logger, `${typeName}.${fieldName}`), - }), - }); -} diff --git a/packages/schema/src/addSchemaLevelResolver.ts b/packages/schema/src/addSchemaLevelResolver.ts deleted file mode 100644 index c82ba97354e..00000000000 --- a/packages/schema/src/addSchemaLevelResolver.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { defaultFieldResolver, GraphQLSchema, GraphQLFieldResolver } from 'graphql'; - -import { ValueOrPromise } from 'value-or-promise'; - -import { mapSchema, MapperKind } from '@graphql-tools/utils'; - -// wraps all resolvers of query, mutation or subscription fields -// with the provided function to simulate a root schema level resolver -export function addSchemaLevelResolver(schema: GraphQLSchema, fn: GraphQLFieldResolver): GraphQLSchema { - // TODO test that schema is a schema, fn is a function - const fnToRunOnlyOnce = runAtMostOncePerRequest(fn); - return mapSchema(schema, { - [MapperKind.ROOT_FIELD]: (fieldConfig, _fieldName, typeName, schema) => { - // XXX this should run at most once per request to simulate a true root resolver - // for graphql-js this is an approximation that works with queries but not mutations - // XXX if the type is a subscription, a same query AST will be ran multiple times so we - // deactivate here the runOnce if it's a subscription. This may not be optimal though... - const subscription = schema.getSubscriptionType(); - if (subscription != null && subscription.name === typeName) { - return { - ...fieldConfig, - resolve: wrapResolver(fieldConfig.resolve, fn), - }; - } - - return { - ...fieldConfig, - resolve: wrapResolver(fieldConfig.resolve, fnToRunOnlyOnce), - }; - }, - }); -} - -// XXX badly named function. this doesn't really wrap, it just chains resolvers... -function wrapResolver( - innerResolver: GraphQLFieldResolver | undefined, - outerResolver: GraphQLFieldResolver -): GraphQLFieldResolver { - return (obj, args, ctx, info) => { - return new ValueOrPromise(() => outerResolver(obj, args, ctx, info)) - .then(root => { - if (innerResolver != null) { - return innerResolver(root, args, ctx, info); - } - return defaultFieldResolver(root, args, ctx, info); - }) - .resolve(); - } -} - -// XXX this function only works for resolvers -// XXX very hacky way to remember if the function -// already ran for this request. This will only work -// if people don't actually cache the operation. -// if they do cache the operation, they will have to -// manually remove the __runAtMostOnce before every request. -function runAtMostOncePerRequest(fn: GraphQLFieldResolver): GraphQLFieldResolver { - let value: any; - const randomNumber = Math.random(); - return (root, args, ctx, info) => { - if (!info.operation['__runAtMostOnce']) { - info.operation['__runAtMostOnce'] = {}; - } - if (!info.operation['__runAtMostOnce'][randomNumber]) { - info.operation['__runAtMostOnce'][randomNumber] = true; - value = fn(root, args, ctx, info); - } - return value; - }; -} diff --git a/packages/schema/src/attachDirectiveResolvers.ts b/packages/schema/src/attachDirectiveResolvers.ts deleted file mode 100644 index 8ced8d0fc20..00000000000 --- a/packages/schema/src/attachDirectiveResolvers.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { GraphQLSchema, defaultFieldResolver } from 'graphql'; - -import { IDirectiveResolvers, mapSchema, MapperKind, getDirectives } from '@graphql-tools/utils'; - -export function attachDirectiveResolvers( - schema: GraphQLSchema, - directiveResolvers: IDirectiveResolvers -): GraphQLSchema { - if (typeof directiveResolvers !== 'object') { - throw new Error(`Expected directiveResolvers to be of type object, got ${typeof directiveResolvers}`); - } - - if (Array.isArray(directiveResolvers)) { - throw new Error('Expected directiveResolvers to be of type object, got Array'); - } - - return mapSchema(schema, { - [MapperKind.OBJECT_FIELD]: fieldConfig => { - const newFieldConfig = { ...fieldConfig }; - - const directives = getDirectives(schema, fieldConfig); - Object.keys(directives).forEach(directiveName => { - if (directiveResolvers[directiveName]) { - const resolver = directiveResolvers[directiveName]; - const originalResolver = newFieldConfig.resolve != null ? newFieldConfig.resolve : defaultFieldResolver; - const directiveArgs = directives[directiveName]; - newFieldConfig.resolve = (source, originalArgs, context, info) => { - return resolver( - () => - new Promise((resolve, reject) => { - const result = originalResolver(source, originalArgs, context, info); - if (result instanceof Error) { - reject(result); - } - resolve(result); - }), - source, - directiveArgs, - context, - info - ); - }; - } - }); - - return newFieldConfig; - }, - }); -} diff --git a/packages/schema/src/buildSchemaFromTypeDefinitions.ts b/packages/schema/src/buildSchemaFromTypeDefinitions.ts deleted file mode 100644 index 6836602caa9..00000000000 --- a/packages/schema/src/buildSchemaFromTypeDefinitions.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { extendSchema, buildASTSchema, GraphQLSchema, DocumentNode } from 'graphql'; - -import { ITypeDefinitions, GraphQLParseOptions, parseGraphQLSDL, isDocumentNode } from '@graphql-tools/utils'; - -import { filterAndExtractExtensionDefinitions } from './extensionDefinitions'; -import { concatenateTypeDefs } from './concatenateTypeDefs'; - -export function buildSchemaFromTypeDefinitions( - typeDefinitions: ITypeDefinitions, - parseOptions?: GraphQLParseOptions, - noExtensionExtraction?: boolean -): GraphQLSchema { - const document = buildDocumentFromTypeDefinitions(typeDefinitions, parseOptions); - - if (noExtensionExtraction) { - return buildASTSchema(document); - } - - const { typesAst, extensionsAst } = filterAndExtractExtensionDefinitions(document); - - const backcompatOptions = { commentDescriptions: true }; - let schema: GraphQLSchema = buildASTSchema(typesAst, backcompatOptions); - - if (extensionsAst.definitions.length > 0) { - schema = extendSchema(schema, extensionsAst, backcompatOptions); - } - - return schema; -} - -export function buildDocumentFromTypeDefinitions( - typeDefinitions: ITypeDefinitions, - parseOptions?: GraphQLParseOptions -): DocumentNode { - let document: DocumentNode; - if (typeof typeDefinitions === 'string') { - document = parseGraphQLSDL('', typeDefinitions, parseOptions).document; - } else if (Array.isArray(typeDefinitions)) { - document = parseGraphQLSDL('', concatenateTypeDefs(typeDefinitions), parseOptions).document; - } else if (isDocumentNode(typeDefinitions)) { - document = typeDefinitions; - } else { - const type = typeof typeDefinitions; - throw new Error(`typeDefs must be a string, array or schema AST, got ${type}`); - } - - return document; -} diff --git a/packages/schema/src/concatenateTypeDefs.ts b/packages/schema/src/concatenateTypeDefs.ts deleted file mode 100644 index 546c60e3b04..00000000000 --- a/packages/schema/src/concatenateTypeDefs.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { print, ASTNode } from 'graphql'; - -import { ITypedef } from '@graphql-tools/utils'; - -export function concatenateTypeDefs( - typeDefinitionsAry: Array, - calledFunctionRefs = new Set() -): string { - const resolvedTypeDefinitions = new Set(); - typeDefinitionsAry.forEach((typeDef: ITypedef) => { - if (typeof typeDef === 'function') { - if (!calledFunctionRefs.has(typeDef)) { - calledFunctionRefs.add(typeDef); - resolvedTypeDefinitions.add(concatenateTypeDefs(typeDef(), calledFunctionRefs)); - } - } else if (typeof typeDef === 'string') { - resolvedTypeDefinitions.add(typeDef.trim()); - } else if ((typeDef as ASTNode).kind !== undefined) { - resolvedTypeDefinitions.add(print(typeDef).trim()); - } else { - const type = typeof typeDef; - throw new Error(`typeDef array must contain only strings, documents, or functions, got ${type}`); - } - }); - return [...resolvedTypeDefinitions].join('\n'); -} diff --git a/packages/schema/src/decorateWithLogger.ts b/packages/schema/src/decorateWithLogger.ts deleted file mode 100644 index 3345a1e8de3..00000000000 --- a/packages/schema/src/decorateWithLogger.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { defaultFieldResolver, GraphQLFieldResolver } from 'graphql'; -import { Maybe } from 'packages/graphql-tools/src'; -import { ILogger } from './types'; - -/* - * fn: The function to decorate with the logger - * logger: an object instance of type Logger - * hint: an optional hint to add to the error's message - */ -export function decorateWithLogger( - fn: Maybe>, - logger: ILogger, - hint: string -): GraphQLFieldResolver { - const resolver = fn != null ? fn : defaultFieldResolver; - - const logError = (e: Error) => { - // TODO: clone the error properly - const newE = new Error(); - newE.stack = e.stack; - /* istanbul ignore else: always get the hint from addErrorLoggingToSchema */ - if (hint) { - newE['originalMessage'] = e.message; - newE.message = `Error in resolver ${hint}\n${e.message}`; - } - logger.log(newE); - }; - - return (root, args, ctx, info) => { - try { - const result = resolver(root, args, ctx, info); - // If the resolver returns a Promise log any Promise rejects. - if (result && typeof result.then === 'function' && typeof result.catch === 'function') { - result.catch((reason: Error | string) => { - // make sure that it's an error we're logging. - const error = reason instanceof Error ? reason : new Error(reason); - logError(error); - - // We don't want to leave an unhandled exception so pass on error. - return reason; - }); - } - return result; - } catch (e) { - logError(e); - // we want to pass on the error, just in case. - throw e; - } - }; -} diff --git a/packages/schema/src/extensionDefinitions.ts b/packages/schema/src/extensionDefinitions.ts deleted file mode 100644 index d509b1da691..00000000000 --- a/packages/schema/src/extensionDefinitions.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { DocumentNode, DefinitionNode, Kind } from 'graphql'; - -const isExtensionNode = (def: DefinitionNode) => - def.kind === Kind.OBJECT_TYPE_EXTENSION || - def.kind === Kind.INTERFACE_TYPE_EXTENSION || - def.kind === Kind.INPUT_OBJECT_TYPE_EXTENSION || - def.kind === Kind.UNION_TYPE_EXTENSION || - def.kind === Kind.ENUM_TYPE_EXTENSION || - def.kind === Kind.SCALAR_TYPE_EXTENSION || - def.kind === Kind.SCHEMA_EXTENSION; - -export function filterAndExtractExtensionDefinitions(ast: DocumentNode) { - const extensionDefs: DefinitionNode[] = []; - const typesDefs: DefinitionNode[] = []; - ast.definitions.forEach(def => { - if (isExtensionNode(def)) { - extensionDefs.push(def); - } else { - typesDefs.push(def); - } - }); - - return { - typesAst: { - ...ast, - definitions: typesDefs, - }, - extensionsAst: { - ...ast, - definitions: extensionDefs, - }, - }; -} - -export function filterExtensionDefinitions(ast: DocumentNode) { - const { typesAst } = filterAndExtractExtensionDefinitions(ast); - return typesAst; -} - -export function extractExtensionDefinitions(ast: DocumentNode) { - const { extensionsAst } = filterAndExtractExtensionDefinitions(ast); - return extensionsAst; -} diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts index 15ad2b2fafd..4d090541b25 100644 --- a/packages/schema/src/index.ts +++ b/packages/schema/src/index.ts @@ -1,15 +1,7 @@ -export { addSchemaLevelResolver } from './addSchemaLevelResolver'; export { assertResolversPresent } from './assertResolversPresent'; -export { attachDirectiveResolvers } from './attachDirectiveResolvers'; -export { buildSchemaFromTypeDefinitions, buildDocumentFromTypeDefinitions } from './buildSchemaFromTypeDefinitions'; export { chainResolvers } from './chainResolvers'; -export { concatenateTypeDefs } from './concatenateTypeDefs'; -export { decorateWithLogger } from './decorateWithLogger'; -export * from './extensionDefinitions'; export { addResolversToSchema } from './addResolversToSchema'; export { checkForResolveTypeResolver } from './checkForResolveTypeResolver'; export { extendResolversFromInterfaces } from './extendResolversFromInterfaces'; -export { addErrorLoggingToSchema } from './addErrorLoggingToSchema'; -export { addCatchUndefinedToSchema } from './addCatchUndefinedToSchema'; export * from './makeExecutableSchema'; export * from './types'; diff --git a/packages/schema/src/makeExecutableSchema.ts b/packages/schema/src/makeExecutableSchema.ts index 552b47520cf..671afd9dff7 100644 --- a/packages/schema/src/makeExecutableSchema.ts +++ b/packages/schema/src/makeExecutableSchema.ts @@ -1,15 +1,11 @@ -import { GraphQLFieldResolver } from 'graphql'; +import { buildASTSchema } from 'graphql'; -import { mergeDeep, SchemaDirectiveVisitor, pruneSchema } from '@graphql-tools/utils'; +import { pruneSchema } from '@graphql-tools/utils'; import { addResolversToSchema } from './addResolversToSchema'; -import { attachDirectiveResolvers } from './attachDirectiveResolvers'; import { assertResolversPresent } from './assertResolversPresent'; -import { addSchemaLevelResolver } from './addSchemaLevelResolver'; -import { buildSchemaFromTypeDefinitions } from './buildSchemaFromTypeDefinitions'; -import { addErrorLoggingToSchema } from './addErrorLoggingToSchema'; -import { addCatchUndefinedToSchema } from './addCatchUndefinedToSchema'; import { ExecutableSchemaTransformation, IExecutableSchemaDefinition } from './types'; +import { mergeResolvers, mergeTypeDefs } from '@graphql-tools/merge'; /** * Builds a schema from the provided type definitions and resolvers. @@ -58,17 +54,12 @@ import { ExecutableSchemaTransformation, IExecutableSchemaDefinition } from './t export function makeExecutableSchema({ typeDefs, resolvers = {}, - logger, - allowUndefinedInResolve = true, resolverValidationOptions = {}, - directiveResolvers, - schemaDirectives, schemaTransforms: userProvidedSchemaTransforms, parseOptions = {}, inheritResolversFromInterfaces = false, pruningOptions, updateResolversInPlace = false, - noExtensionExtraction = false, }: IExecutableSchemaDefinition) { // Validate and clean up arguments if (typeof resolverValidationOptions !== 'object') { @@ -83,11 +74,10 @@ export function makeExecutableSchema({ const schemaTransforms: ExecutableSchemaTransformation[] = [ schema => { // We allow passing in an array of resolver maps, in which case we merge them - const resolverMap: any = Array.isArray(resolvers) ? resolvers.reduce(mergeDeep, {}) : resolvers; const schemaWithResolvers = addResolversToSchema({ schema, - resolvers: resolverMap, + resolvers: mergeResolvers(resolvers), resolverValidationOptions, inheritResolversFromInterfaces, updateResolversInPlace, @@ -101,46 +91,18 @@ export function makeExecutableSchema({ }, ]; - if (!allowUndefinedInResolve) { - schemaTransforms.push(addCatchUndefinedToSchema); - } - - if (logger != null) { - schemaTransforms.push(schema => addErrorLoggingToSchema(schema, logger)); - } - - if (typeof resolvers['__schema'] === 'function') { - // TODO a bit of a hack now, better rewrite generateSchema to attach it there. - // not doing that now, because I'd have to rewrite a lot of tests. - schemaTransforms.push(schema => - addSchemaLevelResolver(schema, resolvers['__schema'] as GraphQLFieldResolver) - ); - } - if (userProvidedSchemaTransforms) { schemaTransforms.push(schema => userProvidedSchemaTransforms.reduce((s, schemaTransform) => schemaTransform(s), schema) ); } - // directive resolvers are implemented using SchemaDirectiveVisitor.visitSchemaDirectives - // schema visiting modifies the schema in place - if (directiveResolvers != null) { - schemaTransforms.push(schema => attachDirectiveResolvers(schema, directiveResolvers)); - } - - if (schemaDirectives != null) { - schemaTransforms.push(schema => { - SchemaDirectiveVisitor.visitSchemaDirectives(schema, schemaDirectives); - return schema; - }); - } - if (pruningOptions) { schemaTransforms.push(pruneSchema); } - const schemaFromTypeDefs = buildSchemaFromTypeDefinitions(typeDefs, parseOptions, noExtensionExtraction); + const mergedTypeDefs = mergeTypeDefs(typeDefs, parseOptions); + const schemaFromTypeDefs = buildASTSchema(mergedTypeDefs, parseOptions); return schemaTransforms.reduce((schema, schemaTransform) => schemaTransform(schema), schemaFromTypeDefs); } diff --git a/packages/schema/src/types.ts b/packages/schema/src/types.ts index e263d1340c1..ed28779d686 100644 --- a/packages/schema/src/types.ts +++ b/packages/schema/src/types.ts @@ -1,19 +1,13 @@ import { GraphQLSchema } from 'graphql'; import { - ITypeDefinitions, + TypeSource, IResolvers, IResolverValidationOptions, - IDirectiveResolvers, - SchemaDirectiveVisitorClass, GraphQLParseOptions, PruneSchemaOptions, } from '@graphql-tools/utils'; -export interface ILogger { - log: (error: Error) => void; -} - /** * Configuration object for creating an executable schema */ @@ -21,34 +15,15 @@ export interface IExecutableSchemaDefinition { /** * The type definitions used to create the schema */ - typeDefs: ITypeDefinitions; + typeDefs: TypeSource; /** * Object describing the field resolvers for the provided type definitions */ resolvers?: IResolvers | Array>; - /** - * Logger instance used to print errors to the server console that are - * usually swallowed by GraphQL. - */ - logger?: ILogger; - /** - * Set to `false` to have resolvers throw an if they return undefined, which - * can help make debugging easier - */ - allowUndefinedInResolve?: boolean; /** * Additional options for validating the provided resolvers */ resolverValidationOptions?: IResolverValidationOptions; - /** - * Map of directive resolvers - */ - directiveResolvers?: IDirectiveResolvers; - /** - * A map of schema directives used with the legacy class-based implementation - * of schema directives - */ - schemaDirectives?: Record; /** * An array of schema transformation functions */ @@ -71,10 +46,6 @@ export interface IExecutableSchemaDefinition { * Do not create a schema again and use the one from `buildASTSchema` */ updateResolversInPlace?: boolean; - /** - * Do not extract and apply extensions separately and leave it to `buildASTSchema` - */ - noExtensionExtraction?: boolean; } export type ExecutableSchemaTransformation = (schema: GraphQLSchema) => GraphQLSchema; diff --git a/packages/schema/tests/Logger.ts b/packages/schema/tests/Logger.ts deleted file mode 100644 index 5a50ace70e9..00000000000 --- a/packages/schema/tests/Logger.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * A very simple class for logging errors - */ - -import { ILogger } from '@graphql-tools/schema'; - -export class Logger implements ILogger { - public errors: Array; - public name: string | undefined; - private readonly callback: undefined | ((...args: any[]) => any); - - constructor(name?: string, callback?: (...args: any[]) => any) { - this.name = name; - this.errors = []; - this.callback = callback; - // TODO: should assert that callback is a function - } - - public log(err: Error) { - this.errors.push(err); - if (typeof this.callback === 'function') { - this.callback(err); - } - } - - public printOneError(e: Error): string { - return e.stack ? e.stack : ''; - } - - public printAllErrors() { - return this.errors.reduce( - (agg: string, e: Error) => `${agg}\n${this.printOneError(e)}`, - '', - ); - } -} diff --git a/packages/schema/tests/extensionExtraction.test.ts b/packages/schema/tests/extensionExtraction.test.ts deleted file mode 100644 index b972530441d..00000000000 --- a/packages/schema/tests/extensionExtraction.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { parse } from 'graphql'; - -import { extractExtensionDefinitions } from '@graphql-tools/schema'; - -describe('Extension extraction', () => { - test('extracts extended inputs', () => { - const typeDefs = ` - input Input { - foo: String - } - - extend input Input { - bar: String - } - `; - - const astDocument = parse(typeDefs); - const extensionAst = extractExtensionDefinitions(astDocument); - - expect(extensionAst.definitions).toHaveLength(1); - expect(extensionAst.definitions[0].kind).toBe('InputObjectTypeExtension'); - }); - - test('extracts extended unions', () => { - const typeDefs = ` - type Person { - name: String! - } - type Location { - name: String! - } - union Searchable = Person | Location - - type Post { - name: String! - } - extend union Searchable = Post - `; - - const astDocument = parse(typeDefs); - const extensionAst = extractExtensionDefinitions(astDocument); - - expect(extensionAst.definitions).toHaveLength(1); - expect(extensionAst.definitions[0].kind).toBe('UnionTypeExtension'); - }); - - test('extracts extended enums', () => { - const typeDefs = ` - enum Color { - RED - GREEN - } - - extend enum Color { - BLUE - } - `; - - const astDocument = parse(typeDefs); - const extensionAst = extractExtensionDefinitions(astDocument); - - expect(extensionAst.definitions).toHaveLength(1); - expect(extensionAst.definitions[0].kind).toBe('EnumTypeExtension'); - }); -}); diff --git a/packages/schema/tests/logger.test.ts b/packages/schema/tests/logger.test.ts deleted file mode 100644 index d7fc6c0e1e0..00000000000 --- a/packages/schema/tests/logger.test.ts +++ /dev/null @@ -1,409 +0,0 @@ -/* eslint-disable promise/param-names */ -import { graphql } from 'graphql'; - -import { makeExecutableSchema } from '@graphql-tools/schema'; - -import { Logger } from './Logger'; - -describe('Logger', () => { - test('logs the errors', () => { - const shorthand = ` - type RootQuery { - just_a_field: Int - } - type RootMutation { - species(name: String): String - stuff: String - } - schema { - query: RootQuery - mutation: RootMutation - } - `; - const resolve = { - RootMutation: { - species: () => { - throw new Error('oops!'); - }, - stuff: () => { - throw new Error('oh noes!'); - }, - }, - }; - const logger = new Logger(); - const jsSchema = makeExecutableSchema({ - typeDefs: shorthand, - resolvers: resolve, - logger, - }); - // calling the mutation here so the errors will be ordered. - const testQuery = 'mutation { species, stuff }'; - const expected0 = 'Error in resolver RootMutation.species\noops!'; - const expected1 = 'Error in resolver RootMutation.stuff\noh noes!'; - return graphql(jsSchema, testQuery).then(() => { - expect(logger.errors.length).toEqual(2); - expect(logger.errors[0].message).toEqual(expected0); - expect(logger.errors[1].message).toEqual(expected1); - }); - }); - - test('also forwards the errors when you tell it to', () => { - const shorthand = ` - type RootQuery { - species(name: String): String - } - schema { - query: RootQuery - } - `; - const resolve = { - RootQuery: { - species: () => { - throw new Error('oops!'); - }, - }, - }; - let loggedErr: Error | null = null; - const logger = new Logger('LoggyMcLogface', (e: Error) => { - loggedErr = e; - }); - const jsSchema = makeExecutableSchema({ - typeDefs: shorthand, - resolvers: resolve, - logger, - }); - const testQuery = '{ species }'; - return graphql(jsSchema, testQuery).then(() => { - expect(loggedErr).toEqual(logger.errors[0]); - }); - }); - - test('prints the errors when you want it to', () => { - const shorthand = ` - type RootQuery { - species(name: String): String - } - schema { - query: RootQuery - } - `; - const resolve = { - RootQuery: { - species: (_root: any, { name }: { name: string }) => { - if (name) { - throw new Error(name); - } - throw new Error('oops!'); - }, - }, - }; - const logger = new Logger(); - const jsSchema = makeExecutableSchema({ - typeDefs: shorthand, - resolvers: resolve, - logger, - }); - const testQuery = '{ q: species, p: species(name: "Peter") }'; - return graphql(jsSchema, testQuery).then(() => { - const allErrors = logger.printAllErrors(); - expect(allErrors).toMatch(/oops/); - expect(allErrors).toMatch(/Peter/); - }); - }); - - test('logs any Promise reject errors', () => { - const shorthand = ` - type RootQuery { - just_a_field: Int - } - type RootMutation { - species(name: String): String - stuff: String - } - schema { - query: RootQuery - mutation: RootMutation - } - `; - const resolve = { - RootMutation: { - species: () => - new Promise((_, reject) => { - reject(new Error('oops!')); - }), - stuff: () => - new Promise((_, reject) => { - reject(new Error('oh noes!')); - }), - }, - }; - const logger = new Logger(); - const jsSchema = makeExecutableSchema({ - typeDefs: shorthand, - resolvers: resolve, - logger, - }); - - const testQuery = 'mutation { species, stuff }'; - const expected0 = 'Error in resolver RootMutation.species\noops!'; - const expected1 = 'Error in resolver RootMutation.stuff\noh noes!'; - return graphql(jsSchema, testQuery).then(() => { - expect(logger.errors.length).toEqual(2); - expect(logger.errors[0].message).toEqual(expected0); - expect(logger.errors[1].message).toEqual(expected1); - }); - }); - - test('all Promise rejects will log an Error', () => { - const shorthand = ` - type RootQuery { - species(name: String): String - } - schema { - query: RootQuery - } - `; - const resolve = { - RootQuery: { - species: () => - new Promise((_, reject) => { - reject(new Error('oops!')); - }), - }, - }; - - let loggedErr: Error | null = null; - const logger = new Logger('LoggyMcLogface', (e: Error) => { - loggedErr = e; - }); - const jsSchema = makeExecutableSchema({ - typeDefs: shorthand, - resolvers: resolve, - logger, - }); - - const testQuery = '{ species }'; - return graphql(jsSchema, testQuery).then(() => { - expect(loggedErr).toEqual(logger.errors[0]); - }); - }); -}); - - - -describe('providing useful errors from resolvers', () => { - test('logs an error if a resolver fails', () => { - const shorthand = ` - type RootQuery { - species(name: String): String - } - schema { - query: RootQuery - } - `; - const resolve = { - RootQuery: { - species: (): string => { - throw new Error('oops!'); - }, - }, - }; - - // TODO: Should use a spy here instead of logger class - // to make sure we don't duplicate tests from Logger. - const logger = new Logger(); - const jsSchema = makeExecutableSchema({ - typeDefs: shorthand, - resolvers: resolve, - logger, - }); - const testQuery = '{ species }'; - const expected = 'Error in resolver RootQuery.species\noops!'; - return graphql(jsSchema, testQuery).then((_res) => { - expect(logger.errors.length).toEqual(1); - expect(logger.errors[0].message).toEqual(expected); - }); - }); - - test('will throw errors on undefined if you tell it to', () => { - const shorthand = ` - type RootQuery { - species(name: String): String - stuff: String - } - schema { - query: RootQuery - } - `; - const resolve = { - RootQuery: { - species: (): string | undefined => undefined, - stuff: () => 'stuff', - }, - }; - - const logger = new Logger(); - const jsSchema = makeExecutableSchema({ - typeDefs: shorthand, - resolvers: resolve, - logger, - allowUndefinedInResolve: false, - }); - const testQuery = '{ species, stuff }'; - const expectedErr = /Resolver for "RootQuery.species" returned undefined/; - const expectedResData = { species: null as string | null, stuff: 'stuff' }; - return graphql(jsSchema, testQuery).then((res) => { - expect(logger.errors.length).toEqual(1); - expect(logger.errors[0].message).toMatch(expectedErr); - expect(res.data).toEqual(expectedResData); - }); - }); - - test('decorateToCatchUndefined preserves default resolvers', () => { - const shorthand = ` - type Thread { - name: String - } - type RootQuery { - thread(name: String): Thread - } - schema { - query: RootQuery - } - `; - const resolve = { - RootQuery: { - thread(_root: any, args: Record) { - return args; - }, - }, - }; - - const jsSchema = makeExecutableSchema({ - typeDefs: shorthand, - resolvers: resolve, - allowUndefinedInResolve: false, - }); - const testQuery = `{ - thread(name: "SomeThread") { - name - } - }`; - const expectedResData = { - thread: { - name: 'SomeThread', - }, - }; - return graphql(jsSchema, testQuery).then((res) => { - expect(res.data).toEqual(expectedResData); - }); - }); - - test('decorateToCatchUndefined throws even if default resolvers are preserved', () => { - const shorthand = ` - type Thread { - name: String - } - type RootQuery { - thread(name: String): Thread - } - schema { - query: RootQuery - } - `; - const resolve = { - RootQuery: { - thread(_root: any, _args: Record) { - return { name: (): any => undefined }; - }, - }, - }; - - const jsSchema = makeExecutableSchema({ - typeDefs: shorthand, - resolvers: resolve, - allowUndefinedInResolve: false, - }); - const testQuery = `{ - thread { - name - } - }`; - return graphql(jsSchema, testQuery).then((res) => { - expect(res.errors?.[0].originalError?.message).toBe( - 'Resolver for "Thread.name" returned undefined', - ); - }); - }); - - test('will use default resolver when returning function properties ', () => { - const shorthand = ` - type Thread { - name: String - } - type RootQuery { - thread(name: String): Thread - } - schema { - query: RootQuery - } - `; - const resolve = { - RootQuery: { - thread(_root: any, args: Record) { - return { name: () => args.name }; - }, - }, - }; - - const jsSchema = makeExecutableSchema({ - typeDefs: shorthand, - resolvers: resolve, - allowUndefinedInResolve: false, - }); - const testQuery = `{ - thread(name: "SomeThread") { - name - } - }`; - const expectedResData = { - thread: { - name: 'SomeThread', - }, - }; - return graphql(jsSchema, testQuery).then((res) => { - expect(res.data).toEqual(expectedResData); - }); - }); - - test('will not throw errors on undefined by default', () => { - const shorthand = ` - type RootQuery { - species(name: String): String - stuff: String - } - schema { - query: RootQuery - } - `; - const resolve = { - RootQuery: { - species: (): string | undefined => undefined, - stuff: () => 'stuff', - }, - }; - - const logger = new Logger(); - const jsSchema = makeExecutableSchema({ - typeDefs: shorthand, - resolvers: resolve, - logger, - }); - const testQuery = '{ species, stuff }'; - const expectedResData = { species: null as string | null, stuff: 'stuff' }; - return graphql(jsSchema, testQuery).then((res) => { - expect(logger.errors.length).toEqual(0); - expect(res.data).toEqual(expectedResData); - }); - }); -}); diff --git a/packages/schema/tests/resolution.test.ts b/packages/schema/tests/resolution.test.ts deleted file mode 100644 index 9cc71a3c43c..00000000000 --- a/packages/schema/tests/resolution.test.ts +++ /dev/null @@ -1,148 +0,0 @@ -/* eslint-disable promise/param-names */ -import { - parse, - graphql, - subscribe, - graphqlSync, -} from 'graphql'; -import { PubSub } from 'graphql-subscriptions'; - -import { makeExecutableSchema, addSchemaLevelResolver } from '@graphql-tools/schema'; - -import { ExecutionResult } from '@graphql-tools/utils'; - -describe('Resolve', () => { - describe('addSchemaLevelResolver', () => { - const pubsub = new PubSub(); - const typeDefs = ` - type RootQuery { - printRoot: String! - printRootAgain: String! - } - - type RootMutation { - printRoot: String! - } - - type RootSubscription { - printRoot: String! - } - - schema { - query: RootQuery - mutation: RootMutation - subscription: RootSubscription - } - `; - const printRoot = (root: any) => root.toString(); - const resolvers = { - RootQuery: { - printRoot, - printRootAgain: printRoot, - }, - RootMutation: { - printRoot, - }, - RootSubscription: { - printRoot: { - subscribe: () => pubsub.asyncIterator('printRootChannel'), - }, - }, - }; - let schema = makeExecutableSchema({ typeDefs, resolvers }); - let schemaLevelResolverCalls = 0; - schema = addSchemaLevelResolver(schema, (root) => { - schemaLevelResolverCalls += 1; - return root; - }); - - test('should run the schema level resolver once in a same query', () => { - schemaLevelResolverCalls = 0; - const root = 'queryRoot'; - return graphql( - schema, - ` - query TestOnce { - printRoot - printRootAgain - } - `, - root, - ).then(({ data }) => { - expect(data).toEqual({ - printRoot: root, - printRootAgain: root, - }); - expect(schemaLevelResolverCalls).toEqual(1); - }); - }); - - test('should isolate roots from the different operation types', async () => { - schemaLevelResolverCalls = 0; - const queryRoot = 'queryRoot'; - const mutationRoot = 'mutationRoot'; - const subscriptionRoot = 'subscriptionRoot'; - const subscriptionRoot2 = 'subscriptionRoot2'; - - const sub = await subscribe( - schema, - parse(` - subscription TestSubscription { - printRoot - } - `), - ) as AsyncIterableIterator; - - const payload1 = sub.next(); - await pubsub.publish('printRootChannel', { printRoot: subscriptionRoot }); - - expect(await payload1).toEqual({ done: false, value: { data: { printRoot: subscriptionRoot } } }); - expect(schemaLevelResolverCalls).toEqual(1); - - const queryResult = await graphql( - schema, - ` - query TestQuery { - printRoot - } - `, - queryRoot, - ); - - expect(queryResult).toEqual({ data: { printRoot: queryRoot } }); - expect(schemaLevelResolverCalls).toEqual(2); - - const mutationResult = await graphql( - schema, - ` - mutation TestMutation { - printRoot - } - `, - mutationRoot, - ); - - expect(mutationResult).toEqual({ data: { printRoot: mutationRoot } }); - expect(schemaLevelResolverCalls).toEqual(3); - - await pubsub.publish('printRootChannel', { printRoot: subscriptionRoot2 }); - - expect(await sub.next()).toEqual({ done: false, value: { data: { printRoot: subscriptionRoot2 } } }); - expect(schemaLevelResolverCalls).toEqual(4); - }); - - it('should not force an otherwise synchronous operation to be asynchronous', () => { - const queryRoot = 'queryRoot'; - // This will throw an error if schema has any asynchronous resolvers - graphqlSync( - schema, - ` - query TestQuery { - printRoot - } - `, - queryRoot, - ); - }); - }); -}); diff --git a/packages/schema/tests/schemaGenerator.test.ts b/packages/schema/tests/schemaGenerator.test.ts index 483192650a7..ae5aed0582d 100644 --- a/packages/schema/tests/schemaGenerator.test.ts +++ b/packages/schema/tests/schemaGenerator.test.ts @@ -20,30 +20,19 @@ import { DocumentNode, GraphQLBoolean, graphqlSync, - GraphQLSchema, - GraphQLFieldResolver, } from 'graphql'; import { makeExecutableSchema, - addErrorLoggingToSchema, - addSchemaLevelResolver, addResolversToSchema, - attachDirectiveResolvers, chainResolvers, - concatenateTypeDefs, - ILogger, } from '@graphql-tools/schema'; import { IResolverValidationOptions, IResolvers, - IDirectiveResolvers, - NextResolverFn, - VisitSchemaKind, - ITypeDefinitions, - visitSchema, - ExecutionResult + ExecutionResult, + TypeSource } from '@graphql-tools/utils'; import TypeA from './fixtures/circularSchemaA'; @@ -88,7 +77,6 @@ const testSchema = ` } `; const testResolvers = { - __schema: () => ({ stuff: 'stuff', species: 'ROOT' }), RootQuery: { usecontext: (_r: any, _a: Record, ctx: any) => ctx.usecontext, species: (root: any, { name }: { name: string }) => @@ -118,22 +106,22 @@ describe('generating schema from shorthand', () => { test('throws an error if typeDefinitionNodes is neither string nor array nor schema AST', () => { expect(() => makeExecutableSchema({ - typeDefs: ({} as unknown) as ITypeDefinitions, + typeDefs: ({} as unknown) as TypeSource, resolvers: {}, }), ).toThrowError( - 'typeDefs must be a string, array or schema AST, got object', + 'typeDefs must contain only strings, documents, schemas, or functions, got object', ); }); test('throws an error if typeDefinitionNode array contains not only functions and strings', () => { expect(() => makeExecutableSchema({ - typeDefs: ([17] as unknown) as ITypeDefinitions, + typeDefs: ([17] as unknown) as TypeSource, resolvers: {}, }), ).toThrowError( - 'typeDef array must contain only strings, documents, or functions, got number', + 'typeDefs must contain only strings, documents, schemas, or functions, got number', ); }); @@ -360,8 +348,8 @@ describe('generating schema from shorthand', () => { resolvers: {}, }); expect(jsSchema.getQueryType()?.name).toBe('Query'); - expect(jsSchema.getQueryType()?.getFields().foo).toBeDefined(); - expect(jsSchema.getQueryType()?.getFields().bar).toBeDefined(); + expect(jsSchema.getQueryType()?.getFields()['foo']).toBeDefined(); + expect(jsSchema.getQueryType()?.getFields()['bar']).toBeDefined(); }); test('allow for a map of extensions in field resolver', () => { @@ -389,30 +377,6 @@ describe('generating schema from shorthand', () => { expect(extensions!.verbose).toBe(true); }); - test('can concatenateTypeDefs created by a function inside a closure', () => { - const typeA = { typeDefs: () => ['type TypeA { foo: String }'] }; - const typeB = { typeDefs: () => ['type TypeB { bar: String }'] }; - const typeC = { typeDefs: () => ['type TypeC { foo: String }'] }; - const typeD = { typeDefs: () => ['type TypeD { bar: String }'] }; - - function combineTypeDefs(...args: Array): any { - return { typeDefs: () => args.map((o) => o.typeDefs) }; - } - - const combinedAandB = combineTypeDefs(typeA, typeB); - const combinedCandD = combineTypeDefs(typeC, typeD); - - const result = concatenateTypeDefs([ - combinedAandB.typeDefs, - combinedCandD.typeDefs, - ]); - - expect(result).toMatch('type TypeA'); - expect(result).toMatch('type TypeB'); - expect(result).toMatch('type TypeC'); - expect(result).toMatch('type TypeD'); - }); - test('properly deduplicates the array of type DefinitionNodes', () => { const typeDefAry = [ ` @@ -751,7 +715,7 @@ describe('generating schema from shorthand', () => { const QueryResolver = class QueryResolver { private internalVersion = 1 - version(root: any, args: any, context: any) { + version() { return this.internalVersion } } @@ -911,80 +875,6 @@ describe('generating schema from shorthand', () => { expect(testType!.astNode!.directives!.length).toBe(1); }); - test('retains scalars after walking/recreating the schema', () => { - const shorthand = ` - scalar Test - - type Foo { - testField: Test - } - - type Query { - test: Test - testIn(input: Test): Test - } - `; - const resolveFunctions = { - Test: new GraphQLScalarType({ - name: 'Test', - description: 'Test resolver', - serialize(value) { - if (typeof value !== 'string' || value.indexOf('scalar:') !== 0) { - return `scalar:${value as string}`; - } - return value; - }, - parseValue(value) { - return `scalar:${value as string}`; - }, - parseLiteral(ast: any) { - switch (ast.kind) { - case Kind.STRING: - case Kind.INT: - return `scalar:${ast.value as string}`; - default: - return null; - } - }, - }), - Query: { - testIn(_: any, { input }: any) { - expect(input).toMatch('scalar:'); - return input; - }, - test() { - return 42; - }, - }, - }; - const walkedSchema = visitSchema( - makeExecutableSchema({ - typeDefs: shorthand, - resolvers: resolveFunctions, - }), - { - [VisitSchemaKind.ENUM_TYPE](type: GraphQLEnumType) { - return type; - }, - }, - ); - expect(walkedSchema.getType('Test')).toBeInstanceOf(GraphQLScalarType); - expect(walkedSchema.getType('Test')).toHaveProperty('description'); - expect(walkedSchema.getType('Test')!.description).toBe('Test resolver'); - const testQuery = ` - { - test - testIn(input: 1) - }`; - const resultPromise = graphql(walkedSchema, testQuery); - return resultPromise.then((result) => - expect(result.data).toEqual({ - test: 'scalar:42', - testIn: 'scalar:1', - }), - ); - }); - test('should support custom scalar usage on client-side query execution', () => { const shorthand = ` scalar CustomScalar @@ -2012,157 +1902,6 @@ To disable this validator, use: }); }); -describe('Add error logging to schema', () => { - test('throws an error if no logger is provided', () => { - expect(() => - addErrorLoggingToSchema(({} as unknown) as GraphQLSchema), - ).toThrow('Must provide a logger'); - }); - test('throws an error if logger.log is not a function', () => { - expect(() => - addErrorLoggingToSchema( - ({} as unknown) as GraphQLSchema, - ({ log: '1' } as unknown) as ILogger, - ), - ).toThrow('Logger.log must be a function'); - }); -}); - -describe('Attaching external data fetchers to schema', () => { - describe('Schema level resolver', () => { - test('actually runs', () => { - let jsSchema = makeExecutableSchema({ - typeDefs: testSchema, - resolvers: testResolvers, - }); - const rootResolver = () => ({ species: 'ROOT' }); - jsSchema = addSchemaLevelResolver(jsSchema, rootResolver); - const query = `{ - species(name: "strix") - }`; - return graphql(jsSchema, query).then((res) => { - expect(res.data!.species).toBe('ROOTstrix'); - }); - }); - - test('can wrap fields that do not have a resolver defined', () => { - let jsSchema = makeExecutableSchema({ - typeDefs: testSchema, - resolvers: testResolvers, - }); - const rootResolver = () => ({ stuff: 'stuff' }); - jsSchema = addSchemaLevelResolver(jsSchema, rootResolver); - const query = `{ - stuff - }`; - return graphql(jsSchema, query).then((res) => { - expect(res.data!.stuff).toBe('stuff'); - }); - }); - - test('runs only once per query', () => { - const simpleResolvers = { - RootQuery: { - usecontext: (_r: any, _a: Record, ctx: any) => - ctx.usecontext, - species: (root: any, { name }: { name: string }) => - (root.species as string) + name, - }, - }; - let jsSchema = makeExecutableSchema({ - typeDefs: testSchema, - resolvers: simpleResolvers, - }); - let count = 0; - const rootResolver = () => { - if (count === 0) { - count += 1; - return { stuff: 'stuff', species: 'some ' }; - } - return { stuff: 'EEE', species: 'EEE' }; - }; - jsSchema = addSchemaLevelResolver(jsSchema, rootResolver); - const query = `{ - species(name: "strix") - stuff - }`; - const expected = { - species: 'some strix', - stuff: 'stuff', - }; - return graphql(jsSchema, query).then((res) => { - expect(res.data).toEqual(expected); - }); - }); - - test('runs twice for two queries', () => { - const simpleResolvers = { - RootQuery: { - usecontext: (_r: any, _a: Record, ctx: any) => - ctx.usecontext, - species: (root: any, { name }: { name: string }) => - (root.species as string) + name, - }, - }; - let jsSchema = makeExecutableSchema({ - typeDefs: testSchema, - resolvers: simpleResolvers, - }); - let count = 0; - const rootResolver = () => { - if (count === 0) { - count += 1; - return { stuff: 'stuff', species: 'some ' }; - } - if (count === 1) { - count += 1; - return { stuff: 'stuff2', species: 'species2 ' }; - } - return { stuff: 'EEE', species: 'EEE' }; - }; - jsSchema = addSchemaLevelResolver(jsSchema, rootResolver); - const query = `{ - species(name: "strix") - stuff - }`; - const expected = { - species: 'some strix', - stuff: 'stuff', - }; - const expected2 = { - species: 'species2 strix', - stuff: 'stuff2', - }; - return graphql(jsSchema, query).then((res) => { - expect(res.data).toEqual(expected); - return graphql(jsSchema, query).then((res2) => - expect(res2.data).toEqual(expected2), - ); - }); - }); - - test('can attach things to context', () => { - let jsSchema = makeExecutableSchema({ - typeDefs: testSchema, - resolvers: testResolvers, - }); - const rootResolver = (_o: any, _a: Record, ctx: any) => { - ctx.usecontext = 'ABC'; - }; - jsSchema = addSchemaLevelResolver(jsSchema, rootResolver); - const query = `{ - usecontext - }`; - const expected = { - usecontext: 'ABC', - }; - return graphql(jsSchema, query, {}, {}).then((res) => { - expect(res.data).toEqual(expected); - }); - }); - }); -}); - describe('Generating a full graphQL schema with resolvers and connectors', () => { test('outputs a working GraphQL schema', () => { const schema = makeExecutableSchema({ @@ -2179,7 +1918,7 @@ describe('Generating a full graphQL schema with resolvers and connectors', () => stuff: 'stuff', usecontext: 'ABC', }; - return graphql(schema, query, {}, { usecontext: 'ABC' }).then((res) => { + return graphql(schema, query, { stuff: 'stuff', species: 'ROOT' }, { usecontext: 'ABC' }).then((res) => { expect(res.data).toEqual(expected); }); }); @@ -2212,248 +1951,6 @@ describe('chainResolvers', () => { }); }); -describe('attachDirectiveResolvers on field', () => { - const testSchemaWithDirectives = ` - directive @upper on FIELD_DEFINITION - directive @lower on FIELD_DEFINITION - directive @default(value: String!) on FIELD_DEFINITION - directive @catchError on FIELD_DEFINITION - - type TestObject { - hello: String @upper - } - type RootQuery { - hello: String @upper - withDefault: String @default(value: "some default_value") - object: TestObject - asyncResolver: String @upper - multiDirectives: String @upper @lower - throwError: String @catchError - } - schema { - query: RootQuery - } - `; - - const testObject = { - hello: 'giau. tran minh', - }; - - const testResolversDirectives = { - RootQuery: { - hello: () => 'giau. tran minh', - object: () => testObject, - asyncResolver: async () => Promise.resolve('giau. tran minh'), - multiDirectives: () => 'Giau. Tran Minh', - throwError: () => { - throw new Error('This error for testing'); - }, - }, - }; - - const directiveResolvers: IDirectiveResolvers = { - lower( - next: NextResolverFn, - _src: any, - _args: { [argName: string]: any }, - _context: any, - ) { - return next().then((str) => { - if (typeof str === 'string') { - return str.toLowerCase(); - } - return str; - }); - }, - upper( - next: NextResolverFn, - _src: any, - _args: { [argName: string]: any }, - _context: any, - ) { - return next().then((str) => { - if (typeof str === 'string') { - return str.toUpperCase(); - } - return str; - }); - }, - default( - next: NextResolverFn, - _src: any, - args: { [argName: string]: any }, - _context: any, - ) { - return next().then((res) => { - if (undefined === res) { - return args.value; - } - return res; - }); - }, - catchError( - next: NextResolverFn, - _src: any, - _args: { [argName: string]: any }, - _context: any, - ) { - return next().catch((error) => error.message); - }, - }; - - test('throws error if directiveResolvers argument is an array', () => { - const jsSchema = makeExecutableSchema({ - typeDefs: testSchema, - resolvers: testResolvers, - }); - expect(() => - attachDirectiveResolvers(jsSchema, ([ - 1, - ] as unknown) as IDirectiveResolvers), - ).toThrowError( - 'Expected directiveResolvers to be of type object, got Array', - ); - }); - - test('throws error if directiveResolvers argument is not an object', () => { - const jsSchema = makeExecutableSchema({ - typeDefs: testSchema, - resolvers: testResolvers, - }); - return expect(() => - attachDirectiveResolvers( - jsSchema, - ('a' as unknown) as IDirectiveResolvers, - ), - ).toThrowError( - 'Expected directiveResolvers to be of type object, got string', - ); - }); - - test('upper String from resolvers', () => { - const schema = makeExecutableSchema({ - typeDefs: testSchemaWithDirectives, - resolvers: testResolversDirectives, - directiveResolvers, - }); - const query = `{ - hello - }`; - const expected = { - hello: 'GIAU. TRAN MINH', - }; - return graphql(schema, query, {}, {}).then((res) => { - expect(res.data).toEqual(expected); - }); - }); - - test('using default resolver for object property', () => { - const schema = makeExecutableSchema({ - typeDefs: testSchemaWithDirectives, - resolvers: testResolversDirectives, - directiveResolvers, - }); - const query = `{ - object { - hello - } - }`; - const expected = { - object: { - hello: 'GIAU. TRAN MINH', - }, - }; - return graphql(schema, query, {}, {}).then((res) => { - expect(res.data).toEqual(expected); - }); - }); - - test('passes in directive arguments to the directive resolver', () => { - const schema = makeExecutableSchema({ - typeDefs: testSchemaWithDirectives, - resolvers: testResolversDirectives, - directiveResolvers, - }); - const query = `{ - withDefault - }`; - const expected = { - withDefault: 'some default_value', - }; - return graphql(schema, query, {}, {}).then((res) => { - expect(res.data).toEqual(expected); - }); - }); - - test('No effect if missing directive resolvers', () => { - const schema = makeExecutableSchema({ - typeDefs: testSchemaWithDirectives, - resolvers: testResolversDirectives, - directiveResolvers: {}, // Empty resolver - }); - const query = `{ - hello - }`; - const expected = { - hello: 'giau. tran minh', - }; - return graphql(schema, query, {}, {}).then((res) => { - expect(res.data).toEqual(expected); - }); - }); - - test('If resolver return Promise, keep using it', () => { - const schema = makeExecutableSchema({ - typeDefs: testSchemaWithDirectives, - resolvers: testResolversDirectives, - directiveResolvers, - }); - const query = `{ - asyncResolver - }`; - const expected = { - asyncResolver: 'GIAU. TRAN MINH', - }; - return graphql(schema, query, {}, {}).then((res) => { - expect(res.data).toEqual(expected); - }); - }); - - test('Multi directives apply with LTR order', () => { - const schema = makeExecutableSchema({ - typeDefs: testSchemaWithDirectives, - resolvers: testResolversDirectives, - directiveResolvers, - }); - const query = `{ - multiDirectives - }`; - const expected = { - multiDirectives: 'giau. tran minh', - }; - return graphql(schema, query, {}, {}).then((res) => { - expect(res.data).toEqual(expected); - }); - }); - - test('Allow to catch error from next resolver', () => { - const schema = makeExecutableSchema({ - typeDefs: testSchemaWithDirectives, - resolvers: testResolversDirectives, - directiveResolvers, - }); - const query = `{ - throwError - }`; - const expected = { - throwError: 'This error for testing', - }; - return graphql(schema, query, {}, {}).then((res) => { - expect(res.data).toEqual(expected); - }); - }); -}); - describe('can specify lexical parser options', () => { test("can specify 'noLocation' option", () => { const schema = makeExecutableSchema({ diff --git a/packages/stitch/src/mergeCandidates.ts b/packages/stitch/src/mergeCandidates.ts index bc69006f02c..007005789f3 100644 --- a/packages/stitch/src/mergeCandidates.ts +++ b/packages/stitch/src/mergeCandidates.ts @@ -50,7 +50,6 @@ import { validateInputObjectConsistency, } from './mergeValidations'; -import { fieldToFieldConfig, inputFieldToFieldConfig, Maybe } from '@graphql-tools/utils'; import { isSubschemaConfig } from '@graphql-tools/delegate'; export function mergeCandidates>( @@ -456,10 +455,11 @@ function fieldConfigMapFromTypeCandidates>( const fieldConfigCandidatesMap: Record>> = Object.create(null); candidates.forEach(candidate => { - const fieldMap = (candidate.type as GraphQLObjectType | GraphQLInterfaceType).getFields(); - Object.keys(fieldMap).forEach(fieldName => { + const typeConfig = (candidate.type as GraphQLObjectType | GraphQLInterfaceType).toConfig(); + const fieldConfigMap = typeConfig.fields; + Object.entries(fieldConfigMap).forEach(([fieldName, fieldConfig]) => { const fieldConfigCandidate = { - fieldConfig: fieldToFieldConfig(fieldMap[fieldName]), + fieldConfig, fieldName, type: candidate.type as GraphQLObjectType | GraphQLInterfaceType, subschema: candidate.subschema, @@ -528,13 +528,14 @@ function inputFieldConfigMapFromTypeCandidates>( const fieldInclusionMap: Record = Object.create(null); candidates.forEach(candidate => { - const inputFieldMap = (candidate.type as GraphQLInputObjectType).getFields(); - Object.keys(inputFieldMap).forEach(fieldName => { + const typeConfig = (candidate.type as GraphQLInputObjectType).toConfig(); + const inputFieldConfigMap = typeConfig.fields; + Object.entries(inputFieldConfigMap).forEach(([fieldName, inputFieldConfig]) => { fieldInclusionMap[fieldName] = fieldInclusionMap[fieldName] || 0; fieldInclusionMap[fieldName] += 1; const inputFieldConfigCandidate = { - inputFieldConfig: inputFieldToFieldConfig(inputFieldMap[fieldName]), + inputFieldConfig, fieldName, type: candidate.type as GraphQLInputObjectType, subschema: candidate.subschema, diff --git a/packages/stitch/src/stitchSchemas.ts b/packages/stitch/src/stitchSchemas.ts index 0530e79235d..1b3bffc9320 100644 --- a/packages/stitch/src/stitchSchemas.ts +++ b/packages/stitch/src/stitchSchemas.ts @@ -7,17 +7,9 @@ import { extendSchema, } from 'graphql'; -import { SchemaDirectiveVisitor, mergeDeep, IResolvers, pruneSchema } from '@graphql-tools/utils'; +import { IResolvers, pruneSchema } from '@graphql-tools/utils'; -import { - addResolversToSchema, - addSchemaLevelResolver, - addErrorLoggingToSchema, - addCatchUndefinedToSchema, - assertResolversPresent, - attachDirectiveResolvers, - extendResolversFromInterfaces, -} from '@graphql-tools/schema'; +import { addResolversToSchema, assertResolversPresent, extendResolversFromInterfaces } from '@graphql-tools/schema'; import { SubschemaConfig, isSubschemaConfig, Subschema, defaultMergedResolver } from '@graphql-tools/delegate'; @@ -30,6 +22,7 @@ import { isolateComputedFieldsTransformer, splitMergedTypeEntryPointsTransformer, } from './subschemaConfigTransforms'; +import { mergeResolvers } from '@graphql-tools/merge'; export function stitchSchemas>({ subschemas = [], @@ -41,12 +34,8 @@ export function stitchSchemas>({ typeMergingOptions, subschemaConfigTransforms = defaultSubschemaConfigTransforms, resolvers = {}, - schemaDirectives, inheritResolversFromInterfaces = false, - logger, - allowUndefinedInResolve = true, resolverValidationOptions = {}, - directiveResolvers, schemaTransforms = [], parseOptions = {}, pruningOptions, @@ -145,7 +134,7 @@ export function stitchSchemas>({ }); // We allow passing in an array of resolver maps, in which case we merge them - const resolverMap: IResolvers = Array.isArray(resolvers) ? resolvers.reduce(mergeDeep, {}) : resolvers; + const resolverMap: IResolvers = mergeResolvers(resolvers); const finalResolvers = inheritResolversFromInterfaces ? extendResolversFromInterfaces(schema, resolverMap) @@ -168,32 +157,10 @@ export function stitchSchemas>({ schema = addStitchingInfo(schema, stitchingInfo); - if (!allowUndefinedInResolve) { - schema = addCatchUndefinedToSchema(schema); - } - - if (logger != null) { - schema = addErrorLoggingToSchema(schema, logger); - } - - if (typeof finalResolvers['__schema'] === 'function') { - // TODO a bit of a hack now, better rewrite generateSchema to attach it there. - // not doing that now, because I'd have to rewrite a lot of tests. - schema = addSchemaLevelResolver(schema, finalResolvers['__schema']); - } - schemaTransforms.forEach(schemaTransform => { schema = schemaTransform(schema); }); - if (directiveResolvers != null) { - schema = attachDirectiveResolvers(schema, directiveResolvers); - } - - if (schemaDirectives != null) { - SchemaDirectiveVisitor.visitSchemaDirectives(schema, schemaDirectives); - } - if (pruningOptions) { schema = pruneSchema(schema, pruningOptions); } diff --git a/packages/stitch/src/typeCandidates.ts b/packages/stitch/src/typeCandidates.ts index 363667e4f5a..8eac67e0bd1 100644 --- a/packages/stitch/src/typeCandidates.ts +++ b/packages/stitch/src/typeCandidates.ts @@ -12,13 +12,13 @@ import { import { wrapSchema } from '@graphql-tools/wrap'; import { Subschema, SubschemaConfig, StitchingInfo } from '@graphql-tools/delegate'; -import { GraphQLParseOptions, ITypeDefinitions, rewireTypes, TypeMap } from '@graphql-tools/utils'; -import { buildDocumentFromTypeDefinitions } from '@graphql-tools/schema'; +import { GraphQLParseOptions, TypeSource, rewireTypes, TypeMap } from '@graphql-tools/utils'; import typeFromAST from './typeFromAST'; import { MergeTypeCandidate, MergeTypeFilter, OnTypeConflict, TypeMergingOptions } from './types'; import { mergeCandidates } from './mergeCandidates'; import { extractDefinitions } from './definitions'; +import { mergeTypeDefs } from '@graphql-tools/merge'; type CandidateSelector> = ( candidates: Array> @@ -42,7 +42,7 @@ export function buildTypeCandidates>({ GraphQLSchema | SubschemaConfig >; types: Array; - typeDefs: ITypeDefinitions | undefined; + typeDefs: TypeSource; parseOptions: GraphQLParseOptions; extensions: Array; directiveMap: Record; @@ -61,7 +61,7 @@ export function buildTypeCandidates>({ let document: DocumentNode | undefined; let extraction: ReturnType | undefined; if ((typeDefs && !Array.isArray(typeDefs)) || (Array.isArray(typeDefs) && typeDefs.length)) { - document = buildDocumentFromTypeDefinitions(typeDefs, parseOptions); + document = mergeTypeDefs(typeDefs, parseOptions); extraction = extractDefinitions(document); schemaDef = extraction.schemaDefs[0]; schemaExtensions = schemaExtensions.concat(extraction.schemaExtensions); diff --git a/packages/stitch/src/types.ts b/packages/stitch/src/types.ts index d8a49b16a72..0d35a8af977 100644 --- a/packages/stitch/src/types.ts +++ b/packages/stitch/src/types.ts @@ -11,7 +11,7 @@ import { GraphQLEnumValueConfig, GraphQLEnumType, } from 'graphql'; -import { ITypeDefinitions, Maybe } from '@graphql-tools/utils'; +import { TypeSource } from '@graphql-tools/utils'; import { Subschema, SubschemaConfig } from '@graphql-tools/delegate'; import { IExecutableSchemaDefinition } from '@graphql-tools/schema'; @@ -55,7 +55,7 @@ export interface IStitchSchemasOptions> subschemas?: Array< GraphQLSchema | SubschemaConfig | Array> >; - typeDefs?: ITypeDefinitions; + typeDefs?: TypeSource; types?: Array; onTypeConflict?: OnTypeConflict; mergeDirectives?: boolean | undefined; diff --git a/packages/stitch/tests/stitchSchemas.test.ts b/packages/stitch/tests/stitchSchemas.test.ts index beaf1a7b1ac..2037cc14ecb 100644 --- a/packages/stitch/tests/stitchSchemas.test.ts +++ b/packages/stitch/tests/stitchSchemas.test.ts @@ -1,12 +1,10 @@ import { graphql, GraphQLSchema, - GraphQLField, GraphQLObjectType, GraphQLScalarType, subscribe, parse, - defaultFieldResolver, findDeprecatedUsages, printSchema, GraphQLResolveInfo, @@ -18,7 +16,6 @@ import { stitchSchemas } from '../src/stitchSchemas'; import { cloneSchema, getResolversFromSchema, - SchemaDirectiveVisitor, IResolvers, ExecutionResult, assertSome, @@ -356,20 +353,6 @@ testCombinations.forEach((combination) => { codeCoverageTypeDefs, schemaDirectiveTypeDefs, ], - schemaDirectives: { - upper: class extends SchemaDirectiveVisitor { - public visitFieldDefinition(field: GraphQLField) { - const { resolve = defaultFieldResolver } = field; - field.resolve = async function (...args) { - const result = await resolve.apply(this, args); - if (typeof result === 'string') { - return result.toUpperCase(); - } - return result; - }; - } - }, - }, mergeDirectives: true, resolvers: { Property: { @@ -3087,31 +3070,6 @@ fragment BookingFragment on Booking { }); }); - describe('schema directives', () => { - test('should work with schema directives', async () => { - const result = await graphql( - stitchedSchema, - ` - query { - propertyById(id: "p1") { - someField - } - } - `, - undefined, - {}, - ); - - expect(result).toEqual({ - data: { - propertyById: { - someField: 'SOMEFIELD', - }, - }, - }); - }); - }); - describe('regression', () => { test('should not pass extra arguments to delegates', async () => { const result = await graphql( diff --git a/packages/utils/package.json b/packages/utils/package.json index 911687c707e..52c2341c81c 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -11,8 +11,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" @@ -35,8 +35,6 @@ "graphql-scalars": "1.10.0" }, "dependencies": { - "@ardatan/aggregate-error": "0.0.6", - "camel-case": "4.1.2", "tslib": "~2.3.0" }, "publishConfig": { diff --git a/packages/utils/src/AggregateError.ts b/packages/utils/src/AggregateError.ts new file mode 100644 index 00000000000..1300c199ade --- /dev/null +++ b/packages/utils/src/AggregateError.ts @@ -0,0 +1,20 @@ +let AggregateErrorImpl = globalThis.AggregateError; + +if (typeof AggregateErrorImpl === 'undefined') { + class AggregateErrorClass extends Error implements AggregateError { + errors: Error[]; + constructor(maybeErrors: Iterable, message = '') { + super(message); + this.name = 'AggregateError'; + Error.captureStackTrace(this, AggregateErrorClass); + this.errors = [...maybeErrors].map(maybeError => + maybeError instanceof Error ? maybeError : new Error(maybeError) + ); + } + } + AggregateErrorImpl = function (errors: Iterable, message?: string) { + return new AggregateErrorClass(errors, message); + } as AggregateErrorConstructor; +} + +export { AggregateErrorImpl as AggregateError }; diff --git a/packages/utils/src/Interfaces.ts b/packages/utils/src/Interfaces.ts index bcd11634d6a..b0fa4fe9bc0 100644 --- a/packages/utils/src/Interfaces.ts +++ b/packages/utils/src/Interfaces.ts @@ -46,10 +46,10 @@ import { InputObjectTypeExtensionNode, InputObjectTypeDefinitionNode, GraphQLType, + Source, + DefinitionNode, } from 'graphql'; -import { SchemaVisitor } from './SchemaVisitor'; - // graphql-js < v15 backwards compatible ExecutionResult // See: https://github.com/graphql/graphql-js/pull/2490 @@ -237,9 +237,14 @@ export type IFieldResolver, TRetu info: GraphQLResolveInfo ) => TReturn; -export type ITypedef = string | DocumentNode | (() => Array); - -export type ITypeDefinitions = string | DocumentNode | Array; +export type TypeSource = + | string + | Source + | DocumentNode + | GraphQLSchema + | DefinitionNode + | Array + | (() => TypeSource); export type IObjectTypeResolver = { [key: string]: IFieldResolver | IFieldResolverOptions; @@ -304,18 +309,6 @@ export type IDefaultValueIteratorFn = (type: GraphQLInputType, value: any) => vo export type NextResolverFn = () => Promise; -export type DirectiveResolverFn = ( - next: NextResolverFn, - source: TSource, - args: { [argName: string]: any }, - context: TContext, - info: GraphQLResolveInfo -) => any; - -export interface IDirectiveResolvers { - [directiveName: string]: DirectiveResolverFn; -} - export interface Request { document: DocumentNode; variables: Record; @@ -336,79 +329,6 @@ export type VisitableSchemaType = | GraphQLEnumType | GraphQLEnumValue; -export type VisitorSelector = ( - type: VisitableSchemaType, - methodName: string -) => Array; - -export enum VisitSchemaKind { - TYPE = 'VisitSchemaKind.TYPE', - SCALAR_TYPE = 'VisitSchemaKind.SCALAR_TYPE', - ENUM_TYPE = 'VisitSchemaKind.ENUM_TYPE', - COMPOSITE_TYPE = 'VisitSchemaKind.COMPOSITE_TYPE', - OBJECT_TYPE = 'VisitSchemaKind.OBJECT_TYPE', - INPUT_OBJECT_TYPE = 'VisitSchemaKind.INPUT_OBJECT_TYPE', - ABSTRACT_TYPE = 'VisitSchemaKind.ABSTRACT_TYPE', - UNION_TYPE = 'VisitSchemaKind.UNION_TYPE', - INTERFACE_TYPE = 'VisitSchemaKind.INTERFACE_TYPE', - ROOT_OBJECT = 'VisitSchemaKind.ROOT_OBJECT', - QUERY = 'VisitSchemaKind.QUERY', - MUTATION = 'VisitSchemaKind.MUTATION', - SUBSCRIPTION = 'VisitSchemaKind.SUBSCRIPTION', -} - -export interface SchemaVisitorMap { - [VisitSchemaKind.TYPE]?: NamedTypeVisitor; - [VisitSchemaKind.SCALAR_TYPE]?: ScalarTypeVisitor; - [VisitSchemaKind.ENUM_TYPE]?: EnumTypeVisitor; - [VisitSchemaKind.COMPOSITE_TYPE]?: CompositeTypeVisitor; - [VisitSchemaKind.OBJECT_TYPE]?: ObjectTypeVisitor; - [VisitSchemaKind.INPUT_OBJECT_TYPE]?: InputObjectTypeVisitor; - [VisitSchemaKind.ABSTRACT_TYPE]?: AbstractTypeVisitor; - [VisitSchemaKind.UNION_TYPE]?: UnionTypeVisitor; - [VisitSchemaKind.INTERFACE_TYPE]?: InterfaceTypeVisitor; - [VisitSchemaKind.ROOT_OBJECT]?: ObjectTypeVisitor; - [VisitSchemaKind.QUERY]?: ObjectTypeVisitor; - [VisitSchemaKind.MUTATION]?: ObjectTypeVisitor; - [VisitSchemaKind.SUBSCRIPTION]?: ObjectTypeVisitor; -} - -export type NamedTypeVisitor = (type: GraphQLNamedType, schema: GraphQLSchema) => GraphQLNamedType | null | undefined; - -export type ScalarTypeVisitor = ( - type: GraphQLScalarType, - schema: GraphQLSchema -) => GraphQLScalarType | null | undefined; - -export type EnumTypeVisitor = (type: GraphQLEnumType, schema: GraphQLSchema) => GraphQLEnumType | null | undefined; - -export type CompositeTypeVisitor = ( - type: GraphQLObjectType | GraphQLInterfaceType | GraphQLUnionType, - schema: GraphQLSchema -) => GraphQLObjectType | GraphQLInterfaceType | GraphQLUnionType | null | undefined; - -export type ObjectTypeVisitor = ( - type: GraphQLObjectType, - schema: GraphQLSchema -) => GraphQLObjectType | null | undefined; - -export type InputObjectTypeVisitor = ( - type: GraphQLInputObjectType, - schema: GraphQLSchema -) => GraphQLInputObjectType | null | undefined; - -export type AbstractTypeVisitor = ( - type: GraphQLInterfaceType | GraphQLUnionType, - schema: GraphQLSchema -) => GraphQLInterfaceType | GraphQLUnionType | null | undefined; - -export type UnionTypeVisitor = (type: GraphQLUnionType, schema: GraphQLSchema) => GraphQLUnionType | null | undefined; - -export type InterfaceTypeVisitor = ( - type: GraphQLInterfaceType, - schema: GraphQLSchema -) => GraphQLInterfaceType | null | undefined; - export enum MapperKind { TYPE = 'MapperKind.TYPE', SCALAR_TYPE = 'MapperKind.SCALAR_TYPE', diff --git a/packages/utils/src/SchemaDirectiveVisitor.ts b/packages/utils/src/SchemaDirectiveVisitor.ts deleted file mode 100644 index 2add8dd28d3..00000000000 --- a/packages/utils/src/SchemaDirectiveVisitor.ts +++ /dev/null @@ -1,324 +0,0 @@ -import { - GraphQLDirective, - GraphQLSchema, - DirectiveLocationEnum, - TypeSystemExtensionNode, - valueFromASTUntyped, -} from 'graphql'; - -import { VisitableSchemaType } from './Interfaces'; - -import { SchemaVisitor } from './SchemaVisitor'; -import { visitSchema } from './visitSchema'; -import { getArgumentValues } from './getArgumentValues'; -import { Maybe } from 'packages/graphql-tools/src'; - -// This class represents a reusable implementation of a @directive that may -// appear in a GraphQL schema written in Schema Definition Language. -// -// By overriding one or more visit{Object,Union,...} methods, a subclass -// registers interest in certain schema types, such as GraphQLObjectType, -// GraphQLUnionType, etc. When SchemaDirectiveVisitor.visitSchemaDirectives is -// called with a GraphQLSchema object and a map of visitor subclasses, the -// overridden methods of those subclasses allow the visitors to obtain -// references to any type objects that have @directives attached to them, -// enabling visitors to inspect or modify the schema as appropriate. -// -// For example, if a directive called @rest(url: "...") appears after a field -// definition, a SchemaDirectiveVisitor subclass could provide meaning to that -// directive by overriding the visitFieldDefinition method (which receives a -// GraphQLField parameter), and then the body of that visitor method could -// manipulate the field's resolver function to fetch data from a REST endpoint -// described by the url argument passed to the @rest directive: -// -// const typeDefs = ` -// type Query { -// people: [Person] @rest(url: "/api/v1/people") -// }`; -// -// const schema = makeExecutableSchema({ typeDefs }); -// -// SchemaDirectiveVisitor.visitSchemaDirectives(schema, { -// rest: class extends SchemaDirectiveVisitor { -// public visitFieldDefinition(field: GraphQLField) { -// const { url } = this.args; -// field.resolve = () => fetch(url); -// } -// } -// }); -// -// The subclass in this example is defined as an anonymous class expression, -// for brevity. A truly reusable SchemaDirectiveVisitor would most likely be -// defined in a library using a named class declaration, and then exported for -// consumption by other modules and packages. -// -// See below for a complete list of overridable visitor methods, their -// parameter types, and more details about the properties exposed by instances -// of the SchemaDirectiveVisitor class. - -export class SchemaDirectiveVisitor extends SchemaVisitor { - // The name of the directive this visitor is allowed to visit (that is, the - // identifier that appears after the @ character in the schema). Note that - // this property is per-instance rather than static because subclasses of - // SchemaDirectiveVisitor can be instantiated multiple times to visit - // directives of different names. In other words, SchemaDirectiveVisitor - // implementations are effectively anonymous, and it's up to the caller of - // SchemaDirectiveVisitor.visitSchemaDirectives to assign names to them. - public name: string; - - // A map from parameter names to argument values, as obtained from a - // specific occurrence of a @directive(arg1: value1, arg2: value2, ...) in - // the schema. Visitor methods may refer to this object via this.args. - public args: TArgs; - - // A reference to the type object that this visitor was created to visit. - public visitedType: VisitableSchemaType; - - // A shared object that will be available to all visitor instances via - // this.context. Callers of visitSchemaDirectives can provide their own - // object, or just use the default empty object. - public context: TContext; - - // Override this method to return a custom GraphQLDirective (or modify one - // already present in the schema) to enforce argument types, provide default - // argument values, or specify schema locations where this @directive may - // appear. By default, any declaration found in the schema will be returned. - public static getDirectiveDeclaration( - directiveName: string, - schema: GraphQLSchema - ): GraphQLDirective | null | undefined { - return schema.getDirective(directiveName); - } - - // Call SchemaDirectiveVisitor.visitSchemaDirectives to visit every - // @directive in the schema and create an appropriate SchemaDirectiveVisitor - // instance to visit the object decorated by the @directive. - public static visitSchemaDirectives( - schema: GraphQLSchema, - // The keys of this object correspond to directive names as they appear - // in the schema, and the values should be subclasses (not instances!) - // of the SchemaDirectiveVisitor class. This distinction is important - // because a new SchemaDirectiveVisitor instance will be created each - // time a matching directive is found in the schema AST, with arguments - // and other metadata specific to that occurrence. To help prevent the - // mistake of passing instances, the SchemaDirectiveVisitor constructor - // method is marked as protected. - directiveVisitors: Record, - // Optional context object that will be available to all visitor instances - // via this.context. Defaults to an empty null-prototype object. - context: Record = Object.create(null), - // The visitSchemaDirectives method returns a map from directive names to - // lists of SchemaDirectiveVisitors created while visiting the schema. - pathToDirectivesInExtensions = ['directives'] - ): Record> { - // If the schema declares any directives for public consumption, record - // them here so that we can properly coerce arguments when/if we encounter - // an occurrence of the directive while walking the schema below. - const declaredDirectives = this.getDeclaredDirectives(schema, directiveVisitors); - - // Map from directive names to lists of SchemaDirectiveVisitor instances - // created while visiting the schema. - const createdVisitors: Record> = Object.keys(directiveVisitors).reduce( - (prev, item) => ({ - ...prev, - [item]: [], - }), - {} - ); - - const directiveVisitorMap: Record = Object.entries(directiveVisitors).reduce( - (prev, [key, value]) => ({ - ...prev, - [key]: value, - }), - {} - ); - - function visitorSelector(type: VisitableSchemaType, methodName: string): Array { - const directivesInExtensions = pathToDirectivesInExtensions.reduce( - (acc, pathSegment) => (acc == null ? acc : acc[pathSegment]), - type?.extensions - ); - - const directives: Record> = Object.create(null); - - if (directivesInExtensions != null) { - Object.entries(directivesInExtensions).forEach(([directiveName, directiveValue]) => { - if (!directives[directiveName]) { - directives[directiveName] = [directiveValue]; - } else { - directives[directiveName].push([directiveValue]); - } - }); - } else { - let directiveNodes = type?.astNode?.directives ?? []; - - const extensionASTNodes: Maybe> = ( - type as { - extensionASTNodes?: Array; - } - ).extensionASTNodes; - - if (extensionASTNodes != null) { - extensionASTNodes.forEach(extensionASTNode => { - if (extensionASTNode.directives != null) { - directiveNodes = directiveNodes.concat(extensionASTNode.directives); - } - }); - } - - directiveNodes.forEach(directiveNode => { - const directiveName = directiveNode.name.value; - - const decl = declaredDirectives[directiveName]; - let args: Record; - - if (decl != null) { - // If this directive was explicitly declared, use the declared - // argument types (and any default values) to check, coerce, and/or - // supply default values for the given arguments. - args = getArgumentValues(decl, directiveNode); - } else { - // If this directive was not explicitly declared, just convert the - // argument nodes to their corresponding JavaScript values. - args = Object.create(null); - if (directiveNode.arguments != null) { - directiveNode.arguments.forEach(arg => { - args[arg.name.value] = valueFromASTUntyped(arg.value); - }); - } - } - - if (!directives[directiveName]) { - directives[directiveName] = [args]; - } else { - directives[directiveName].push(args); - } - }); - } - - const visitors: Array = []; - - Object.entries(directives).forEach(([directiveName, directiveValues]) => { - if (!(directiveName in directiveVisitorMap)) { - return; - } - - const VisitorClass = directiveVisitorMap[directiveName]; - - // Avoid creating visitor objects if visitorClass does not override - // the visitor method named by methodName. - if (!VisitorClass.implementsVisitorMethod(methodName)) { - return; - } - - directiveValues.forEach(directiveValue => { - // As foretold in comments near the top of the visitSchemaDirectives - // method, this is where instances of the SchemaDirectiveVisitor class - // get created and assigned names. While subclasses could override the - // constructor method, the constructor is marked as protected, so - // these are the only arguments that will ever be passed. - visitors.push( - new VisitorClass({ - name: directiveName, - args: directiveValue, - visitedType: type, - schema, - context, - }) - ); - }); - }); - - if (visitors.length > 0) { - visitors.forEach(visitor => { - createdVisitors[visitor.name].push(visitor); - }); - } - - return visitors; - } - - visitSchema(schema, visitorSelector); - - return createdVisitors; - } - - protected static getDeclaredDirectives( - schema: GraphQLSchema, - directiveVisitors: Record - ): Record { - const declaredDirectives: Record = schema.getDirectives().reduce( - (prev, curr) => ({ - ...prev, - [curr.name]: curr, - }), - {} - ); - // If the visitor subclass overrides getDirectiveDeclaration, and it - // returns a non-null GraphQLDirective, use that instead of any directive - // declared in the schema itself. Reasoning: if a SchemaDirectiveVisitor - // goes to the trouble of implementing getDirectiveDeclaration, it should - // be able to rely on that implementation. - Object.entries(directiveVisitors).forEach(([directiveName, visitorClass]) => { - const decl = visitorClass.getDirectiveDeclaration(directiveName, schema); - if (decl != null) { - declaredDirectives[directiveName] = decl; - } - }); - - Object.entries(declaredDirectives).forEach(([name, decl]) => { - if (!(name in directiveVisitors)) { - // SchemaDirectiveVisitors.visitSchemaDirectives might be called - // multiple times with partial directiveVisitors maps, so it's not - // necessarily an error for directiveVisitors to be missing an - // implementation of a directive that was declared in the schema. - return; - } - const visitorClass = directiveVisitors[name]; - - decl.locations.forEach(loc => { - const visitorMethodName = directiveLocationToVisitorMethodName(loc); - if ( - SchemaVisitor.implementsVisitorMethod(visitorMethodName) && - !visitorClass.implementsVisitorMethod(visitorMethodName) - ) { - // While visitor subclasses may implement extra visitor methods, - // it's definitely a mistake if the GraphQLDirective declares itself - // applicable to certain schema locations, and the visitor subclass - // does not implement all the corresponding methods. - throw new Error(`SchemaDirectiveVisitor for @${name} must implement ${visitorMethodName} method`); - } - }); - }); - - return declaredDirectives; - } - - // Mark the constructor protected to enforce passing SchemaDirectiveVisitor - // subclasses (not instances) to visitSchemaDirectives. - protected constructor(config: { - name: string; - args: TArgs; - visitedType: VisitableSchemaType; - schema: GraphQLSchema; - context: TContext; - }) { - super(); - this.name = config.name; - this.args = config.args; - this.visitedType = config.visitedType; - this.schema = config.schema; - this.context = config.context; - } -} - -// Convert a string like "FIELD_DEFINITION" to "visitFieldDefinition". -function directiveLocationToVisitorMethodName(loc: DirectiveLocationEnum) { - return ( - 'visit' + - loc.replace(/([^_]*)_?/g, (_wholeMatch, part: string) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()) - ); -} - -export type SchemaDirectiveVisitorClass = typeof SchemaDirectiveVisitor; diff --git a/packages/utils/src/SchemaVisitor.ts b/packages/utils/src/SchemaVisitor.ts deleted file mode 100644 index 7f49ba0aa82..00000000000 --- a/packages/utils/src/SchemaVisitor.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { - GraphQLArgument, - GraphQLEnumType, - GraphQLEnumValue, - GraphQLField, - GraphQLInputField, - GraphQLInputObjectType, - GraphQLInterfaceType, - GraphQLObjectType, - GraphQLScalarType, - GraphQLSchema, - GraphQLUnionType, -} from 'graphql'; - -// Abstract base class of any visitor implementation, defining the available -// visitor methods along with their parameter types, and providing a static -// helper function for determining whether a subclass implements a given -// visitor method, as opposed to inheriting one of the stubs defined here. -export abstract class SchemaVisitor { - // All SchemaVisitor instances are created while visiting a specific - // GraphQLSchema object, so this property holds a reference to that object, - // in case a visitor method needs to refer to this.schema. - public schema!: GraphQLSchema; - - // Determine if this SchemaVisitor (sub)class implements a particular - // visitor method. - public static implementsVisitorMethod(methodName: string): boolean { - if (!methodName.startsWith('visit')) { - return false; - } - - const method = this.prototype[methodName]; - if (typeof method !== 'function') { - return false; - } - - if (this.name === 'SchemaVisitor') { - // The SchemaVisitor class implements every visitor method. - return true; - } - - const stub = SchemaVisitor.prototype[methodName]; - if (method === stub) { - // If this.prototype[methodName] was just inherited from SchemaVisitor, - // then this class does not really implement the method. - return false; - } - - return true; - } - - // Concrete subclasses of SchemaVisitor should override one or more of these - // visitor methods, in order to express their interest in handling certain - // schema types/locations. Each method may return null to remove the given - // type from the schema, a non-null value of the same type to update the - // type in the schema, or nothing to leave the type as it was. - - // eslint-disable-next-line @typescript-eslint/no-empty-function - public visitSchema(_schema: GraphQLSchema): void {} - - public visitScalar( - _scalar: GraphQLScalarType - // eslint-disable-next-line @typescript-eslint/no-empty-function - ): GraphQLScalarType | void | null {} - - public visitObject( - _object: GraphQLObjectType - // eslint-disable-next-line @typescript-eslint/no-empty-function - ): GraphQLObjectType | void | null {} - - public visitFieldDefinition( - _field: GraphQLField, - _details: { - objectType: GraphQLObjectType | GraphQLInterfaceType; - } - // eslint-disable-next-line @typescript-eslint/no-empty-function - ): GraphQLField | void | null {} - - public visitArgumentDefinition( - _argument: GraphQLArgument, - _details: { - field: GraphQLField; - objectType: GraphQLObjectType | GraphQLInterfaceType; - } - // eslint-disable-next-line @typescript-eslint/no-empty-function - ): GraphQLArgument | void | null {} - - public visitInterface( - _iface: GraphQLInterfaceType - // eslint-disable-next-line @typescript-eslint/no-empty-function - ): GraphQLInterfaceType | void | null {} - - // eslint-disable-next-line @typescript-eslint/no-empty-function - public visitUnion(_union: GraphQLUnionType): GraphQLUnionType | void | null {} - - // eslint-disable-next-line @typescript-eslint/no-empty-function - public visitEnum(_type: GraphQLEnumType): GraphQLEnumType | void | null {} - - public visitEnumValue( - _value: GraphQLEnumValue, - _details: { - enumType: GraphQLEnumType; - } - // eslint-disable-next-line @typescript-eslint/no-empty-function - ): GraphQLEnumValue | void | null {} - - public visitInputObject( - _object: GraphQLInputObjectType - // eslint-disable-next-line @typescript-eslint/no-empty-function - ): GraphQLInputObjectType | void | null {} - - public visitInputFieldDefinition( - _field: GraphQLInputField, - _details: { - objectType: GraphQLInputObjectType; - } - // eslint-disable-next-line @typescript-eslint/no-empty-function - ): GraphQLInputField | void | null {} -} diff --git a/packages/utils/src/build-operation-for-field.ts b/packages/utils/src/build-operation-for-field.ts index 15e17e24fc1..745c2bfabc6 100644 --- a/packages/utils/src/build-operation-for-field.ts +++ b/packages/utils/src/build-operation-for-field.ts @@ -27,7 +27,6 @@ import { isEnumType, Kind, } from 'graphql'; -import { camelCase } from 'camel-case'; let operationVariables: VariableDefinitionNode[] = []; let fieldTypeMap = new Map(); @@ -44,10 +43,6 @@ function resetFieldMap() { fieldTypeMap = new Map(); } -function buildOperationName(name: string) { - return camelCase(name); -} - export type Skip = string[]; export type Force = string[]; export type Ignore = string[]; @@ -131,7 +126,7 @@ function buildOperationAndCollectVariables({ }; const type = typeMap[kind]; const field = type.getFields()[fieldName]; - const operationName = buildOperationName(`${fieldName}_${kind}`); + const operationName = `${fieldName}_${kind}`; if (field.args) { field.args.forEach(arg => { @@ -394,7 +389,7 @@ function resolveVariable(arg: GraphQLArgument, name?: string): VariableDefinitio } function getArgumentName(name: string, path: string[]): string { - return camelCase([...path, name].join('_')); + return [...path, name].join('_'); } function resolveField({ diff --git a/packages/utils/src/debug-log.ts b/packages/utils/src/debug-log.ts deleted file mode 100644 index 292ec974cee..00000000000 --- a/packages/utils/src/debug-log.ts +++ /dev/null @@ -1,6 +0,0 @@ -export function debugLog(...args: any[]): void { - if (process && process.env && process.env['DEBUG'] && !process.env['GQL_tools_NODEBUG']) { - // tslint:disable-next-line: no-console - console.log(...args); - } -} diff --git a/packages/utils/src/fix-windows-path.ts b/packages/utils/src/fix-windows-path.ts deleted file mode 100644 index 746bed0a8d0..00000000000 --- a/packages/utils/src/fix-windows-path.ts +++ /dev/null @@ -1 +0,0 @@ -export const fixWindowsPath = (path: string) => path.replace(/\\/g, '/'); diff --git a/packages/utils/src/flatten-array.ts b/packages/utils/src/flatten-array.ts deleted file mode 100644 index 094f04f4918..00000000000 --- a/packages/utils/src/flatten-array.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const flattenArray = (arr: any): any[] => - arr.reduce((acc: any, next: any) => acc.concat(Array.isArray(next) ? flattenArray(next) : next), []); diff --git a/packages/utils/src/getArgumentValues.ts b/packages/utils/src/getArgumentValues.ts index f3e64b6afd4..1ad1003b6e6 100644 --- a/packages/utils/src/getArgumentValues.ts +++ b/packages/utils/src/getArgumentValues.ts @@ -11,7 +11,7 @@ import { ArgumentNode, } from 'graphql'; -import { inspect } from './inspect'; +import { inspect } from 'util'; /** * Prepares an object map of argument values given a list of argument diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 531c3a96ea9..5fc559f2755 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,8 +1,5 @@ export * from './loaders'; export * from './helpers'; -export * from './debug-log'; -export * from './fix-windows-path'; -export * from './flatten-array'; export * from './get-directives'; export * from './get-fields-with-directives'; export * from './get-implementing-types'; @@ -19,9 +16,6 @@ export * from './types'; export * from './filterSchema'; export * from './clone'; export * from './heal'; -export * from './SchemaVisitor'; -export * from './SchemaDirectiveVisitor'; -export * from './visitSchema'; export * from './getResolversFromSchema'; export * from './forEachField'; export * from './forEachDefaultValue'; @@ -42,7 +36,6 @@ export * from './mapAsyncIterator'; export * from './updateArgument'; export * from './implementsAbstractType'; export * from './errors'; -export * from './toConfig'; export * from './observableToAsyncIterable'; export * from './visitResult'; export * from './getArgumentValues'; @@ -52,3 +45,4 @@ export * from './isDocumentNode'; export * from './astFromValueUntyped'; export * from './executor'; export * from './withCancel'; +export * from './AggregateError'; diff --git a/packages/utils/src/inspect.ts b/packages/utils/src/inspect.ts deleted file mode 100644 index 9f2f474e324..00000000000 --- a/packages/utils/src/inspect.ts +++ /dev/null @@ -1,113 +0,0 @@ -const MAX_ARRAY_LENGTH = 10; -const MAX_RECURSIVE_DEPTH = 2; - -/** - * Used to print values in error messages. - */ -export function inspect(value: any): string { - return formatValue(value, []); -} - -function formatValue(value: any, seenValues: Array): string { - switch (typeof value) { - case 'string': - return JSON.stringify(value); - case 'function': - return value.name ? `[function ${(value as (...args: any[]) => any).name}]` : '[function]'; - case 'object': - if (value === null) { - return 'null'; - } - return formatObjectValue(value, seenValues); - default: - return String(value); - } -} - -function formatObjectValue(value: any, previouslySeenValues: Array): string { - if (previouslySeenValues.indexOf(value) !== -1) { - return '[Circular]'; - } - - const seenValues = [...previouslySeenValues, value]; - const customInspectFn = getCustomFn(value); - - if (customInspectFn !== undefined) { - const customValue = customInspectFn.call(value); - - // check for infinite recursion - if (customValue !== value) { - return typeof customValue === 'string' ? customValue : formatValue(customValue, seenValues); - } - } else if (Array.isArray(value)) { - return formatArray(value, seenValues); - } - - return formatObject(value, seenValues); -} - -function formatObject(object: any, seenValues: Array) { - const keys = Object.keys(object); - if (keys.length === 0) { - return '{}'; - } - - if (seenValues.length > MAX_RECURSIVE_DEPTH) { - return '[' + getObjectTag(object) + ']'; - } - - const properties = keys.map(key => { - const value = formatValue(object[key], seenValues); - return key + ': ' + value; - }); - - return '{ ' + properties.join(', ') + ' }'; -} - -function formatArray(array: Array, seenValues: Array): string { - if (array.length === 0) { - return '[]'; - } - - if (seenValues.length > MAX_RECURSIVE_DEPTH) { - return '[Array]'; - } - - const len = Math.min(MAX_ARRAY_LENGTH, array.length); - const remaining = array.length - len; - const items = []; - - for (let i = 0; i < len; ++i) { - items.push(formatValue(array[i], seenValues)); - } - - if (remaining === 1) { - items.push('... 1 more item'); - } else if (remaining > 1) { - items.push(`... ${remaining.toString(10)} more items`); - } - - return '[' + items.join(', ') + ']'; -} - -function getCustomFn(obj: any) { - if (typeof obj.inspect === 'function') { - return obj.inspect; - } -} - -function getObjectTag(obj: any): string { - const tag = Object.prototype.toString - .call(obj) - .replace(/^\[object /, '') - .replace(/]$/, ''); - - if (tag === 'Object' && typeof obj.constructor === 'function') { - const name = obj.constructor.name; - if (typeof name === 'string' && name !== '') { - return name; - } - } - - return tag; -} diff --git a/packages/utils/src/toConfig.ts b/packages/utils/src/toConfig.ts deleted file mode 100644 index 623b68395db..00000000000 --- a/packages/utils/src/toConfig.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { - GraphQLArgument, - GraphQLFieldConfigArgumentMap, - GraphQLField, - GraphQLInputField, - GraphQLInputFieldConfig, - GraphQLArgumentConfig, - GraphQLFieldConfig, -} from 'graphql'; - -export function inputFieldToFieldConfig(field: GraphQLInputField): GraphQLInputFieldConfig { - return { - description: field.description, - type: field.type, - defaultValue: field.defaultValue, - extensions: field.extensions, - astNode: field.astNode, - }; -} - -export function fieldToFieldConfig(field: GraphQLField): GraphQLFieldConfig { - return { - description: field.description, - type: field.type, - args: argsToFieldConfigArgumentMap(field.args), - resolve: field.resolve, - subscribe: field.subscribe, - deprecationReason: field.deprecationReason, - extensions: field.extensions, - astNode: field.astNode, - }; -} - -export function argsToFieldConfigArgumentMap(args: ReadonlyArray): GraphQLFieldConfigArgumentMap { - const newArguments = {}; - args.forEach(arg => { - newArguments[arg.name] = argumentToArgumentConfig(arg); - }); - - return newArguments; -} - -export function argumentToArgumentConfig(arg: GraphQLArgument): GraphQLArgumentConfig { - return { - description: arg.description, - type: arg.type, - defaultValue: arg.defaultValue, - extensions: arg.extensions, - astNode: arg.astNode, - }; -} diff --git a/packages/utils/src/validate-documents.ts b/packages/utils/src/validate-documents.ts index 0c24bd25f3e..9cf306dba02 100644 --- a/packages/utils/src/validate-documents.ts +++ b/packages/utils/src/validate-documents.ts @@ -9,7 +9,7 @@ import { ASTVisitor, } from 'graphql'; import { Source } from './loaders'; -import AggregateError from '@ardatan/aggregate-error'; +import { AggregateError } from './AggregateError'; export type ValidationRule = (context: ValidationContext) => ASTVisitor; @@ -92,7 +92,7 @@ export function checkValidationErrors(loadDocumentErrors: ReadonlyArray | SchemaVisitor | SchemaVisitorMap -): GraphQLSchema { - const visitorSelector = - typeof visitorOrVisitorSelector === 'function' ? visitorOrVisitorSelector : () => visitorOrVisitorSelector; - - // Helper function that calls visitorSelector and applies the resulting - // visitors to the given type, with arguments [type, ...args]. - function callMethod(methodName: string, type: T, ...args: Array): T | null { - let visitors = visitorSelector(type, methodName); - visitors = Array.isArray(visitors) ? visitors : [visitors]; - - let finalType: T | null = type; - visitors.every(visitorOrVisitorDef => { - let newType; - if (isSchemaVisitor(visitorOrVisitorDef)) { - newType = visitorOrVisitorDef[methodName](finalType, ...args); - } else if ( - isNamedType(finalType) && - (methodName === 'visitScalar' || - methodName === 'visitEnum' || - methodName === 'visitObject' || - methodName === 'visitInputObject' || - methodName === 'visitUnion' || - methodName === 'visitInterface') - ) { - const specifiers = getTypeSpecifiers(finalType, schema); - const typeVisitor = getVisitor(visitorOrVisitorDef, specifiers); - newType = typeVisitor != null ? typeVisitor(finalType, schema) : undefined; - } - - if (typeof newType === 'undefined') { - // Keep going without modifying type. - return true; - } - - if (methodName === 'visitSchema' || isSchema(finalType)) { - throw new Error(`Method ${methodName} cannot replace schema with ${newType as string}`); - } - - if (newType === null) { - // Stop the loop and return null form callMethod, which will cause - // the type to be removed from the schema. - finalType = null; - return false; - } - - // Update type to the new type returned by the visitor method, so that - // later directives will see the new type, and callMethod will return - // the final type. - finalType = newType; - return true; - }); - - // If there were no directives for this type object, or if all visitor - // methods returned nothing, type will be returned unmodified. - return finalType; - } - - // Recursive helper function that calls any appropriate visitor methods for - // each object in the schema, then traverses the object's children (if any). - function visit(type: T): T | null { - if (isSchema(type)) { - // Unlike the other types, the root GraphQLSchema object cannot be - // replaced by visitor methods, because that would make life very hard - // for SchemaVisitor subclasses that rely on the original schema object. - callMethod('visitSchema', type); - - const typeMap: Record = type.getTypeMap(); - Object.entries(typeMap).forEach(([typeName, namedType]) => { - if (!typeName.startsWith('__') && namedType != null) { - // Call visit recursively to let it determine which concrete - // subclass of GraphQLNamedType we found in the type map. - // We do not use updateEachKey because we want to preserve - // deleted types in the typeMap so that other types that reference - // the deleted types can be healed. - typeMap[typeName] = visit(namedType); - } - }); - - return type; - } - - if (isObjectType(type)) { - // Note that callMethod('visitObject', type) may not actually call any - // methods, if there are no @directive annotations associated with this - // type, or if this SchemaDirectiveVisitor subclass does not override - // the visitObject method. - const newObject = callMethod('visitObject', type); - if (newObject != null) { - visitFields(newObject); - } - return newObject; - } - - if (isInterfaceType(type)) { - const newInterface = callMethod('visitInterface', type); - if (newInterface != null) { - visitFields(newInterface); - } - return newInterface; - } - - if (isInputObjectType(type)) { - const newInputObject = callMethod('visitInputObject', type); - - if (newInputObject != null) { - const fieldMap = newInputObject.getFields() as Record; - for (const key of Object.keys(fieldMap)) { - const result = callMethod('visitInputFieldDefinition', fieldMap[key], { - // Since we call a different method for input object fields, we - // can't reuse the visitFields function here. - objectType: newInputObject, - }); - if (result) { - fieldMap[key] = result; - } else { - delete fieldMap[key]; - } - } - } - - return newInputObject; - } - - if (isScalarType(type)) { - return callMethod('visitScalar', type); - } - - if (isUnionType(type)) { - return callMethod('visitUnion', type); - } - - if (isEnumType(type)) { - let newEnum = callMethod('visitEnum', type); - - if (newEnum != null) { - const newValues: Array = newEnum - .getValues() - .map(value => - callMethod('visitEnumValue', value, { - enumType: newEnum, - }) - ) - .filter(isSome); - - // Recreate the enum type if any of the values changed - const valuesUpdated = newValues.some((value, index) => value !== newEnum!.getValues()[index]); - if (valuesUpdated) { - newEnum = new GraphQLEnumType({ - ...(newEnum as GraphQLEnumType).toConfig(), - values: newValues.reduce( - (prev, value) => ({ - ...prev, - [value.name]: { - value: value.value, - deprecationReason: value.deprecationReason, - description: value.description, - astNode: value.astNode, - }, - }), - {} - ), - }) as GraphQLEnumType & T; - } - } - - return newEnum; - } - - throw new Error(`Unexpected schema type: ${type as unknown as string}`); - } - - function visitFields(type: GraphQLObjectType | GraphQLInterfaceType) { - const fieldMap = type.getFields(); - for (const [key, field] of Object.entries(fieldMap)) { - // It would be nice if we could call visit(field) recursively here, but - // GraphQLField is merely a type, not a value that can be detected using - // an instanceof check, so we have to visit the fields in this lexical - // context, so that TypeScript can validate the call to - // visitFieldDefinition. - const newField = callMethod('visitFieldDefinition', field, { - // While any field visitor needs a reference to the field object, some - // field visitors may also need to know the enclosing (parent) type, - // perhaps to determine if the parent is a GraphQLObjectType or a - // GraphQLInterfaceType. To obtain a reference to the parent, a - // visitor method can have a second parameter, which will be an object - // with an .objectType property referring to the parent. - objectType: type, - }); - - if (newField?.args != null) { - newField.args = newField.args - .map(arg => - callMethod('visitArgumentDefinition', arg, { - // Like visitFieldDefinition, visitArgumentDefinition takes a - // second parameter that provides additional context, namely the - // parent .field and grandparent .objectType. Remember that the - // current GraphQLSchema is always available via this.schema. - field: newField, - objectType: type, - }) - ) - .filter(isSome); - } - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (newField) { - fieldMap[key] = newField; - } else { - delete fieldMap[key]; - } - } - } - - visit(schema); - - // Automatically update any references to named schema types replaced - // during the traversal, so implementers don't have to worry about that. - healSchema(schema); - - // Return schema for convenience, even though schema parameter has all updated types. - return schema; -} - -function getTypeSpecifiers(type: GraphQLType, schema: GraphQLSchema): Array { - const specifiers = [VisitSchemaKind.TYPE]; - if (isObjectType(type)) { - specifiers.push(VisitSchemaKind.COMPOSITE_TYPE, VisitSchemaKind.OBJECT_TYPE); - const query = schema.getQueryType(); - const mutation = schema.getMutationType(); - const subscription = schema.getSubscriptionType(); - if (type === query) { - specifiers.push(VisitSchemaKind.ROOT_OBJECT, VisitSchemaKind.QUERY); - } else if (type === mutation) { - specifiers.push(VisitSchemaKind.ROOT_OBJECT, VisitSchemaKind.MUTATION); - } else if (type === subscription) { - specifiers.push(VisitSchemaKind.ROOT_OBJECT, VisitSchemaKind.SUBSCRIPTION); - } - } else if (isInputType(type)) { - specifiers.push(VisitSchemaKind.INPUT_OBJECT_TYPE); - } else if (isInterfaceType(type)) { - specifiers.push(VisitSchemaKind.COMPOSITE_TYPE, VisitSchemaKind.ABSTRACT_TYPE, VisitSchemaKind.INTERFACE_TYPE); - } else if (isUnionType(type)) { - specifiers.push(VisitSchemaKind.COMPOSITE_TYPE, VisitSchemaKind.ABSTRACT_TYPE, VisitSchemaKind.UNION_TYPE); - } else if (isEnumType(type)) { - specifiers.push(VisitSchemaKind.ENUM_TYPE); - } else if (isScalarType(type)) { - specifiers.push(VisitSchemaKind.SCALAR_TYPE); - } - - return specifiers; -} - -function getVisitor(visitorDef: SchemaVisitorMap, specifiers: Array): NamedTypeVisitor | null { - let typeVisitor: NamedTypeVisitor | undefined; - const stack = [...specifiers]; - while (!typeVisitor && stack.length > 0) { - const next = stack.pop()!; - typeVisitor = visitorDef[next] as NamedTypeVisitor; - } - - return typeVisitor != null ? typeVisitor : null; -} diff --git a/packages/utils/tests/build-operation-node-for-field.spec.ts b/packages/utils/tests/build-operation-node-for-field.spec.ts index 01b3e506d1c..53e9612e24f 100644 --- a/packages/utils/tests/build-operation-node-for-field.spec.ts +++ b/packages/utils/tests/build-operation-node-for-field.spec.ts @@ -84,7 +84,7 @@ test('should work with Query', async () => { expect(clean(document)).toEqual( clean(/* GraphQL */ ` - query meQuery { + query me_query { me { id name @@ -131,7 +131,7 @@ test('should work with Query and variables', async () => { expect(clean(document)).toEqual( clean(/* GraphQL */ ` - query userQuery($id: ID!) { + query user_query($id: ID!) { user(id: $id) { id name @@ -178,7 +178,7 @@ test('should work with Query and complicated variable', async () => { expect(clean(document)).toEqual( clean(/* GraphQL */ ` - query menuByIngredientsQuery($ingredients: [String!]!) { + query menuByIngredients_query($ingredients: [String!]!) { menuByIngredients(ingredients: $ingredients) { ... on Pizza { dough @@ -211,7 +211,7 @@ test('should work with Union', async () => { expect(clean(document)).toEqual( clean(/* GraphQL */ ` - query menuQuery { + query menu_query { menu { ... on Pizza { dough @@ -244,7 +244,7 @@ test('should work with mutation', async () => { expect(clean(document)).toEqual( clean(/* GraphQL */ ` - mutation addSaladMutation($ingredients: [String!]!) { + mutation addSalad_mutation($ingredients: [String!]!) { addSalad(ingredients: $ingredients) { ... on CaeserSalad { ingredients @@ -271,7 +271,7 @@ test('should work with mutation and unions', async () => { expect(clean(document)).toEqual( clean(/* GraphQL */ ` - mutation addRandomFoodMutation { + mutation addRandomFood_mutation { addRandomFood { ... on Pizza { dough @@ -304,9 +304,9 @@ test('should work with Query and nested variables', async () => { expect(clean(document)).toEqual( clean(/* GraphQL */ ` - query feedQuery($feedCommentsFilter: String!) { + query feed_query($feed_comments_filter: String!) { feed { - comments(filter: $feedCommentsFilter) + comments(filter: $feed_comments_filter) } } `) @@ -324,7 +324,7 @@ test('should be able to ignore using models when requested', async () => { expect(clean(document)).toEqual( clean(/* GraphQL */ ` - query userQuery($id: ID!) { + query user_query($id: ID!) { user(id: $id) { id name @@ -373,7 +373,7 @@ test('should work with Subscription', async () => { expect(clean(document)).toEqual( clean(/* GraphQL */ ` - subscription onFoodSubscription { + subscription onFood_subscription { onFood { ... on Pizza { dough @@ -423,7 +423,7 @@ test('should work with circular ref (default depth limit === 1)', async () => { expect(clean(document)).toEqual( clean(/* GraphQL */ ` - query aQuery { + query a_query { a { b { c { @@ -465,7 +465,7 @@ test('should work with circular ref (custom depth limit)', async () => { expect(clean(document)).toEqual( clean(/* GraphQL */ ` - query aQuery { + query a_query { a { b { c { @@ -510,7 +510,7 @@ test('arguments', async () => { expect(clean(document)).toEqual( clean(/* GraphQL */ ` - query usersQuery($pageInfo: PageInfoInput!) { + query users_query($pageInfo: PageInfoInput!) { users(pageInfo: $pageInfo) { id name @@ -538,7 +538,7 @@ test('selectedFields', async () => { expect(clean(document)).toEqual( clean(/* GraphQL */ ` - query userQuery($id: ID!) { + query user_query($id: ID!) { user(id: $id) { favoriteFood { ... on Pizza { diff --git a/packages/utils/tests/directives.test.ts b/packages/utils/tests/directives.test.ts deleted file mode 100644 index 7ade95d4bcc..00000000000 --- a/packages/utils/tests/directives.test.ts +++ /dev/null @@ -1,1601 +0,0 @@ -import '../../testing/to-be-similar-string'; -import { createHash } from 'crypto'; - -import { - GraphQLArgument, - GraphQLEnumType, - GraphQLEnumValue, - GraphQLField, - GraphQLID, - GraphQLInputField, - GraphQLInputObjectType, - GraphQLObjectType, - GraphQLScalarType, - GraphQLSchema, - GraphQLString, - defaultFieldResolver, - graphql, - GraphQLNonNull, - GraphQLList, - GraphQLUnionType, - GraphQLInt, - GraphQLOutputType, - isNonNullType, - isScalarType, - isListType, - TypeSystemExtensionNode, - GraphQLDirective, - DirectiveLocation, - print, -} from 'graphql'; -import formatDate from 'dateformat'; - -import { makeExecutableSchema } from '@graphql-tools/schema'; -import { - VisitableSchemaType, - SchemaDirectiveVisitor, - SchemaVisitor, - visitSchema, - ExecutionResult, - astFromDirective, -} from '@graphql-tools/utils'; -import { assertGraphQLEnumType, assertGraphQLInputObjectType, assertGraphQLInterfaceType, assertGraphQLObjectType, assertGraphQLScalerType, assertGraphQLUnionType } from '../../testing/assertion'; - -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('are included in the schema AST', () => { - const schema = makeExecutableSchema({ - typeDefs, - resolvers: { - Gender: { - NONBINARY: 'NB', - FEMALE: 'F', - MALE: 'M', - }, - }, - }); - - function checkDirectives( - type: VisitableSchemaType, - typeDirectiveNames: Array, - fieldDirectiveMap: Record> = {}, - ) { - expect(getDirectiveNames(type)).toEqual(typeDirectiveNames); - - Object.keys(fieldDirectiveMap).forEach((key) => { - expect( - getDirectiveNames((type as GraphQLObjectType).getFields()[key]), - ).toEqual(fieldDirectiveMap[key]); - }); - } - - function getDirectiveNames(type: VisitableSchemaType): Array { - let directives = (type.astNode?.directives ?? []).map((d) => d.name.value); - const extensionASTNodes = (type as { - extensionASTNodes?: Array; - }).extensionASTNodes; - if (extensionASTNodes != null) { - extensionASTNodes.forEach((extensionASTNode) => { - directives = directives.concat( - (extensionASTNode.directives ?? []).map((d) => d.name.value), - ); - }); - } - return directives; - } - - expect(getDirectiveNames(schema)).toEqual([ - 'schemaDirective', - 'schemaExtensionDirective', - ]); - - const queryType = schema.getQueryType() - assertGraphQLObjectType(queryType) - checkDirectives( - queryType, - ['queryTypeDirective', 'queryTypeExtensionDirective'], - { - people: ['queryFieldDirective'], - }, - ); - - const GenderType = schema.getType('Gender') - assertGraphQLEnumType(GenderType) - expect(getDirectiveNames(GenderType)).toEqual([ - 'enumTypeDirective', - 'enumTypeExtensionDirective', - ]); - - const nonBinary = (schema.getType( - 'Gender', - ) as GraphQLEnumType).getValues()[0]; - expect(getDirectiveNames(nonBinary)).toEqual(['enumValueDirective']); - - const DateType = schema.getType('Date') - assertGraphQLScalerType(DateType) - checkDirectives(DateType, [ - 'dateDirective', - 'dateExtensionDirective', - ]); - - const NamedType = schema.getType('Named') - assertGraphQLInterfaceType(NamedType) - checkDirectives( - NamedType, - ['interfaceDirective', 'interfaceExtensionDirective'], - { - name: ['interfaceFieldDirective'], - }, - ); - - const PersonInput = schema.getType('PersonInput') - assertGraphQLInputObjectType(PersonInput) - checkDirectives( - PersonInput, - ['inputTypeDirective', 'inputTypeExtensionDirective'], - { - name: ['inputFieldDirective'], - gender: [], - }, - ); - const MutationType = schema.getMutationType() - assertGraphQLObjectType(MutationType) - checkDirectives( - MutationType, - ['mutationTypeDirective', 'mutationTypeExtensionDirective'], - { - addPerson: ['mutationMethodDirective'], - }, - ); - expect( - getDirectiveNames(MutationType.getFields().addPerson.args[0]), - ).toEqual(['mutationArgumentDirective']); - - const PersonType = schema.getType('Person') - assertGraphQLObjectType(PersonType) - checkDirectives( - PersonType, - ['objectTypeDirective', 'objectTypeExtensionDirective'], - { - id: ['objectFieldDirective'], - name: [], - }, - ); - - const WhateverUnionType = schema.getType('WhateverUnion') - assertGraphQLUnionType(WhateverUnionType) - checkDirectives(WhateverUnionType, [ - 'unionDirective', - 'unionExtensionDirective', - ]); - }); - - test('works with enum and its resolvers', () => { - const schema = makeExecutableSchema({ - typeDefs: ` - enum DateFormat { - LOCAL - ISO - } - - directive @date(format: DateFormat) on FIELD_DEFINITION - - scalar Date - - type Query { - today: Date @date(format: LOCAL) - } - `, - resolvers: { - DateFormat: { - LOCAL: 'local', - ISO: 'iso', - }, - }, - }); - - expect(schema.getType('DateFormat')).toBeDefined(); - expect(schema.getDirective('date')).toBeDefined(); - }); - - test('can be implemented with SchemaDirectiveVisitor', () => { - const visited: Set = new Set(); - const schema = makeExecutableSchema({ typeDefs }); - - SchemaDirectiveVisitor.visitSchemaDirectives(schema, { - // The directive subclass can be defined anonymously inline! - queryTypeDirective: class extends SchemaDirectiveVisitor { - public static description = 'A @directive for query object types'; - public visitObject(object: GraphQLObjectType) { - expect(object).toBe(schema.getQueryType()); - visited.add(object); - } - }, - queryTypeExtensionDirective: class extends SchemaDirectiveVisitor { - public static description = 'A @directive for query object types'; - public visitObject(object: GraphQLObjectType) { - expect(object).toBe(schema.getQueryType()); - visited.add(object); - } - }, - }); - - expect(visited.size).toBe(1); - }); - - test('can visit the schema itself', () => { - const visited: Array = []; - const schema = makeExecutableSchema({ typeDefs }); - SchemaDirectiveVisitor.visitSchemaDirectives(schema, { - schemaDirective: class extends SchemaDirectiveVisitor { - public visitSchema(s: GraphQLSchema) { - visited.push(s); - } - }, - schemaExtensionDirective: class extends SchemaDirectiveVisitor { - public visitSchema(s: GraphQLSchema) { - visited.push(s); - } - }, - }); - expect(visited.length).toBe(2); - expect(visited[0]).toBe(schema); - expect(visited[1]).toBe(schema); - }); - - test('can visit fields within object types', () => { - const schema = makeExecutableSchema({ typeDefs }); - - let mutationObjectType: GraphQLObjectType; - let mutationField: GraphQLField; - let enumObjectType: GraphQLEnumType; - let inputObjectType: GraphQLInputObjectType; - - SchemaDirectiveVisitor.visitSchemaDirectives(schema, { - mutationTypeDirective: class extends SchemaDirectiveVisitor { - public visitObject(object: GraphQLObjectType) { - mutationObjectType = object; - expect(this.visitedType).toBe(object); - expect(object.name).toBe('Mutation'); - } - }, - - mutationTypeExtensionDirective: class extends SchemaDirectiveVisitor { - public visitObject(object: GraphQLObjectType) { - mutationObjectType = object; - expect(this.visitedType).toBe(object); - expect(object.name).toBe('Mutation'); - } - }, - - mutationMethodDirective: class extends SchemaDirectiveVisitor { - public visitFieldDefinition( - field: GraphQLField, - details: { - objectType: GraphQLObjectType; - }, - ) { - expect(this.visitedType).toBe(field); - expect(field.name).toBe('addPerson'); - expect(details.objectType).toBe(mutationObjectType); - expect(field.args.length).toBe(1); - mutationField = field; - } - }, - - mutationArgumentDirective: class extends SchemaDirectiveVisitor { - public visitArgumentDefinition( - arg: GraphQLArgument, - details: { - field: GraphQLField; - objectType: GraphQLObjectType; - }, - ) { - expect(this.visitedType).toBe(arg); - expect(arg.name).toBe('input'); - expect(details.field).toBe(mutationField); - expect(details.objectType).toBe(mutationObjectType); - expect(details.field.args[0]).toBe(arg); - } - }, - - enumTypeDirective: class extends SchemaDirectiveVisitor { - public visitEnum(enumType: GraphQLEnumType) { - expect(this.visitedType).toBe(enumType); - expect(enumType.name).toBe('Gender'); - enumObjectType = enumType; - } - }, - - enumTypeExtensionDirective: class extends SchemaDirectiveVisitor { - public visitEnum(enumType: GraphQLEnumType) { - expect(this.visitedType).toBe(enumType); - expect(enumType.name).toBe('Gender'); - enumObjectType = enumType; - } - }, - - enumValueDirective: class extends SchemaDirectiveVisitor { - public visitEnumValue( - value: GraphQLEnumValue, - details: { - enumType: GraphQLEnumType; - }, - ) { - expect(this.visitedType).toBe(value); - expect(value.name).toBe('NONBINARY'); - expect(value.value).toBe('NONBINARY'); - expect(details.enumType).toBe(enumObjectType); - } - }, - - inputTypeDirective: class extends SchemaDirectiveVisitor { - public visitInputObject(object: GraphQLInputObjectType) { - inputObjectType = object; - expect(this.visitedType).toBe(object); - expect(object.name).toBe('PersonInput'); - } - }, - - inputTypeExtensionDirective: class extends SchemaDirectiveVisitor { - public visitInputObject(object: GraphQLInputObjectType) { - inputObjectType = object; - expect(this.visitedType).toBe(object); - expect(object.name).toBe('PersonInput'); - } - }, - - inputFieldDirective: class extends SchemaDirectiveVisitor { - public visitInputFieldDefinition( - field: GraphQLInputField, - details: { - objectType: GraphQLInputObjectType; - }, - ) { - expect(this.visitedType).toBe(field); - expect(field.name).toBe('name'); - expect(details.objectType).toBe(inputObjectType); - } - }, - }); - }); - - test('can check if a visitor method is implemented', () => { - class Visitor extends SchemaVisitor { - // eslint-disable-next-line @typescript-eslint/no-empty-function - public notVisitorMethod() {} - - public visitObject(object: GraphQLObjectType) { - return object; - } - } - - expect(Visitor.implementsVisitorMethod('notVisitorMethod')).toBe(false); - - expect(Visitor.implementsVisitorMethod('visitObject')).toBe(true); - - expect(Visitor.implementsVisitorMethod('visitInputFieldDefinition')).toBe( - false, - ); - - expect(Visitor.implementsVisitorMethod('visitBogusType')).toBe(false); - }); - - test('can use visitSchema for simple visitor patterns', () => { - class SimpleVisitor extends SchemaVisitor { - public visitCount = 0; - public names: Array = []; - - constructor(s: GraphQLSchema) { - super(); - this.schema = s; - } - - public visit() { - // More complicated visitor implementations might use the - // visitorSelector function more selectively, but this SimpleVisitor - // class always volunteers itself to visit any schema type. - visitSchema(this.schema, () => [this]); - } - - public visitObject(object: GraphQLObjectType) { - expect(this.schema.getType(object.name)).toBe(object); - this.names.push(object.name); - } - } - - const schema = makeExecutableSchema({ typeDefs }); - const visitor = new SimpleVisitor(schema); - visitor.visit(); - expect(visitor.names.sort((a, b) => a.localeCompare(b))).toEqual([ - 'Mutation', - 'Person', - 'Query', - ]); - }); - - test('can use SchemaDirectiveVisitor as a no-op visitor', () => { - const schema = makeExecutableSchema({ typeDefs }); - const methodNamesEncountered = new Set(); - - class EnthusiasticVisitor extends SchemaDirectiveVisitor { - public static implementsVisitorMethod(name: string) { - // Pretend this class implements all visitor methods. This is safe - // because the SchemaVisitor base class provides empty stubs for all - // the visitor methods that might be called. - methodNamesEncountered.add(name); - return true; - } - } - - EnthusiasticVisitor.visitSchemaDirectives(schema, { - schemaDirective: EnthusiasticVisitor, - queryTypeDirective: EnthusiasticVisitor, - queryFieldDirective: EnthusiasticVisitor, - enumTypeDirective: EnthusiasticVisitor, - enumValueDirective: EnthusiasticVisitor, - dateDirective: EnthusiasticVisitor, - interfaceDirective: EnthusiasticVisitor, - interfaceFieldDirective: EnthusiasticVisitor, - inputTypeDirective: EnthusiasticVisitor, - inputFieldDirective: EnthusiasticVisitor, - mutationTypeDirective: EnthusiasticVisitor, - mutationArgumentDirective: EnthusiasticVisitor, - mutationMethodDirective: EnthusiasticVisitor, - objectTypeDirective: EnthusiasticVisitor, - objectFieldDirective: EnthusiasticVisitor, - unionDirective: EnthusiasticVisitor, - }); - - for (const methodName of methodNamesEncountered) { - expect(methodName in SchemaVisitor.prototype).toBeTruthy(); - } - }); - - test('can handle declared arguments', () => { - const schemaText = ` - directive @oyez( - times: Int = 5, - party: Party = IMPARTIAL, - ) on OBJECT | FIELD_DEFINITION - - schema { - query: Courtroom - } - - type Courtroom @oyez { - judge: String @oyez(times: 0) - marshall: String @oyez - } - - enum Party { - DEFENSE - PROSECUTION - IMPARTIAL - }`; - - const schema = makeExecutableSchema({ typeDefs: schemaText }); - const context = { - objectCount: 0, - fieldCount: 0, - }; - - const visitors = SchemaDirectiveVisitor.visitSchemaDirectives( - schema, - { - oyez: class extends SchemaDirectiveVisitor { - public static getDirectiveDeclaration( - name: string, - theSchema: GraphQLSchema, - ) { - expect(theSchema).toBe(schema); - const prev = schema.getDirective(name); - prev?.args.some((arg) => { - if (arg.name === 'times') { - // Override the default value of the times argument to be 3 - // instead of 5. - arg.defaultValue = 3; - return true; - } - return false; - }); - return prev; - } - - public visitObject() { - ++this.context.objectCount; - expect(this.args.times).toBe(3); - } - - public visitFieldDefinition(field: GraphQLField) { - ++this.context.fieldCount; - if (field.name === 'judge') { - expect(this.args.times).toBe(0); - } else if (field.name === 'marshall') { - expect(this.args.times).toBe(3); - } - expect(this.args.party).toBe('IMPARTIAL'); - } - }, - }, - context, - ); - - expect(context.objectCount).toBe(1); - expect(context.fieldCount).toBe(2); - - expect(Object.keys(visitors)).toEqual(['oyez']); - expect( - visitors.oyez.map( - (v) => - (v.visitedType as GraphQLObjectType | GraphQLField).name, - ), - ).toEqual(['Courtroom', 'judge', 'marshall']); - }); - - test('can be used to implement the @upper example', () => { - const schema = makeExecutableSchema({ - typeDefs: ` - directive @upper on FIELD_DEFINITION - - type Query { - hello: String @upper - }`, - schemaDirectives: { - upper: class extends SchemaDirectiveVisitor { - public visitFieldDefinition(field: GraphQLField) { - const { resolve = defaultFieldResolver } = field; - field.resolve = async function (...args) { - const result = await resolve.apply(this, args); - if (typeof result === 'string') { - return result.toUpperCase(); - } - return result; - }; - } - }, - }, - resolvers: { - Query: { - hello() { - return 'hello world'; - }, - }, - }, - }); - - return graphql( - schema, - ` - query { - hello - } - `, - ).then(({ data }) => { - expect(data).toEqual({ - hello: 'HELLO WORLD', - }); - }); - }); - - test('can be used to implement the @date example', () => { - const schema = makeExecutableSchema({ - typeDefs: ` - directive @date(format: String) on FIELD_DEFINITION - - scalar Date - - type Query { - today: Date @date(format: "mmmm d, yyyy") - }`, - - schemaDirectives: { - date: class extends SchemaDirectiveVisitor { - public visitFieldDefinition(field: GraphQLField) { - const { resolve = defaultFieldResolver } = field; - const { format } = this.args; - field.type = GraphQLString; - field.resolve = async function (...args) { - const date = await resolve.apply(this, args); - return formatDate(date, format, true); - }; - } - }, - }, - - resolvers: { - Query: { - today() { - return new Date(1519688273858).toUTCString(); - }, - }, - }, - }); - - 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 () => { - class FormattableDateDirective extends SchemaDirectiveVisitor { - public visitFieldDefinition(field: GraphQLField) { - const { resolve = defaultFieldResolver } = field; - const { defaultFormat } = this.args; - - field.args.push( - Object.create({ - name: 'format', - type: GraphQLString, - }), - ); - - field.type = GraphQLString; - field.resolve = async function ( - source, - { format, ...args }, - context, - info, - ) { - const newFormat = format || defaultFormat; - const date = await resolve.call(this, source, args, context, info); - return formatDate(date, newFormat, true); - }; - } - } - - const schema = makeExecutableSchema({ - typeDefs: ` - directive @date( - defaultFormat: String = "mmmm d, yyyy" - ) on FIELD_DEFINITION - - scalar Date - - type Query { - today: Date @date - }`, - - schemaDirectives: { - date: FormattableDateDirective, - }, - - resolvers: { - Query: { - today() { - return new Date(1521131357195); - }, - }, - }, - }); - - 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 @intl example', () => { - function translate(text: string, path: Array, locale: string) { - expect(text).toBe('hello'); - expect(path).toEqual(['Query', 'greeting']); - expect(locale).toBe('fr'); - return 'bonjour'; - } - - const context = { - locale: 'fr', - }; - - const schema = makeExecutableSchema({ - typeDefs: ` - directive @intl on FIELD_DEFINITION - - type Query { - greeting: String @intl - }`, - - schemaDirectives: { - intl: class extends SchemaDirectiveVisitor { - public visitFieldDefinition( - field: GraphQLField, - details: { - objectType: GraphQLObjectType; - }, - ) { - const { resolve = defaultFieldResolver } = field; - field.resolve = async function (...args: Parameters) { - const defaultText = await resolve.apply(this, args); - // In this example, path would be ["Query", "greeting"]: - const path = [details.objectType.name, field.name]; - expect(args[2]).toBe(context); - return translate(defaultText, path, context.locale); - }; - } - }, - }, - - resolvers: { - Query: { - greeting() { - return 'hello'; - }, - }, - }, - }); - - return graphql( - schema, - ` - query { - greeting - } - `, - null, - context, - ).then(({ data }) => { - expect(data).toEqual({ - greeting: 'bonjour', - }); - }); - }); - - test('can be used to implement the @auth example', async () => { - const roles = ['UNKNOWN', 'USER', 'REVIEWER', 'ADMIN']; - - function getUser(token: string) { - return { - hasRole(role: string) { - const tokenIndex = roles.indexOf(token); - const roleIndex = roles.indexOf(role); - return roleIndex >= 0 && tokenIndex >= roleIndex; - }, - }; - } - - class AuthDirective extends SchemaDirectiveVisitor { - public visitObject(type: GraphQLObjectType) { - this.ensureFieldsWrapped(type); - (type as any)._requiredAuthRole = this.args.requires; - } - - // Visitor methods for nested types like fields and arguments - // also receive a details object that provides information about - // the parent and grandparent types. - public visitFieldDefinition( - field: GraphQLField, - details: { objectType: GraphQLObjectType }, - ) { - this.ensureFieldsWrapped(details.objectType); - (field as any)._requiredAuthRole = this.args.requires; - } - - public ensureFieldsWrapped(objectType: GraphQLObjectType) { - // Mark the GraphQLObjectType object to avoid re-wrapping: - if ((objectType as any)._authFieldsWrapped) { - return; - } - (objectType as any)._authFieldsWrapped = true; - - const fields = objectType.getFields(); - - Object.keys(fields).forEach((fieldName) => { - const field = fields[fieldName]; - const { resolve = defaultFieldResolver } = field; - field.resolve = function (...args: Parameters) { - // Get the required Role from the field first, falling back - // to the objectType if no Role is required by the field: - const requiredRole = - (field as any)._requiredAuthRole || - (objectType as any)._requiredAuthRole; - - if (!requiredRole) { - return resolve.apply(this, args); - } - - const context = args[2]; - const user = getUser(context.headers.authToken); - if (!user.hasRole(requiredRole)) { - throw new Error('not authorized'); - } - - return resolve.apply(this, args); - }; - }); - } - } - - const schema = makeExecutableSchema({ - typeDefs: ` - directive @auth( - requires: Role = ADMIN, - ) on OBJECT | FIELD_DEFINITION - - enum Role { - ADMIN - REVIEWER - USER - UNKNOWN - } - - type User @auth(requires: USER) { - name: String - banned: Boolean @auth(requires: ADMIN) - canPost: Boolean @auth(requires: REVIEWER) - } - - type Query { - users: [User] - }`, - - schemaDirectives: { - auth: AuthDirective, - }, - - resolvers: { - Query: { - users() { - return [ - { - banned: true, - canPost: false, - name: 'Ben', - }, - ]; - }, - }, - }, - }); - - 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 () => { - class LimitedLengthType extends GraphQLScalarType { - constructor(type: GraphQLScalarType, maxLength: number) { - super({ - name: `LengthAtMost${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 schema = makeExecutableSchema({ - typeDefs: ` - directive @length(max: Int) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION - - type Query { - books: [Book] - } - - type Book { - title: String @length(max: 10) - } - - type Mutation { - createBook(book: BookInput): Book - } - - input BookInput { - title: String! @length(max: 10) - }`, - - schemaDirectives: { - length: class extends SchemaDirectiveVisitor { - public visitInputFieldDefinition(field: GraphQLInputField) { - this.wrapType(field); - } - - public visitFieldDefinition(field: GraphQLField) { - this.wrapType(field); - } - - private wrapType(field: GraphQLInputField | GraphQLField) { - if (isNonNullType(field.type) && isScalarType(field.type.ofType)) { - field.type = new GraphQLNonNull( - new LimitedLengthType(field.type.ofType, this.args.max), - ); - } else if (isScalarType(field.type)) { - field.type = new LimitedLengthType(field.type, this.args.max); - } else { - throw new Error(`Not a scalar type: ${field.type.toString()}`); - } - } - }, - }, - - resolvers: { - Query: { - books() { - return [ - { - title: 'abcdefghijklmnopqrstuvwxyz', - }, - ]; - }, - }, - Mutation: { - createBook(_parent, args) { - return args.book; - }, - }, - }, - }); - - 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', () => { - const schema = makeExecutableSchema({ - typeDefs: ` - directive @uniqueID(name: String, from: [String]) on OBJECT - - 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 - }`, - - schemaDirectives: { - uniqueID: class extends SchemaDirectiveVisitor { - public visitObject(type: GraphQLObjectType) { - const { name, from } = this.args; - type.getFields()[name] = Object.create({ - name, - type: GraphQLID, - description: 'Unique ID', - args: [], - resolve(object: any) { - const hash = createHash('sha1'); - hash.update(type.name); - from.forEach((fieldName: string) => { - hash.update(String(object[fieldName])); - }); - return hash.digest('hex'); - }, - }); - } - }, - }, - - resolvers: { - Query: { - people() { - return [ - { - personID: 1, - name: 'Ben', - }, - ]; - }, - locations() { - return [ - { - locationID: 1, - address: '140 10th St', - }, - ]; - }, - }, - }, - }); - - 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', () => { - const schema = makeExecutableSchema({ - typeDefs, - schemaDirectives: { - objectTypeDirective: class extends SchemaDirectiveVisitor { - public visitObject(object: GraphQLObjectType) { - return Object.create(object, { - name: { value: 'Human' }, - }); - } - }, - }, - }); - - 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', () => { - 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) - }`, - - schemaDirectives: { - remove: class extends SchemaDirectiveVisitor { - public visitEnumValue(): any { - if (this.args.if) { - return null; - } - } - }, - }, - }); - - 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 value", () => { - const schema = makeExecutableSchema({ - typeDefs: ` - directive @value(new: String!) on ENUM_VALUE - - type Query { - device: Device - } - - enum Device { - PHONE - TABLET - LAPTOP @value(new: "COMPUTER") - }`, - - schemaDirectives: { - value: class extends SchemaDirectiveVisitor { - public visitEnumValue(value: GraphQLEnumValue): GraphQLEnumValue { - return { - ...value, - value: this.args.new, - }; - } - }, - }, - }); - - 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', () => { - 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 - }`, - - schemaDirectives: { - rename: class extends SchemaDirectiveVisitor { - public visitObject(object: GraphQLObjectType) { - object.name = this.args.to; - } - }, - }, - }); - - 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)', () => { - const visited = new Set(); - makeExecutableSchema({ - typeDefs: ` - directive @hasScope(scope: [String]) on QUERY | FIELD | OBJECT - - type Query @hasScope { - oyez: String - }`, - - schemaDirectives: { - hasScope: class extends SchemaDirectiveVisitor { - public visitObject(object: GraphQLObjectType) { - expect(object.name).toBe('Query'); - visited.add(object); - } - }, - }, - }); - - expect(visited.size).toBe(1); - }); - - test('allows multiple directives when first replaces type (issue #851)', () => { - const schema = makeExecutableSchema({ - typeDefs: ` - directive @upper on FIELD_DEFINITION - directive @reverse on FIELD_DEFINITION - - type Query { - hello: String @upper @reverse - }`, - schemaDirectives: { - upper: class extends SchemaDirectiveVisitor { - public visitFieldDefinition(field: GraphQLField) { - const { resolve = defaultFieldResolver } = field; - const newField = { ...field }; - - newField.resolve = async function (...args: Parameters) { - const result = await resolve.apply(this, args); - if (typeof result === 'string') { - return result.toUpperCase(); - } - return result; - }; - - return newField; - } - }, - reverse: class extends SchemaDirectiveVisitor { - public visitFieldDefinition(field: GraphQLField) { - const { resolve = defaultFieldResolver } = field; - field.resolve = async function (...args: Parameters) { - const result = await resolve.apply(this, args); - if (typeof result === 'string') { - return result.split('').reverse().join(''); - } - return result; - }; - } - }, - }, - resolvers: { - Query: { - hello() { - return 'hello world'; - }, - }, - }, - }); - - return graphql( - schema, - ` - query { - hello - } - `, - ).then(({ data }) => { - expect(data).toEqual({ - hello: 'DLROW OLLEH', - }); - }); - }); - - test('preserves ability to create fields of different types with same name (issue 1462)', () => { - function validateStr(value: any, { - min = null, - message = null, - } : { - min: null | number, - message: null | string, - }) { - if(min && value.length < min) { - throw new Error(message || `Please ensure the value is at least ${min} characters.`); - } - } - - class ConstraintType extends GraphQLScalarType { - constructor( - type: GraphQLScalarType, - args: { - min: number, - message: string, - }, - ) { - super({ - name: 'ConstraintType', - serialize: (value) => type.serialize(value), - parseValue: (value) => { - const trimmed = value.trim(); - validateStr(trimmed, args); - return type.parseValue(trimmed); - } - }); - } - } - - class ConstraintDirective extends SchemaDirectiveVisitor { - visitInputFieldDefinition(field: GraphQLInputField) { - if (isNonNullType(field.type) && isScalarType(field.type.ofType)) { - field.type = new GraphQLNonNull( - new ConstraintType(field.type.ofType, this.args) - ); - } else if (isScalarType(field.type)) { - field.type = new ConstraintType(field.type, this.args); - } else { - throw new Error(`Not a scalar type: ${field.type}`); - } - } - } - - const schema = makeExecutableSchema({ - typeDefs: ` - directive @constraint(min: Int, message: String) on INPUT_FIELD_DEFINITION - - input BookInput { - name: String! @constraint(min: 10, message: "Book input error!") - } - - input AuthorInput { - name: String! @constraint(min: 4, message: "Author input error") - } - - type Query { - getBookById(id: Int): String - } - - type Mutation { - createBook(input: BookInput!): String - createAuthor(input: AuthorInput!): String - } - `, - resolvers: { - Mutation: { - createBook() { - return 'yes'; - }, - createAuthor() { - return 'no'; - } - } - }, - schemaDirectives: { - constraint: ConstraintDirective - } - }); - - return graphql( - schema, - ` - mutation { - createAuthor(input: { - name: "M" - }) - } - `, - ).then(({ errors }) => { - expect(errors?.[0].originalError).toEqual(new Error('Author input error')); - }); - }); - it('should print a directive correctly from GraphQLDirective object using astFromDirective and print', () => { - const sampleDirective = new GraphQLDirective({ - name: 'sample', - args: { - foo: { - type: GraphQLString - } - }, - locations: [DirectiveLocation.FIELD_DEFINITION] - }); - expect( - print( - astFromDirective(sampleDirective) - ) - ).toBeSimilarString(/* GraphQL */` - directive @sample(foo: String) on FIELD_DEFINITION - `); - }) -}); diff --git a/packages/utils/tests/get-directives.spec.ts b/packages/utils/tests/get-directives.spec.ts index d1ef5d501e6..0af6716fc25 100644 --- a/packages/utils/tests/get-directives.spec.ts +++ b/packages/utils/tests/get-directives.spec.ts @@ -9,10 +9,8 @@ describe('getDirectives', () => { test: String } `; - const schema = makeExecutableSchema({ typeDefs, resolvers: {}, allowUndefinedInResolve: true }); - const QueryType = schema.getQueryType() - assertGraphQLObjectType(QueryType) - const directivesMap = getDirectives(schema, QueryType); + const schema = makeExecutableSchema({ typeDefs, resolvers: {} }) as GraphQLSchema; + const directivesMap = getDirectives(schema, schema.getQueryType()); expect(directivesMap).toEqual({}); }); @@ -24,10 +22,8 @@ describe('getDirectives', () => { } `; - const schema = makeExecutableSchema({ typeDefs, resolvers: {}, allowUndefinedInResolve: true }); - const QueryType = schema.getQueryType() - assertGraphQLObjectType(QueryType) - const directivesMap = getDirectives(schema, QueryType.getFields().test); + const schema = makeExecutableSchema({ typeDefs, resolvers: {} }) as GraphQLSchema; + const directivesMap = getDirectives(schema, schema.getQueryType().getFields().test); expect(directivesMap).toEqual({ deprecated: { reason: 'No longer supported', @@ -44,10 +40,8 @@ describe('getDirectives', () => { directive @mydir on FIELD_DEFINITION `; - const schema = makeExecutableSchema({ typeDefs, resolvers: {}, allowUndefinedInResolve: true }); - const QueryType = schema.getQueryType() - assertGraphQLObjectType(QueryType) - const directivesMap = getDirectives(schema, QueryType.getFields().test); + const schema = makeExecutableSchema({ typeDefs, resolvers: {} }) as GraphQLSchema; + const directivesMap = getDirectives(schema, schema.getQueryType().getFields().test); expect(directivesMap).toEqual({ mydir: {}, }); @@ -62,10 +56,8 @@ describe('getDirectives', () => { directive @mydir(f1: String) on FIELD_DEFINITION `; - const schema = makeExecutableSchema({ typeDefs, resolvers: {}, allowUndefinedInResolve: true }) - const QueryType = schema.getQueryType() - assertGraphQLObjectType(QueryType) - const directivesMap = getDirectives(schema, QueryType.getFields().test); + const schema = makeExecutableSchema({ typeDefs, resolvers: {} }) as GraphQLSchema; + const directivesMap = getDirectives(schema, schema.getQueryType().getFields().test); expect(directivesMap).toEqual({ mydir: { f1: 'test', @@ -82,25 +74,23 @@ describe('getDirectives', () => { directive @mydir(f1: String) on FIELD_DEFINITION `; - const schema = makeExecutableSchema({ typeDefs, resolvers: {}, allowUndefinedInResolve: true }) - const QueryType = schema.getQueryType() - assertGraphQLObjectType(QueryType) - const directivesMap = getDirectives(schema, QueryType.getFields().test); + const schema = makeExecutableSchema({ typeDefs, resolvers: {} }) as GraphQLSchema; + const directivesMap = getDirectives(schema, schema.getQueryType().getFields().test); expect(directivesMap).toEqual({ mydir: {}, }); }); - it('provides the extension definition over base', () => { + it('provides the extension definition', () => { const schema = makeExecutableSchema({ typeDefs: ` directive @mydir(arg: String) on OBJECT - extend type Query @mydir(arg: "ext1") { - second: String - } type Query @mydir(arg: "base") { first: String } + extend type Query @mydir(arg: "ext1") { + second: String + } ` }); const QueryType = schema.getQueryType() diff --git a/packages/utils/tests/validate-documents.spec.ts b/packages/utils/tests/validate-documents.spec.ts index 5f1b12e6169..62dff298b84 100644 --- a/packages/utils/tests/validate-documents.spec.ts +++ b/packages/utils/tests/validate-documents.spec.ts @@ -53,7 +53,8 @@ describe('validateGraphQlDocuments', () => { try { checkValidationErrors(result); expect(true).toBeFalsy(); - } catch (errors) { + } catch (aggregateError) { + const { errors } = aggregateError; expect(Symbol.iterator in errors).toBeTruthy(); const generator = errors[Symbol.iterator](); @@ -112,8 +113,8 @@ describe('checkValidationErrors', () => { let errors; try { checkValidationErrors(loadDocumentErrors as any); - } catch (_errors) { - errors = _errors; + } catch (aggregateError) { + errors = aggregateError.errors; } expect(Symbol.iterator in errors).toBeTruthy(); diff --git a/packages/wrap/src/introspect.ts b/packages/wrap/src/introspect.ts index 08ef51fdf9c..34e91c3f8cf 100644 --- a/packages/wrap/src/introspect.ts +++ b/packages/wrap/src/introspect.ts @@ -10,15 +10,14 @@ import { import { ValueOrPromise } from 'value-or-promise'; -import { AsyncExecutor, Executor, SyncExecutor, ExecutionResult } from '@graphql-tools/utils'; -import AggregateError from '@ardatan/aggregate-error'; +import { AsyncExecutor, Executor, SyncExecutor, ExecutionResult, AggregateError } from '@graphql-tools/utils'; function getSchemaFromIntrospection(introspectionResult: ExecutionResult): GraphQLSchema { if (introspectionResult?.data?.__schema) { return buildClientSchema(introspectionResult.data); } else if (introspectionResult?.errors?.length) { if (introspectionResult.errors.length > 1) { - const combinedError = new AggregateError(introspectionResult.errors); + const combinedError = new AggregateError(introspectionResult.errors, 'Could not obtain introspection result'); throw combinedError; } const error = introspectionResult.errors[0]; @@ -34,10 +33,14 @@ export function introspectSchema options?: IntrospectionOptions ): TExecutor extends AsyncExecutor ? Promise : GraphQLSchema { const parsedIntrospectionQuery: DocumentNode = parse(getIntrospectionQuery(options)); - return new ValueOrPromise(() => (executor as Executor)({ - document: parsedIntrospectionQuery, - context, - })).then(introspection => getSchemaFromIntrospection(introspection)).resolve() as any; + return new ValueOrPromise(() => + (executor as Executor)({ + document: parsedIntrospectionQuery, + context, + }) + ) + .then(introspection => getSchemaFromIntrospection(introspection)) + .resolve() as any; } // Keep for backwards compatibility. Will be removed on next release. diff --git a/website/docs/directive-resolvers.md b/website/docs/directive-resolvers.md deleted file mode 100644 index fcf91884492..00000000000 --- a/website/docs/directive-resolvers.md +++ /dev/null @@ -1,178 +0,0 @@ ---- -id: directive-resolvers -title: Directive resolvers -description: A set of utilities to build your JavaScript GraphQL schema in a concise and powerful way. ---- - -## Directive example - -Let's take a look at how we can create `@upper` Directive to upper-case a string returned from resolve on Field - -To start, let's grab the schema definition string from the `makeExecutableSchema` example [in the "Generating a schema" article](/docs/generate-schema/#example). - -```js -import { makeExecutableSchema } from '@graphql-tools/schema'; -import { graphql } from 'graphql'; - -// Construct a schema, using GraphQL schema language -const typeDefs = ` - directive @upper on FIELD_DEFINITION - - type Query { - hello: String @upper - } -`; - -// Implement resolvers for out custom Directive -const directiveResolvers = { - upper( - next, - src, - args, - context, - ) { - return next().then((str) => { - if (typeof(str) === 'string') { - return str.toUpperCase(); - } - return str; - }); - }, -} - -// Provide resolver functions for your schema fields -const resolvers = { - Query: { - hello: (root, args, context) => { - return 'Hello world!'; - }, - }, -}; - -export const schema = makeExecutableSchema({ - typeDefs, - resolvers, - directiveResolvers, -}); - -const query = ` -query UPPER_HELLO { - hello -} -`; - -graphql(schema, query).then((result) => console.log('Got result', result)); -``` - -> Note: next() always return a Promise for consistency, resolved with original resolver value or rejected with an error. - -## Multi-Directives example - -Multi-Directives on a field will be apply with LTR order. - -```js -// graphql-tools combines a schema string with resolvers. -import { makeExecutableSchema } from '@graphql-tools/schema'; - -// Construct a schema, using GraphQL schema language -const typeDefs = ` - directive @upper on FIELD_DEFINITION - directive @concat(value: String!) on FIELD_DEFINITION - - type Query { - foo: String @concat(value: "@gmail.com") @upper - } -`; - -// Customs directives, check https://github.com/ardatan/graphql-tools/pull/518 -// for more examples -const directiveResolvers = { - upper( - next, - src, - args, - context, - ) { - return next().then((str) => { - if (typeof(str) === 'string') { - return str.toUpperCase(); - } - return str; - }); - }, - concat( - next, - src, - args, - context, - ) { - return next().then((str) => { - if (typeof(str) !== 'undefined') { - return `${str}${args.value}`; - } - return str; - }); - }, -} - -// Provide resolver functions for your schema fields -const resolvers = { - Query: { - foo: (root, args, context) => { - return 'foo'; - }, - }, -}; - -// Required: Export the GraphQL.js schema object as "schema" -export const schema = makeExecutableSchema({ - typeDefs, - resolvers, - directiveResolvers, -}); -``` - -The result with query `{foo}` will be: -```json -{ - "data": { - "foo": "FOO@GMAIL.COM" - } -} -``` - -## API - -### directiveResolvers option - -```js -import { makeExecutableSchema } from '@graphql-tools/schema'; - -const directiveResolvers = { - // directive resolvers implement -}; - -const schema = makeExecutableSchema({ - // ... other options - directiveResolvers, -}) -``` - -`makeExecutableSchema` has new option field is `directiveResolvers`, a map object for custom Directive's resolvers. - -### attachDirectiveResolvers - -```js -import { attachDirectiveResolvers } from '@graphql-tools/schema'; - -const directiveResolvers = { - // directive resolvers implement -}; - -schemaWithDirectiveResolvers = attachDirectiveResolvers( - schema, - directiveResolvers, -); -``` - -Given an instance of GraphQLSchema and a `directiveResolvers` map object, `attachDirectiveResolvers` returns a new schema in which all fields' resolver have been wrapped with directive resolvers. diff --git a/website/docs/generate-schema.md b/website/docs/generate-schema.md index 9f8acafa0e4..426cbf5d222 100644 --- a/website/docs/generate-schema.md +++ b/website/docs/generate-schema.md @@ -219,10 +219,7 @@ const jsSchema = makeExecutableSchema({ typeDefs, resolvers, // optional logger, // optional - allowUndefinedInResolve: false, // optional resolverValidationOptions: {}, // optional - directiveResolvers: null, // optional - schemaDirectives: null, // optional schemaTransforms: [], // optional parseOptions: {}, // optional inheritResolversFromInterfaces: false // optional @@ -233,12 +230,8 @@ const jsSchema = makeExecutableSchema({ - `resolvers` is an optional argument _(empty object by default)_ and should be an object or an array of objects that follow the pattern explained in [article on resolvers](/docs/resolvers/) -- `logger` is an optional argument, which can be used to print errors to the server console that are usually swallowed by GraphQL. The `logger` argument should be an object with a `log` function, eg. `const logger = { log: e => console.log(e) }` - - `parseOptions` is an optional argument which allows customization of parse when specifying `typeDefs` as a string. -- `allowUndefinedInResolve` is an optional argument, which is `true` by default. When set to `false`, causes your resolver to throw errors if they return undefined, which can help make debugging easier. - - `resolverValidationOptions` is an optional argument with the following properties, each of which can be set to `error`, `warn`, or `ignore`: - `requireResolversForArgs` will cause `makeExecutableSchema` to throw an error (`error`) or issue a warning (`warn`)unless a resolver is defined for every field with arguments. The default is `ignore`, causing this validator to be skipped. @@ -254,4 +247,3 @@ const jsSchema = makeExecutableSchema({ - `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/) -- `schemaDirectives` is an optional argument _(empty object by default)_ and can be used to specify the [earlier class-based implementation of schema directives](/docs/legacy-schema-directives/) diff --git a/website/docs/legacy-schema-directives.md b/website/docs/legacy-schema-directives.md deleted file mode 100644 index 0bd464b614c..00000000000 --- a/website/docs/legacy-schema-directives.md +++ /dev/null @@ -1,714 +0,0 @@ ---- -id: legacy-schema-directives -title: Schema directives -description: Using and implementing custom directives to transform schema types, fields, and arguments ---- - -A _directive_ is an identifier preceded by a `@` character, optionally followed by a list of named arguments, which can appear after almost any form of syntax in the GraphQL query or schema languages. Here's an example from the [GraphQL draft specification](http://facebook.github.io/graphql/draft/#sec-Type-System.Directives) that illustrates several of these possibilities: - -```typescript -directive @deprecated( - reason: String = "No longer supported" -) on FIELD_DEFINITION | ENUM_VALUE - -type ExampleType { - newField: String - oldField: String @deprecated(reason: "Use `newField`.") -} -``` - -As you can see, the usage of `@deprecated(reason: ...)` _follows_ the field that it pertains to (`oldField`), though the syntax might remind you of "decorators" in other languages, which usually appear on the line above. Directives are typically _declared_ once, using the `directive @deprecated ... on ...` syntax, and then _used_ zero or more times throughout the schema document, using the `@deprecated(reason: ...)` syntax. - -The possible applications of directive syntax are numerous: enforcing access permissions, formatting date strings, auto-generating resolver functions for a particular backend API, marking strings for internationalization, synthesizing globally unique object identifiers, specifying caching behavior, skipping or including or deprecating fields, and just about anything else you can imagine. - -This document focuses on directives that appear in GraphQL _schemas_ (as opposed to queries) written in [Schema Definition Language](https://github.com/facebook/graphql/pull/90), or SDL for short. In the following sections, you will see how custom directives can be implemented and used to modify the structure and behavior of a GraphQL schema in ways that would not be possible using SDL syntax alone. - -## (At least) two strategies - -`graphql-tools` provides [a newer functional approach](/docs/schema-directives/) for directive-based schema modification. The remainder of this document describes the earlier class-based mechanism. We believe the newer approach is easier to reason about, but older class-based schema directives are still supported, as long as the in-place schema modification techniques they employ do not yield an invalid schema. - -## Using schema directives - -Most of this document is concerned with _implementing_ schema directives, and some of the examples are quite complicated. No matter how many tools and best practices you have at your disposal, it can be difficult to implement a non-trivial class-based 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 `schemaDirectives` argument, which is an object that maps directive names to directive implementations: - -```js -import { makeExecutableSchema } from '@graphql-tools/schema'; -import { RenameDirective } from 'fake-rename-directive-package'; - -const typeDefs = ` -type Person @rename(to: "Human") { - name: String! - currentDateMinusDateOfBirth: Int @rename(to: "age") -}`; - -const schema = makeExecutableSchema({ - typeDefs, - schemaDirectives: { - rename: RenameDirective, - }, -}); -``` - -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. - -Everything you read below addresses some aspect of how a directive like `@rename(to: ...)` could be implemented. If that's not something you care about right now, feel free to skip the rest of this document. When you need it, it will be here. - -## Implementing schema directives - -Since the GraphQL specification does not discuss any specific implementation strategy for directives, it's up to each GraphQL server framework to expose an API for implementing new directives. - -GraphQL Tools provides a convenient yet powerful tool for implementing directive syntax: the [`SchemaDirectiveVisitor`](https://github.com/ardatan/graphql-tools/blob/master/src/utils/SchemaDirectiveVisitor.ts) class. - -To implement a schema directive using `SchemaDirectiveVisitor`, simply create a subclass of `SchemaDirectiveVisitor` that overrides one or more of the following visitor methods: - -- `visitSchema(schema: GraphQLSchema)` -- `visitScalar(scalar: GraphQLScalarType)` -- `visitObject(object: GraphQLObjectType)` -- `visitFieldDefinition(field: GraphQLField, details: { objectType: GraphQLObjectType | GraphQLInterfaceType })` -- `visitArgumentDefinition(argument: GraphQLArgument, objectType: GraphQLObjectType | GraphQLInterfaceType })` -- `visitInterface(iface: GraphQLInterfaceType)` -- `visitUnion(union: GraphQLUnionType)` -- `visitEnum(type: GraphQLEnumType)` -- `visitEnumValue(value: GraphQLEnumValue, details: { enumType: GraphQLEnumType })` -- `visitInputObject(object: GraphQLInputObjectType)` -- `visitInputFieldDefinition(field: GraphQLInputField, details: { objectType: GraphQLInputObjectType })` - -By overriding methods like `visitObject`, a subclass of `SchemaDirectiveVisitor` expresses interest in certain schema types such as `GraphQLObjectType` (the first parameter type of `visitObject`). - -These method names correspond to all possible [locations](https://github.com/graphql/graphql-js/blob/a62eea88d5844a3bd9725c0f3c30950a78727f3e/src/language/directiveLocation.js#L22-L33) where a directive may be used in a schema. For example, the location `INPUT_FIELD_DEFINITION` is handled by `visitInputFieldDefinition`. - -Here is one possible implementation of the `@deprecated` directive we saw above: - -```typescript -import { SchemaDirectiveVisitor } from '@graphql-tools/utils'; -import { GraphQLField, GraphQLEnumValue } from 'graphql'; - -class DeprecatedDirective extends SchemaDirectiveVisitor { - public visitFieldDefinition(field: GraphQLField) { - field.isDeprecated = true; - field.deprecationReason = this.args.reason; - } - - public visitEnumValue(value: GraphQLEnumValue) { - value.isDeprecated = true; - value.deprecationReason = this.args.reason; - } -} -``` - -In order to apply this implementation to a schema that contains `@deprecated` directives, simply pass the `DeprecatedDirective` class to the `makeExecutableSchema` function via the `schemaDirectives` option: - -```typescript -import { makeExecutableSchema } from '@graphql-tools/schema'; - -const typeDefs = ` -type ExampleType { - newField: String - oldField: String @deprecated(reason: "Use \`newField\`.") -}`; - -const schema = makeExecutableSchema({ - typeDefs, - schemaDirectives: { - deprecated: DeprecatedDirective, - }, -}); -``` - -Alternatively, if you want to modify an existing schema object, you can use the `SchemaDirectiveVisitor.visitSchemaDirectives` interface directly: - -```typescript -import { SchemaDirectiveVisitor } from '@graphql-tools/utils'; - -SchemaDirectiveVisitor.visitSchemaDirectives(schema, { - deprecated: DeprecatedDirective, -}); -``` - -This syntax is especially useful for code-first schemas that wish to make use of directive implementations. For code-first schemas, directives are read from the `directives` key within the `extensions` field for each GraphQL entity, unless a different path is provided, as per below: - -```typescript -import { SchemaDirectiveVisitor } from '@graphql-tools/utils'; - -SchemaDirectiveVisitor.visitSchemaDirectives( - schema, - { - deprecated: DeprecatedDirective, - }, - undefined, - ['custom', 'path', 'to', 'directives', 'within', 'the', 'extensions', 'object'] -); -``` - -The second argument to `visitSchemaDirectives` refers to a shared context object that may be passed to `SchemaDirectiveVisitor` classes -- it is not often used, and can usually be safely set to undefined. - -See [this `graphql-js` issue](https://github.com/graphql/graphql-js/issues/1343) for more information on directives with code-first schemas. We follow the [Gatsby and graphql-compose convention](https://github.com/graphql/graphql-js/issues/1343#issuecomment-479877640) of reading directives from the `extensions` field, but allow customization as above. - -Note that a subclass of `SchemaDirectiveVisitor` may be instantiated multiple times to visit multiple different occurrences of the `@deprecated` directive. That's why you provide a class rather than an instance of that class. - -If for some reason you have a schema that uses another name for the `@deprecated` directive, but you want to use the same implementation, you can! The same `DeprecatedDirective` class can be passed with a different name, simply by changing its key in the `schemaDirectives` object passed to `makeExecutableSchema`. In other words, `SchemaDirectiveVisitor` implementations are effectively anonymous, so it's up to whoever uses them to assign names to them. - -## Examples - -To appreciate the range of possibilities enabled by `SchemaDirectiveVisitor`, let's examine a variety of practical examples. - -### Uppercasing strings - -Suppose you want to ensure a string-valued field is converted to uppercase. Though this use case is simple, it's a good example of a directive implementation that works by wrapping a field's `resolve` function: - -```js -import { defaultFieldResolver } from 'graphql'; -import { SchemaDirectiveVisitor } from '@graphql-tools/utils'; -import { makeExecutableSchema } from '@graphql-tools/schema'; - -const typeDefs = ` -directive @upper on FIELD_DEFINITION - -type Query { - hello: String @upper -}`; - -class UpperCaseDirective extends SchemaDirectiveVisitor { - visitFieldDefinition(field) { - const { resolve = defaultFieldResolver } = field; - field.resolve = async function (...args) { - const result = await resolve.apply(this, args); - if (typeof result === 'string') { - return result.toUpperCase(); - } - return result; - }; - } -} - -const schema = makeExecutableSchema({ - typeDefs, - schemaDirectives: { - upper: UpperCaseDirective, - upperCase: UpperCaseDirective, - }, -}); -``` - -Notice how easy it is to handle both `@upper` and `@upperCase` with the same `UpperCaseDirective` implementation. - -### Fetching data from a REST API - -Suppose you've defined an object type that corresponds to a [REST](https://en.wikipedia.org/wiki/Representational_state_transfer) resource, and you want to avoid implementing resolver functions for every field: - -```js -import { SchemaDirectiveVisitor } from '@graphql-tools/utils'; -import { makeExecutableSchema } from '@graphql-tools/schema'; - -const typeDefs = ` -directive @rest(url: String) on FIELD_DEFINITION - -type Query { - people: [Person] @rest(url: "/api/v1/people") -}`; - -class RestDirective extends SchemaDirectiveVisitor { - public visitFieldDefinition(field) { - const { url } = this.args; - field.resolve = () => fetch(url); - } -} - -const schema = makeExecutableSchema({ - typeDefs, - schemaDirectives: { - rest: RestDirective - } -}); -``` - -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. - -### Formatting date strings - -Suppose your resolver returns a `Date` object but you want to return a formatted string to the client: - -```js -import { SchemaDirectiveVisitor } from '@graphql-tools/utils'; -import { makeExecutableSchema } from '@graphql-tools/schema'; - -const typeDefs = ` -directive @date(format: String) on FIELD_DEFINITION - -scalar Date - -type Post { - published: Date @date(format: "mmmm d, yyyy") -}`; - -class DateFormatDirective extends SchemaDirectiveVisitor { - visitFieldDefinition(field) { - const { resolve = defaultFieldResolver } = field; - const { format } = this.args; - field.resolve = async function (...args) { - const date = await resolve.apply(this, args); - return require('dateformat')(date, format); - }; - // The formatted Date becomes a String, so the field type must change: - field.type = GraphQLString; - } -} - -const schema = makeExecutableSchema({ - typeDefs, - schemaDirectives: { - date: DateFormatDirective, - }, -}); -``` - -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: - -```js -import { SchemaDirectiveVisitor } from '@graphql-tools/utils'; -import { makeExecutableSchema } from '@graphql-tools/schema'; -import formatDate from "dateformat"; -import { defaultFieldResolver, GraphQLString } from "graphql"; - -const typeDefs = ` -directive @date( - defaultFormat: String = "mmmm d, yyyy" -) on FIELD_DEFINITION - -scalar Date - -type Query { - today: Date @date -}`; - -class FormattableDateDirective extends SchemaDirectiveVisitor { - public visitFieldDefinition(field) { - const { resolve = defaultFieldResolver } = field; - const { defaultFormat } = this.args; - - field.args.push({ - name: 'format', - type: GraphQLString - }); - - field.resolve = async function ( - source, - { format, ...otherArgs }, - context, - info, - ) { - const date = await resolve.call(this, source, otherArgs, context, info); - // If a format argument was not provided, default to the optional - // defaultFormat argument taken by the @date directive: - return formatDate(date, format || defaultFormat); - }; - - field.type = GraphQLString; - } -} - -const schema = makeExecutableSchema({ - typeDefs, - schemaDirectives: { - date: FormattableDateDirective - } -}); -``` - -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: - -```js -import { graphql } from 'graphql'; - -graphql( - schema, - ` - query { - today - } - ` -).then(result => { - // Logs with the default "mmmm d, yyyy" format: - console.log(result.data.today); -}); - -graphql( - schema, - ` - query { - today(format: "d mmm yyyy") - } - ` -).then(result => { - // Logs with the requested "d mmm yyyy" format: - console.log(result.data.today); -}); -``` - -### Marking strings for internationalization - -Suppose you have a function called `translate` that takes a string, a path identifying that string's role in your application, and a target locale for the translation. - -Here's how you might make sure `translate` is used to localize the `greeting` field of a `Query` type: - -```js -import { SchemaDirectiveVisitor } from '@graphql-tools/utils'; -import { makeExecutableSchema } from '@graphql-tools/schema'; - -const typeDefs = ` -directive @intl on FIELD_DEFINITION - -type Query { - greeting: String @intl -}`; - -class IntlDirective extends SchemaDirectiveVisitor { - visitFieldDefinition(field, details) { - const { resolve = defaultFieldResolver } = field; - field.resolve = async function (...args) { - const context = args[2]; - const defaultText = await resolve.apply(this, args); - // In this example, path would be ["Query", "greeting"]: - const path = [details.objectType.name, field.name]; - return translate(defaultText, path, context.locale); - }; - } -} - -const schema = makeExecutableSchema({ - typeDefs, - schemaDirectives: { - intl: IntlDirective, - }, -}); -``` - -GraphQL is great for internationalization, since a GraphQL server can access unlimited translation data, and clients can simply ask for the translations they need. - -### Enforcing access permissions - -Imagine a hypothetical `@auth` directive that takes an argument `requires` of type `Role`, which defaults to `ADMIN`. This `@auth` directive can appear on an `OBJECT` like `User` to set default access permissions for all `User` fields, as well as appearing on individual fields, to enforce field-specific `@auth` restrictions: - -```graphql -directive @auth(requires: Role = ADMIN) on OBJECT | FIELD_DEFINITION - -enum Role { - ADMIN - REVIEWER - USER - UNKNOWN -} - -type User @auth(requires: USER) { - name: String - banned: Boolean @auth(requires: ADMIN) - canPost: Boolean @auth(requires: REVIEWER) -} -``` - -What makes this example tricky is that the `OBJECT` version of the directive needs to wrap all fields of the object, even though some of those fields may be individually wrapped by `@auth` directives at the `FIELD_DEFINITION` level, and we would prefer not to rewrap resolvers if we can help it: - -```js -import { SchemaDirectiveVisitor } from '@graphql-tools/utils'; -import { makeExecutableSchema } from '@graphql-tools/schema'; - -class AuthDirective extends SchemaDirectiveVisitor { - visitObject(type) { - this.ensureFieldsWrapped(type); - type._requiredAuthRole = this.args.requires; - } - // Visitor methods for nested types like fields and arguments - // also receive a details object that provides information about - // the parent and grandparent types. - visitFieldDefinition(field, details) { - this.ensureFieldsWrapped(details.objectType); - field._requiredAuthRole = this.args.requires; - } - - ensureFieldsWrapped(objectType) { - // Mark the GraphQLObjectType object to avoid re-wrapping: - if (objectType._authFieldsWrapped) return; - objectType._authFieldsWrapped = true; - - const fields = objectType.getFields(); - - Object.keys(fields).forEach(fieldName => { - const field = fields[fieldName]; - const { resolve = defaultFieldResolver } = field; - field.resolve = async function (...args) { - // Get the required Role from the field first, falling back - // to the objectType if no Role is required by the field: - const requiredRole = field._requiredAuthRole || objectType._requiredAuthRole; - - if (!requiredRole) { - return resolve.apply(this, args); - } - - const context = args[2]; - const user = await getUser(context.headers.authToken); - if (!user.hasRole(requiredRole)) { - throw new Error('not authorized'); - } - - return resolve.apply(this, args); - }; - }); - } -} - -const schema = makeExecutableSchema({ - typeDefs, - schemaDirectives: { - auth: AuthDirective, - authorized: AuthDirective, - authenticated: AuthDirective, - }, -}); -``` - -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. - -### Enforcing value restrictions - -Suppose you want to enforce a maximum length for a string-valued field: - -```js -import { SchemaDirectiveVisitor } from '@graphql-tools/utils'; -import { makeExecutableSchema } from '@graphql-tools/schema'; -import { GraphQLScalarType, isNonNullType, isScalarType, GraphQLNonNull } from 'graphql'; - -const typeDefs = ` -directive @length(max: Int) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION - -type Query { - books: [Book] -} - -type Book { - title: String @length(max: 50) -} - -type Mutation { - createBook(book: BookInput): Book -} - -input BookInput { - title: String! @length(max: 50) -}`; - -class LengthDirective extends SchemaDirectiveVisitor { - visitInputFieldDefinition(field) { - this.wrapType(field); - } - - visitFieldDefinition(field) { - this.wrapType(field); - } - - // Replace field.type with a custom GraphQLScalarType that enforces the - // length restriction. - wrapType(field) { - if (isNonNullType(field.type) && isScalarType(field.type.ofType)) { - field.type = new GraphQLNonNull(new LimitedLengthType(field.type.ofType, this.args.max)); - } else if (isScalarType(field.type)) { - field.type = new LimitedLengthType(field.type, this.args.max); - } else { - throw new Error(`Not a scalar type: ${field.type}`); - } - } -} - -class LimitedLengthType extends GraphQLScalarType { - constructor(type, maxLength) { - super({ - name: `LengthAtMost${maxLength}`, - - // For more information about GraphQLScalar type (de)serialization, - // see the graphql-js implementation: - // https://github.com/graphql/graphql-js/blob/31ae8a8e8312/src/type/definition.js#L425-L446 - - serialize(value) { - value = type.serialize(value); - assert.isAtMost(value.length, maxLength); - return value; - }, - - parseValue(value) { - return type.parseValue(value); - }, - - parseLiteral(ast) { - return type.parseLiteral(ast); - }, - }); - } -} - -const schema = makeExecutableSchema({ - typeDefs, - schemaDirectives: { - length: LengthDirective, - }, -}); -``` - -### Synthesizing unique IDs - -Suppose your database uses incrementing IDs for each resource type, so IDs are not unique across all resource types. Here’s how you might synthesize a field called `uid` that combines the object type with various field values to produce an ID that’s unique across your schema: - -```js -import { GraphQLID } from 'graphql'; -import { createHash } from 'crypto'; -import { SchemaDirectiveVisitor } from '@graphql-tools/utils'; -import { makeExecutableSchema } from '@graphql-tools/schema'; - -const typeDefs = ` -directive @uniqueID( - # The name of the new ID field, "uid" by default: - name: String = "uid" - - # Which fields to include in the new ID: - from: [String] = ["id"] -) on OBJECT - -# Since this type just uses the default values of name and from, -# we don't have to pass any arguments to the directive: -type Location @uniqueID { - id: Int - address: String -} - -# This type uses both the person's name and the personID field, -# in addition to the "Person" type name, to construct the ID: -type Person @uniqueID(from: ["name", "personID"]) { - personID: Int - name: String -}`; - -class UniqueIdDirective extends SchemaDirectiveVisitor { - visitObject(type) { - const { name, from } = this.args; - const fields = type.getFields(); - if (name in fields) { - throw new Error(`Conflicting field name ${name}`); - } - fields[name] = { - name, - type: GraphQLID, - description: 'Unique ID', - args: [], - resolve(object) { - const hash = createHash('sha1'); - hash.update(type.name); - from.forEach(fieldName => { - hash.update(String(object[fieldName])); - }); - return hash.digest('hex'); - }, - }; - } -} - -const schema = makeExecutableSchema({ - typeDefs, - schemaDirectives: { - uniqueID: UniqueIdDirective, - }, -}); -``` - -## Declaring schema directives - -While the above examples should be sufficient to implement any `@directive` used in your schema, SDL syntax also supports declaring the names, argument types, default argument values, and permissible locations of any available directives: - -```js -directive @auth( - requires: Role = ADMIN, -) on OBJECT | FIELD_DEFINITION - -enum Role { - ADMIN - REVIEWER - USER - UNKNOWN -} - -type User @auth(requires: USER) { - name: String - banned: Boolean @auth(requires: ADMIN) - canPost: Boolean @auth(requires: REVIEWER) -} -``` - -This hypothetical `@auth` directive takes an argument named `requires` of type `Role`, which defaults to `ADMIN` if `@auth` is used without passing an explicit `requires` argument. The `@auth` directive can appear on an `OBJECT` like `User` to set a default access control for all `User` fields, and also on individual fields, to enforce field-specific `@auth` restrictions. - -Enforcing the requirements of the declaration is something a `SchemaDirectiveVisitor` implementation could do itself, in theory, but the SDL syntax is easier to read and write, and provides value even if you're not using the `SchemaDirectiveVisitor` abstraction. - -However, if you're implementing a reusable `SchemaDirectiveVisitor` for public consumption, you will probably not be the person writing the SDL syntax, so you may not have control over which directives the schema author decides to declare, and how. That's why a well-implemented, reusable `SchemaDirectiveVisitor` should consider overriding the `getDirectiveDeclaration` method: - -```typescript -import { SchemaDirectiveVisitor } from '@graphql-tools/utils'; -import { makeExecutableSchema } from '@graphql-tools/schema'; -import { - DirectiveLocation, - GraphQLDirective, - GraphQLEnumType, -} from "graphql"; - -class AuthDirective extends SchemaDirectiveVisitor { - public visitObject(object: GraphQLObjectType) {...} - public visitFieldDefinition(field: GraphQLField) {...} - - public static getDirectiveDeclaration( - directiveName: string, - schema: GraphQLSchema, - ): GraphQLDirective { - const previousDirective = schema.getDirective(directiveName); - if (previousDirective) { - // If a previous directive declaration exists in the schema, it may be - // better to modify it than to return a new GraphQLDirective object. - previousDirective.args.forEach(arg => { - if (arg.name === 'requires') { - // Lower the default minimum Role from ADMIN to REVIEWER. - arg.defaultValue = 'REVIEWER'; - } - }); - - return previousDirective; - } - - // If a previous directive with this name was not found in the schema, - // there are several options: - // - // 1. Construct a new GraphQLDirective (see below). - // 2. Throw an exception to force the client to declare the directive. - // 3. Return null, and forget about declaring this directive. - // - // All three are valid options, since the visitor will still work without - // any declared directives. In fact, unless you're publishing a directive - // implementation for public consumption, you can probably just ignore - // getDirectiveDeclaration altogether. - - return new GraphQLDirective({ - name: directiveName, - locations: [ - DirectiveLocation.OBJECT, - DirectiveLocation.FIELD_DEFINITION, - ], - args: { - requires: { - // Having the schema available here is important for obtaining - // references to existing type objects, such as the Role enum. - type: (schema.getType('Role') as GraphQLEnumType), - // Set the default minimum Role to REVIEWER. - defaultValue: 'REVIEWER', - } - }] - }); - } -} -``` - -Since the `getDirectiveDeclaration` method receives not only the name of the directive but also the `GraphQLSchema` object, it can modify and/or reuse previous declarations found in the schema, as an alternative to returning a totally new `GraphQLDirective` object. Either way, if the visitor returns a non-null `GraphQLDirective` from `getDirectiveDeclaration`, that declaration will be used to check arguments and permissible locations. diff --git a/website/docs/schema-directives.md b/website/docs/schema-directives.md index 36f3caed1f4..f9ced8a2d55 100644 --- a/website/docs/schema-directives.md +++ b/website/docs/schema-directives.md @@ -23,10 +23,6 @@ The possible applications of directive syntax are numerous: enforcing access per This document focuses on directives that appear in GraphQL _schemas_ (as opposed to queries) written in [Schema Definition Language](https://github.com/facebook/graphql/pull/90), or SDL for short. In the following sections, you will see how custom directives can be implemented and used to modify the structure and behavior of a GraphQL schema in ways that would not be possible using SDL syntax alone. -## (At least) two strategies - -Earlier versions of `graphql-tools` provides a class-based mechanism for directive-based schema modification. The documentation for the class-based version is [still available](/docs/legacy-schema-directives/), but the remainder of this document describes the newer functional mechanism. We believe the newer approach is easier to reason about, but older class-based schema directives are still supported. - ## Using schema directives 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. @@ -692,7 +688,7 @@ In theory, access to the query directives is available within the `info` resolve The `makeExecutableSchema` function also takes a `directiveResolvers` option that can be used for implementing certain kinds of `@directive`s on fields that have resolver functions. -The new abstraction is more general, since it can visit any kind of schema syntax, and do much more than just wrap resolver functions. However, the old `directiveResolvers` API has been [left in place](directive-resolvers) for backwards compatibility, though it is now implemented in terms of `mapSchema`: +The new abstraction is more general, since it can visit any kind of schema syntax, and do much more than just wrap resolver functions. However, the old `directiveResolvers` API has been left in place for backwards compatibility, though it is now implemented in terms of `mapSchema`: ```typescript export function attachDirectiveResolvers( diff --git a/website/sidebars.js b/website/sidebars.js index 10f25f94a8d..b614f8cd3dc 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -10,7 +10,6 @@ module.exports = { "mocking", "connectors", "schema-directives", - "directive-resolvers", "schema-delegation", "remote-schemas", "schema-wrapping", diff --git a/yarn.lock b/yarn.lock index 4e51053a40f..e49d91cde11 100644 --- a/yarn.lock +++ b/yarn.lock @@ -144,13 +144,6 @@ tslib "^1.10.0" zen-observable "^0.8.14" -"@ardatan/aggregate-error@0.0.6": - version "0.0.6" - resolved "https://registry.yarnpkg.com/@ardatan/aggregate-error/-/aggregate-error-0.0.6.tgz#fe6924771ea40fc98dc7a7045c2e872dc8527609" - integrity sha512-vyrkEHG1jrukmzTPtyWB4NLPauUw5bQeg4uhn8f+1SSynmrOcyvlb1GKQjjgoBzElLdfXCRYX8UnBlhklOHYRQ== - dependencies: - tslib "~2.0.1" - "@babel/code-frame@7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" @@ -1908,15 +1901,6 @@ dependencies: purgecss "^3.1.3" -"@graphql-tools/mock@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@graphql-tools/mock/-/mock-7.0.0.tgz#b43858f47fedfbf7d8bbbf7d33e6acb64b8b7da7" - integrity sha512-ShO8D9HudgnhqoWeKb3iejGtPV8elFqSO1U0O70g3FH3W/CBW2abXfuyodBUevXVGIjyqzfkNzVtpIE0qiOVVQ== - dependencies: - "@graphql-tools/schema" "^7.0.0" - "@graphql-tools/utils" "^7.0.0" - tslib "~2.0.1" - "@graphql-typed-document-node/core@^3.0.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.0.tgz#0eee6373e11418bfe0b5638f654df7a4ca6a3950" @@ -4113,7 +4097,7 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -camel-case@4.1.2, camel-case@^4.1.1: +camel-case@^4.1.1: version "4.1.2" resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a" integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw== @@ -12727,11 +12711,6 @@ tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@~2.3.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e" integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg== -tslib@~2.0.1: - version "2.0.3" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c" - integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ== - tslib@~2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c" From e2d41b290a058e9f1676af1bbf008555575670a6 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Mon, 21 Jun 2021 18:51:14 +0300 Subject: [PATCH 2/3] Fix TS --- packages/load/src/filter-document-kind.ts | 2 +- packages/loaders/code-file/src/index.ts | 4 +- packages/merge/src/merge-resolvers.ts | 48 +++++++------------ packages/merge/src/merge-schemas.ts | 4 +- packages/stitch/src/mergeCandidates.ts | 6 ++- packages/stitch/src/stitchSchemas.ts | 2 +- .../src/subschemaConfigTransforms/index.ts | 2 +- packages/stitch/src/types.ts | 2 +- packages/utils/src/get-directives.ts | 2 +- packages/utils/src/visitResult.ts | 2 +- 10 files changed, 30 insertions(+), 44 deletions(-) diff --git a/packages/load/src/filter-document-kind.ts b/packages/load/src/filter-document-kind.ts index 5f63ab4b9a1..aa49a3e155f 100644 --- a/packages/load/src/filter-document-kind.ts +++ b/packages/load/src/filter-document-kind.ts @@ -18,7 +18,7 @@ export const filterKind = (content: DocumentNode | undefined, filterKinds: null if (invalidDefinitions.length > 0) { invalidDefinitions.forEach(d => { - if (env.DEBUG) { + if (env['DEBUG']) { console.error(`Filtered document of kind ${d.kind} due to filter policy (${filterKinds.join(', ')})`); } }); diff --git a/packages/loaders/code-file/src/index.ts b/packages/loaders/code-file/src/index.ts index cc74ac21d40..cd88c9292d1 100644 --- a/packages/loaders/code-file/src/index.ts +++ b/packages/loaders/code-file/src/index.ts @@ -134,7 +134,7 @@ export class CodeFileLoader implements UniversalLoader { return parseGraphQLSDL(pointer, sdl, options); } } catch (e) { - if (env.DEBUG) { + if (env['DEBUG']) { console.error(`Failed to load schema from code file "${normalizedFilePath}": ${e.message}`); } errors.push(e); @@ -179,7 +179,7 @@ export class CodeFileLoader implements UniversalLoader { return parseGraphQLSDL(pointer, sdl, options); } } catch (e) { - if (env.DEBUG) { + if (env['DEBUG']) { console.error(`Failed to load schema from code file "${normalizedFilePath}": ${e.message}`); } errors.push(e); diff --git a/packages/merge/src/merge-resolvers.ts b/packages/merge/src/merge-resolvers.ts index 87e88db18c2..4c66d5c7365 100644 --- a/packages/merge/src/merge-resolvers.ts +++ b/packages/merge/src/merge-resolvers.ts @@ -1,7 +1,4 @@ -import { IResolvers, Maybe, mergeDeep } from '@graphql-tools/utils'; - -export type ResolversFactory = (...args: any[]) => IResolvers; -export type ResolversDefinition = IResolvers | ResolversFactory; +import { IResolvers, mergeDeep } from '@graphql-tools/utils'; /** * Additional options for merging resolvers @@ -39,12 +36,12 @@ export interface MergeResolversOptions { * const resolvers = mergeResolvers(resolversArray) * ``` */ -export function mergeResolvers>( - resolversDefinitions: Maybe, +export function mergeResolvers( + resolversDefinitions: IResolvers | IResolvers[], options?: MergeResolversOptions -): T { +): IResolvers { if (!resolversDefinitions || (Array.isArray(resolversDefinitions) && resolversDefinitions.length === 0)) { - return {} as T; + return {}; } if (!Array.isArray(resolversDefinitions)) { @@ -55,38 +52,25 @@ export function mergeResolvers return resolversDefinitions[0]; } - type TFactory = (...args: any[]) => T; - const resolversFactories = new Array(); - const resolvers = new Array(); + const resolvers = new Array>(); for (let resolversDefinition of resolversDefinitions) { if (Array.isArray(resolversDefinition)) { resolversDefinition = mergeResolvers(resolversDefinition); } - if (typeof resolversDefinition === 'function') { - resolversFactories.push(resolversDefinition as unknown as TFactory); - } else if (typeof resolversDefinition === 'object') { + if (typeof resolversDefinition === 'object') { resolvers.push(resolversDefinition); } } - let result: T = {} as T; - if (resolversFactories.length) { - result = ((...args: any[]) => { - const resultsOfFactories = resolversFactories.map(factory => factory(...args)); - return resolvers.concat(resultsOfFactories).reduce(mergeDeep, {}); - }) as any; - } else { - result = resolvers.reduce(mergeDeep, {} as T); - } - if (options && options.exclusions) { - for (const exclusion of options.exclusions) { - const [typeName, fieldName] = exclusion.split('.'); - if (!fieldName || fieldName === '*') { - delete result[typeName]; - } else if (result[typeName]) { - delete result[typeName][fieldName]; - } + const result = resolvers.reduce(mergeDeep, {}); + + options?.exclusions?.forEach(exclusion => { + const [typeName, fieldName] = exclusion.split('.'); + if (!fieldName || fieldName === '*') { + delete result[typeName]; + } else if (result[typeName]) { + delete result[typeName][fieldName]; } - } + }); return result; } diff --git a/packages/merge/src/merge-schemas.ts b/packages/merge/src/merge-schemas.ts index b841019c929..b20181e3dba 100644 --- a/packages/merge/src/merge-schemas.ts +++ b/packages/merge/src/merge-schemas.ts @@ -46,7 +46,7 @@ const defaultResolverValidationOptions: Partial = { * @param config Configuration object */ export function mergeSchemas(config: MergeSchemasConfig) { - const typeDefs = mergeTypeDefs([config.schemas, config.typeDefs], config); + const typeDefs = mergeTypeDefs([config.schemas, config.typeDefs || []], config); const extractedResolvers: IResolvers[] = []; const extractedExtensions: SchemaExtensions[] = []; for (const schema of config.schemas) { @@ -67,7 +67,7 @@ export function mergeSchemas(config: MergeSchemasConfig) { */ export async function mergeSchemasAsync(config: MergeSchemasConfig) { const [typeDefs, resolvers, extensions] = await Promise.all([ - mergeTypeDefs([config.schemas, config.typeDefs], config), + mergeTypeDefs([config.schemas, config.typeDefs || []], config), Promise.all(config.schemas.map(async schema => getResolversFromSchema(schema))).then(extractedResolvers => mergeResolvers([...extractedResolvers, ...ensureResolvers(config)], config) ), diff --git a/packages/stitch/src/mergeCandidates.ts b/packages/stitch/src/mergeCandidates.ts index 007005789f3..be4aab4eaf7 100644 --- a/packages/stitch/src/mergeCandidates.ts +++ b/packages/stitch/src/mergeCandidates.ts @@ -51,6 +51,7 @@ import { } from './mergeValidations'; import { isSubschemaConfig } from '@graphql-tools/delegate'; +import { Maybe } from '@graphql-tools/utils'; export function mergeCandidates>( typeName: string, @@ -523,8 +524,9 @@ function inputFieldConfigMapFromTypeCandidates>( candidates: Array>, typeMergingOptions?: TypeMergingOptions ): GraphQLInputFieldConfigMap { - const inputFieldConfigCandidatesMap: Record>> = - Object.create(null); + const inputFieldConfigCandidatesMap: Record>> = Object.create( + null + ); const fieldInclusionMap: Record = Object.create(null); candidates.forEach(candidate => { diff --git a/packages/stitch/src/stitchSchemas.ts b/packages/stitch/src/stitchSchemas.ts index 1b3bffc9320..c76c006c604 100644 --- a/packages/stitch/src/stitchSchemas.ts +++ b/packages/stitch/src/stitchSchemas.ts @@ -91,7 +91,7 @@ export function stitchSchemas>({ subschemas: transformedSubschemas, originalSubschemaMap, types, - typeDefs, + typeDefs: typeDefs || [], parseOptions, extensions, directiveMap, diff --git a/packages/stitch/src/subschemaConfigTransforms/index.ts b/packages/stitch/src/subschemaConfigTransforms/index.ts index ebf70699ab4..f9ea965783e 100644 --- a/packages/stitch/src/subschemaConfigTransforms/index.ts +++ b/packages/stitch/src/subschemaConfigTransforms/index.ts @@ -1,4 +1,4 @@ -import { SubschemaConfigTransform } from 'packages/graphql-tools/src'; +import { SubschemaConfigTransform } from '../types'; import { computedDirectiveTransformer } from './computedDirectiveTransformer'; export { computedDirectiveTransformer } from './computedDirectiveTransformer'; diff --git a/packages/stitch/src/types.ts b/packages/stitch/src/types.ts index 0d35a8af977..f7ebed4bcc7 100644 --- a/packages/stitch/src/types.ts +++ b/packages/stitch/src/types.ts @@ -11,7 +11,7 @@ import { GraphQLEnumValueConfig, GraphQLEnumType, } from 'graphql'; -import { TypeSource } from '@graphql-tools/utils'; +import { Maybe, TypeSource } from '@graphql-tools/utils'; import { Subschema, SubschemaConfig } from '@graphql-tools/delegate'; import { IExecutableSchemaDefinition } from '@graphql-tools/schema'; diff --git a/packages/utils/src/get-directives.ts b/packages/utils/src/get-directives.ts index 24668414f05..a8c21af9b6d 100644 --- a/packages/utils/src/get-directives.ts +++ b/packages/utils/src/get-directives.ts @@ -23,7 +23,7 @@ import { GraphQLEnumValueConfig, EnumValueDefinitionNode, } from 'graphql'; -import { Maybe } from 'packages/graphql-tools/src'; +import { Maybe } from '@graphql-tools/utils'; import { getArgumentValues } from './getArgumentValues'; diff --git a/packages/utils/src/visitResult.ts b/packages/utils/src/visitResult.ts index fc870ea29aa..3fc168b71ee 100644 --- a/packages/utils/src/visitResult.ts +++ b/packages/utils/src/visitResult.ts @@ -17,7 +17,7 @@ import { import { Request, GraphQLExecutionContext, ExecutionResult } from './Interfaces'; import { collectFields } from './collectFields'; -import { Maybe } from 'packages/graphql-tools/src'; +import { Maybe } from '@graphql-tools/utils'; export type ValueVisitor = (value: any) => any; From 894f9fc64d432e48cd1848ec6fcf98e86dc45547 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Mon, 21 Jun 2021 18:54:24 +0300 Subject: [PATCH 3/3] More --- packages/delegate/src/resolveExternalValue.ts | 3 +-- packages/merge/tests/extract-extensions-from-schema.spec.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/delegate/src/resolveExternalValue.ts b/packages/delegate/src/resolveExternalValue.ts index 5c7885ad39d..6262c046490 100644 --- a/packages/delegate/src/resolveExternalValue.ts +++ b/packages/delegate/src/resolveExternalValue.ts @@ -13,14 +13,13 @@ import { locatedError, } from 'graphql'; -import { AggregateError } from '@graphql-tools/utils'; +import { AggregateError, Maybe } from '@graphql-tools/utils'; import { StitchingInfo, SubschemaConfig } from './types'; import { annotateExternalObject, isExternalObject } from './externalObjects'; import { getFieldsNotInSubschema } from './getFieldsNotInSubschema'; import { mergeFields } from './mergeFields'; import { Subschema } from './Subschema'; -import { Maybe } from '@graphql-tools/utils'; export function resolveExternalValue( result: any, diff --git a/packages/merge/tests/extract-extensions-from-schema.spec.ts b/packages/merge/tests/extract-extensions-from-schema.spec.ts index 6e2cae3fe1a..4e360fa3f12 100644 --- a/packages/merge/tests/extract-extensions-from-schema.spec.ts +++ b/packages/merge/tests/extract-extensions-from-schema.spec.ts @@ -1,6 +1,6 @@ import { buildSchema, GraphQLSchema, printSchema, buildASTSchema, parse } from 'graphql'; import { assertGraphQLEnumType, assertGraphQLInputObjectType, assertGraphQLObjectType, assertGraphQLInterfaceType, assertGraphQLUnionType, assertGraphQLScalerType } from '../../testing/assertion'; -import { assertSome } from 'packages/utils/src/helpers'; +import { assertSome } from '@graphql-tools/utils'; import { extractExtensionsFromSchema, mergeExtensions, applyExtensions } from '../src/extensions' describe('extensions', () => {