From 7c72fae10955cd491d4d2c0333ea7135ccbe05a8 Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Sat, 9 Jan 2021 12:55:00 -0500 Subject: [PATCH 01/14] flatten updates. --- .gitignore | 1 + package.json | 18 +- packages/batch-delegate/package.json | 6 +- packages/batch-execute/package.json | 2 +- packages/delegate/package.json | 2 +- packages/delegate/src/types.ts | 3 +- packages/graphql-tag-pluck/CHANGELOG.md | 6 + packages/graphql-tag-pluck/package.json | 8 +- packages/graphql-tag-pluck/src/index.ts | 20 +- .../tests/graphql-tag-pluck.test.ts | 415 +++++++++ packages/graphql-tag-pluck/yarn.lock | 516 +++++++++++ packages/graphql-tools/package.json | 2 +- packages/import/package.json | 2 +- packages/links/package.json | 2 +- packages/load-files/package.json | 4 +- packages/load/package.json | 4 +- packages/loaders/apollo-engine/package.json | 2 +- packages/loaders/code-file/package.json | 2 +- packages/loaders/git/package.json | 2 +- packages/loaders/github/package.json | 2 +- packages/loaders/graphql-file/package.json | 2 +- packages/loaders/json-file/package.json | 2 +- packages/loaders/module/package.json | 2 +- packages/loaders/prisma/package.json | 6 +- .../prisma/src/prisma-yml/Environment.ts | 4 +- .../__snapshots__/Environment.test.ts.snap | 2 +- .../PrismaDefinition.test.ts.snap | 2 +- .../loaders/prisma/src/prisma-yml/yaml.ts | 2 +- packages/loaders/url/package.json | 2 +- packages/merge/package.json | 2 +- .../merge/src/typedefs-mergers/enum-values.ts | 2 +- packages/merge/src/typedefs-mergers/enum.ts | 2 +- packages/merge/src/typedefs-mergers/fields.ts | 2 +- packages/mock/package.json | 2 +- packages/node-require/package.json | 2 +- packages/optimize/package.json | 2 +- .../relay-operation-optimizer/package.json | 2 +- packages/resolvers-composition/package.json | 4 +- packages/schema/package.json | 2 +- packages/stitch/CHANGELOG.md | 20 + packages/stitch/package.json | 6 +- packages/stitch/src/isolateComputedFields.ts | 4 +- packages/stitch/src/mergeCandidates.ts | 198 +++- packages/stitch/src/selectionSetArgs.ts | 2 +- packages/stitch/src/stitchingInfo.ts | 16 +- packages/stitch/src/types.ts | 12 + packages/stitch/tests/mergeCanonical.test.ts | 253 ++++++ .../tests/typeMergingWithDirectives.test.ts | 4 +- .../tests/typeMergingWithExtensions.test.ts | 16 +- .../tests/typeMergingWithInterfaces.test.ts | 503 +++++++++++ packages/stitching-directives/CHANGELOG.md | 17 + packages/stitching-directives/package.json | 6 +- .../src/defaultStitchingDirectiveOptions.ts | 1 + .../src/stitchingDirectives.ts | 34 +- .../src/stitchingDirectivesTransformer.ts | 275 ++++-- packages/stitching-directives/src/types.ts | 1 + .../stitchingDirectivesTransformer.test.ts | 229 ++++- packages/utils/CHANGELOG.md | 8 + packages/utils/package.json | 4 +- packages/utils/src/selectionSets.ts | 5 +- packages/webpack-loader/package.json | 2 +- packages/wrap/package.json | 2 +- .../transformFilterInputObjectFields.test.ts | 69 ++ .../tests/transformFilterToSchema.test.ts | 109 +++ .../wrap/tests/transformFilterTypes.test.ts | 70 ++ .../wrap/tests/transformMapLeafValues.test.ts | 54 ++ .../transformRenameInputObjectFields.test.ts | 121 +++ .../tests/transformRenameRootTypes.test.ts | 61 ++ .../wrap/tests/transformRenameTypes.test.ts | 135 +++ .../transformTransformEnumValues.test.ts | 114 +++ .../wrap/tests/transformWrapQuery.test.ts | 139 +++ packages/wrap/tests/transforms.test.ts | 854 ------------------ scripts/build-api-docs.js | 263 +++--- scripts/typedoc-theme/theme.js | 34 + website/.gitignore | 3 +- website/docs/migration-from-import.md | 6 +- website/docs/schema-delegation.md | 8 +- website/docs/schema-wrapping.md | 358 ++++---- website/docs/stitch-combining-schemas.md | 27 +- website/docs/stitch-directives-sdl.md | 36 +- website/docs/stitch-schema-extensions.md | 2 +- website/docs/stitch-type-merging.md | 106 ++- website/docusaurus.config.js | 2 +- website/package.json | 4 +- website/sidebars.js | 46 + .../{sidebars.template.json => sidebars.json} | 0 yarn.lock | 619 ++++++++----- 87 files changed, 4288 insertions(+), 1635 deletions(-) create mode 100644 packages/graphql-tag-pluck/yarn.lock create mode 100644 packages/stitch/tests/mergeCanonical.test.ts create mode 100644 packages/stitch/tests/typeMergingWithInterfaces.test.ts create mode 100644 packages/wrap/tests/transformFilterInputObjectFields.test.ts create mode 100644 packages/wrap/tests/transformFilterToSchema.test.ts create mode 100644 packages/wrap/tests/transformFilterTypes.test.ts create mode 100644 packages/wrap/tests/transformMapLeafValues.test.ts create mode 100644 packages/wrap/tests/transformRenameInputObjectFields.test.ts create mode 100644 packages/wrap/tests/transformRenameRootTypes.test.ts create mode 100644 packages/wrap/tests/transformRenameTypes.test.ts create mode 100644 packages/wrap/tests/transformTransformEnumValues.test.ts create mode 100644 packages/wrap/tests/transformWrapQuery.test.ts create mode 100644 scripts/typedoc-theme/theme.js create mode 100644 website/sidebars.js rename website/{sidebars.template.json => sidebars.json} (100%) diff --git a/.gitignore b/.gitignore index c8941577293..1ee51b7fe45 100644 --- a/.gitignore +++ b/.gitignore @@ -67,6 +67,7 @@ build temp .idea .bob +.DS_Store test-results/ junit.xml diff --git a/package.json b/package.json index 09d243718f1..ad27598fc48 100644 --- a/package.json +++ b/package.json @@ -44,12 +44,12 @@ "devDependencies": { "patch-package": "6.2.2", "@changesets/cli": "2.12.0", - "@types/jest": "26.0.19", - "@types/node": "14.14.17", - "@typescript-eslint/eslint-plugin": "4.11.1", - "@typescript-eslint/parser": "4.11.1", + "@types/jest": "26.0.20", + "@types/node": "14.14.20", + "@typescript-eslint/eslint-plugin": "4.12.0", + "@typescript-eslint/parser": "4.12.0", "@ardatan/bob-the-bundler": "1.2.2", - "eslint": "7.16.0", + "eslint": "7.17.0", "eslint-config-prettier": "7.1.0", "eslint-config-standard": "16.0.2", "eslint-plugin-import": "2.22.1", @@ -57,16 +57,16 @@ "eslint-plugin-promise": "4.2.1", "eslint-plugin-standard": "5.0.0", "graphql": "15.4.0", - "graphql-helix": "1.2.0", + "graphql-helix": "1.2.1", "graphql-subscriptions": "1.1.0", - "husky": "4.3.6", + "husky": "4.3.7", "jest": "26.6.3", "lint-staged": "10.5.3", "nock": "13.0.5", "prettier": "2.2.1", "ts-jest": "26.4.4", - "typedoc": "0.20.5", - "typedoc-plugin-markdown": "3.2.1", + "typedoc": "0.20.13", + "typedoc-plugin-markdown": "3.4.0", "typescript": "4.1.3" }, "husky": { diff --git a/packages/batch-delegate/package.json b/packages/batch-delegate/package.json index 06de7bcf845..b22fa980a21 100644 --- a/packages/batch-delegate/package.json +++ b/packages/batch-delegate/package.json @@ -24,12 +24,12 @@ "dependencies": { "@graphql-tools/delegate": "^7.0.8", "dataloader": "2.0.0", - "tslib": "~2.0.1" + "tslib": "~2.1.0" }, "devDependencies": { "@graphql-tools/schema": "7.1.2", - "@graphql-tools/stitch": "7.1.6", - "@graphql-tools/utils": "7.2.3" + "@graphql-tools/stitch": "7.1.8", + "@graphql-tools/utils": "7.2.4" }, "publishConfig": { "access": "public", diff --git a/packages/batch-execute/package.json b/packages/batch-execute/package.json index 0d0805b0d5d..f5f7b138319 100644 --- a/packages/batch-execute/package.json +++ b/packages/batch-execute/package.json @@ -25,7 +25,7 @@ "@graphql-tools/utils": "^7.0.0", "dataloader": "2.0.0", "is-promise": "4.0.0", - "tslib": "~2.0.1" + "tslib": "~2.1.0" }, "publishConfig": { "access": "public", diff --git a/packages/delegate/package.json b/packages/delegate/package.json index c9ec778cfbf..c9053b1bab8 100644 --- a/packages/delegate/package.json +++ b/packages/delegate/package.json @@ -28,7 +28,7 @@ "@ardatan/aggregate-error": "0.0.6", "dataloader": "2.0.0", "is-promise": "4.0.0", - "tslib": "~2.0.1" + "tslib": "~2.1.0" }, "publishConfig": { "access": "public", diff --git a/packages/delegate/src/types.ts b/packages/delegate/src/types.ts index edc1ca3f238..2aa71649fcf 100644 --- a/packages/delegate/src/types.ts +++ b/packages/delegate/src/types.ts @@ -172,9 +172,10 @@ export interface SubschemaConfig { export interface MergedTypeConfig extends MergedTypeResolverOptions { selectionSet?: string; - fields?: Record; + fields?: Record; computedFields?: Record; key?: (originalResult: any) => K; + canonical?: boolean; resolve?: MergedTypeResolver; } diff --git a/packages/graphql-tag-pluck/CHANGELOG.md b/packages/graphql-tag-pluck/CHANGELOG.md index 9ab6939ccdd..a8bef401430 100644 --- a/packages/graphql-tag-pluck/CHANGELOG.md +++ b/packages/graphql-tag-pluck/CHANGELOG.md @@ -1,5 +1,11 @@ # @graphql-tools/graphql-tag-pluck +## 6.4.0 + +### Minor Changes + +- 76162cc8: feat(graphql-tag-pluck): vue 3 support + ## 6.3.0 ### Minor Changes diff --git a/packages/graphql-tag-pluck/package.json b/packages/graphql-tag-pluck/package.json index d9dc35b2e72..5e16407270c 100644 --- a/packages/graphql-tag-pluck/package.json +++ b/packages/graphql-tag-pluck/package.json @@ -1,6 +1,6 @@ { "name": "@graphql-tools/graphql-tag-pluck", - "version": "6.3.0", + "version": "6.4.0", "description": "Pluck graphql-tag template literals", "license": "MIT", "repository": { @@ -23,14 +23,14 @@ "@babel/traverse": "7.12.12", "@babel/types": "7.12.12", "@graphql-tools/utils": "^7.0.0", - "tslib": "~2.0.1" + "tslib": "~2.1.0" }, "optionalDependencies": { - "vue-template-compiler": "^2.6.12" + "@vue/compiler-sfc": "^3.0.4" }, "devDependencies": { "@types/babel__traverse": "7.11.0", - "vue-template-compiler": "2.6.12" + "@vue/compiler-sfc": "3.0.5" }, "publishConfig": { "access": "public", diff --git a/packages/graphql-tag-pluck/src/index.ts b/packages/graphql-tag-pluck/src/index.ts index 2736034015f..56c8856de50 100644 --- a/packages/graphql-tag-pluck/src/index.ts +++ b/packages/graphql-tag-pluck/src/index.ts @@ -107,10 +107,12 @@ export interface GraphQLTagPluckOptions { const supportedExtensions = ['.js', '.jsx', '.ts', '.tsx', '.flow', '.flow.js', '.flow.jsx', '.vue']; // tslint:disable-next-line: no-implicit-dependencies -function parseWithVue(vueTemplateCompiler: typeof import('vue-template-compiler'), fileData: string) { - const parsed = vueTemplateCompiler.parseComponent(fileData); +function parseWithVue(vueTemplateCompiler: typeof import('@vue/compiler-sfc'), fileData: string) { + const { descriptor } = vueTemplateCompiler.parse(fileData); - return parsed.script ? parsed.script.content : ''; + return descriptor.script || descriptor.scriptSetup + ? vueTemplateCompiler.compileScript(descriptor, { id: '' }).content + : ''; } /** @@ -214,20 +216,20 @@ const MissingVueTemplateCompilerError = new Error( Via NPM: - $ npm install vue-template-compiler + $ npm install @vue/compiler-sfc Via Yarn: - $ yarn add vue-template-compiler + $ yarn add @vue/compiler-sfc `) ); async function pluckVueFileScript(fileData: string) { // tslint:disable-next-line: no-implicit-dependencies - let vueTemplateCompiler: typeof import('vue-template-compiler'); + let vueTemplateCompiler: typeof import('@vue/compiler-sfc'); try { // tslint:disable-next-line: no-implicit-dependencies - vueTemplateCompiler = await import('vue-template-compiler'); + vueTemplateCompiler = await import('@vue/compiler-sfc'); } catch (e) { throw MissingVueTemplateCompilerError; } @@ -237,11 +239,11 @@ async function pluckVueFileScript(fileData: string) { function pluckVueFileScriptSync(fileData: string) { // tslint:disable-next-line: no-implicit-dependencies - let vueTemplateCompiler: typeof import('vue-template-compiler'); + let vueTemplateCompiler: typeof import('@vue/compiler-sfc'); try { // tslint:disable-next-line: no-implicit-dependencies - vueTemplateCompiler = require('vue-template-compiler'); + vueTemplateCompiler = require('@vue/compiler-sfc'); } catch (e) { throw MissingVueTemplateCompilerError; } diff --git a/packages/graphql-tag-pluck/tests/graphql-tag-pluck.test.ts b/packages/graphql-tag-pluck/tests/graphql-tag-pluck.test.ts index 705635f909c..ef4d3ef8a2e 100644 --- a/packages/graphql-tag-pluck/tests/graphql-tag-pluck.test.ts +++ b/packages/graphql-tag-pluck/tests/graphql-tag-pluck.test.ts @@ -339,6 +339,421 @@ describe('graphql-tag-pluck', () => { } `)); }); + it('should pluck graphql-tag template literals from .vue 3 JavaScript file', async () => { + const gqlString = await pluck('tmp-XXXXXX.vue', freeText(` + + + + + + `)); + + expect(gqlString).toEqual(freeText(` + query IndexQuery { + site { + siteMetadata { + title + } + } + } + `)); + }); + + it('should pluck graphql-tag template literals from .vue 3 TS/Pug/SCSS file', async () => { + const gqlString = await pluck('tmp-XXXXXX.vue', freeText(` + + + + + + `)); + + expect(gqlString).toEqual(freeText(` + query IndexQuery { + site { + siteMetadata { + title + } + } + } + `)); + }); + + it('should pluck graphql-tag template literals from .vue 3 setup sugar JavaScript file', async () => { + const gqlString = await pluck('tmp-XXXXXX.vue', freeText(` + + + + + + + `)); + + expect(gqlString).toEqual(freeText(` + query IndexQuery { + site { + siteMetadata { + title + } + } + } + `)); + }); + + it('should pluck graphql-tag template literals from .vue 3 setup sugar TS/Pug/SCSS file', async () => { + const gqlString = await pluck('tmp-XXXXXX.vue', freeText(` + + + + + + + `)); + + expect(gqlString).toEqual(freeText(` + query IndexQuery { + site { + siteMetadata { + title + } + } + } + `)); + }); + it('should pluck graphql-tag template literals from .vue 3 outside setup sugar JavaScript file', async () => { + const gqlString = await pluck('tmp-XXXXXX.vue', freeText(` + + + + + + + `)); + + expect(gqlString).toEqual(freeText(` + query IndexQuery { + site { + siteMetadata { + title + } + } + } + `)); + }); + + it('should pluck graphql-tag template literals from .vue 3 outside setup sugar TS/Pug/SCSS file', async () => { + const gqlString = await pluck('tmp-XXXXXX.vue', freeText(` + + + + + + + `)); + + expect(gqlString).toEqual(freeText(` + query IndexQuery { + site { + siteMetadata { + title + } + } + } + `)); + }); + + it('should pluck graphql-tag template literals from .vue 3 setup JavaScript file', async () => { + const gqlString = await pluck('tmp-XXXXXX.vue', freeText(` + + + + + + `)); + + expect(gqlString).toEqual(freeText(` + query IndexQuery { + site { + siteMetadata { + title + } + } + } + `)); + }); + + it('should pluck graphql-tag template literals from .vue 3 setup TS/Pug/SCSS file', async () => { + const gqlString = await pluck('tmp-XXXXXX.vue', freeText(` + + + + + + `)); + + expect(gqlString).toEqual(freeText(` + query IndexQuery { + site { + siteMetadata { + title + } + } + } + `)); + }); it('should pluck graphql-tag template literals from .tsx file with generic jsx elements', async () => { const gqlString = await pluck('tmp-XXXXXX.tsx', freeText(` diff --git a/packages/graphql-tag-pluck/yarn.lock b/packages/graphql-tag-pluck/yarn.lock new file mode 100644 index 00000000000..e0fe887d3b4 --- /dev/null +++ b/packages/graphql-tag-pluck/yarn.lock @@ -0,0 +1,516 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@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", "@babel/code-frame@^7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" + integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== + dependencies: + "@babel/highlight" "^7.10.4" + +"@babel/generator@^7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.11.tgz#98a7df7b8c358c9a37ab07a24056853016aba3af" + integrity sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA== + dependencies: + "@babel/types" "^7.12.11" + jsesc "^2.5.1" + source-map "^0.5.0" + +"@babel/helper-function-name@^7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz#1fd7738aee5dcf53c3ecff24f1da9c511ec47b42" + integrity sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA== + dependencies: + "@babel/helper-get-function-arity" "^7.12.10" + "@babel/template" "^7.12.7" + "@babel/types" "^7.12.11" + +"@babel/helper-get-function-arity@^7.12.10": + version "7.12.10" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz#b158817a3165b5faa2047825dfa61970ddcc16cf" + integrity sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag== + dependencies: + "@babel/types" "^7.12.10" + +"@babel/helper-split-export-declaration@^7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz#1b4cc424458643c47d37022223da33d76ea4603a" + integrity sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g== + dependencies: + "@babel/types" "^7.12.11" + +"@babel/helper-validator-identifier@^7.10.4", "@babel/helper-validator-identifier@^7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" + integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== + +"@babel/highlight@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" + integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/parser@7.12.11", "@babel/parser@^7.12.0", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.11.tgz#9ce3595bcd74bc5c466905e86c535b8b25011e79" + integrity sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg== + +"@babel/template@^7.12.7": + version "7.12.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc" + integrity sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/parser" "^7.12.7" + "@babel/types" "^7.12.7" + +"@babel/traverse@7.12.12": + version "7.12.12" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.12.tgz#d0cd87892704edd8da002d674bc811ce64743376" + integrity sha512-s88i0X0lPy45RrLM8b9mz8RPH5FqO9G9p7ti59cToE44xFm1Q+Pjh5Gq4SXBbtb88X7Uy7pexeqRIQDDMNkL0w== + dependencies: + "@babel/code-frame" "^7.12.11" + "@babel/generator" "^7.12.11" + "@babel/helper-function-name" "^7.12.11" + "@babel/helper-split-export-declaration" "^7.12.11" + "@babel/parser" "^7.12.11" + "@babel/types" "^7.12.12" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.19" + +"@babel/types@7.12.12", "@babel/types@^7.12.0", "@babel/types@^7.12.10", "@babel/types@^7.12.11", "@babel/types@^7.12.12", "@babel/types@^7.12.7", "@babel/types@^7.3.0": + version "7.12.12" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.12.tgz#4608a6ec313abbd87afa55004d373ad04a96c299" + integrity sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ== + dependencies: + "@babel/helper-validator-identifier" "^7.12.11" + lodash "^4.17.19" + to-fast-properties "^2.0.0" + +"@graphql-tools/utils@^7.0.0": + version "7.2.3" + resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-7.2.3.tgz#4bb2ae0bef62df1f342f2a769434fbb105dd0d84" + integrity sha512-9MvSKeo+8DM72706FvrUP8figQjRzSwBswWrXviyWyt3wSkk6MU2cURQKfMpc0I6nswZvkDSqYoQQ/6mazoXxA== + dependencies: + "@ardatan/aggregate-error" "0.0.6" + camel-case "4.1.2" + tslib "~2.0.1" + +"@types/babel__traverse@7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.11.0.tgz#b9a1efa635201ba9bc850323a8793ee2d36c04a0" + integrity sha512-kSjgDMZONiIfSH1Nxcr5JIRMwUetDki63FSQfpTCz8ogF3Ulqm8+mr5f78dUYs6vMiB6gBusQqfQmBvHZj/lwg== + dependencies: + "@babel/types" "^7.3.0" + +"@vue/compiler-core@3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.0.4.tgz#0122aca6eada4cb28b39ed930af917444755e330" + integrity sha512-snpMICsbWTZqBFnPB03qr4DtiSxVYfDF3DvbDSkN9Z9NTM8Chl8E/lYhKBSsvauq91DAWAh8PU3lr9vrLyQsug== + dependencies: + "@babel/parser" "^7.12.0" + "@babel/types" "^7.12.0" + "@vue/shared" "3.0.4" + estree-walker "^2.0.1" + source-map "^0.6.1" + +"@vue/compiler-dom@3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.0.4.tgz#834fd4b15c5698cf9f4505c2bfbccca058a843eb" + integrity sha512-FOxbHBIkkGjYQeTz1DlXQjS1Ms8EPXQWsdTdTPeohoS0KzCz6RiOjiAG+jLtMi6Nr5GX2h0TlCvcnI8mcsicFQ== + dependencies: + "@vue/compiler-core" "3.0.4" + "@vue/shared" "3.0.4" + +"@vue/compiler-sfc@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.0.4.tgz#2119fe1e68d2c268aafa20461c82c139a9adf8e0" + integrity sha512-brDn6HTuK6R3oBCjtMPPsIpyJEZFinlnxjtBXww/goFJOJBAU9CrsdegwyZItNnixCFUIg4CLv4Nj1Eg/eKlfg== + dependencies: + "@babel/parser" "^7.12.0" + "@babel/types" "^7.12.0" + "@vue/compiler-core" "3.0.4" + "@vue/compiler-dom" "3.0.4" + "@vue/compiler-ssr" "3.0.4" + "@vue/shared" "3.0.4" + consolidate "^0.16.0" + estree-walker "^2.0.1" + hash-sum "^2.0.0" + lru-cache "^5.1.1" + magic-string "^0.25.7" + merge-source-map "^1.1.0" + postcss "^7.0.32" + postcss-modules "^3.2.2" + postcss-selector-parser "^6.0.4" + source-map "^0.6.1" + +"@vue/compiler-ssr@3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.0.4.tgz#ccbd1f55734d51d1402fad825ac102002a7a07c7" + integrity sha512-4aYWQEL4+LS4+D44K9Z7xMOWMEjBsz4Li9nMcj2rxRQ35ewK6uFPodvs6ORP60iBDSkwUFZoldFlNemQlu1BFw== + dependencies: + "@vue/compiler-dom" "3.0.4" + "@vue/shared" "3.0.4" + +"@vue/shared@3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.0.4.tgz#6dc50f593bdfdeaa6183d1dbc15e2d45e7c6b8b3" + integrity sha512-Swfbz31AaMX48CpFl+YmIrqOH9MgJMTrltG9e26A4ZxYx9LjGuMV+41WnxFzS3Bc9nbrc6sDPM37G6nIT8NJSg== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + +bluebird@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +camel-case@4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a" + integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw== + dependencies: + pascal-case "^3.1.2" + tslib "^2.0.3" + +chalk@^2.0.0, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +consolidate@^0.16.0: + version "0.16.0" + resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.16.0.tgz#a11864768930f2f19431660a65906668f5fbdc16" + integrity sha512-Nhl1wzCslqXYTJVDyJCu3ODohy9OfBMB5uD2BiBTzd7w+QY0lBzafkR8y8755yMYHAaMD4NuzbAw03/xzfw+eQ== + dependencies: + bluebird "^3.7.2" + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +debug@^4.1.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" + integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== + dependencies: + ms "2.1.2" + +emojis-list@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" + integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +estree-walker@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + +generic-names@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/generic-names/-/generic-names-2.0.1.tgz#f8a378ead2ccaa7a34f0317b05554832ae41b872" + integrity sha512-kPCHWa1m9wGG/OwQpeweTwM/PYiQLrUIxXbt/P4Nic3LbGjCP0YwrALHW1uNLKZ0LIMg+RF+XRlj2ekT9ZlZAQ== + dependencies: + loader-utils "^1.1.0" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +hash-sum@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-2.0.0.tgz#81d01bb5de8ea4a214ad5d6ead1b523460b0b45a" + integrity sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg== + +icss-replace-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" + integrity sha1-Bupvg2ead0njhs/h/oEq5dsiPe0= + +icss-utils@^4.0.0, icss-utils@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467" + integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA== + dependencies: + postcss "^7.0.14" + +indexes-of@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" + integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + +loader-utils@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" + integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^1.0.1" + +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= + +lodash@^4.17.19: + version "4.17.20" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" + integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== + +lower-case@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" + integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== + dependencies: + tslib "^2.0.3" + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +magic-string@^0.25.7: + version "0.25.7" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" + integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== + dependencies: + sourcemap-codec "^1.4.4" + +merge-source-map@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646" + integrity sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw== + dependencies: + source-map "^0.6.1" + +minimist@^1.2.0: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +no-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" + integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== + dependencies: + lower-case "^2.0.2" + tslib "^2.0.3" + +pascal-case@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb" + integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + +postcss-modules-extract-imports@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e" + integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ== + dependencies: + postcss "^7.0.5" + +postcss-modules-local-by-default@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz#bb14e0cc78279d504dbdcbfd7e0ca28993ffbbb0" + integrity sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw== + dependencies: + icss-utils "^4.1.1" + postcss "^7.0.32" + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.1.0" + +postcss-modules-scope@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee" + integrity sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ== + dependencies: + postcss "^7.0.6" + postcss-selector-parser "^6.0.0" + +postcss-modules-values@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10" + integrity sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg== + dependencies: + icss-utils "^4.0.0" + postcss "^7.0.6" + +postcss-modules@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/postcss-modules/-/postcss-modules-3.2.2.tgz#ee390de0f9f18e761e1778dfb9be26685c02c51f" + integrity sha512-JQ8IAqHELxC0N6tyCg2UF40pACY5oiL6UpiqqcIFRWqgDYO8B0jnxzoQ0EOpPrWXvcpu6BSbQU/3vSiq7w8Nhw== + dependencies: + generic-names "^2.0.1" + icss-replace-symbols "^1.1.0" + lodash.camelcase "^4.3.0" + postcss "^7.0.32" + postcss-modules-extract-imports "^2.0.0" + postcss-modules-local-by-default "^3.0.2" + postcss-modules-scope "^2.2.0" + postcss-modules-values "^3.0.0" + string-hash "^1.1.1" + +postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: + version "6.0.4" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3" + integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw== + dependencies: + cssesc "^3.0.0" + indexes-of "^1.0.1" + uniq "^1.0.1" + util-deprecate "^1.0.2" + +postcss-value-parser@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" + integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== + +postcss@^7.0.14, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: + version "7.0.35" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24" + integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg== + dependencies: + chalk "^2.4.2" + source-map "^0.6.1" + supports-color "^6.1.0" + +source-map@^0.5.0: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + +source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +sourcemap-codec@^1.4.4: + version "1.4.8" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + +string-hash@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b" + integrity sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs= + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + dependencies: + has-flag "^3.0.0" + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + +tslib@^2.0.3, 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.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" + integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== + +uniq@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" + integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= + +util-deprecate@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== diff --git a/packages/graphql-tools/package.json b/packages/graphql-tools/package.json index cffae72d4ac..eb1f15c57cd 100644 --- a/packages/graphql-tools/package.json +++ b/packages/graphql-tools/package.json @@ -47,6 +47,6 @@ "@graphql-tools/stitch": "^7.0.1", "@graphql-tools/utils": "^7.0.1", "@graphql-tools/wrap": "^7.0.0", - "tslib": "~2.0.1" + "tslib": "~2.1.0" } } diff --git a/packages/import/package.json b/packages/import/package.json index e3f421cb1d5..88338bc5003 100644 --- a/packages/import/package.json +++ b/packages/import/package.json @@ -25,6 +25,6 @@ }, "dependencies": { "resolve-from": "5.0.0", - "tslib": "~2.0.1" + "tslib": "~2.1.0" } } diff --git a/packages/links/package.json b/packages/links/package.json index 027b0058b0e..a19a8743aa5 100644 --- a/packages/links/package.json +++ b/packages/links/package.json @@ -41,7 +41,7 @@ "cross-fetch": "3.0.6", "form-data": "3.0.0", "is-promise": "4.0.0", - "tslib": "~2.0.1" + "tslib": "~2.1.0" }, "publishConfig": { "access": "public", diff --git a/packages/load-files/package.json b/packages/load-files/package.json index ca5676c8be9..6a17ef33240 100644 --- a/packages/load-files/package.json +++ b/packages/load-files/package.json @@ -20,9 +20,9 @@ "graphql": "^14.0.0 || ^15.0.0" }, "dependencies": { - "globby": "11.0.1", + "globby": "11.0.2", "unixify": "1.0.0", - "tslib": "~2.0.1" + "tslib": "~2.1.0" }, "publishConfig": { "access": "public", diff --git a/packages/load/package.json b/packages/load/package.json index 7510721147e..2482631e900 100644 --- a/packages/load/package.json +++ b/packages/load/package.json @@ -28,11 +28,11 @@ "dependencies": { "@graphql-tools/utils": "^7.0.0", "@graphql-tools/merge": "^6.2.5", - "globby": "11.0.1", + "globby": "11.0.2", "import-from": "3.0.0", "is-glob": "4.0.1", "p-limit": "3.1.0", - "tslib": "~2.0.1", + "tslib": "~2.1.0", "unixify": "1.0.0", "valid-url": "1.0.9" }, diff --git a/packages/loaders/apollo-engine/package.json b/packages/loaders/apollo-engine/package.json index 826949c2585..1dfdefc8852 100644 --- a/packages/loaders/apollo-engine/package.json +++ b/packages/loaders/apollo-engine/package.json @@ -22,7 +22,7 @@ "dependencies": { "@graphql-tools/utils": "^7.0.0", "cross-fetch": "3.0.6", - "tslib": "~2.0.1" + "tslib": "~2.1.0" }, "publishConfig": { "access": "public", diff --git a/packages/loaders/code-file/package.json b/packages/loaders/code-file/package.json index a6692f71329..d318587c62c 100644 --- a/packages/loaders/code-file/package.json +++ b/packages/loaders/code-file/package.json @@ -22,7 +22,7 @@ "dependencies": { "@graphql-tools/utils": "^7.0.0", "@graphql-tools/graphql-tag-pluck": "^6.2.6", - "tslib": "~2.0.1" + "tslib": "~2.1.0" }, "publishConfig": { "access": "public", diff --git a/packages/loaders/git/package.json b/packages/loaders/git/package.json index cd85359a12c..f934586b028 100644 --- a/packages/loaders/git/package.json +++ b/packages/loaders/git/package.json @@ -22,7 +22,7 @@ "dependencies": { "@graphql-tools/utils": "^7.0.0", "@graphql-tools/graphql-tag-pluck": "^6.2.6", - "tslib": "~2.0.1" + "tslib": "~2.1.0" }, "publishConfig": { "access": "public", diff --git a/packages/loaders/github/package.json b/packages/loaders/github/package.json index 03f3ff753aa..f42520a091d 100644 --- a/packages/loaders/github/package.json +++ b/packages/loaders/github/package.json @@ -23,7 +23,7 @@ "@graphql-tools/utils": "^7.0.0", "@graphql-tools/graphql-tag-pluck": "^6.2.6", "cross-fetch": "3.0.6", - "tslib": "~2.0.1" + "tslib": "~2.1.0" }, "publishConfig": { "access": "public", diff --git a/packages/loaders/graphql-file/package.json b/packages/loaders/graphql-file/package.json index b55e6b9d8b4..8018de385a9 100644 --- a/packages/loaders/graphql-file/package.json +++ b/packages/loaders/graphql-file/package.json @@ -25,7 +25,7 @@ "dependencies": { "@graphql-tools/import": "^6.2.5", "@graphql-tools/utils": "^7.0.0", - "tslib": "~2.0.1" + "tslib": "~2.1.0" }, "publishConfig": { "access": "public", diff --git a/packages/loaders/json-file/package.json b/packages/loaders/json-file/package.json index 17fc0085252..f7fb4de8ace 100644 --- a/packages/loaders/json-file/package.json +++ b/packages/loaders/json-file/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@graphql-tools/utils": "^7.0.0", - "tslib": "~2.0.1" + "tslib": "~2.1.0" }, "publishConfig": { "access": "public", diff --git a/packages/loaders/module/package.json b/packages/loaders/module/package.json index 8f5b3ac859b..bce0e30b60a 100644 --- a/packages/loaders/module/package.json +++ b/packages/loaders/module/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@graphql-tools/utils": "^7.0.0", - "tslib": "~2.0.1" + "tslib": "~2.1.0" }, "publishConfig": { "access": "public", diff --git a/packages/loaders/prisma/package.json b/packages/loaders/prisma/package.json index cc6de7862e6..509918978f5 100644 --- a/packages/loaders/prisma/package.json +++ b/packages/loaders/prisma/package.json @@ -23,7 +23,7 @@ "@graphql-tools/url-loader": "^6.3.1", "@graphql-tools/utils": "^7.0.0", "@types/http-proxy-agent": "^2.0.2", - "@types/js-yaml": "^3.12.5", + "@types/js-yaml": "^4.0.0", "@types/json-stable-stringify": "^1.0.32", "@types/jsonwebtoken": "^8.5.0", "ajv": "^6.12.6", @@ -35,13 +35,13 @@ "http-proxy-agent": "^4.0.1", "https-proxy-agent": "^5.0.0", "isomorphic-fetch": "^3.0.0", - "js-yaml": "^3.14.0", + "js-yaml": "^4.0.0", "json-stable-stringify": "^1.0.1", "jsonwebtoken": "^8.5.1", "lodash": "^4.17.20", "replaceall": "^0.1.6", "scuid": "^1.1.0", - "tslib": "~2.0.1", + "tslib": "~2.1.0", "yaml-ast-parser": "^0.0.43" }, "publishConfig": { diff --git a/packages/loaders/prisma/src/prisma-yml/Environment.ts b/packages/loaders/prisma/src/prisma-yml/Environment.ts index 8f168df0a42..a5dd773ee5a 100644 --- a/packages/loaders/prisma/src/prisma-yml/Environment.ts +++ b/packages/loaders/prisma/src/prisma-yml/Environment.ts @@ -183,7 +183,7 @@ export class Environment { clusters: this.getLocalClusterConfig(), }; // parse & stringify to rm undefined for yaml parser - const rcString = yaml.safeDump(JSON.parse(JSON.stringify(rc))); + const rcString = yaml.dump(JSON.parse(JSON.stringify(rc))); fs.writeFileSync(this.rcPath, rcString); } @@ -216,7 +216,7 @@ export class Environment { if (file) { let content; try { - content = yaml.safeLoad(file); + content = yaml.load(file); } catch (e) { throw new Error(`Yaml parsing error in ${filePath}: ${e.message}`); } diff --git a/packages/loaders/prisma/src/prisma-yml/__snapshots__/Environment.test.ts.snap b/packages/loaders/prisma/src/prisma-yml/__snapshots__/Environment.test.ts.snap index 0f4afe5abab..e4ee14de877 100644 --- a/packages/loaders/prisma/src/prisma-yml/__snapshots__/Environment.test.ts.snap +++ b/packages/loaders/prisma/src/prisma-yml/__snapshots__/Environment.test.ts.snap @@ -97,7 +97,7 @@ Array [ exports[`Environment persists .prisma correctly 1`] = ` "clusters: cluster: - host: 'http://localhost:60000' + host: http://localhost:60000 clusterSecret: '' " `; diff --git a/packages/loaders/prisma/src/prisma-yml/__snapshots__/PrismaDefinition.test.ts.snap b/packages/loaders/prisma/src/prisma-yml/__snapshots__/PrismaDefinition.test.ts.snap index 6543283d69e..1e83a95aed8 100644 --- a/packages/loaders/prisma/src/prisma-yml/__snapshots__/PrismaDefinition.test.ts.snap +++ b/packages/loaders/prisma/src/prisma-yml/__snapshots__/PrismaDefinition.test.ts.snap @@ -97,7 +97,7 @@ Array [ exports[`Environment persists .prisma correctly 1`] = ` "clusters: cluster: - host: 'http://localhost:60000' + host: http://localhost:60000 clusterSecret: '' " `; diff --git a/packages/loaders/prisma/src/prisma-yml/yaml.ts b/packages/loaders/prisma/src/prisma-yml/yaml.ts index 00edaace255..bd32816f575 100644 --- a/packages/loaders/prisma/src/prisma-yml/yaml.ts +++ b/packages/loaders/prisma/src/prisma-yml/yaml.ts @@ -29,7 +29,7 @@ export async function readDefinition( throw new Error(`${filePath} could not be found.`); } const file = fs.readFileSync(filePath, 'utf-8'); - const json = yaml.safeLoad(file) as PrismaDefinition; + const json = yaml.load(file) as PrismaDefinition; // we need this copy because populateJson runs inplace const jsonCopy = { ...json }; diff --git a/packages/loaders/url/package.json b/packages/loaders/url/package.json index 4c6843f4b94..ecd77479a3a 100644 --- a/packages/loaders/url/package.json +++ b/packages/loaders/url/package.json @@ -44,7 +44,7 @@ "is-promise": "4.0.0", "isomorphic-ws": "4.0.1", "isomorphic-form-data": "2.0.0", - "tslib": "~2.0.1", + "tslib": "~2.1.0", "valid-url": "1.0.9", "graphql-ws": "3.2.0", "ws": "7.4.2", diff --git a/packages/merge/package.json b/packages/merge/package.json index d3301b4712c..a88a935cb13 100644 --- a/packages/merge/package.json +++ b/packages/merge/package.json @@ -25,7 +25,7 @@ "dependencies": { "@graphql-tools/schema": "^7.0.0", "@graphql-tools/utils": "^7.0.0", - "tslib": "~2.0.1" + "tslib": "~2.1.0" }, "publishConfig": { "access": "public", diff --git a/packages/merge/src/typedefs-mergers/enum-values.ts b/packages/merge/src/typedefs-mergers/enum-values.ts index 64d5e977bab..57b7750182a 100644 --- a/packages/merge/src/typedefs-mergers/enum-values.ts +++ b/packages/merge/src/typedefs-mergers/enum-values.ts @@ -6,7 +6,7 @@ import { compareNodes } from '@graphql-tools/utils'; export function mergeEnumValues( first: ReadonlyArray, second: ReadonlyArray, - config: Config + config?: Config ): EnumValueDefinitionNode[] { const enumValueMap = new Map(); for (const firstValue of first) { diff --git a/packages/merge/src/typedefs-mergers/enum.ts b/packages/merge/src/typedefs-mergers/enum.ts index 8ea93c9eca6..1670a3c202c 100644 --- a/packages/merge/src/typedefs-mergers/enum.ts +++ b/packages/merge/src/typedefs-mergers/enum.ts @@ -18,7 +18,7 @@ export function mergeEnum( : 'EnumTypeExtension', loc: e1.loc, directives: mergeDirectives(e1.directives, e2.directives, config), - values: mergeEnumValues(e1.values, e2.values, config), + values: mergeEnumValues(e2.values, e1.values, config), } as any; } diff --git a/packages/merge/src/typedefs-mergers/fields.ts b/packages/merge/src/typedefs-mergers/fields.ts index 6591156edb8..7f6d49d3373 100644 --- a/packages/merge/src/typedefs-mergers/fields.ts +++ b/packages/merge/src/typedefs-mergers/fields.ts @@ -26,7 +26,7 @@ export function mergeFields, f2: ReadonlyArray, - config: Config + config?: Config ): T[] { const result: T[] = [...f2]; diff --git a/packages/mock/package.json b/packages/mock/package.json index e724c5951a1..d019e7b2e9f 100644 --- a/packages/mock/package.json +++ b/packages/mock/package.json @@ -24,7 +24,7 @@ "dependencies": { "@graphql-tools/schema": "^7.0.0", "@graphql-tools/utils": "^7.0.0", - "tslib": "~2.0.1" + "tslib": "~2.1.0" }, "devDependencies": { "casual": "1.6.2" diff --git a/packages/node-require/package.json b/packages/node-require/package.json index b7039fed819..5e96c628761 100644 --- a/packages/node-require/package.json +++ b/packages/node-require/package.json @@ -24,7 +24,7 @@ "dependencies": { "@graphql-tools/load": "^6.2.4", "@graphql-tools/graphql-file-loader": "^6.2.4", - "tslib": "~2.0.1" + "tslib": "~2.1.0" }, "publishConfig": { "access": "public", diff --git a/packages/optimize/package.json b/packages/optimize/package.json index 4f21b380bee..eef1261bd76 100644 --- a/packages/optimize/package.json +++ b/packages/optimize/package.json @@ -22,7 +22,7 @@ "input": "./src/index.ts" }, "dependencies": { - "tslib": "~2.0.1" + "tslib": "~2.1.0" }, "publishConfig": { "access": "public", diff --git a/packages/relay-operation-optimizer/package.json b/packages/relay-operation-optimizer/package.json index bf6d10aeeec..5ce5ae8200d 100644 --- a/packages/relay-operation-optimizer/package.json +++ b/packages/relay-operation-optimizer/package.json @@ -33,7 +33,7 @@ "dependencies": { "@graphql-tools/utils": "^7.1.0", "relay-compiler": "10.1.2", - "tslib": "~2.0.1" + "tslib": "~2.1.0" }, "devDependencies": { "@types/relay-compiler": "8.0.0" diff --git a/packages/resolvers-composition/package.json b/packages/resolvers-composition/package.json index 33c72b9094d..0c059ccc8d4 100644 --- a/packages/resolvers-composition/package.json +++ b/packages/resolvers-composition/package.json @@ -20,12 +20,12 @@ "graphql": "^14.0.0 || ^15.0.0" }, "devDependencies": { - "@types/lodash": "4.14.166" + "@types/lodash": "4.14.167" }, "dependencies": { "@graphql-tools/utils": "^7.0.0", "lodash": "4.17.20", - "tslib": "~2.0.1" + "tslib": "~2.1.0" }, "publishConfig": { "access": "public", diff --git a/packages/schema/package.json b/packages/schema/package.json index 06643f6179e..dd1454ca0f2 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -26,7 +26,7 @@ }, "dependencies": { "@graphql-tools/utils": "^7.1.2", - "tslib": "~2.0.1" + "tslib": "~2.1.0" }, "publishConfig": { "access": "public", diff --git a/packages/stitch/CHANGELOG.md b/packages/stitch/CHANGELOG.md index f822a84352b..1cb00ad75e7 100644 --- a/packages/stitch/CHANGELOG.md +++ b/packages/stitch/CHANGELOG.md @@ -1,5 +1,25 @@ # @graphql-tools/stitch +## 7.1.8 + +### Patch Changes + +- 6e50d9fc: enhance(stitching-directives): use keyField + + When using simple keys, i.e. when using the keyField argument to `@merge`, the keyField can be added implicitly to the types's key. In most cases, therefore, `@key` should not be required at all. + +- Updated dependencies [6e50d9fc] + - @graphql-tools/utils@7.2.4 + +## 7.1.7 + +### Patch Changes + +- 06a6acbe: fix(stitch): computed fields should work with merge resolvers that return abstract types + + see: https://github.com/ardatan/graphql-tools/pull/2432#issuecomment-753729191 + and: https://github.com/gmac/schema-stitching-handbook/pull/17 + ## 7.1.6 ### Patch Changes diff --git a/packages/stitch/package.json b/packages/stitch/package.json index 32820af5048..46129bc0dc5 100644 --- a/packages/stitch/package.json +++ b/packages/stitch/package.json @@ -1,6 +1,6 @@ { "name": "@graphql-tools/stitch", - "version": "7.1.6", + "version": "7.1.8", "description": "A set of utils for faster development of GraphQL tools", "repository": { "type": "git", @@ -29,10 +29,10 @@ "@graphql-tools/delegate": "^7.0.8", "@graphql-tools/merge": "^6.2.6", "@graphql-tools/schema": "^7.1.2", - "@graphql-tools/utils": "^7.1.6", + "@graphql-tools/utils": "^7.2.4", "@graphql-tools/wrap": "^7.0.3", "is-promise": "4.0.0", - "tslib": "~2.0.1" + "tslib": "~2.1.0" }, "publishConfig": { "access": "public", diff --git a/packages/stitch/src/isolateComputedFields.ts b/packages/stitch/src/isolateComputedFields.ts index 4f4cbc74c7d..a4b66a85a52 100644 --- a/packages/stitch/src/isolateComputedFields.ts +++ b/packages/stitch/src/isolateComputedFields.ts @@ -80,7 +80,7 @@ function filterBaseSubschema( Object.keys(filteredSchema.getTypeMap()).forEach(typeName => { const type = filteredSchema.getType(typeName); if (isObjectType(type) || isInterfaceType(type)) { - filteredFields[typeName] = {}; + filteredFields[typeName] = { __typename: true }; const fieldMap = type.getFields(); Object.keys(fieldMap).forEach(fieldName => { filteredFields[typeName][fieldName] = true; @@ -151,7 +151,7 @@ function filterIsolatedSubschema(subschemaConfig: SubschemaConfig): SubschemaCon Object.keys(filteredSchema.getTypeMap()).forEach(typeName => { const type = filteredSchema.getType(typeName); if (isObjectType(type) || isInterfaceType(type)) { - filteredFields[typeName] = {}; + filteredFields[typeName] = { __typename: true }; const fieldMap = type.getFields(); Object.keys(fieldMap).forEach(fieldName => { filteredFields[typeName][fieldName] = true; diff --git a/packages/stitch/src/mergeCandidates.ts b/packages/stitch/src/mergeCandidates.ts index 5810725bd87..51e11accee4 100644 --- a/packages/stitch/src/mergeCandidates.ts +++ b/packages/stitch/src/mergeCandidates.ts @@ -10,10 +10,13 @@ import { isUnionType, isEnumType, isInputObjectType, + GraphQLFieldConfig, GraphQLFieldConfigMap, GraphQLInputObjectType, + GraphQLInputFieldConfig, GraphQLInputFieldConfigMap, ObjectTypeDefinitionNode, + FieldDefinitionNode, InputObjectTypeDefinitionNode, InterfaceTypeDefinitionNode, UnionTypeDefinitionNode, @@ -33,8 +36,10 @@ import { TypeMergingOptions, MergeFieldConfigCandidate, MergeInputFieldConfigCandidate, + MergeEnumValueConfigCandidate, } from './types'; import { fieldToFieldConfig, inputFieldToFieldConfig } from '@graphql-tools/utils'; +import { isSubschemaConfig } from '@graphql-tools/delegate'; export function mergeCandidates( typeName: string, @@ -68,6 +73,8 @@ function mergeObjectTypeCandidates( candidates: Array, typeMergingOptions: TypeMergingOptions ): GraphQLObjectType { + candidates = orderedTypeCandidates(candidates, typeMergingOptions); + const description = mergeTypeDescriptions(candidates, typeMergingOptions); const fields = fieldConfigMapFromTypeCandidates(candidates, typeMergingOptions); const typeConfigs = candidates.map(candidate => (candidate.type as GraphQLObjectType).toConfig()); @@ -84,6 +91,17 @@ function mergeObjectTypeCandidates( const interfaces = Object.keys(interfaceMap).map(interfaceName => interfaceMap[interfaceName]); const astNodes = pluck('astNode', candidates); + const fieldAstNodes = Object.values(fields) + .map(({ astNode }) => astNode) + .filter(n => n != null); + + if (astNodes.length > 1 && fieldAstNodes.length) { + astNodes.push({ + ...astNodes[astNodes.length - 1], + fields: JSON.parse(JSON.stringify(fieldAstNodes)), + }); + } + const astNode = astNodes .slice(1) .reduce( @@ -92,7 +110,6 @@ function mergeObjectTypeCandidates( ); const extensionASTNodes = [].concat(pluck>('extensionASTNodes', candidates)); - const extensions = Object.assign({}, ...pluck>('extensions', candidates)); const typeConfig = { @@ -113,10 +130,23 @@ function mergeInputObjectTypeCandidates( candidates: Array, typeMergingOptions: TypeMergingOptions ): GraphQLInputObjectType { + candidates = orderedTypeCandidates(candidates, typeMergingOptions); + const description = mergeTypeDescriptions(candidates, typeMergingOptions); const fields = inputFieldConfigMapFromTypeCandidates(candidates, typeMergingOptions); const astNodes = pluck('astNode', candidates); + const fieldAstNodes = Object.values(fields) + .map(({ astNode }) => astNode) + .filter(n => n != null); + + if (astNodes.length > 1 && fieldAstNodes.length) { + astNodes.push({ + ...astNodes[astNodes.length - 1], + fields: JSON.parse(JSON.stringify(fieldAstNodes)), + }); + } + const astNode = astNodes .slice(1) .reduce( @@ -149,6 +179,8 @@ function mergeInterfaceTypeCandidates( candidates: Array, typeMergingOptions: TypeMergingOptions ): GraphQLInterfaceType { + candidates = orderedTypeCandidates(candidates, typeMergingOptions); + const description = mergeTypeDescriptions(candidates, typeMergingOptions); const fields = fieldConfigMapFromTypeCandidates(candidates, typeMergingOptions); const typeConfigs = candidates.map(candidate => (candidate.type as GraphQLInterfaceType).toConfig()); @@ -165,6 +197,17 @@ function mergeInterfaceTypeCandidates( const interfaces = Object.keys(interfaceMap).map(interfaceName => interfaceMap[interfaceName]); const astNodes = pluck('astNode', candidates); + const fieldAstNodes = Object.values(fields) + .map(({ astNode }) => astNode) + .filter(n => n != null); + + if (astNodes.length > 1 && fieldAstNodes.length) { + astNodes.push({ + ...astNodes[astNodes.length - 1], + fields: JSON.parse(JSON.stringify(fieldAstNodes)), + }); + } + const astNode = astNodes .slice(1) .reduce( @@ -194,8 +237,8 @@ function mergeUnionTypeCandidates( candidates: Array, typeMergingOptions: TypeMergingOptions ): GraphQLUnionType { + candidates = orderedTypeCandidates(candidates, typeMergingOptions); const description = mergeTypeDescriptions(candidates, typeMergingOptions); - const typeConfigs = candidates.map(candidate => (candidate.type as GraphQLUnionType).toConfig()); const typeMap = typeConfigs.reduce((acc, typeConfig) => { typeConfig.types.forEach(type => { @@ -234,18 +277,23 @@ function mergeEnumTypeCandidates( candidates: Array, typeMergingOptions: TypeMergingOptions ): GraphQLEnumType { - const description = mergeTypeDescriptions(candidates, typeMergingOptions); + candidates = orderedTypeCandidates(candidates, typeMergingOptions); - const typeConfigs = candidates.map(candidate => (candidate.type as GraphQLEnumType).toConfig()); - const values = typeConfigs.reduce( - (acc, typeConfig) => ({ - ...acc, - ...typeConfig.values, - }), - {} - ); + const description = mergeTypeDescriptions(candidates, typeMergingOptions); + const values = enumValueConfigMapFromTypeCandidates(candidates, typeMergingOptions); const astNodes = pluck('astNode', candidates); + const valueAstNodes = Object.values(values) + .map(({ astNode }) => astNode) + .filter(n => n != null); + + if (astNodes.length > 1 && valueAstNodes.length) { + astNodes.push({ + ...astNodes[astNodes.length - 1], + values: JSON.parse(JSON.stringify(valueAstNodes)), + }); + } + const astNode = astNodes .slice(1) .reduce((acc, astNode) => mergeEnum(astNode, acc as EnumTypeDefinitionNode) as EnumTypeDefinitionNode, astNodes[0]); @@ -266,13 +314,56 @@ function mergeEnumTypeCandidates( return new GraphQLEnumType(typeConfig); } +function enumValueConfigMapFromTypeCandidates( + candidates: Array, + typeMergingOptions: TypeMergingOptions +): GraphQLEnumValueConfigMap { + const enumValueConfigCandidatesMap: Record> = Object.create(null); + + candidates.forEach(candidate => { + const valueMap = (candidate.type as GraphQLEnumType).toConfig().values; + Object.keys(valueMap).forEach(enumValue => { + const enumValueConfigCandidate = { + enumValueConfig: valueMap[enumValue], + enumValue, + type: candidate.type as GraphQLEnumType, + subschema: candidate.subschema, + transformedSubschema: candidate.transformedSubschema, + }; + + if (enumValue in enumValueConfigCandidatesMap) { + enumValueConfigCandidatesMap[enumValue].push(enumValueConfigCandidate); + } else { + enumValueConfigCandidatesMap[enumValue] = [enumValueConfigCandidate]; + } + }); + }); + + const enumValueConfigMap = Object.create(null); + + Object.keys(enumValueConfigCandidatesMap).forEach(enumValue => { + const enumValueConfigMerger = typeMergingOptions?.enumValueConfigMerger ?? defaultEnumValueConfigMerger; + enumValueConfigMap[enumValue] = enumValueConfigMerger(enumValueConfigCandidatesMap[enumValue]); + }); + + return JSON.parse(JSON.stringify(enumValueConfigMap)) as GraphQLEnumValueConfigMap; +} + +function defaultEnumValueConfigMerger(candidates: Array) { + const preferred = candidates.find( + ({ type, subschema }) => isSubschemaConfig(subschema) && subschema.merge?.[type.name]?.canonical + ); + return (preferred || candidates[candidates.length - 1]).enumValueConfig; +} + function mergeScalarTypeCandidates( typeName: string, candidates: Array, typeMergingOptions: TypeMergingOptions ): GraphQLScalarType { - const description = mergeTypeDescriptions(candidates, typeMergingOptions); + candidates = orderedTypeCandidates(candidates, typeMergingOptions); + const description = mergeTypeDescriptions(candidates, typeMergingOptions); const serializeFns = pluck>('serialize', candidates); const serialize = serializeFns[serializeFns.length - 1]; @@ -285,7 +376,10 @@ function mergeScalarTypeCandidates( const astNodes = pluck('astNode', candidates); const astNode = astNodes .slice(1) - .reduce((acc, astNode) => mergeScalar(acc, astNode), astNodes[0]) as ScalarTypeDefinitionNode; + .reduce( + (acc, astNode) => mergeScalar(astNode, acc as ScalarTypeDefinitionNode) as ScalarTypeDefinitionNode, + astNodes[0] + ); const extensionASTNodes = [].concat(pluck>('extensionASTNodes', candidates)); @@ -305,6 +399,30 @@ function mergeScalarTypeCandidates( return new GraphQLScalarType(typeConfig); } +function orderedTypeCandidates( + candidates: Array, + typeMergingOptions: TypeMergingOptions +): Array { + const selectCanonicalTypeCandidate = + typeMergingOptions?.selectCanonicalTypeCandidate ?? defaultSelectCanonicalTypeCandidate; + const candidate = selectCanonicalTypeCandidate(candidates); + return candidates.sort((_a, b) => (b === candidate ? -1 : 0)); +} + +function defaultSelectCanonicalTypeCandidate(candidates: Array): MergeTypeCandidate { + const canonical: Array = candidates.filter(({ type, subschema }) => + isSubschemaConfig(subschema) ? subschema.merge?.[type.name]?.canonical : false + ); + + if (canonical.length > 1) { + throw new Error(`Multiple canonical definitions for "${canonical[0].type.name}"`); + } else if (canonical.length) { + return canonical[0]; + } + + return candidates[candidates.length - 1]; +} + function mergeTypeDescriptions(candidates: Array, typeMergingOptions: TypeMergingOptions): string { const typeDescriptionsMerger = typeMergingOptions?.typeDescriptionsMerger ?? defaultTypeDescriptionMerger; return typeDescriptionsMerger(candidates); @@ -354,6 +472,26 @@ function mergeFieldConfigs(candidates: Array, typeMer } function defaultFieldConfigMerger(candidates: Array) { + const canonicalByField: Array> = []; + const canonicalByType: Array> = []; + + candidates.forEach(({ type, fieldName, fieldConfig, subschema }) => { + if (!isSubschemaConfig(subschema)) return; + if (subschema.merge?.[type.name]?.fields?.[fieldName]?.canonical) { + canonicalByField.push(fieldConfig); + } else if (subschema.merge?.[type.name]?.canonical) { + canonicalByType.push(fieldConfig); + } + }); + + if (canonicalByField.length > 1) { + throw new Error(`Multiple canonical definitions for "${candidates[0].type.name}.${candidates[0].fieldName}"`); + } else if (canonicalByField.length) { + return canonicalByField[0]; + } else if (canonicalByType.length) { + return canonicalByType[0]; + } + return candidates[candidates.length - 1].fieldConfig; } @@ -385,23 +523,33 @@ function inputFieldConfigMapFromTypeCandidates( const inputFieldConfigMap = Object.create(null); Object.keys(inputFieldConfigCandidatesMap).forEach(fieldName => { - inputFieldConfigMap[fieldName] = mergeInputFieldConfigs( - inputFieldConfigCandidatesMap[fieldName], - typeMergingOptions - ); + const inputFieldConfigMerger = typeMergingOptions?.inputFieldConfigMerger ?? defaultInputFieldConfigMerger; + inputFieldConfigMap[fieldName] = inputFieldConfigMerger(inputFieldConfigCandidatesMap[fieldName]); }); return inputFieldConfigMap; } -function mergeInputFieldConfigs( - candidates: Array, - typeMergingOptions: TypeMergingOptions -) { - const inputFieldConfigMerger = typeMergingOptions?.inputFieldConfigMerger ?? defaultInputFieldConfigMerger; - return inputFieldConfigMerger(candidates); -} - function defaultInputFieldConfigMerger(candidates: Array) { + const canonicalByField: Array = []; + const canonicalByType: Array = []; + + candidates.forEach(({ type, fieldName, inputFieldConfig, subschema }) => { + if (!isSubschemaConfig(subschema)) return; + if (subschema.merge?.[type.name]?.fields?.[fieldName]?.canonical) { + canonicalByField.push(inputFieldConfig); + } else if (subschema.merge?.[type.name]?.canonical) { + canonicalByType.push(inputFieldConfig); + } + }); + + if (canonicalByField.length > 1) { + throw new Error(`Multiple canonical definitions for "${candidates[0].type.name}.${candidates[0].fieldName}"`); + } else if (canonicalByField.length) { + return canonicalByField[0]; + } else if (canonicalByType.length) { + return canonicalByType[0]; + } + return candidates[candidates.length - 1].inputFieldConfig; } diff --git a/packages/stitch/src/selectionSetArgs.ts b/packages/stitch/src/selectionSetArgs.ts index 2e5f583d934..4dbc164d5b2 100644 --- a/packages/stitch/src/selectionSetArgs.ts +++ b/packages/stitch/src/selectionSetArgs.ts @@ -5,7 +5,7 @@ export const forwardArgsToSelectionSet: ( selectionSet: string, mapping?: Record ) => (field: FieldNode) => SelectionSetNode = (selectionSet: string, mapping?: Record) => { - const selectionSetDef = parseSelectionSet(selectionSet); + const selectionSetDef = parseSelectionSet(selectionSet, { noLocation: true }); return (field: FieldNode): SelectionSetNode => { const selections = selectionSetDef.selections.map( (selectionNode): SelectionNode => { diff --git a/packages/stitch/src/stitchingInfo.ts b/packages/stitch/src/stitchingInfo.ts index 8ccb2f5832a..3d50557d91f 100644 --- a/packages/stitch/src/stitchingInfo.ts +++ b/packages/stitch/src/stitchingInfo.ts @@ -38,7 +38,7 @@ export function createStitchingInfo( mergedTypeInfo.selectionSets.forEach((selectionSet, subschemaConfig) => { const schema = subschemaConfig.transformedSchema; - const type = schema.getType(typeName) as GraphQLObjectType | GraphQLInterfaceType; + const type = schema.getType(typeName) as GraphQLObjectType; const fields = type.getFields(); Object.keys(fields).forEach(fieldName => { const field = fields[fieldName]; @@ -49,7 +49,7 @@ export function createStitchingInfo( if (selectionSetsByField[typeName][fieldName] == null) { selectionSetsByField[typeName][fieldName] = { kind: Kind.SELECTION_SET, - selections: [parseSelectionSet('{ __typename }').selections[0]], + selections: [parseSelectionSet('{ __typename }', { noLocation: true }).selections[0]], }; } selectionSetsByField[typeName][fieldName].selections = selectionSetsByField[typeName][ @@ -63,7 +63,7 @@ export function createStitchingInfo( if (selectionSetsByField[typeName][fieldName] == null) { selectionSetsByField[typeName][fieldName] = { kind: Kind.SELECTION_SET, - selections: [parseSelectionSet('{ __typename }').selections[0]], + selections: [parseSelectionSet('{ __typename }', { noLocation: true }).selections[0]], }; } selectionSetsByField[typeName][fieldName].selections = selectionSetsByField[typeName][ @@ -130,7 +130,7 @@ function createMergedTypes( } if (mergedTypeConfig.selectionSet) { - const selectionSet = parseSelectionSet(mergedTypeConfig.selectionSet); + const selectionSet = parseSelectionSet(mergedTypeConfig.selectionSet, { noLocation: true }); selectionSets.set(subschema, selectionSet); } @@ -139,7 +139,7 @@ function createMergedTypes( Object.keys(mergedTypeConfig.fields).forEach(fieldName => { if (mergedTypeConfig.fields[fieldName].selectionSet) { const rawFieldSelectionSet = mergedTypeConfig.fields[fieldName].selectionSet; - parsedFieldSelectionSets[fieldName] = parseSelectionSet(rawFieldSelectionSet); + parsedFieldSelectionSets[fieldName] = parseSelectionSet(rawFieldSelectionSet, { noLocation: true }); } }); fieldSelectionSets.set(subschema, parsedFieldSelectionSets); @@ -150,7 +150,7 @@ function createMergedTypes( Object.keys(mergedTypeConfig.computedFields).forEach(fieldName => { if (mergedTypeConfig.computedFields[fieldName].selectionSet) { const rawFieldSelectionSet = mergedTypeConfig.computedFields[fieldName].selectionSet; - parsedFieldSelectionSets[fieldName] = parseSelectionSet(rawFieldSelectionSet); + parsedFieldSelectionSets[fieldName] = parseSelectionSet(rawFieldSelectionSet, { noLocation: true }); } }); fieldSelectionSets.set(subschema, parsedFieldSelectionSets); @@ -235,7 +235,7 @@ export function completeStitchingInfo( const selectionSetsByType = Object.create(null); [schema.getQueryType(), schema.getMutationType].forEach(rootType => { if (rootType) { - selectionSetsByType[rootType.name] = parseSelectionSet('{ __typename }'); + selectionSetsByType[rootType.name] = parseSelectionSet('{ __typename }', { noLocation: true }); } }); @@ -261,7 +261,7 @@ export function completeStitchingInfo( dynamicSelectionSetsByField[typeName][fieldName].push(field.selectionSet); } else { - const selectionSet = parseSelectionSet(field.selectionSet); + const selectionSet = parseSelectionSet(field.selectionSet, { noLocation: true }); if (!(typeName in selectionSetsByField)) { selectionSetsByField[typeName] = Object.create(null); } diff --git a/packages/stitch/src/types.ts b/packages/stitch/src/types.ts index 2a520af6d76..07ea4ec43fb 100644 --- a/packages/stitch/src/types.ts +++ b/packages/stitch/src/types.ts @@ -8,6 +8,8 @@ import { GraphQLInterfaceType, GraphQLInputFieldConfig, GraphQLInputObjectType, + GraphQLEnumValueConfig, + GraphQLEnumType, } from 'graphql'; import { ITypeDefinitions, TypeMap } from '@graphql-tools/utils'; import { MergedTypeResolver, Subschema, SubschemaConfig } from '@graphql-tools/delegate'; @@ -35,6 +37,14 @@ export interface MergeInputFieldConfigCandidate { transformedSubschema?: Subschema; } +export interface MergeEnumValueConfigCandidate { + enumValueConfig: GraphQLEnumValueConfig; + enumValue: string; + type: GraphQLEnumType; + subschema?: GraphQLSchema | SubschemaConfig; + transformedSubschema?: Subschema; +} + export type MergeTypeFilter = (mergeTypeCandidates: Array, typeName: string) => boolean; export interface MergedTypeInfo { @@ -70,9 +80,11 @@ export interface IStitchSchemasOptions extends Omit SubschemaConfig; export interface TypeMergingOptions { + selectCanonicalTypeCandidate?: (candidates: Array) => MergeTypeCandidate; typeDescriptionsMerger?: (candidates: Array) => string; fieldConfigMerger?: (candidates: Array) => GraphQLFieldConfig; inputFieldConfigMerger?: (candidates: Array) => GraphQLInputFieldConfig; + enumValueConfigMerger?: (candidates: Array) => GraphQLEnumValueConfig; } export type OnTypeConflict = ( diff --git a/packages/stitch/tests/mergeCanonical.test.ts b/packages/stitch/tests/mergeCanonical.test.ts new file mode 100644 index 00000000000..aeb42953613 --- /dev/null +++ b/packages/stitch/tests/mergeCanonical.test.ts @@ -0,0 +1,253 @@ +import { makeExecutableSchema } from '@graphql-tools/schema'; +import { stitchSchemas } from '@graphql-tools/stitch'; +import { getDirectives } from '@graphql-tools/utils'; +import { GraphQLObjectType, GraphQLInterfaceType, GraphQLInputObjectType, GraphQLEnumType, GraphQLUnionType, GraphQLScalarType } from 'graphql'; + +describe('merge canonical types', () => { + const firstSchema = makeExecutableSchema({ + typeDefs: ` + directive @mydir(value: String) on OBJECT | INTERFACE | INPUT_OBJECT | UNION | ENUM | SCALAR | FIELD_DEFINITION | INPUT_FIELD_DEFINITION + + "first" + type Product implements IProduct @mydir(value: "first") { + "first" + id: ID! @mydir(value: "first") @deprecated(reason: "first") + "first" + url: String @mydir(value: "first") @deprecated(reason: "first") + } + + "first" + interface IProduct @mydir(value: "first") { + "first" + id: ID! @mydir(value: "first") + "first" + url: String @mydir(value: "first") + } + + "first" + input ProductInput @mydir(value: "first") { + "first" + id: ID @mydir(value: "first") + "first" + url: String @mydir(value: "first") + } + + "first" + enum ProductEnum @mydir(value: "first") { + "first" + YES + "first" + NO + } + + "first" + union ProductUnion @mydir(value: "first") = Product + + "first" + scalar ProductScalar @mydir(value: "first") + ` + }); + + const secondSchema = makeExecutableSchema({ + typeDefs: ` + directive @mydir(value: String) on OBJECT | INTERFACE | INPUT_OBJECT | UNION | ENUM | SCALAR | FIELD_DEFINITION | INPUT_FIELD_DEFINITION + + "second" + type Product implements IProduct @mydir(value: "second") { + "second" + id: ID! @mydir(value: "second") @deprecated(reason: "second") + "second" + url: String @mydir(value: "second") @deprecated(reason: "second") + } + + "second" + interface IProduct @mydir(value: "second") { + "second" + id: ID! @mydir(value: "second") + "second" + url: String @mydir(value: "second") + } + + "second" + input ProductInput @mydir(value: "second") { + "second" + id: ID @mydir(value: "second") + "second" + url: String @mydir(value: "second") + } + + "second" + enum ProductEnum @mydir(value: "second") { + "second" + YES + "second" + NO + "second" + MAYBE + } + + "second" + union ProductUnion @mydir(value: "second") = Product + + "second" + scalar ProductScalar @mydir(value: "second") + ` + }); + + const gatewaySchema = stitchSchemas({ + subschemas: [ + { + schema: firstSchema, + merge: { + Product: { + selectionSet: '{ id }', + fieldName: 'product', + args: ({ id }) => ({ id }), + canonical: true, + }, + IProduct: { + canonical: true, + }, + ProductInput: { + canonical: true, + }, + ProductEnum: { + canonical: true, + }, + ProductUnion: { + canonical: true, + }, + ProductScalar: { + canonical: true, + }, + } + }, + { + schema: secondSchema, + merge: { + Product: { + selectionSet: '{ id }', + fieldName: 'product', + args: ({ id }) => ({ id }), + fields: { + url: { canonical: true }, + } + }, + IProduct: { + fields: { + url: { canonical: true }, + } + }, + ProductInput: { + fields: { + url: { canonical: true }, + } + } + } + }, + ], + typeDefs: ` + directive @mydir(value: String) on OBJECT | INTERFACE | INPUT_OBJECT | UNION | ENUM | SCALAR | FIELD_DEFINITION | INPUT_FIELD_DEFINITION + + "third" + type Product implements IProduct @mydir(value: "third") { + "third" + id: ID! @mydir(value: "third") + "third" + url: String @mydir(value: "third") + } + + "third" + interface IProduct @mydir(value: "third") { + "third" + id: ID! @mydir(value: "third") + "third" + url: String @mydir(value: "third") + } + + "third" + input ProductInput @mydir(value: "third") { + "third" + id: ID @mydir(value: "third") + "third" + url: String @mydir(value: "third") + } + + "third" + enum ProductEnum @mydir(value: "third") { + "third" + YES + "third" + NO + } + + "third" + union ProductUnion @mydir(value: "third") = Product + + "third" + scalar ProductScalar @mydir(value: "third") + ` + }); + + it('merges prioritized descriptions', () => { + expect(gatewaySchema.getType('Product').description).toEqual('first'); + expect(gatewaySchema.getType('IProduct').description).toEqual('first'); + expect(gatewaySchema.getType('ProductInput').description).toEqual('first'); + expect(gatewaySchema.getType('ProductEnum').description).toEqual('first'); + expect(gatewaySchema.getType('ProductUnion').description).toEqual('first'); + expect(gatewaySchema.getType('ProductScalar').description).toEqual('first'); + + const objectType = gatewaySchema.getType('Product') as GraphQLObjectType; + const interfaceType = gatewaySchema.getType('IProduct') as GraphQLInterfaceType; + const inputType = gatewaySchema.getType('ProductInput') as GraphQLInputObjectType; + const enumType = gatewaySchema.getType('ProductEnum') as GraphQLEnumType; + + expect(objectType.getFields().id.description).toEqual('first'); + expect(interfaceType.getFields().id.description).toEqual('first'); + expect(inputType.getFields().id.description).toEqual('first'); + + expect(objectType.getFields().url.description).toEqual('second'); + expect(interfaceType.getFields().url.description).toEqual('second'); + expect(inputType.getFields().url.description).toEqual('second'); + + expect(enumType.toConfig().values.YES.description).toEqual('first'); + expect(enumType.toConfig().values.NO.description).toEqual('first'); + expect(enumType.toConfig().values.MAYBE.description).toEqual('second'); + }); + + it('merges prioritized ASTs', () => { + const objectType = gatewaySchema.getType('Product') as GraphQLObjectType; + const interfaceType = gatewaySchema.getType('IProduct') as GraphQLInterfaceType; + const inputType = gatewaySchema.getType('ProductInput') as GraphQLInputObjectType; + const enumType = gatewaySchema.getType('ProductEnum') as GraphQLEnumType; + const unionType = gatewaySchema.getType('ProductUnion') as GraphQLUnionType; + const scalarType = gatewaySchema.getType('ProductScalar') as GraphQLScalarType; + + expect(getDirectives(firstSchema, objectType.toConfig()).mydir.value).toEqual('first'); + expect(getDirectives(firstSchema, interfaceType.toConfig()).mydir.value).toEqual('first'); + expect(getDirectives(firstSchema, inputType.toConfig()).mydir.value).toEqual('first'); + expect(getDirectives(firstSchema, enumType.toConfig()).mydir.value).toEqual('first'); + expect(getDirectives(firstSchema, unionType.toConfig()).mydir.value).toEqual('first'); + expect(getDirectives(firstSchema, scalarType.toConfig()).mydir.value).toEqual('first'); + + expect(getDirectives(firstSchema, objectType.getFields().id).mydir.value).toEqual('first'); + expect(getDirectives(firstSchema, objectType.getFields().url).mydir.value).toEqual('second'); + expect(getDirectives(firstSchema, interfaceType.getFields().id).mydir.value).toEqual('first'); + expect(getDirectives(firstSchema, interfaceType.getFields().url).mydir.value).toEqual('second'); + expect(getDirectives(firstSchema, inputType.getFields().id).mydir.value).toEqual('first'); + expect(getDirectives(firstSchema, inputType.getFields().url).mydir.value).toEqual('second'); + + expect(enumType.toConfig().astNode.values.map(v => v.description.value)).toEqual(['first', 'first', 'second']); + expect(enumType.toConfig().values.YES.astNode.description.value).toEqual('first'); + expect(enumType.toConfig().values.NO.astNode.description.value).toEqual('first'); + expect(enumType.toConfig().values.MAYBE.astNode.description.value).toEqual('second'); + }); + + it('merges prioritized deprecations', () => { + const objectType = gatewaySchema.getType('Product') as GraphQLObjectType; + expect(objectType.getFields().id.deprecationReason).toEqual('first'); + expect(objectType.getFields().url.deprecationReason).toEqual('second'); + expect(getDirectives(firstSchema, objectType.getFields().id).deprecated.reason).toEqual('first'); + expect(getDirectives(firstSchema, objectType.getFields().url).deprecated.reason).toEqual('second'); + }); +}); diff --git a/packages/stitch/tests/typeMergingWithDirectives.test.ts b/packages/stitch/tests/typeMergingWithDirectives.test.ts index 4e39fa79647..b9b4877d9ce 100644 --- a/packages/stitch/tests/typeMergingWithDirectives.test.ts +++ b/packages/stitch/tests/typeMergingWithDirectives.test.ts @@ -175,7 +175,9 @@ describe('merging using type merging', () => { # EQUIVALENT TO: # _productsByUpc(upcs: [String!]!): [Product] @merge(argsExpr: "upcs: [[$key.upc]]") } - type Product @key(selectionSet: "{ upc }") { + # @key is not necessary when using keyField + # type Product @key(selectionSet: "{ upc }") { + type Product { upc: String! name: String price: Int diff --git a/packages/stitch/tests/typeMergingWithExtensions.test.ts b/packages/stitch/tests/typeMergingWithExtensions.test.ts index 30662220e96..da7e87bb3cd 100644 --- a/packages/stitch/tests/typeMergingWithExtensions.test.ts +++ b/packages/stitch/tests/typeMergingWithExtensions.test.ts @@ -181,13 +181,15 @@ describe('merging using type merging', () => { price: { type: GraphQLInt }, weight: { type: GraphQLInt }, }), - extensions: { - directives: { - key: { - selectionSet: '{ upc }', - }, - }, - }, + // key is not necessary when using keyField + // + // extensions: { + // directives: { + // key: { + // selectionSet: '{ upc }', + // }, + // }, + // }, }); productsSchemaTypes.Query = new GraphQLObjectType({ diff --git a/packages/stitch/tests/typeMergingWithInterfaces.test.ts b/packages/stitch/tests/typeMergingWithInterfaces.test.ts new file mode 100644 index 00000000000..93b79376fa3 --- /dev/null +++ b/packages/stitch/tests/typeMergingWithInterfaces.test.ts @@ -0,0 +1,503 @@ +// Conversion of Apollo Federation demo +// Compare: https://github.com/apollographql/federation-demo +// See also: +// https://github.com/ardatan/graphql-tools/issues/1697 +// https://github.com/ardatan/graphql-tools/issues/1710 +// https://github.com/ardatan/graphql-tools/issues/1959 + +import { graphql } from 'graphql'; + +import { makeExecutableSchema } from '@graphql-tools/schema'; +import { ExecutionResult } from '@graphql-tools/utils'; +import { stitchSchemas } from '@graphql-tools/stitch'; + +import { stitchingDirectives } from '@graphql-tools/stitching-directives'; + +describe('merging using type merging', () => { + const { allStitchingDirectivesTypeDefs, stitchingDirectivesValidator, stitchingDirectivesTransformer } = stitchingDirectives(); + + const users = [ + { + id: '1', + name: 'Ada Lovelace', + birthDate: '1815-12-10', + username: '@ada' + }, + { + id: '2', + name: 'Alan Turing', + birthDate: '1912-06-23', + username: '@complete', + }, + ]; + const accountsSchema = makeExecutableSchema({ + // @merge directive will direct the gateway to use the tagged resolver to resolve objects + // of that type. By default: + // 1. the key for that type will be sent to the resolvers first argument. + // 2. an array of keys will sent if the resolver returns a list. + // + // Note: the subschema can rely on the gateway correctly sending the indicated key and + // so it is safe to use a non-validated scalar argument. In the next example, the subschema + // will choose to strongly type the `keys` argument, but it is not strictly necessary. + typeDefs: ` + ${allStitchingDirectivesTypeDefs} + scalar _Key + union _Entity = User + type Query { + me: User + _entities(keys: [_Key!]!): [_Entity] @merge + } + type User @key(selectionSet: "{ id }") { + id: ID! + name: String + username: String + } + `, + resolvers: { + Query: { + me: () => users[0], + _entities: (_root, { keys }) => { + return keys.map((key: Record) => ({ ...key, ...users.find(u => u.id === key.id) })); + }, + }, + }, + schemaTransforms: [stitchingDirectivesValidator], + }); + + const inventory = [ + { upc: '1', inStock: true }, + { upc: '2', inStock: false }, + { upc: '3', inStock: true } + ]; + + const inventorySchema = makeExecutableSchema({ + // @merge directive will direct the gateway to use the tagged resolver to resolve objects + // of that type. By default: + // 1. the key for that type will be sent to the resolvers first argument. + // 2. an array of keys will sent if the resolver returns a list. + // + // In this example, the key is constructed by using @key and @computed selection sets. + // The @computed directive for a given field instructs the gateway to only add the required + // additional selections when the tagged field is included within a query. In addition, + // the @computed directive will defer resolution of these fields even when queries originate + // within this subschema. The resolver for the mostStockedProduct therefore correctly returns + // an object with a `upc` property, but without `price` and `weight`. The gateway will use + // the `upc` to retrive the `price` and `weight` from the external services and return to this + // service for the `shippingEstimate`. Resolution for @computed fields thereby differs when + // querying via the gateway versus when querying the subservice directly. + // + // Note as well how the merging resolver used by the gateway includes the `price` and `weight` + // from the key within its internal representation of a Product even though they are not + // by this subschema. + // + // This example strongly types the ProductKey using an input object. This is optional, + // as the subschema can rely on the gateway always sending in the correctly typed data. + // It is typed correctly; `upc` is always specified, but `price` and `weight` will only + // be included when `shippingEstimate` is included within the query. + // + typeDefs: ` + ${allStitchingDirectivesTypeDefs} + scalar _Key + union _Entity = Product + type Product @key(selectionSet: "{ upc }") { + upc: String! + inStock: Boolean + shippingEstimate: Int @computed(selectionSet: "{ price weight }") + } + type Query { + mostStockedProduct: Product + _entities(keys: [_Key!]!): [_Entity]! @merge + } + `, + resolvers: { + Product: { + shippingEstimate: product => { + if (product.price > 1000) { + return 0 // free for expensive items + } + return Math.round(product.weight * 0.5) || null; // estimate is based on weight + } + }, + Query: { + mostStockedProduct: () => inventory.find(i => i.upc === '3'), + _entities: (_root, { keys }) => { + return keys.map((key: Record) => ({ ...key, ...inventory.find(i => i.upc === key.upc) })); + }, + }, + }, + schemaTransforms: [stitchingDirectivesValidator], + }); + + const products = [ + { + upc: '1', + name: 'Table', + price: 899, + weight: 100 + }, + { + upc: '2', + name: 'Couch', + price: 1299, + weight: 1000 + }, + { + upc: '3', + name: 'Chair', + price: 54, + weight: 50 + } + ]; + + const productsSchema = makeExecutableSchema({ + // @merge directive will direct the gateway to use the tagged resolver to resolve objects + // of that type. By default: + // 1. the key for that type will be sent to the resolvers first argument. + // 2. an array of keys will sent if the resolver returns a list. + // + // In this example, the `keyField` argument for the @merge directive is used to customize + // the portion of the key that the gateway will pass to the resolver. + // + // Alternatively, the `argsExpr` argument can be used to allow more customization: + // + // Rules for evaluation of these arguments are as follows: + // + // A. any expression enclosed by double brackets will be evaluated once for each of the + // requested keys, and then sent as a list. + // B. selections from the key can be referenced by using the $ sign and dot notation, so that + // $key.upc refers to the `upc` field of the key. + // + typeDefs: ` + ${allStitchingDirectivesTypeDefs} + scalar _Key + union _Entity = Product + type Query { + topProducts(first: Int = 2): [Product] + _entities(keys: [_Key!]!): [_Entity] @merge + } + type Product @key(selectionSet: "{ upc }") { + upc: String! + name: String + price: Int + weight: Int + } + `, + resolvers: { + Query: { + topProducts: (_root, args) => products.slice(0, args.first), + _entities: (_root, { keys }) => { + return keys.map((key: Record) => ({ ...key, ...products.find(product => product.upc === key.upc) })); + } + } + }, + schemaTransforms: [stitchingDirectivesValidator], + }); + + const usernames = [ + { id: '1', username: '@ada' }, + { id: '2', username: '@complete' }, + ]; + + const reviews = [ + { + id: '1', + authorId: '1', + product: { upc: '1' }, + body: 'Love it!', + }, + { + id: '2', + authorId: '1', + product: { upc: '2' }, + body: 'Too expensive.', + }, + { + id: '3', + authorId: '2', + product: { upc: '3' }, + body: 'Could be better.', + }, + { + id: '4', + authorId: '2', + product: { upc: '1' }, + body: 'Prefer something else.', + }, + ]; + + const reviewsSchema = makeExecutableSchema({ + // @merge directive will direct the gateway to use the tagged resolver to resolve objects + // of that type. By default: + // 1. the key for that type will be sent to the resolvers first argument. + // 2. an array of keys will sent if the resolver returns a list. + // + // In this example, the `keyArg` argument for the @merge directive is used to set the + // argument to which the gateway will pass the key. + // + // The equivalent `argsExpr` is also included. This example highlights how when using + // `argsExpr`, the $ sign without dot notation will pass the entire key as an object. + // This allows arbitary nesting of the key input as needed. + // + typeDefs: ` + ${allStitchingDirectivesTypeDefs} + scalar _Key + union _Entity = User | Product | Review + type Review { + id: ID! + body: String + author: User + product: Product + } + type User @key(selectionSet: "{ id }") { + id: ID! + username: String + numberOfReviews: Int + reviews: [Review] + } + type Product @key(selectionSet: "{ upc }") { + upc: String! + reviews: [Review] + } + type Query { + _entities(keys: [_Key!]!): [_Entity] @merge + } + `, + resolvers: { + Review: { + author: (review) => ({ id: review.authorId }), + }, + User: { + reviews: (user) => reviews.filter(review => review.authorId === user.id), + numberOfReviews: (user) => reviews.filter(review => review.authorId === user.id).length, + username: (user) => { + const found = usernames.find(username => username.id === user.id) + return found ? found.username : null + }, + }, + Product: { + reviews: (product) => reviews.filter(review => review.product.upc === product.upc), + }, + Query: { + _entities: (_root, { keys }) => { + return keys.map((key: Record) => { + if (key.__typename === 'Review') { + return ({ ...key, ...reviews.find(review => review.id === key.id) }); + } + + return { ...key }; + }); + }, + }, + }, + schemaTransforms: [stitchingDirectivesValidator], + }); + + const stitchedSchema = stitchSchemas({ + subschemas: [ + { + schema: accountsSchema, + batch: true, + }, + { + schema: inventorySchema, + batch: true, + }, + { + schema: productsSchema, + batch: true, + }, + { + schema: reviewsSchema, + batch: true, + }], + subschemaConfigTransforms: [stitchingDirectivesTransformer], + }); + + test('can stitch from products to inventory schema including mixture of computed and non-computed fields', async () => { + const result = await graphql( + stitchedSchema, + ` + query { + topProducts { + upc + inStock + shippingEstimate + } + } + `, + undefined, + {}, + ); + + const expectedResult: ExecutionResult = { + data: { + topProducts: [{ + upc: '1', + inStock: true, + shippingEstimate: 50, + }, { + upc: '2', + inStock: false, + shippingEstimate: 0, + }], + }, + }; + + expect(result).toEqual(expectedResult); + }); + + test('can stitch from accounts to reviews to products to inventory', async () => { + const result = await graphql( + stitchedSchema, + ` + query { + me { + reviews { + product { + upc + price + weight + } + } + } + } + `, + undefined, + {}, + ); + + const expectedResult: ExecutionResult = { + data: { + me: { + reviews: [ + { product: { price: 899, upc: '1', weight: 100 } }, + { product: { price: 1299, upc: '2', weight: 1000 } }, + ], + } + }, + }; + + expect(result).toEqual(expectedResult); + }); + + test('can stitch from accounts to reviews to products to inventory', async () => { + const result = await graphql( + stitchedSchema, + ` + query { + me { + reviews { + product { + upc + price + weight + shippingEstimate + } + } + } + } + `, + undefined, + {}, + ); + + const expectedResult: ExecutionResult = { + data: { + me: { + reviews: [ + { + product: { + price: 899, + upc: '1', + weight: 100, + shippingEstimate: 50, + }, + }, + { + product: { + price: 1299, + upc: '2', + weight: 1000, + shippingEstimate: 0, + } + }, + ], + }, + }, + }; + + expect(result).toEqual(expectedResult); + }); + + test('can stitch from accounts to reviews to products to inventory even when entire key not requested', async () => { + const result = await graphql( + stitchedSchema, + ` + query { + me { + reviews { + product { + upc + shippingEstimate + } + } + } + } + `, + undefined, + {}, + ); + + const expectedResult: ExecutionResult = { + data: { + me: { + reviews: [ + { + product: { + upc: '1', + shippingEstimate: 50, + }, + }, + { + product: { + upc: '2', + shippingEstimate: 0, + } + }, + ], + }, + }, + }; + + expect(result).toEqual(expectedResult); + }); + + test('can stitch from inventory to products and then back to inventory', async () => { + const result = await graphql( + stitchedSchema, + ` + query { + mostStockedProduct { + upc + inStock + shippingEstimate + } + } + `, + undefined, + {}, + ); + + const expectedResult: ExecutionResult = { + data: { + mostStockedProduct: { + upc: '3', + inStock: true, + shippingEstimate: 25, + }, + }, + }; + + expect(result).toEqual(expectedResult); + }); +}); diff --git a/packages/stitching-directives/CHANGELOG.md b/packages/stitching-directives/CHANGELOG.md index 7b25141a99a..0334aa15e07 100644 --- a/packages/stitching-directives/CHANGELOG.md +++ b/packages/stitching-directives/CHANGELOG.md @@ -1,5 +1,22 @@ # @graphql-tools/stitching-directives +## 1.1.2 + +### Patch Changes + +- 6e50d9fc: enhance(stitching-directives): use keyField + + When using simple keys, i.e. when using the keyField argument to `@merge`, the keyField can be added implicitly to the types's key. In most cases, therefore, `@key` should not be required at all. + +- Updated dependencies [6e50d9fc] + - @graphql-tools/utils@7.2.4 + +## 1.1.1 + +### Patch Changes + +- 394c4775: fix(stitching-directives): fix abstract types + ## 1.1.0 ### Minor Changes diff --git a/packages/stitching-directives/package.json b/packages/stitching-directives/package.json index e7d2075d060..9ee4248c990 100644 --- a/packages/stitching-directives/package.json +++ b/packages/stitching-directives/package.json @@ -1,6 +1,6 @@ { "name": "@graphql-tools/stitching-directives", - "version": "1.1.0", + "version": "1.1.2", "description": "A set of utils for faster development of GraphQL tools", "repository": { "type": "git", @@ -23,8 +23,8 @@ }, "dependencies": { "@graphql-tools/delegate": "^7.0.0", - "@graphql-tools/utils": "^7.2.0", - "tslib": "~2.0.1" + "@graphql-tools/utils": "^7.2.4", + "tslib": "~2.1.0" }, "devDependencies": { "@graphql-tools/schema": "^7.1.2" diff --git a/packages/stitching-directives/src/defaultStitchingDirectiveOptions.ts b/packages/stitching-directives/src/defaultStitchingDirectiveOptions.ts index 88427758f51..59dbe7376b2 100644 --- a/packages/stitching-directives/src/defaultStitchingDirectiveOptions.ts +++ b/packages/stitching-directives/src/defaultStitchingDirectiveOptions.ts @@ -3,6 +3,7 @@ import { StitchingDirectivesOptions } from './types'; export const defaultStitchingDirectiveOptions: StitchingDirectivesOptions = { keyDirectiveName: 'key', computedDirectiveName: 'computed', + canonicalDirectiveName: 'canonical', mergeDirectiveName: 'merge', pathToDirectivesInExtensions: ['directives'], }; diff --git a/packages/stitching-directives/src/stitchingDirectives.ts b/packages/stitching-directives/src/stitchingDirectives.ts index 5474a5a2e5d..19f72d9586b 100644 --- a/packages/stitching-directives/src/stitchingDirectives.ts +++ b/packages/stitching-directives/src/stitchingDirectives.ts @@ -14,6 +14,7 @@ export function stitchingDirectives( keyDirectiveTypeDefs: string; computedDirectiveTypeDefs: string; mergeDirectiveTypeDefs: string; + canonicalDirectiveTypeDefs: string; stitchingDirectivesTypeDefs: string; // for backwards compatibility allStitchingDirectivesTypeDefs: string; stitchingDirectivesValidator: (schema: GraphQLSchema) => GraphQLSchema; @@ -21,6 +22,7 @@ export function stitchingDirectives( keyDirective: GraphQLDirective; computedDirective: GraphQLDirective; mergeDirective: GraphQLDirective; + canonicalDirective: GraphQLDirective; allStitchingDirectives: Array; } { const finalOptions = { @@ -28,11 +30,12 @@ export function stitchingDirectives( ...options, }; - const { keyDirectiveName, computedDirectiveName, mergeDirectiveName } = finalOptions; + const { keyDirectiveName, computedDirectiveName, mergeDirectiveName, canonicalDirectiveName } = finalOptions; const keyDirectiveTypeDefs = `directive @${keyDirectiveName}(selectionSet: String!) on OBJECT`; const computedDirectiveTypeDefs = `directive @${computedDirectiveName}(selectionSet: String!) on FIELD_DEFINITION`; const mergeDirectiveTypeDefs = `directive @${mergeDirectiveName}(argsExpr: String, keyArg: String, keyField: String, key: [String!], additionalArgs: String) on FIELD_DEFINITION`; + const canonicalDirectiveTypeDefs = `directive @${canonicalDirectiveName} on OBJECT | INTERFACE | INPUT_OBJECT | UNION | ENUM | SCALAR | FIELD_DEFINITION | INPUT_FIELD_DEFINITION`; const keyDirective = new GraphQLDirective({ name: keyDirectiveName, @@ -62,22 +65,39 @@ export function stitchingDirectives( }, }); - const allStitchingDirectivesTypeDefs = ` - ${keyDirectiveTypeDefs} - ${computedDirectiveTypeDefs} - ${mergeDirectiveTypeDefs} - `; + const canonicalDirective = new GraphQLDirective({ + name: canonicalDirectiveName, + locations: [ + 'OBJECT', + 'INTERFACE', + 'INPUT_OBJECT', + 'UNION', + 'ENUM', + 'SCALAR', + 'FIELD_DEFINITION', + 'INPUT_FIELD_DEFINITION', + ], + }); + + const allStitchingDirectivesTypeDefs = [ + keyDirectiveTypeDefs, + computedDirectiveTypeDefs, + mergeDirectiveTypeDefs, + canonicalDirectiveTypeDefs, + ].join('\n'); return { keyDirectiveTypeDefs, computedDirectiveTypeDefs, mergeDirectiveTypeDefs, + canonicalDirectiveTypeDefs, stitchingDirectivesTypeDefs: allStitchingDirectivesTypeDefs, // for backwards compatibility allStitchingDirectivesTypeDefs, keyDirective, computedDirective, mergeDirective, - allStitchingDirectives: [keyDirective, computedDirective, mergeDirective], + canonicalDirective, + allStitchingDirectives: [keyDirective, computedDirective, mergeDirective, canonicalDirective], stitchingDirectivesValidator: stitchingDirectivesValidator(finalOptions), stitchingDirectivesTransformer: stitchingDirectivesTransformer(finalOptions), }; diff --git a/packages/stitching-directives/src/stitchingDirectivesTransformer.ts b/packages/stitching-directives/src/stitchingDirectivesTransformer.ts index edbd7173ece..d8043094e20 100644 --- a/packages/stitching-directives/src/stitchingDirectivesTransformer.ts +++ b/packages/stitching-directives/src/stitchingDirectivesTransformer.ts @@ -1,6 +1,8 @@ import { + getNamedType, getNullableType, GraphQLNamedType, + GraphQLSchema, isInterfaceType, isListType, isObjectType, @@ -8,6 +10,7 @@ import { Kind, parseValue, print, + SelectionNode, SelectionSetNode, valueFromASTUntyped, } from 'graphql'; @@ -32,7 +35,13 @@ import { stitchingDirectivesValidator } from './stitchingDirectivesValidator'; export function stitchingDirectivesTransformer( options: StitchingDirectivesOptions = {} ): (subschemaConfig: SubschemaConfig) => SubschemaConfig { - const { keyDirectiveName, computedDirectiveName, mergeDirectiveName, pathToDirectivesInExtensions } = { + const { + keyDirectiveName, + computedDirectiveName, + mergeDirectiveName, + canonicalDirectiveName, + pathToDirectivesInExtensions, + } = { ...defaultStitchingDirectiveOptions, ...options, }; @@ -43,36 +52,138 @@ export function stitchingDirectivesTransformer( const selectionSetsByType: Record = Object.create(null); const computedFieldSelectionSets: Record> = Object.create(null); const mergedTypesResolversInfo: Record = Object.create(null); + const canonicalTypesInfo: Record }> = Object.create( + null + ); const schema = subschemaConfig.schema; // gateway should also run validation stitchingDirectivesValidator(options)(schema); + function setCanonicalDefinition(typeName: string, fieldName?: string): void { + canonicalTypesInfo[typeName] = canonicalTypesInfo[typeName] || Object.create(null); + if (fieldName) { + canonicalTypesInfo[typeName].fields = canonicalTypesInfo[typeName].fields || Object.create(null); + canonicalTypesInfo[typeName].fields[fieldName] = true; + } else { + canonicalTypesInfo[typeName].canonical = true; + } + } + mapSchema(schema, { [MapperKind.OBJECT_TYPE]: type => { const directives = getDirectives(schema, type, pathToDirectivesInExtensions); - if (directives[keyDirectiveName]) { - const directiveArgumentMap = directives[keyDirectiveName]; - const selectionSet = parseSelectionSet(directiveArgumentMap.selectionSet); + const keyDirective = directives[keyDirectiveName]; + if (keyDirective) { + const selectionSet = parseSelectionSet(keyDirective.selectionSet, { noLocation: true }); selectionSetsByType[type.name] = selectionSet; } + if (directives[canonicalDirectiveName]) { + setCanonicalDefinition(type.name); + } + return undefined; }, [MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName, typeName) => { const directives = getDirectives(schema, fieldConfig, pathToDirectivesInExtensions); - if (directives[computedDirectiveName]) { - const directiveArgumentMap = directives[computedDirectiveName]; - const selectionSet = parseSelectionSet(directiveArgumentMap.selectionSet); + const computedDirective = directives[computedDirectiveName]; + if (computedDirective) { + const selectionSet = parseSelectionSet(computedDirective.selectionSet, { noLocation: true }); if (!computedFieldSelectionSets[typeName]) { computedFieldSelectionSets[typeName] = Object.create(null); } computedFieldSelectionSets[typeName][fieldName] = selectionSet; } + const mergeDirectiveKeyField = directives[mergeDirectiveName]?.keyField; + if (mergeDirectiveKeyField) { + const selectionSet = parseSelectionSet(`{ ${mergeDirectiveKeyField}}`, { noLocation: true }); + + const typeNames: Array = directives[mergeDirectiveName]?.types; + + const returnType = getNamedType(fieldConfig.type); + + forEachConcreteType(schema, returnType, directives[mergeDirectiveName]?.types, typeName => { + if (typeNames == null || typeNames.includes(typeName)) { + const existingSelectionSet = selectionSetsByType[typeName]; + selectionSetsByType[typeName] = existingSelectionSet + ? mergeSelectionSets(existingSelectionSet, selectionSet) + : selectionSet; + } + }); + } + + if (directives[canonicalDirectiveName]) { + setCanonicalDefinition(typeName, fieldName); + } + + return undefined; + }, + [MapperKind.INTERFACE_TYPE]: type => { + const directives = getDirectives(schema, type, pathToDirectivesInExtensions); + + if (directives[canonicalDirectiveName]) { + setCanonicalDefinition(type.name); + } + + return undefined; + }, + [MapperKind.INTERFACE_FIELD]: (fieldConfig, fieldName, typeName) => { + const directives = getDirectives(schema, fieldConfig, pathToDirectivesInExtensions); + + if (directives[canonicalDirectiveName]) { + setCanonicalDefinition(typeName, fieldName); + } + + return undefined; + }, + [MapperKind.INPUT_OBJECT_TYPE]: type => { + const directives = getDirectives(schema, type, pathToDirectivesInExtensions); + + if (directives[canonicalDirectiveName]) { + setCanonicalDefinition(type.name); + } + + return undefined; + }, + [MapperKind.INPUT_OBJECT_FIELD]: (inputFieldConfig, fieldName, typeName) => { + const directives = getDirectives(schema, inputFieldConfig, pathToDirectivesInExtensions); + + if (directives[canonicalDirectiveName]) { + setCanonicalDefinition(typeName, fieldName); + } + + return undefined; + }, + [MapperKind.UNION_TYPE]: type => { + const directives = getDirectives(schema, type, pathToDirectivesInExtensions); + + if (directives[canonicalDirectiveName]) { + setCanonicalDefinition(type.name); + } + + return undefined; + }, + [MapperKind.ENUM_TYPE]: type => { + const directives = getDirectives(schema, type, pathToDirectivesInExtensions); + + if (directives[canonicalDirectiveName]) { + setCanonicalDefinition(type.name); + } + + return undefined; + }, + [MapperKind.SCALAR_TYPE]: type => { + const directives = getDirectives(schema, type, pathToDirectivesInExtensions); + + if (directives[canonicalDirectiveName]) { + setCanonicalDefinition(type.name); + } + return undefined; }, }); @@ -80,7 +191,7 @@ export function stitchingDirectivesTransformer( if (subschemaConfig.merge) { Object.entries(subschemaConfig.merge).forEach(([typeName, mergedTypeConfig]) => { if (mergedTypeConfig.selectionSet) { - const selectionSet = parseSelectionSet(mergedTypeConfig.selectionSet); + const selectionSet = parseSelectionSet(mergedTypeConfig.selectionSet, { noLocation: true }); if (selectionSet) { if (selectionSetsByType[typeName]) { selectionSetsByType[typeName] = mergeSelectionSets(selectionSetsByType[typeName], selectionSet); @@ -91,7 +202,7 @@ export function stitchingDirectivesTransformer( } if (mergedTypeConfig.computedFields) { Object.entries(mergedTypeConfig.computedFields).forEach(([fieldName, computedFieldConfig]) => { - const selectionSet = parseSelectionSet(computedFieldConfig.selectionSet); + const selectionSet = parseSelectionSet(computedFieldConfig.selectionSet, { noLocation: true }); if (selectionSet) { if (computedFieldSelectionSets[typeName]?.[fieldName]) { computedFieldSelectionSets[typeName][fieldName] = mergeSelectionSets( @@ -137,13 +248,9 @@ export function stitchingDirectivesTransformer( if (directives[mergeDirectiveName]) { const directiveArgumentMap = directives[mergeDirectiveName]; - let returnType = getNullableType(fieldConfig.type); - let returnsList = false; - - if (isListType(returnType)) { - returnsList = true; - returnType = getNullableType(returnType.ofType); - } + const returnType = getNullableType(fieldConfig.type); + const returnsList = isListType(returnType); + const namedType = getNamedType(returnType); let mergeArgsExpr: string = directiveArgumentMap.argsExpr; @@ -163,48 +270,25 @@ export function stitchingDirectivesTransformer( }); } - const parsedMergeArgsExpr = parseMergeArgsExpr( - mergeArgsExpr, - allSelectionSetsByType[(returnType as GraphQLNamedType).name] - ); - - const additionalArgs = directiveArgumentMap.additionalArgs; - if (additionalArgs != null) { - parsedMergeArgsExpr.args = mergeDeep( - parsedMergeArgsExpr.args, - valueFromASTUntyped(parseValue(`{ ${additionalArgs} }`, { noLocation: true })) - ); - } - const typeNames: Array = directiveArgumentMap.types; - if (isInterfaceType(returnType)) { - getImplementingTypes(returnType.name, schema).forEach(typeName => { - if (typeNames == null || typeNames.includes(typeName)) { - mergedTypesResolversInfo[typeName] = { - fieldName, - returnsList, - ...parsedMergeArgsExpr, - }; - } - }); - } else if (isUnionType(returnType)) { - returnType.getTypes().forEach(type => { - if (typeNames == null || typeNames.includes(type.name)) { - mergedTypesResolversInfo[type.name] = { - fieldName, - returnsList, - ...parsedMergeArgsExpr, - }; - } - }); - } else if (isObjectType(returnType)) { - mergedTypesResolversInfo[returnType.name] = { + forEachConcreteTypeName(namedType, schema, typeNames, typeName => { + const parsedMergeArgsExpr = parseMergeArgsExpr(mergeArgsExpr, allSelectionSetsByType[typeName]); + + const additionalArgs = directiveArgumentMap.additionalArgs; + if (additionalArgs != null) { + parsedMergeArgsExpr.args = mergeDeep( + parsedMergeArgsExpr.args, + valueFromASTUntyped(parseValue(`{ ${additionalArgs} }`, { noLocation: true })) + ); + } + + mergedTypesResolversInfo[typeName] = { fieldName, returnsList, ...parsedMergeArgsExpr, }; - } + }); } return undefined; @@ -270,10 +354,61 @@ export function stitchingDirectivesTransformer( } }); + Object.entries(canonicalTypesInfo).forEach(([typeName, canonicalTypeInfo]) => { + if (newSubschemaConfig.merge == null) { + newSubschemaConfig.merge = Object.create(null); + } + + if (newSubschemaConfig.merge[typeName] == null) { + newSubschemaConfig.merge[typeName] = Object.create(null); + } + + const mergeTypeConfig = newSubschemaConfig.merge[typeName]; + + if (canonicalTypeInfo.canonical) { + mergeTypeConfig.canonical = true; + } + + if (canonicalTypeInfo.fields) { + if (mergeTypeConfig.fields == null) { + mergeTypeConfig.fields = Object.create(null); + } + Object.keys(canonicalTypeInfo.fields).forEach(fieldName => { + if (mergeTypeConfig.fields[fieldName] == null) { + mergeTypeConfig.fields[fieldName] = Object.create(null); + } + mergeTypeConfig.fields[fieldName].canonical = true; + }); + } + }); + return newSubschemaConfig; }; } +function forEachConcreteType( + schema: GraphQLSchema, + type: GraphQLNamedType, + typeNames: Array, + fn: (typeName: string) => void +) { + if (isInterfaceType(type)) { + getImplementingTypes(type.name, schema).forEach(typeName => { + if (typeNames == null || typeNames.includes(typeName)) { + fn(typeName); + } + }); + } else if (isUnionType(type)) { + type.getTypes().forEach(({ name: typeName }) => { + if (typeNames == null || typeNames.includes(typeName)) { + fn(typeName); + } + }); + } else if (isObjectType(type)) { + fn(type.name); + } +} + function generateKeyFn(mergedTypeResolverInfo: MergedTypeResolverInfo): (originalResult: any) => any { const keyDeclarations: Array = [].concat( ...mergedTypeResolverInfo.expansions.map(expansion => expansion.keyDeclarations) @@ -345,11 +480,43 @@ function buildKey(key: Array): string { return JSON.stringify(mergedObect).replace(/"/g, ''); } -function mergeSelectionSets(set1: SelectionSetNode, set2: SelectionSetNode): SelectionSetNode { +function mergeSelectionSets(selectionSet1: SelectionSetNode, selectionSet2: SelectionSetNode): SelectionSetNode { + const normalizedSelections: Record = Object.create(null); + + [selectionSet1, selectionSet2].forEach(set => { + set.selections.forEach(selection => { + const normalizedSelection = print(selection); + normalizedSelections[normalizedSelection] = selection; + }); + }); + const newSelectionSet = { kind: Kind.SELECTION_SET, - selections: set1.selections.concat(set2.selections), + selections: Object.values(normalizedSelections), }; return newSelectionSet; } + +function forEachConcreteTypeName( + returnType: GraphQLNamedType, + schema: GraphQLSchema, + typeNames: Array, + fn: (typeName: string) => void +): void { + if (isInterfaceType(returnType)) { + getImplementingTypes(returnType.name, schema).forEach(typeName => { + if (typeNames == null || typeNames.includes(typeName)) { + fn(typeName); + } + }); + } else if (isUnionType(returnType)) { + returnType.getTypes().forEach(type => { + if (typeNames == null || typeNames.includes(type.name)) { + fn(type.name); + } + }); + } else if (isObjectType(returnType) && (typeNames == null || typeNames.includes(returnType.name))) { + fn(returnType.name); + } +} diff --git a/packages/stitching-directives/src/types.ts b/packages/stitching-directives/src/types.ts index 84340c017f7..fcd99187e85 100644 --- a/packages/stitching-directives/src/types.ts +++ b/packages/stitching-directives/src/types.ts @@ -25,6 +25,7 @@ export interface StitchingDirectivesOptions { keyDirectiveName?: string; computedDirectiveName?: string; mergeDirectiveName?: string; + canonicalDirectiveName?: string; pathToDirectivesInExtensions?: Array; } diff --git a/packages/stitching-directives/tests/stitchingDirectivesTransformer.test.ts b/packages/stitching-directives/tests/stitchingDirectivesTransformer.test.ts index eb47166d984..b458056a90a 100644 --- a/packages/stitching-directives/tests/stitchingDirectivesTransformer.test.ts +++ b/packages/stitching-directives/tests/stitchingDirectivesTransformer.test.ts @@ -35,6 +35,66 @@ describe('type merging directives', () => { expect(transformedSubschemaConfig.merge.User.fieldName).toEqual('_user'); }); + test('adds type selection sets when returns union', () => { + const typeDefs = ` + ${allStitchingDirectivesTypeDefs} + scalar _Key + + union Entity = User + + type Query { + _entity(key: _Key): Entity @merge + } + + type User @key(selectionSet: "{ id }") { + id: ID + name: String + } + `; + + const schema = makeExecutableSchema({ typeDefs }); + + const subschemaConfig = { + schema, + } + + const transformedSubschemaConfig = stitchingDirectivesTransformer(subschemaConfig); + + expect(transformedSubschemaConfig.merge.User.selectionSet).toEqual(print(parseSelectionSet('{ id }'))); + expect(transformedSubschemaConfig.merge.User.fieldName).toEqual('_entity'); + }); + + test('adds type selection sets when returns interface', () => { + const typeDefs = ` + ${allStitchingDirectivesTypeDefs} + scalar _Key + + interface Entity { + id: ID + } + + type Query { + _entity(key: _Key): Entity @merge + } + + type User implements Entity @key(selectionSet: "{ id }") { + id: ID + name: String + } + `; + + const schema = makeExecutableSchema({ typeDefs }); + + const subschemaConfig = { + schema, + } + + const transformedSubschemaConfig = stitchingDirectivesTransformer(subschemaConfig); + + expect(transformedSubschemaConfig.merge.User.selectionSet).toEqual(print(parseSelectionSet('{ id }'))); + expect(transformedSubschemaConfig.merge.User.fieldName).toEqual('_entity'); + }); + test('adds computed selection sets', () => { const typeDefs = ` ${allStitchingDirectivesTypeDefs} @@ -304,14 +364,14 @@ describe('type merging directives', () => { }); }); - test('adds args function when used with keyField argument', () => { + test('adds key and args function when @merge is used with keyField argument', () => { const typeDefs = ` ${allStitchingDirectivesTypeDefs} type Query { _user(id: ID): User @merge(keyField: "id") } - type User @key(selectionSet: "{ id }") { + type User { id: ID name: String } @@ -325,6 +385,8 @@ describe('type merging directives', () => { const transformedSubschemaConfig = stitchingDirectivesTransformer(subschemaConfig); + expect(transformedSubschemaConfig.merge.User.selectionSet).toEqual(`{\n id\n}`); + const argsFn = transformedSubschemaConfig.merge.User.args; const originalResult = { @@ -429,6 +491,106 @@ describe('type merging directives', () => { }); }); + test('adds key and argsFromKeys functions when used without arguments and returns union', () => { + const typeDefs = ` + ${allStitchingDirectivesTypeDefs} + scalar _Key + + union Entity = User + + type Query { + _entity(key: _Key): [Entity] @merge + } + + type User @key(selectionSet: "{ id }") { + id: ID + name: String + } + `; + + const schema = makeExecutableSchema({ typeDefs }); + + const subschemaConfig = { + schema, + } + + const transformedSubschemaConfig = stitchingDirectivesTransformer(subschemaConfig); + + const keyFn = transformedSubschemaConfig.merge.User.key; + const argsFromKeysFn = transformedSubschemaConfig.merge.User.argsFromKeys; + + const originalResult = { + __typename: 'User', + id: '5', + email: 'email@email.com', + }; + + const key = keyFn(originalResult); + const args = argsFromKeysFn([key]); + + expect(key).toEqual({ + __typename: 'User', + id: '5', + }); + expect(args).toEqual({ + key: [{ + __typename: 'User', + id: '5', + }], + }); + }); + + test('adds key and argsFromKeys functions when used without arguments and returns interface', () => { + const typeDefs = ` + ${allStitchingDirectivesTypeDefs} + scalar _Key + + interface Entity { + id: ID + } + + type Query { + _entity(key: _Key): [Entity] @merge + } + + type User implements Entity @key(selectionSet: "{ id }") { + id: ID + name: String + } + `; + + const schema = makeExecutableSchema({ typeDefs }); + + const subschemaConfig = { + schema, + } + + const transformedSubschemaConfig = stitchingDirectivesTransformer(subschemaConfig); + + const keyFn = transformedSubschemaConfig.merge.User.key; + const argsFromKeysFn = transformedSubschemaConfig.merge.User.argsFromKeys; + + const originalResult = { + __typename: 'User', + id: '5', + email: 'email@email.com', + }; + + const key = keyFn(originalResult); + const args = argsFromKeysFn([key]); + + expect(key).toEqual({ + __typename: 'User', + id: '5', + }); + expect(args).toEqual({ + key: [{ + __typename: 'User', + id: '5', + }], + }); + }); + test('adds key and argsFromKeys functions with argsExpr argument using an unqualified key', () => { const typeDefs = ` ${allStitchingDirectivesTypeDefs} @@ -516,4 +678,67 @@ describe('type merging directives', () => { }], }); }); + + test('applies canonical merge attributions', () => { + const typeDefs = ` + ${allStitchingDirectivesTypeDefs} + + type User implements IUser @canonical { + id: ID + name: String @canonical + } + + interface IUser @canonical { + id: ID + name: String @canonical + } + + input UserInput @canonical { + id: ID + name: String @canonical + } + + enum UserEnum @canonical { + VALUE + } + + union UserUnion @canonical = User + + scalar Key @canonical + `; + + const schema = makeExecutableSchema({ typeDefs }); + const subschemaConfig = { schema }; + const transformedSubschemaConfig = stitchingDirectivesTransformer(subschemaConfig); + + expect(transformedSubschemaConfig.merge).toEqual({ + User: { + canonical: true, + fields: { + name: { canonical: true }, + } + }, + IUser: { + canonical: true, + fields: { + name: { canonical: true }, + } + }, + UserInput: { + canonical: true, + fields: { + name: { canonical: true }, + } + }, + UserEnum: { + canonical: true, + }, + UserUnion: { + canonical: true, + }, + Key: { + canonical: true, + } + }); + }); }); diff --git a/packages/utils/CHANGELOG.md b/packages/utils/CHANGELOG.md index 0a7b00a8e06..326dee8fda9 100644 --- a/packages/utils/CHANGELOG.md +++ b/packages/utils/CHANGELOG.md @@ -1,5 +1,13 @@ # @graphql-tools/utils +## 7.2.4 + +### Patch Changes + +- 6e50d9fc: enhance(stitching-directives): use keyField + + When using simple keys, i.e. when using the keyField argument to `@merge`, the keyField can be added implicitly to the types's key. In most cases, therefore, `@key` should not be required at all. + ## 7.2.3 ### Patch Changes diff --git a/packages/utils/package.json b/packages/utils/package.json index 7c3c679c5ab..4faaf935fb9 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@graphql-tools/utils", - "version": "7.2.3", + "version": "7.2.4", "description": "Common package containting utils and types for GraphQL tools", "repository": { "type": "git", @@ -27,7 +27,7 @@ "dependencies": { "@ardatan/aggregate-error": "0.0.6", "camel-case": "4.1.2", - "tslib": "~2.0.1" + "tslib": "~2.1.0" }, "publishConfig": { "access": "public", diff --git a/packages/utils/src/selectionSets.ts b/packages/utils/src/selectionSets.ts index b0b467133fa..6cbb72be594 100644 --- a/packages/utils/src/selectionSets.ts +++ b/packages/utils/src/selectionSets.ts @@ -1,6 +1,7 @@ import { OperationDefinitionNode, SelectionSetNode, parse } from 'graphql'; +import { GraphQLParseOptions } from './Interfaces'; -export function parseSelectionSet(selectionSet: string): SelectionSetNode { - const query = parse(selectionSet).definitions[0] as OperationDefinitionNode; +export function parseSelectionSet(selectionSet: string, options?: GraphQLParseOptions): SelectionSetNode { + const query = parse(selectionSet, options).definitions[0] as OperationDefinitionNode; return query.selectionSet; } diff --git a/packages/webpack-loader/package.json b/packages/webpack-loader/package.json index 6fdd081819f..4e217f08433 100644 --- a/packages/webpack-loader/package.json +++ b/packages/webpack-loader/package.json @@ -24,7 +24,7 @@ "dependencies": { "@graphql-tools/optimize": "1.0.1", "@graphql-tools/webpack-loader-runtime": "^6.2.4", - "tslib": "~2.0.1" + "tslib": "~2.1.0" }, "publishConfig": { "access": "public", diff --git a/packages/wrap/package.json b/packages/wrap/package.json index dd6ea973491..9dbcf43206d 100644 --- a/packages/wrap/package.json +++ b/packages/wrap/package.json @@ -26,7 +26,7 @@ "@graphql-tools/schema": "^7.1.2", "@graphql-tools/utils": "^7.2.1", "is-promise": "4.0.0", - "tslib": "~2.0.1" + "tslib": "~2.1.0" }, "publishConfig": { "access": "public", diff --git a/packages/wrap/tests/transformFilterInputObjectFields.test.ts b/packages/wrap/tests/transformFilterInputObjectFields.test.ts new file mode 100644 index 00000000000..44c050fe8bc --- /dev/null +++ b/packages/wrap/tests/transformFilterInputObjectFields.test.ts @@ -0,0 +1,69 @@ +import { wrapSchema, FilterInputObjectFields } from '@graphql-tools/wrap'; +import { makeExecutableSchema } from '@graphql-tools/schema'; +import { graphql, astFromValue, Kind, GraphQLString } from 'graphql'; + +describe('FilterInputObjectFields', () => { + test('filtering works', async () => { + const schema = makeExecutableSchema({ + typeDefs: ` + input InputObject { + field1: String + field2: String + } + + type OutputObject { + field1: String + field2: String + } + + type Query { + test(argument: InputObject): OutputObject + } + `, + resolvers: { + Query: { + test: (_root, args) => { + return args.argument; + } + } + } + }); + + const transformedSchema = wrapSchema({ + schema, + transforms: [ + new FilterInputObjectFields( + (typeName, fieldName) => (typeName !== 'InputObject' || fieldName !== 'field2'), + (typeName, inputObjectNode) => { + if (typeName === 'InputObject') { + return { + ...inputObjectNode, + fields: [...inputObjectNode.fields, { + kind: Kind.OBJECT_FIELD, + name: { + kind: Kind.NAME, + value: 'field2', + }, + value: astFromValue('field2', GraphQLString), + }], + }; + } + } + ) + ], + }); + + const query = `{ + test(argument: { + field1: "field1" + }) { + field1 + field2 + } + }`; + + const result = await graphql(transformedSchema, query); + expect(result.data.test.field1).toBe('field1'); + expect(result.data.test.field2).toBe('field2'); + }); +}); diff --git a/packages/wrap/tests/transformFilterToSchema.test.ts b/packages/wrap/tests/transformFilterToSchema.test.ts new file mode 100644 index 00000000000..10aa1a6b126 --- /dev/null +++ b/packages/wrap/tests/transformFilterToSchema.test.ts @@ -0,0 +1,109 @@ +import { print, parse } from 'graphql'; +import { FilterToSchema, DelegationContext } from '@graphql-tools/delegate'; +import { bookingSchema } from './fixtures/schemas'; + +// @todo: move this to the delegate package + +describe('FilterToSchema', () => { + let filter: FilterToSchema; + beforeAll(() => { + filter = new FilterToSchema(); + }); + + test('should remove empty selection sets on objects', () => { + const query = parse(` + query customerQuery($id: ID!) { + customerById(id: $id) { + id + name + address { + planet + } + } + } + `); + const filteredQuery = filter.transformRequest({ + document: query, + variables: { + id: 'c1', + } + }, { + targetSchema: bookingSchema + } as DelegationContext, {}); + + const expected = parse(` + query customerQuery($id: ID!) { + customerById(id: $id) { + id + name + } + } + `); + expect(print(filteredQuery.document)).toBe(print(expected)); + }); + + test('should also remove variables when removing empty selection sets', () => { + const query = parse(` + query customerQuery($id: ID!, $limit: Int) { + customerById(id: $id) { + id + name + bookings(limit: $limit) { + paid + } + } + } + `); + const filteredQuery = filter.transformRequest({ + document: query, + variables: { + id: 'c1', + limit: 10, + }, + }, { + targetSchema: bookingSchema + } as DelegationContext, {}); + + const expected = parse(` + query customerQuery($id: ID!) { + customerById(id: $id) { + id + name + } + } + `); + expect(print(filteredQuery.document)).toBe(print(expected)); + }); + + test('should remove empty selection sets on wrapped objects (non-nullable/lists)', () => { + const query = parse(` + query bookingQuery($id: ID!) { + bookingById(id: $id) { + id + propertyId + customer { + favoriteFood + } + } + } + `); + const filteredQuery = filter.transformRequest({ + document: query, + variables: { + id: 'b1', + }, + }, { + targetSchema: bookingSchema + } as DelegationContext, {}); + + const expected = parse(` + query bookingQuery($id: ID!) { + bookingById(id: $id) { + id + propertyId + } + } + `); + expect(print(filteredQuery.document)).toBe(print(expected)); + }); +}); \ No newline at end of file diff --git a/packages/wrap/tests/transformFilterTypes.test.ts b/packages/wrap/tests/transformFilterTypes.test.ts new file mode 100644 index 00000000000..bfde574fd8d --- /dev/null +++ b/packages/wrap/tests/transformFilterTypes.test.ts @@ -0,0 +1,70 @@ +import { wrapSchema, FilterTypes } from '@graphql-tools/wrap'; +import { graphql, GraphQLSchema, GraphQLNamedType } from 'graphql'; +import { bookingSchema } from './fixtures/schemas'; + +describe('FilterTypes', () => { + let schema: GraphQLSchema; + beforeAll(() => { + const typeNames = ['ID', 'String', 'DateTime', 'Query', 'Booking']; + const transforms = [ + new FilterTypes( + (type: GraphQLNamedType) => typeNames.indexOf(type.name) >= 0, + ), + ]; + schema = wrapSchema({ + schema: bookingSchema, + transforms + }); + }); + + test('should work normally', async () => { + const result = await graphql( + schema, + ` + query { + bookingById(id: "b1") { + id + propertyId + startTime + endTime + } + } + `, + ); + + expect(result).toEqual({ + data: { + bookingById: { + endTime: '2016-06-03', + id: 'b1', + propertyId: 'p1', + startTime: '2016-05-04', + }, + }, + }); + }); + + test('should error on removed types', async () => { + const result = await graphql( + schema, + ` + query { + bookingById(id: "b1") { + id + propertyId + startTime + endTime + customer { + id + } + } + } + `, + ); + expect(result.errors).toBeDefined(); + expect(result.errors.length).toBe(1); + expect(result.errors[0].message).toBe( + 'Cannot query field "customer" on type "Booking".', + ); + }); +}); diff --git a/packages/wrap/tests/transformMapLeafValues.test.ts b/packages/wrap/tests/transformMapLeafValues.test.ts new file mode 100644 index 00000000000..9850a6ad21d --- /dev/null +++ b/packages/wrap/tests/transformMapLeafValues.test.ts @@ -0,0 +1,54 @@ +import { wrapSchema, MapLeafValues } from '@graphql-tools/wrap'; +import { makeExecutableSchema } from '@graphql-tools/schema'; +import { graphql } from 'graphql'; + +describe('MapLeafValues', () => { + test('works', async () => { + const schema = makeExecutableSchema({ + typeDefs: ` + enum TestEnum { + ONE + TWO + THREE + } + + type Query { + testEnum(argument: TestEnum): TestEnum + testScalar(argument: Int): Int + } + `, + resolvers: { + Query: { + testEnum: (_root, { argument }) => argument, + testScalar: (_root, { argument }) => argument, + } + } + }); + + const valueIterator = (typeName: string, value: any) => { + if (typeName === 'TestEnum') { + return value === 'ONE' ? 'TWO' : value === 'TWO' ? 'THREE' : 'ONE'; + } else if (typeName === 'Int') { + return value + 5; + } else { + return value; + } + }; + + const transformedSchema = wrapSchema({ + schema, + transforms: [ + new MapLeafValues(valueIterator, valueIterator), + ], + }); + + const query = `{ + testEnum(argument: ONE) + testScalar(argument: 5) + }`; + + const result = await graphql(transformedSchema, query); + expect(result.data.testEnum).toBe('THREE'); + expect(result.data.testScalar).toBe(15); + }); +}); diff --git a/packages/wrap/tests/transformRenameInputObjectFields.test.ts b/packages/wrap/tests/transformRenameInputObjectFields.test.ts new file mode 100644 index 00000000000..3ebbe735dc4 --- /dev/null +++ b/packages/wrap/tests/transformRenameInputObjectFields.test.ts @@ -0,0 +1,121 @@ +import { wrapSchema, RenameInputObjectFields } from '@graphql-tools/wrap'; +import { makeExecutableSchema } from '@graphql-tools/schema'; +import { graphql } from 'graphql'; + +describe('RenameInputObjectFields', () => { + test('renaming with arguments works', async () => { + const schema = makeExecutableSchema({ + typeDefs: ` + input InputObject { + field1: String + field2: String + } + + type OutputObject { + field1: String + field2: String + } + + type Query { + test(argument: InputObject): OutputObject + } + `, + resolvers: { + Query: { + test: (_root, args) => { + return args.argument; + } + } + } + }); + + const transformedSchema = wrapSchema({ + schema, + transforms: [ + new RenameInputObjectFields( + (typeName: string, fieldName: string) => { + if (typeName === 'InputObject' && fieldName === 'field2') { + return 'field3'; + } + }, + ) + ], + }); + + const query = `{ + test(argument: { + field1: "field1" + field3: "field2" + }) { + field1 + field2 + } + }`; + + const result = await graphql(transformedSchema, query); + expect(result.data.test.field1).toBe('field1'); + expect(result.data.test.field2).toBe('field2'); + }); + + test('renaming with variables works', async () => { + const schema = makeExecutableSchema({ + typeDefs: ` + input InputObject { + field1: String + field2: String + nfield: [InputObject!], + } + + type OutputObject { + field1: String + field2: String + } + + type Query { + test(argument: InputObject): OutputObject + } + `, + resolvers: { + Query: { + test: (_root, args) => { + return args.argument; + } + } + } + }); + + const transformedSchema = wrapSchema({ + schema, + transforms: [ + new RenameInputObjectFields( + (typeName: string, fieldName: string) => { + if (typeName === 'InputObject' && fieldName === 'field2') { + return 'field3'; + } + }, + ) + ], + }); + + const query = `query ($argument: InputObject) { + test(argument: $argument) { + field1 + field2 + } + } + `; + const variables = { + argument: { + field1: "field1", + field3: "field2", + nfield: [{ + field1: "field1", + field3: "field2", + }] + } + } + const result = await graphql(transformedSchema, query, {}, {}, variables); + expect(result.data.test.field1).toBe('field1'); + expect(result.data.test.field2).toBe('field2'); + }); +}); diff --git a/packages/wrap/tests/transformRenameRootTypes.test.ts b/packages/wrap/tests/transformRenameRootTypes.test.ts new file mode 100644 index 00000000000..d0a7b7bd2cb --- /dev/null +++ b/packages/wrap/tests/transformRenameRootTypes.test.ts @@ -0,0 +1,61 @@ +import { wrapSchema, RenameRootTypes } from '@graphql-tools/wrap'; +import { makeExecutableSchema } from '@graphql-tools/schema'; +import { addMocksToSchema } from '@graphql-tools/mock'; +import { graphql } from 'graphql'; + +describe('RenameRootTypes', () => { + test('should work', async () => { + let subschema = makeExecutableSchema({ + typeDefs: ` + schema { + query: QueryRoot + mutation: MutationRoot + } + + type QueryRoot { + foo: String! + } + + type MutationRoot { + doSomething: DoSomethingPayload! + } + + type DoSomethingPayload { + query: QueryRoot! + } + `, + }); + + subschema = addMocksToSchema({ schema: subschema }); + + const schema = wrapSchema({ + schema: subschema, + transforms: [ + new RenameRootTypes((name) => (name === 'QueryRoot' ? 'Query' : name)), + ], + }); + + const result = await graphql( + schema, + ` + mutation { + doSomething { + query { + foo + } + } + } + `, + ); + + expect(result).toEqual({ + data: { + doSomething: { + query: { + foo: 'Hello World', + }, + }, + }, + }); + }); +}); diff --git a/packages/wrap/tests/transformRenameTypes.test.ts b/packages/wrap/tests/transformRenameTypes.test.ts new file mode 100644 index 00000000000..a5cfdda7b23 --- /dev/null +++ b/packages/wrap/tests/transformRenameTypes.test.ts @@ -0,0 +1,135 @@ +import { wrapSchema, RenameTypes } from '@graphql-tools/wrap'; +import { graphql, GraphQLSchema } from 'graphql'; +import { propertySchema } from './fixtures/schemas'; + +describe('RenameTypes', () => { + describe('rename type', () => { + let schema: GraphQLSchema; + beforeAll(() => { + const transforms = [ + new RenameTypes( + (name: string) => + ({ + Property: 'House', + Location: 'Spots', + TestInterface: 'TestingInterface', + DateTime: 'Datum', + InputWithDefault: 'DefaultingInput', + TestInterfaceKind: 'TestingInterfaceKinds', + TestImpl1: 'TestImplementation1', + }[name]), + ), + ]; + schema = wrapSchema({ + schema: propertySchema, + transforms, + }); + }); + test('should work', async () => { + const result = await graphql( + schema, + ` + query($input: DefaultingInput!) { + interfaceTest(kind: ONE) { + ... on TestingInterface { + testString + } + } + propertyById(id: "p1") { + ... on House { + id + } + } + dateTimeTest + defaultInputTest(input: $input) + } + `, + {}, + {}, + { + input: { + test: 'bar', + }, + }, + ); + + expect(result).toEqual({ + data: { + dateTimeTest: '1987-09-25T12:00:00', + defaultInputTest: 'bar', + interfaceTest: { + testString: 'test', + }, + propertyById: { + id: 'p1', + }, + }, + }); + }); + }); + + describe('namespacing', () => { + let schema: GraphQLSchema; + beforeAll(() => { + const transforms = [ + new RenameTypes((name: string) => `_${name}`), + new RenameTypes((name: string) => `Property${name}`), + ]; + schema = wrapSchema({ + schema: propertySchema, + transforms, + }); + }); + test('should work', async () => { + const result = await graphql( + schema, + ` + query($input: Property_InputWithDefault!) { + interfaceTest(kind: ONE) { + ... on Property_TestInterface { + testString + } + } + properties(limit: 1) { + __typename + id + } + propertyById(id: "p1") { + ... on Property_Property { + id + } + } + dateTimeTest + defaultInputTest(input: $input) + } + `, + {}, + {}, + { + input: { + test: 'bar', + }, + }, + ); + + expect(result).toEqual({ + data: { + dateTimeTest: '1987-09-25T12:00:00', + defaultInputTest: 'bar', + interfaceTest: { + testString: 'test', + }, + properties: [ + { + __typename: 'Property_Property', + id: 'p1', + }, + ], + propertyById: { + id: 'p1', + }, + }, + }); + }); + }); +}); diff --git a/packages/wrap/tests/transformTransformEnumValues.test.ts b/packages/wrap/tests/transformTransformEnumValues.test.ts new file mode 100644 index 00000000000..d5b2a935a5c --- /dev/null +++ b/packages/wrap/tests/transformTransformEnumValues.test.ts @@ -0,0 +1,114 @@ +import { wrapSchema, TransformEnumValues } from '@graphql-tools/wrap'; +import { makeExecutableSchema } from '@graphql-tools/schema'; +import { graphql, GraphQLEnumType } from 'graphql'; + +describe('TransformEnumValues', () => { + test('works', async () => { + const schema = makeExecutableSchema({ + typeDefs: ` + enum TestEnum { + ONE + } + + type Query { + test(argument: TestEnum): TestEnum + } + `, + resolvers: { + Query: { + test: (_root, { argument }) => argument, + } + } + }); + + const transformedSchema = wrapSchema({ + schema, + transforms: [ + new TransformEnumValues( + (_typeName, _externalValue, valueConfig) => ['UNO', valueConfig], + ) + ], + }); + + const query = `{ + test(argument: UNO) + }`; + + const result = await graphql(transformedSchema, query); + expect(result.errors).toBeUndefined(); + }); + + test('allows modification of external and internal values', async () => { + const schema = makeExecutableSchema({ + typeDefs: ` + enum TestEnum { + ONE + } + + type Query { + test(argument: TestEnum): TestEnum + } + `, + resolvers: { + Query: { + test: (_root, { argument }) => argument, + } + } + }); + + const transformedSchema = wrapSchema({ + schema, + transforms: [ + new TransformEnumValues( + (_typeName, _externalValue, valueConfig) => ['UNO', { + ...valueConfig, + value: 'ONE', + }], + ) + ], + }); + + const query = `{ + test(argument: UNO) + }`; + + const result = await graphql(transformedSchema, query); + expect(result.errors).toBeUndefined(); + expect((transformedSchema.getType('TestEnum') as GraphQLEnumType).getValue('UNO').value).toBe('ONE'); + }); + + test('works with variables', async () => { + const schema = makeExecutableSchema({ + typeDefs: ` + enum TestEnum { + ONE + } + + type Query { + test(argument: TestEnum): TestEnum + } + `, + resolvers: { + Query: { + test: (_root, { argument }) => argument, + } + } + }); + + const transformedSchema = wrapSchema({ + schema, + transforms: [ + new TransformEnumValues( + (_typeName, _externalValue, valueConfig) => ['UNO', valueConfig], + ) + ], + }); + + const query = `query Test($test: TestEnum) { + test(argument: $test) + }`; + + const result = await graphql(transformedSchema, query, undefined, undefined, { test: 'UNO' }); + expect(result.errors).toBeUndefined(); + }); +}); diff --git a/packages/wrap/tests/transformWrapQuery.test.ts b/packages/wrap/tests/transformWrapQuery.test.ts new file mode 100644 index 00000000000..ed39b79bcbb --- /dev/null +++ b/packages/wrap/tests/transformWrapQuery.test.ts @@ -0,0 +1,139 @@ +import { graphql, GraphQLSchema, Kind, SelectionSetNode } from 'graphql'; +import { makeExecutableSchema } from '@graphql-tools/schema'; +import { WrapQuery } from '@graphql-tools/wrap'; +import { delegateToSchema } from '@graphql-tools/delegate'; + +describe('WrapQuery', () => { + let data: any; + let subschema: GraphQLSchema; + let schema: GraphQLSchema; + beforeAll(() => { + data = { + u1: { + id: 'user1', + addressStreetAddress: 'Windy Shore 21 A 7', + addressZip: '12345', + }, + }; + subschema = makeExecutableSchema({ + typeDefs: ` + type User { + id: ID! + addressStreetAddress: String + addressZip: String + } + + type Query { + userById(id: ID!): User + } + `, + resolvers: { + Query: { + userById(_parent, { id }) { + return data[id]; + }, + }, + }, + }); + schema = makeExecutableSchema({ + typeDefs: ` + type User { + id: ID! + address: Address + } + + type Address { + streetAddress: String + zip: String + } + + type Query { + addressByUser(id: ID!): Address + } + `, + resolvers: { + Query: { + addressByUser(_parent, { id }, context, info) { + return delegateToSchema({ + schema: subschema, + operation: 'query', + fieldName: 'userById', + args: { id }, + context, + info, + transforms: [ + // Wrap document takes a subtree as an AST node + new WrapQuery( + // path at which to apply wrapping and extracting + ['userById'], + (subtree: SelectionSetNode) => { + const newSelectionSet = { + kind: Kind.SELECTION_SET, + selections: subtree.selections.map((selection) => { + // just append fragments, not interesting for this + // test + if ( + selection.kind === Kind.INLINE_FRAGMENT || + selection.kind === Kind.FRAGMENT_SPREAD + ) { + return selection; + } + // prepend `address` to name and camelCase + const oldFieldName = selection.name.value; + return { + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: + 'address' + + oldFieldName.charAt(0).toUpperCase() + + oldFieldName.slice(1), + }, + }; + }), + }; + return newSelectionSet; + }, + // how to process the data result at path + (result) => ({ + streetAddress: result.addressStreetAddress, + zip: result.addressZip, + }), + ), + // Wrap a second level field + new WrapQuery( + ['userById', 'zip'], + (subtree: SelectionSetNode) => subtree, + (result) => result, + ), + ], + }); + }, + }, + }, + }); + }); + + test('wrapping delegation, returning selectionSet', async () => { + const result = await graphql( + schema, + ` + query { + addressByUser(id: "u1") { + streetAddress + zip + } + } + `, + ); + + expect(result).toEqual({ + data: { + addressByUser: { + streetAddress: 'Windy Shore 21 A 7', + zip: '12345', + }, + }, + }); + }); +}); diff --git a/packages/wrap/tests/transforms.test.ts b/packages/wrap/tests/transforms.test.ts index d86cf8194ba..a070018be6d 100644 --- a/packages/wrap/tests/transforms.test.ts +++ b/packages/wrap/tests/transforms.test.ts @@ -1,43 +1,25 @@ import { GraphQLSchema, - GraphQLNamedType, GraphQLScalarType, graphql, Kind, SelectionSetNode, - print, - parse, - astFromValue, - GraphQLString, - GraphQLEnumType, } from 'graphql'; import { makeExecutableSchema } from '@graphql-tools/schema'; -import { addMocksToSchema } from '@graphql-tools/mock'; import { wrapSchema, - RenameTypes, - RenameRootTypes, - FilterTypes, WrapQuery, ExtractField, TransformQuery, - FilterInputObjectFields, - RenameInputObjectFields, - MapLeafValues, - TransformEnumValues, } from '@graphql-tools/wrap'; import { delegateToSchema, defaultMergedResolver, - FilterToSchema, - DelegationContext, } from '@graphql-tools/delegate'; -import { propertySchema, bookingSchema } from './fixtures/schemas'; - function createError(message: string, extra?: T) { const error = new Error(message); Object.assign(error, extra); @@ -161,364 +143,6 @@ describe('transforms', () => { }); }); - describe('rename type', () => { - let schema: GraphQLSchema; - beforeAll(() => { - const transforms = [ - new RenameTypes( - (name: string) => - ({ - Property: 'House', - Location: 'Spots', - TestInterface: 'TestingInterface', - DateTime: 'Datum', - InputWithDefault: 'DefaultingInput', - TestInterfaceKind: 'TestingInterfaceKinds', - TestImpl1: 'TestImplementation1', - }[name]), - ), - ]; - schema = wrapSchema({ - schema: propertySchema, - transforms, - }); - }); - test('should work', async () => { - const result = await graphql( - schema, - ` - query($input: DefaultingInput!) { - interfaceTest(kind: ONE) { - ... on TestingInterface { - testString - } - } - propertyById(id: "p1") { - ... on House { - id - } - } - dateTimeTest - defaultInputTest(input: $input) - } - `, - {}, - {}, - { - input: { - test: 'bar', - }, - }, - ); - - expect(result).toEqual({ - data: { - dateTimeTest: '1987-09-25T12:00:00', - defaultInputTest: 'bar', - interfaceTest: { - testString: 'test', - }, - propertyById: { - id: 'p1', - }, - }, - }); - }); - }); - - describe('rename root type', () => { - test('should work', async () => { - let subschema = makeExecutableSchema({ - typeDefs: ` - schema { - query: QueryRoot - mutation: MutationRoot - } - - type QueryRoot { - foo: String! - } - - type MutationRoot { - doSomething: DoSomethingPayload! - } - - type DoSomethingPayload { - query: QueryRoot! - } - `, - }); - - subschema = addMocksToSchema({ schema: subschema }); - - const schema = wrapSchema({ - schema: subschema, - transforms: [ - new RenameRootTypes((name) => (name === 'QueryRoot' ? 'Query' : name)), - ], - }); - - const result = await graphql( - schema, - ` - mutation { - doSomething { - query { - foo - } - } - } - `, - ); - - expect(result).toEqual({ - data: { - doSomething: { - query: { - foo: 'Hello World', - }, - }, - }, - }); - }); -}); - - describe('namespace', () => { - let schema: GraphQLSchema; - beforeAll(() => { - const transforms = [ - new RenameTypes((name: string) => `_${name}`), - new RenameTypes((name: string) => `Property${name}`), - ]; - schema = wrapSchema({ - schema: propertySchema, - transforms, - }); - }); - test('should work', async () => { - const result = await graphql( - schema, - ` - query($input: Property_InputWithDefault!) { - interfaceTest(kind: ONE) { - ... on Property_TestInterface { - testString - } - } - properties(limit: 1) { - __typename - id - } - propertyById(id: "p1") { - ... on Property_Property { - id - } - } - dateTimeTest - defaultInputTest(input: $input) - } - `, - {}, - {}, - { - input: { - test: 'bar', - }, - }, - ); - - expect(result).toEqual({ - data: { - dateTimeTest: '1987-09-25T12:00:00', - defaultInputTest: 'bar', - interfaceTest: { - testString: 'test', - }, - properties: [ - { - __typename: 'Property_Property', - id: 'p1', - }, - ], - propertyById: { - id: 'p1', - }, - }, - }); - }); - }); - - describe('filter to schema', () => { - let filter: FilterToSchema; - beforeAll(() => { - filter = new FilterToSchema(); - }); - - test('should remove empty selection sets on objects', () => { - const query = parse(` - query customerQuery($id: ID!) { - customerById(id: $id) { - id - name - address { - planet - } - } - } - `); - const filteredQuery = filter.transformRequest({ - document: query, - variables: { - id: 'c1', - } - }, { - targetSchema: bookingSchema - } as DelegationContext, {}); - - const expected = parse(` - query customerQuery($id: ID!) { - customerById(id: $id) { - id - name - } - } - `); - expect(print(filteredQuery.document)).toBe(print(expected)); - }); - - test('should also remove variables when removing empty selection sets', () => { - const query = parse(` - query customerQuery($id: ID!, $limit: Int) { - customerById(id: $id) { - id - name - bookings(limit: $limit) { - paid - } - } - } - `); - const filteredQuery = filter.transformRequest({ - document: query, - variables: { - id: 'c1', - limit: 10, - }, - }, { - targetSchema: bookingSchema - } as DelegationContext, {}); - - const expected = parse(` - query customerQuery($id: ID!) { - customerById(id: $id) { - id - name - } - } - `); - expect(print(filteredQuery.document)).toBe(print(expected)); - }); - - test('should remove empty selection sets on wrapped objects (non-nullable/lists)', () => { - const query = parse(` - query bookingQuery($id: ID!) { - bookingById(id: $id) { - id - propertyId - customer { - favoriteFood - } - } - } - `); - const filteredQuery = filter.transformRequest({ - document: query, - variables: { - id: 'b1', - }, - }, { - targetSchema: bookingSchema - } as DelegationContext, {}); - - const expected = parse(` - query bookingQuery($id: ID!) { - bookingById(id: $id) { - id - propertyId - } - } - `); - expect(print(filteredQuery.document)).toBe(print(expected)); - }); - }); - - describe('filter type', () => { - let schema: GraphQLSchema; - beforeAll(() => { - const typeNames = ['ID', 'String', 'DateTime', 'Query', 'Booking']; - const transforms = [ - new FilterTypes( - (type: GraphQLNamedType) => typeNames.indexOf(type.name) >= 0, - ), - ]; - schema = wrapSchema({ - schema: bookingSchema, - transforms - }); - }); - - test('should work normally', async () => { - const result = await graphql( - schema, - ` - query { - bookingById(id: "b1") { - id - propertyId - startTime - endTime - } - } - `, - ); - - expect(result).toEqual({ - data: { - bookingById: { - endTime: '2016-06-03', - id: 'b1', - propertyId: 'p1', - startTime: '2016-05-04', - }, - }, - }); - }); - - test('should error on removed types', async () => { - const result = await graphql( - schema, - ` - query { - bookingById(id: "b1") { - id - propertyId - startTime - endTime - customer { - id - } - } - } - `, - ); - expect(result.errors).toBeDefined(); - expect(result.errors.length).toBe(1); - expect(result.errors[0].message).toBe( - 'Cannot query field "customer" on type "Booking".', - ); - }); - }); - describe('tree operations', () => { let data: any; let subschema: GraphQLSchema; @@ -781,140 +405,6 @@ describe('transforms', () => { }); }); }); - describe('WrapQuery', () => { - let data: any; - let subschema: GraphQLSchema; - let schema: GraphQLSchema; - beforeAll(() => { - data = { - u1: { - id: 'user1', - addressStreetAddress: 'Windy Shore 21 A 7', - addressZip: '12345', - }, - }; - subschema = makeExecutableSchema({ - typeDefs: ` - type User { - id: ID! - addressStreetAddress: String - addressZip: String - } - - type Query { - userById(id: ID!): User - } - `, - resolvers: { - Query: { - userById(_parent, { id }) { - return data[id]; - }, - }, - }, - }); - schema = makeExecutableSchema({ - typeDefs: ` - type User { - id: ID! - address: Address - } - - type Address { - streetAddress: String - zip: String - } - - type Query { - addressByUser(id: ID!): Address - } - `, - resolvers: { - Query: { - addressByUser(_parent, { id }, context, info) { - return delegateToSchema({ - schema: subschema, - operation: 'query', - fieldName: 'userById', - args: { id }, - context, - info, - transforms: [ - // Wrap document takes a subtree as an AST node - new WrapQuery( - // path at which to apply wrapping and extracting - ['userById'], - (subtree: SelectionSetNode) => { - const newSelectionSet = { - kind: Kind.SELECTION_SET, - selections: subtree.selections.map((selection) => { - // just append fragments, not interesting for this - // test - if ( - selection.kind === Kind.INLINE_FRAGMENT || - selection.kind === Kind.FRAGMENT_SPREAD - ) { - return selection; - } - // prepend `address` to name and camelCase - const oldFieldName = selection.name.value; - return { - kind: Kind.FIELD, - name: { - kind: Kind.NAME, - value: - 'address' + - oldFieldName.charAt(0).toUpperCase() + - oldFieldName.slice(1), - }, - }; - }), - }; - return newSelectionSet; - }, - // how to process the data result at path - (result) => ({ - streetAddress: result.addressStreetAddress, - zip: result.addressZip, - }), - ), - // Wrap a second level field - new WrapQuery( - ['userById', 'zip'], - (subtree: SelectionSetNode) => subtree, - (result) => result, - ), - ], - }); - }, - }, - }, - }); - }); - - test('wrapping delegation, returning selectionSet', async () => { - const result = await graphql( - schema, - ` - query { - addressByUser(id: "u1") { - streetAddress - zip - } - } - `, - ); - - expect(result).toEqual({ - data: { - addressByUser: { - streetAddress: 'Windy Shore 21 A 7', - zip: '12345', - }, - }, - }); - }); - }); describe('TransformQuery', () => { let data: any; @@ -1149,347 +639,3 @@ describe('transforms', () => { }); }); }); - -describe('transform input object fields', () => { - test('filtering works', async () => { - const schema = makeExecutableSchema({ - typeDefs: ` - input InputObject { - field1: String - field2: String - } - - type OutputObject { - field1: String - field2: String - } - - type Query { - test(argument: InputObject): OutputObject - } - `, - resolvers: { - Query: { - test: (_root, args) => { - return args.argument; - } - } - } - }); - - const transformedSchema = wrapSchema({ - schema, - transforms: [ - new FilterInputObjectFields( - (typeName, fieldName) => (typeName !== 'InputObject' || fieldName !== 'field2'), - (typeName, inputObjectNode) => { - if (typeName === 'InputObject') { - return { - ...inputObjectNode, - fields: [...inputObjectNode.fields, { - kind: Kind.OBJECT_FIELD, - name: { - kind: Kind.NAME, - value: 'field2', - }, - value: astFromValue('field2', GraphQLString), - }], - }; - } - } - ) - ], - }); - - const query = `{ - test(argument: { - field1: "field1" - }) { - field1 - field2 - } - }`; - - const result = await graphql(transformedSchema, query); - expect(result.data.test.field1).toBe('field1'); - expect(result.data.test.field2).toBe('field2'); - }); - - test('renaming with arguments works', async () => { - const schema = makeExecutableSchema({ - typeDefs: ` - input InputObject { - field1: String - field2: String - } - - type OutputObject { - field1: String - field2: String - } - - type Query { - test(argument: InputObject): OutputObject - } - `, - resolvers: { - Query: { - test: (_root, args) => { - return args.argument; - } - } - } - }); - - const transformedSchema = wrapSchema({ - schema, - transforms: [ - new RenameInputObjectFields( - (typeName: string, fieldName: string) => { - if (typeName === 'InputObject' && fieldName === 'field2') { - return 'field3'; - } - }, - ) - ], - }); - - const query = `{ - test(argument: { - field1: "field1" - field3: "field2" - }) { - field1 - field2 - } - }`; - - const result = await graphql(transformedSchema, query); - expect(result.data.test.field1).toBe('field1'); - expect(result.data.test.field2).toBe('field2'); - }); - - test('renaming with variables works', async () => { - const schema = makeExecutableSchema({ - typeDefs: ` - input InputObject { - field1: String - field2: String - nfield: [InputObject!], - } - - type OutputObject { - field1: String - field2: String - } - - type Query { - test(argument: InputObject): OutputObject - } - `, - resolvers: { - Query: { - test: (_root, args) => { - return args.argument; - } - } - } - }); - - const transformedSchema = wrapSchema({ - schema, - transforms: [ - new RenameInputObjectFields( - (typeName: string, fieldName: string) => { - if (typeName === 'InputObject' && fieldName === 'field2') { - return 'field3'; - } - }, - ) - ], - }); - - const query = `query ($argument: InputObject) { - test(argument: $argument) { - field1 - field2 - } - } - `; - const variables = { - argument: { - field1: "field1", - field3: "field2", - nfield: [{ - field1: "field1", - field3: "field2", - }] - } - } - const result = await graphql(transformedSchema, query, {}, {}, variables); - expect(result.data.test.field1).toBe('field1'); - expect(result.data.test.field2).toBe('field2'); - }); - -}); - -describe('MapLeafValues', () => { - test('works', async () => { - const schema = makeExecutableSchema({ - typeDefs: ` - enum TestEnum { - ONE - TWO - THREE - } - - type Query { - testEnum(argument: TestEnum): TestEnum - testScalar(argument: Int): Int - } - `, - resolvers: { - Query: { - testEnum: (_root, { argument }) => argument, - testScalar: (_root, { argument }) => argument, - } - } - }); - - const valueIterator = (typeName: string, value: any) => { - if (typeName === 'TestEnum') { - return value === 'ONE' ? 'TWO' : value === 'TWO' ? 'THREE' : 'ONE'; - } else if (typeName === 'Int') { - return value + 5; - } else { - return value; - } - }; - - const transformedSchema = wrapSchema({ - schema, - transforms: [ - new MapLeafValues(valueIterator, valueIterator), - ], - }); - - const query = `{ - testEnum(argument: ONE) - testScalar(argument: 5) - }`; - - const result = await graphql(transformedSchema, query); - expect(result.data.testEnum).toBe('THREE'); - expect(result.data.testScalar).toBe(15); - }); -}); - -describe('TransformEnumValues', () => { - test('works', async () => { - const schema = makeExecutableSchema({ - typeDefs: ` - enum TestEnum { - ONE - } - - type Query { - test(argument: TestEnum): TestEnum - } - `, - resolvers: { - Query: { - test: (_root, { argument }) => argument, - } - } - }); - - const transformedSchema = wrapSchema({ - schema, - transforms: [ - new TransformEnumValues( - (_typeName, _externalValue, valueConfig) => ['UNO', valueConfig], - ) - ], - }); - - const query = `{ - test(argument: UNO) - }`; - - const result = await graphql(transformedSchema, query); - expect(result.errors).toBeUndefined(); - }); - test('allows modification of external and internal values', async () => { - const schema = makeExecutableSchema({ - typeDefs: ` - enum TestEnum { - ONE - } - - type Query { - test(argument: TestEnum): TestEnum - } - `, - resolvers: { - Query: { - test: (_root, { argument }) => argument, - } - } - }); - - const transformedSchema = wrapSchema({ - schema, - transforms: [ - new TransformEnumValues( - (_typeName, _externalValue, valueConfig) => ['UNO', { - ...valueConfig, - value: 'ONE', - }], - ) - ], - }); - - const query = `{ - test(argument: UNO) - }`; - - const result = await graphql(transformedSchema, query); - expect(result.errors).toBeUndefined(); - expect((transformedSchema.getType('TestEnum') as GraphQLEnumType).getValue('UNO').value).toBe('ONE'); - }); - - test('works with variables', async () => { - const schema = makeExecutableSchema({ - typeDefs: ` - enum TestEnum { - ONE - } - - type Query { - test(argument: TestEnum): TestEnum - } - `, - resolvers: { - Query: { - test: (_root, { argument }) => argument, - } - } - }); - - const transformedSchema = wrapSchema({ - schema, - transforms: [ - new TransformEnumValues( - (_typeName, _externalValue, valueConfig) => ['UNO', valueConfig], - ) - ], - }); - - const query = `query Test($test: TestEnum) { - test(argument: $test) - }`; - - const result = await graphql(transformedSchema, query, undefined, undefined, { test: 'UNO' }); - expect(result.errors).toBeUndefined(); - }); -}); diff --git a/scripts/build-api-docs.js b/scripts/build-api-docs.js index ae237ca451c..52b6e1d7b19 100644 --- a/scripts/build-api-docs.js +++ b/scripts/build-api-docs.js @@ -4,149 +4,154 @@ const rimraf = require('rimraf'); const TypeDoc = require('typedoc'); const { execSync } = require('child_process'); -// Where to generate the API docs -const outputDir = path.join(__dirname, '../website/docs/api'); -// sidebars.json -const sidebarsTemplatePath = path.join(__dirname, '../website/sidebars.template.json'); -const sidebarsPath = path.join(__dirname, '../website/sidebars.json'); - -// Get the upstream git remote -- we don't want to assume it exists or is named "upstream" -const gitRemote = execSync('git remote -v', { encoding: 'utf-8' }) - .split('\n') - .map(line => line.split('\t')) - .find( - ([_name, description]) => - description.includes('(fetch)') - ); -const gitRemoteName = gitRemote && gitRemote[0]; -if (!gitRemoteName) { - console.log('Unable to locate upstream git remote'); - process.exit(1); -} +const MONOREPO = 'graphql-tools'; -// An array of tuples where the first element is the package's name and the -// the second element is the relative path to the package's entry point -const workspacePackageJson = require('../package.json'); -const { join } = require('path'); -const packageJsonFiles = require('globby').sync(workspacePackageJson.workspaces.map(f => `${f}/package.json`)); -const modules = []; -for (const packageJsonPath of packageJsonFiles) { - const packageJsonContent = require(join(__dirname, '..', packageJsonPath)); - if (!packageJsonContent.private) { - modules.push([packageJsonContent.name, packageJsonPath.replace('./', '').replace('package.json', 'src/index.ts')]); +async function buildApiDocs() { + // Where to generate the API docs + const outputDir = path.join(__dirname, '../website/docs/api'); + const sidebarsPath = path.join(__dirname, '../website/api-sidebar.json'); + + // Get the upstream git remote -- we don't want to assume it exists or is named "upstream" + const gitRemote = execSync('git remote -v', { encoding: 'utf-8' }) + .split('\n') + .map(line => line.split('\t')) + .find(([_name, description]) => description.includes('(fetch)')); + const gitRemoteName = gitRemote && gitRemote[0]; + if (!gitRemoteName) { + console.log('Unable to locate upstream git remote'); + process.exit(1); } -} -// Delete existing docs -rimraf.sync(outputDir); - -// Initialize TypeDoc -const typeDoc = new TypeDoc.Application(); - -typeDoc.options.addReader(new TypeDoc.TSConfigReader()); - -typeDoc.bootstrap({ - mode: 'library', - logger: 'none', - theme: 'docusaurus2', - ignoreCompilerErrors: true, - excludePrivate: true, - excludeProtected: true, - stripInternal: true, - readme: 'none', - hideGenerator: true, - hideBreadcrumbs: true, - skipSidebar: true, - gitRemote: gitRemoteName, - gitRevision: 'master', -}); + // An array of tuples where the first element is the package's name and the + // the second element is the relative path to the package's entry point + const workspacePackageJson = require('../package.json'); + const packageJsonFiles = require('globby').sync(workspacePackageJson.workspaces.map(f => `${f}/package.json`)); + const modules = []; + for (const packageJsonPath of packageJsonFiles) { + const packageJsonContent = require(path.join(__dirname, '..', packageJsonPath)); + // Do not include private and large npm package that contains rest + if (!packageJsonContent.private && packageJsonContent.name !== MONOREPO) { + modules.push([ + packageJsonContent.name, + packageJsonPath.replace('./', '').replace('package.json', 'src/index.ts'), + ]); + } + } + + // Delete existing docs + rimraf.sync(outputDir); -// Generate the API docs -const project = typeDoc.convert(typeDoc.expandInputFiles(modules.map(([_name, filePath]) => filePath))); -typeDoc.generateDocs(project, outputDir); - -// Patch the generated markdown -// See https://github.com/tgreyuk/typedoc-plugin-markdown/pull/128 -['classes', 'enums', 'interfaces', 'modules'].forEach(dirName => { - fs.readdirSync(path.join(outputDir, dirName)).forEach(fileName => { - const filePath = path.join(outputDir, dirName, fileName); - const contents = fs - .readFileSync(filePath, 'utf-8') - // Escape angle brackets - .replace(//g, '>') - // Fix links - .replace(/\[([^\]]+)\]\(([^)]+).md\)/g, '[$1]($2)') - .replace(/\[([^\]]+)\]\((\.\.\/(classes|interfaces|enums)\/([^\)]+))\)/g, '[$1](/docs/api/$3/$4)'); - fs.writeFileSync(filePath, contents); + // Initialize TypeDoc + const typeDoc = new TypeDoc.Application(); + + typeDoc.options.addReader(new TypeDoc.TSConfigReader()); + + typeDoc.bootstrap({ + // mode: 'library', + theme: path.resolve(__dirname, 'typedoc-theme'), + // ignoreCompilerErrors: true, + excludePrivate: true, + excludeProtected: true, + // stripInternal: true, + readme: 'none', + hideGenerator: true, + hideBreadcrumbs: true, + // skipSidebar: true, + gitRemote: gitRemoteName, + gitRevision: 'master', + tsconfig: path.resolve(__dirname, '../tsconfig.build.json'), + entryPoints: modules.map(([_name, filePath]) => filePath), }); -}); -// Remove the generated "index.md" file -// fs.unlinkSync(path.join(outputDir, 'index.md')); + // Generate the API docs + const project = typeDoc.convert(typeDoc.expandInputFiles(modules.map(([_name, filePath]) => filePath))); + await typeDoc.generateDocs(project, outputDir); -// Update each module 's frontmatter and title -modules.forEach(([name, originalFilePath]) => { - const filePath = path.join(outputDir, 'modules', convertEntryFilePath(originalFilePath)); - if (!fs.existsSync(filePath)) { - console.warn(`Module ${name} not found!`); - return; - } - const id = convertNameToId(name); - const oldContent = fs.readFileSync(filePath, 'utf-8'); - const necessaryPart = oldContent.split('\n').slice(5).join('\n'); - const finalContent = ` + // Patch the generated markdown + // See https://github.com/tgreyuk/typedoc-plugin-markdown/pull/128 + ['classes', 'enums', 'interfaces', 'modules'].forEach(dirName => { + fs.readdirSync(path.join(outputDir, dirName)).forEach(fileName => { + const filePath = path.join(outputDir, dirName, fileName); + const contents = fs + .readFileSync(filePath, 'utf-8') + // Escape angle brackets + .replace(//g, '>') + // Fix links + .replace(/\[([^\]]+)\]\(([^)]+).md\)/g, '[$1]($2)') + .replace(/\[([^\]]+)\]\((\.\.\/(classes|interfaces|enums)\/([^\)]+))\)/g, '[$1](/docs/api/$3/$4)'); + fs.writeFileSync(filePath, contents); + }); + }); + + // Remove the generated "index.md" file + // fs.unlinkSync(path.join(outputDir, 'index.md')); + + // Update each module 's frontmatter and title + modules.forEach(([name, originalFilePath]) => { + const filePath = path.join(outputDir, 'modules', convertEntryFilePath(originalFilePath)); + if (!fs.existsSync(filePath)) { + console.warn(`Module ${name} not found!`); + return; + } + const id = convertNameToId(name); + const oldContent = fs.readFileSync(filePath, 'utf-8'); + const necessaryPart = oldContent.split('\n').slice(5).join('\n'); + const finalContent = + ` --- id: "${id}" title: "${name}" sidebar_label: "${id}" --- `.substring(1) + necessaryPart; - fs.writeFileSync( - filePath, - finalContent - ); -}); + fs.writeFileSync(filePath, finalContent); + }); -// Update sidebars.json -const sidebars = require(sidebarsTemplatePath); -sidebars.someSidebar.find(category => category['API Reference'])['API Reference'] = [ - { - Modules: modules.map(([name]) => `api/modules/${convertNameToId(name)}`), - }, - { - Classes: getSidebarItemsByDirectory('classes'), - }, - { - Interfaces: getSidebarItemsByDirectory('interfaces'), - }, - { - Enums: getSidebarItemsByDirectory('enums'), - }, -]; -fs.writeFileSync(sidebarsPath, JSON.stringify(sidebars, null, 2)); - -function convertEntryFilePath(filePath) { - const { dir, name } = path.parse(filePath); - return `_${dir.split('/').join('_').replace(/-/g, '_')}_${name}_.md`; -} + fs.writeFileSync(sidebarsPath, JSON.stringify([ + { + Modules: modules.map(([name]) => `api/modules/${convertNameToId(name)}`), + }, + { + Classes: getSidebarItemsByDirectory('classes'), + }, + { + Interfaces: getSidebarItemsByDirectory('interfaces'), + }, + { + Enums: getSidebarItemsByDirectory('enums'), + }, + ], null, 2)); -function convertNameToId(name) { - return name.replace(/@graphql-tools\//g, ''); -} + function convertEntryFilePath(filePath) { + const { dir, name } = path.parse(filePath); + return `_${dir.split('/').join('_').replace(/-/g, '_')}_${name}_.md` + .replace('_index_', '') + .replace('_packages_', ''); + } -function getSidebarItemsByDirectory(dirName) { - return fs - .readdirSync(path.join(outputDir, dirName)) - .map(fileName => `api/${dirName}/${path.parse(fileName).name}`) - .sort((a, b) => { - const aName = a.split('.').pop(); - const bName = b.split('.').pop(); - if (aName < bName) { - return -1; - } else if (aName > bName) { - return 1; - } - return 0; - }); + function convertNameToId(name) { + return name.replace(`@${MONOREPO}/`, ''); + } + + function getSidebarItemsByDirectory(dirName) { + return fs + .readdirSync(path.join(outputDir, dirName)) + .map(fileName => `api/${dirName}/${path.parse(fileName).name}`) + .sort((a, b) => { + const aName = a.split('.').pop(); + const bName = b.split('.').pop(); + if (aName < bName) { + return -1; + } else if (aName > bName) { + return 1; + } + return 0; + }); + } } + +buildApiDocs().catch(e => { + console.error(e); + process.exit(1); +}); diff --git a/scripts/typedoc-theme/theme.js b/scripts/typedoc-theme/theme.js new file mode 100644 index 00000000000..e1d6a37207c --- /dev/null +++ b/scripts/typedoc-theme/theme.js @@ -0,0 +1,34 @@ +const MarkdownTheme = require('typedoc-plugin-markdown/dist/theme'); +const { PageEvent } = require('typedoc/dist/lib/output/events'); + +exports.default = class DocusaurusTheme extends MarkdownTheme { + /** + * Escape characters for mdx support after render + */ + static formatContents(contents) { + return contents.replace(/\\ -Any additional operation [transforms](/docs/schema-wrapping/) to apply to the query and results. Transforms are specified similarly to the transforms used in conjunction with schema wrapping, but only the operational components of transforms will be used by `delegateToSchema`, i.e. any specified `transformRequest` and `transformResult` functions. +Any additional operation [transforms](/docs/schema-wrapping/) to apply to the query and results. Transforms are specified similarly to the transforms used in conjunction with schema wrapping, but only the operational components of transforms will be used by `delegateToSchema`, i.e. any specified `transformRequest` and `transformResult` functions. The following transforms are automatically applied during schema delegation to translate between source and target types and fields: + +* `ExpandAbstractTypes`: If an abstract type within a document does not exist within the target schema, expand the type to each and any of its implementations that do exist. +* `FilterToSchema`: Remove all fields, variables and fragments for types that don't exist within the target schema. +* `AddTypenameToAbstract`: Add `__typename` to all abstract types in the document, necessary for type resolution of interfaces within the source schema to work. +* `CheckResultAndHandleErrors`: Given a result from a subschema, propagate errors so that they match the correct subfield. Also provide the correct key if aliases are used. +* `AddSelectionSets`: activated by schema stitching to add selection sets into outgoing requests from the gateway schema. These selections collect key fields used to perform queries for related records from other subservices. diff --git a/website/docs/schema-wrapping.md b/website/docs/schema-wrapping.md index 27c05f66bcf..042d2d7d98f 100644 --- a/website/docs/schema-wrapping.md +++ b/website/docs/schema-wrapping.md @@ -6,239 +6,195 @@ description: Wrap schemas to automatically modify schemas, requests and results Schema wrapping (`@graphql-tools/wrap`) creates a modified version of a schema that proxies, or "wraps", the original unmodified schema. This technique is particularily useful when the original schema _cannot_ be changed, such as with [remote schemas](/docs/remote-schemas/). -Schema wrapping works by wrapping the original schema in a new 'gateway' schema that simply delegates all operations to the original subschema. A series of 'transforms' are applied to modify the schema after the initial wrapping is complete. Each transform includes a schema transformation function that changes the gateway schema. It may also include operation transforms, i.e. functions that either modify the operation prior to delegation or modify the result prior to its return. +Schema wrapping works by creating a new "gateway" schema that simply delegates all operations to the original subschema. A series of _transforms_ are applied that may modify the shape of the gateway schema and all proxied operations; these operational transforms may modify an operation prior to delegation, or modify the subschema result prior to its return. ## Getting started -For example, let's consider changing the name of the type in a simple schema. Imagine we've written a function that takes a `GraphQLSchema` and replaces all instances of type `Test` with `NewTest`. +Let's consider changing the name of a type in a simple schema. In this example, we'd like to replace all instances of type `Widget` with `NewWidget`. ```graphql -# old schema -type Test { +# original subschema +type Widget { id: ID! name: String } type Query { - returnTest: Test + widget: Widget } -# new schema - -type NewTest { +# wrapping gateway schema +type NewWidget { id: ID! name: String } type Query { - returnTest: NewTest + widget: NewWidget } ``` -On delegation to the original subschema, we want the `NewTest` type to be automatically mapped to the old `Test` type. - -At first glance, it might seem as though most queries work the same way as before: +Upon delegation to the original subschema, we want the `NewWidget` type to be mapped to the underlying `Widget` type. At first glance, it might seem as though most queries will work the same as before: ```graphql query { - returnTest { + widget { id name } } ``` -Since the fields of the type have not changed, delegating to the old schema is relatively easy here. - -However, the new name begins to matter more when fragments and variables are used: +Since the fields of the type have not changed, delegating to the original subschema is relatively easy here. However, the new name begins to matter when fragments and variables are used: ```graphql query { - returnTest { + widget { id - ... on NewTest { + ... on NewWidget { name } } } ``` -Since the `NewTest` type did not exist on old schema, this fragment will not match anything in the old schema, so it will be filtered out during delegation. - -What we need is a `transformRequest` function that knows how to rename any occurrences of `NewTest` to `Test` before delegating to the old schema. +Since the `NewWidget` type does not exist in the original subschema, this fragment will not match anything there and gets filtered out during delegation. This problem is solved by operational transforms: -By the same reasoning, we also need a `transformResult` function, because any results contain a `__typename` field whose value is `Test`, that name needs to be updated to `NewTest` in the final result. +- **transformRequest**: a function that renames occurrences of `NewWidget -> Widget` before delegating to the original subschema. +- **transformResult**: a function that conversely renames returned `__typename` fields `Widget -> NewWidget` in the final result. -## API - -### Transform - -```ts -export interface Transform> { - transformSchema?: SchemaTransform; - transformRequest?: RequestTransform; - transformResult?: ResultTransform; -} +Conveniently, this task of renaming types is very common and there's a built-in transform available for it. Using the built-in transform with a call to `wrapSchema` gets the job done: -export type SchemaTransform = ( - originalWrappingSchema: GraphQLSchema, - subschemaConfig: SubschemaConfig, - transformedSchema?: GraphQLSchema -) => GraphQLSchema; - -export type RequestTransform> = ( - originalRequest: Request, - delegationContext: DelegationContext, - transformationContext: T -) => Request; - -export type ResultTransform> = ( - originalResult: ExecutionResult, - delegationContext: DelegationContext, - transformationContext: T -) => ExecutionResult; +```js +const { wrapSchema, RenameTypes } = require('@graphql-tools/wrap'); -type Request = { - document: DocumentNode; - variables: Record; - extensions?: Record; +const typeNameMap = { + Widget: 'NewWidget', }; -``` - -### wrapSchema - -Given a `GraphQLSchema` and an array of `Transform` objects, `wrapSchema` produces a new schema with the `transformSchema` methods applied. - -Delegating resolvers are generated to map from new schema root fields to old schema root fields. These automatic resolvers should be sufficient, so you don't have to implement your own. - -The delegating resolvers will apply the operation transforms defined by the `Transform` objects. Each provided `transformRequest` functions will be applies in reverse order, until the request matches the original schema. The `tranformResult` functions will be applied in the opposite order until the result matches the final gateway schema. - -In advanced cases, transforms may wish to create additional delegating root resolvers (for example, when hoisting a field into a root type). This is also possible. The wrapping schema is actually generated twice -- the first run results in a possibly non-executable version, while the second execution also includes the result of the first one within the `transformedSchema` argument so that an executable version with any new proxying resolvers can be created. -Remote schemas can also be wrapped! In fact, this is the primary use case. See documentation regarding [remote schemas](/docs/remote-schemas/) for further details about remote schemas. Note that as explained there, when wrapping remote schemas, you will be wrapping a subschema config object, and the array of transforms should be defined on that object rather than as a second argument to `wrapSchema`. +const schema = wrapSchema({ + schema: originalSchema, + transforms: [new RenameTypes((name) => typeNameMap[name] || name)] +}); +``` ## Built-in transforms -Built-in transforms are ready-made classes implementing the `Transform` interface. They are intended to cover many of the most common schema transformation use cases, but they also serve as examples of how to implement transforms for your own needs. +These are ready-made classes implementing the `Transform` interface. They are intended to cover many common use cases, and they may also serve as examples of how to implement your own [custom transforms](#custom-transforms). -### Modifying types +### Filtering -* `FilterTypes(filter: (type: GraphQLNamedType) => boolean)`: Remove all types for which the `filter` function returns `false`. +Filter transforms are constructed with a filter function that returns a boolean. The transform executes the filter on each schema element within its scope, and rejects elements that do not pass the filter. -* `RenameTypes(renamer, options?)`: Rename types by applying `renamer` to each type name. If `renamer` returns `undefined`, the name will be left unchanged. Options controls whether built-in types and scalars are renamed. Root objects are never renamed by this transform. +- [`FilterTypes`](/docs/api/classes/wrap_src.filtertypes): filters all element types. +- [`FilterRootFields`](/docs/api/classes/wrap_src.filterrootfields): filters fields on the root Query, Mutation, and Subscription objects. +- [`FilterObjectFields`](/docs/api/classes/wrap_src.filterobjectfields): filters fields of Object types. +- [`FilterObjectFieldDirectives`](/docs/api/classes/wrap_src.filterobjectfielddirectives): filters Object field directives. +- [`FilterInterfaceFields`](/docs/api/classes/wrap_src.filterinterfacefields): filters fields of Interface types. +- [`FilterInputObjectFields`](/docs/api/classes/wrap_src.filterinputobjectfields): filters input fields of InputObject types. -```ts -RenameTypes( - (name: string) => string | void, - options?: { - renameBuiltins: Boolean; - renameScalars: Boolean; - }, -) +```js +const schema = wrapSchema({ + schema: originalSchema, + transforms: [ + new FilterTypes((type) => true), + new FilterRootFields((operationName, fieldName, fieldConfig) => true), + new FilterObjectFields((typeName, fieldName, fieldConfig) => true), + new FilterObjectFieldDirectives((directiveName, directiveValue) => true), + new FilterInterfaceFields((typeName, fieldName, fieldConfig) => true), + new FilterInputObjectFields((typeName, fieldName, inputFieldConfig) => true), + ] +}); ``` -### Modifying root fields +### Renaming -* `TransformRootFields(transformer: RootTransformer)`: Given a transformer, arbitrarily transform root fields. The `transformer` can return a `GraphQLFieldConfig` definition, a object with new `name` and a `field`, `null` to remove the field, or `undefined` to leave the field unchanged. +Renaming transforms are constructed with a renamer function that returns a string. The transform executes the renamer on each schema element within its scope, and applies the revised names to gateway schema elements. If a renamer returns `undefined`, the name will be left unchanged. Additional options may control whether built-in types and scalars are renamed, see linked API docs. -```ts -TransformRootFields(transformer: RootTransformer) - -type RootTransformer = ( - operation: 'Query' | 'Mutation' | 'Subscription', - fieldName: string, - fieldConfig: GraphQLField, -) => - | GraphQLFieldConfig - | [string, GraphQLFieldConfig] - | null - | void; -``` +- [`RenameTypes`](/docs/api/classes/wrap_src.renametypes): renames all element types. +- [`RenameRootTypes`](/docs/api/classes/wrap_src.renameroottypes): renames the root Query, Mutation, and Subscription types. +- [`RenameRootFields`](/docs/api/classes/wrap_src.renamerootfields): renames fields on the root Query, Mutation, and Subscription objects. +- [`RenameObjectFields`](/docs/api/classes/wrap_src.renameobjectfields): renames fields of Object types. +- [`RenameInterfaceFields`](/docs/api/classes/wrap_src.renameinterfacefields): renames fields of Interface types. +- [`RenameInputObjectFields`](/docs/api/classes/wrap_src.renameinputobjectfields): renames input fields of InputObject types. -* `FilterRootFields(filter: RootFilter)`: Like `FilterTypes`, removes root fields for which the `filter` function returns `false`. - -```ts -FilterRootFields(filter: RootFilter) - -type RootFilter = ( - operation: 'Query' | 'Mutation' | 'Subscription', - fieldName: string, - fieldConfig: GraphQLFieldConfig, -) => boolean; +```js +const schema = wrapSchema({ + schema: originalSchema, + transforms: [ + new RenameTypes((name) => `New${name}`), + new RenameRootTypes((name) => `New${name}`), + new RenameRootFields((operationName, fieldName, fieldConfig) => `new_${fieldName}`), + new RenameObjectFields((typeName, fieldName, fieldConfig) => `new_${fieldName}`), + new RenameInterfaceFields((typeName, fieldName, fieldConfig) => `new_${fieldName}`), + new RenameInputObjectFields((typeName, fieldName, inputFieldConfig) => `new_${fieldName}`), + ] +}); ``` -* `RenameRootFields(renamer)`: Rename root fields, by applying the `renamer` function to their names. +### Modifying -```ts -RenameRootFields( - renamer: ( - operation: 'Query' | 'Mutation' | 'Subscription', - name: string, - fieldConfig: GraphQLFieldConfig, - ) => string, -) -``` +Modifying transforms allow element names and their definitions to be modified or omitted. They may filter, rename, and make other freeform modifications all at once. These transforms accept element transformer functions that may return one of several outcomes: -### Modifying object fields +1. A modified version of the element config. +2. An array with a modified field name and new element config. +3. `null` to omit the element from the schema. +4. `undefined` to leave the element unchanged. -* `TransformObjectFields(objectFieldTransformer: FieldTransformer, fieldNodeTransformer?: FieldNodeTransformer))`: Given a field transformer, arbitrarily transform fields. The `objectFieldTransformer` can return a `GraphQLFieldConfig` definition, an array with first member being the new field name and second member being the new `GraphQLFieldConfig` definition, `null` to remove the field, or `undefined` to leave the field unchanged. The optional `fieldNodeTransformer`, if specified, is called upon any field of that type in the request; result transformation can be specified by wrapping the field's resolver within the `objectFieldTransformer`. +Available transforms include: -```ts -TransformObjectFields(objectFieldTransformer: FieldTransformer, fieldNodeTransformer: FieldNodeTransformer) - -export type FieldTransformer = ( - typeName: string, - fieldName: string, - fieldConfig: GraphQLFieldConfig, -) => - | GraphQLFieldConfig - | [string, GraphQLFieldConfig] - | null - | undefined; - -export type FieldNodeTransformer = ( - typeName: string, - fieldName: string, - fieldNode: FieldNode, - fragments: Record -) => SelectionNode | Array; +- [`TransformRootFields`](/docs/api/classes/wrap_src.transformrootfields): redefines fields on the root Query, Mutation, and Subscription objects. +- [`TransformObjectFields`](/docs/api/classes/wrap_src.transformobjectfields): redefines fields of Object types. +- [`TransformInterfaceFields`](/docs/api/classes/wrap_src.transforminterfacefields): redefines fields of Interface types. +- [`TransformCompositeFields`](/docs/api/classes/wrap_src.transformcompositefields): redefines composite fields. +- [`TransformInputObjectFields`](/docs/api/classes/wrap_src.transforminputobjectfields): redefines fields of InputObject types. +- [`TransformEnumValues`](/docs/api/classes/wrap_src.transformenumvalues): redefines values of Enum types. + +```js +const schema = wrapSchema({ + schema: originalSchema, + transforms: [ + new TransformRootFields((operationName, fieldName, fieldConfig) => fieldConfig), + new TransformObjectFields((typeName, fieldName, fieldConfig) => [`new_${fieldName}`, fieldConfig]), + new TransformInterfaceFields((typeName, fieldName, fieldConfig) => null), + new TransformCompositeFields((typeName, fieldName, fieldConfig) => undefined), + new TransformInputObjectFields((typeName, fieldName, inputFieldConfig) => [`new_${fieldName}`, inputFieldConfig]), + new TransformEnumValues((typeName, enumValue, enumValueConfig) => [`NEW_${enumValue}`, enumValueConfig]), + ] +}); ``` -* `FilterObjectFields(filter: ObjectFilter)`: Removes object fields for which the `filter` function returns `false`. +These transforms accept an optional second node transformer function. When specified, the node transformer is called upon any element of the given kind in a request; transforming the result is possible by wrapping the element's resolver with the element transformer function (first argument). -```ts -FilterObjectFields(filter: ObjectFilter) +### Grooming -type ObjectFilter = ( - typeName: string, - fieldName: string, - fieldConfig: GraphQLFieldConfig, -) => boolean; -``` +These transforms eliminate unwanted or unnecessary elements from a schema. These are configured in a variety of ways, so consult API documentation for specific options. -* `RenameObjectFields(renamer)`: Rename object fields, by applying the `renamer` function to their names. +- [`PruneSchema`](/docs/api/classes/wrap_src.pruneschema): eliminates unreachable elements from the schema. This is generally useful to include _after_ a filter transform so that orphaned types and values are eliminated from the schema. Accepts [pruneSchema](/docs/api/modules/utils#pruneschema) options. +- [`RemoveObjectFieldDeprecations`](/docs/api/classes/wrap_src.removeobjectfielddeprecations): accepts a string or regex describing a deprecation to remove from the gateway schema. Fields matching this deprecation will be un-deprecated. Useful for normalizing [computed fields](/docs/stitch-type-merging#computed-fields) that are activated by the gateway wrapper. +- [`RemoveObjectFieldDirectives`](/docs/api/classes/wrap_src.removeobjectfielddirectives): removes object field directives that match a directive name and optional argument criteria. +- [`RemoveObjectFieldsWithDeprecation`](/docs/api/classes/wrap_src.removeobjectfieldswithdeprecation): removes object fields whose deprecation reason matches the provided string or regex. +- [`RemoveObjectFieldsWithDirective`](/docs/api/classes/wrap_src.removeobjectfieldswithdirective): removes object fields with a schema directive matching a given name and optional argument criteria. -```ts -RenameObjectFields( - renamer: ( - typeName: string, - fieldName: string, - fieldConfig: GraphQLFieldConfig, - ) => string, -) +```js +const schema = wrapSchema({ + schema: originalSchema, + transforms: [ + new PruneSchema(options), + new RemoveObjectFieldDeprecations(/^gateway access only/), + new RemoveObjectFieldDirectives('deprecated', { reason: /^gateway access only/ }), + new RemoveObjectFieldsWithDeprecation(/^gateway access only/), + new RemoveObjectFieldsWithDirective('deprecated', { reason: /^gateway access only/ }), + ] +}); ``` -### Additional Operation Transforms +### Operational It may be sometimes useful to add additional transforms to manually change an operation request or result when using `delegateToSchema`. Common use cases may be move selections around or to wrap them. The following built-in transforms may be useful in those cases. -* `ExtractField({ from: Array, to: Array })` - move selection at `from` path to `to` path. - -* `WrapQuery( - path: Array, - wrapper: QueryWrapper, - extractor: (result: any) => any, - )` - wrap a selection at `path` using function `wrapper`. Apply `extractor` at the same path to get the result. This is used to get a result nested inside other result +- `ExtractField({ from: Array, to: Array })` move selection at `from` path to `to` path. +- `WrapQuery(path: Array, wrapper: QueryWrapper, extractor: (result: any) => any)` wrap a selection at `path` using function `wrapper`. Apply `extractor` at the same path to get the result. This is used to get a result nested inside other result. ```js transforms: [ @@ -303,18 +259,82 @@ transforms: [ }) ``` -## delegateToSchema (delegation) transforms +## Custom transforms -The following transforms are automatically applied by `delegateToSchema` during schema delegation, to translate between source and target types and fields: +Custom transforms are fairly straightforward to write. They are simply objects with up to three methods: -* `ExpandAbstractTypes`: If an abstract type within a document does not exist within the target schema, expand the type to each and any of its implementations that do exist. -* `FilterToSchema`: Remove all fields, variables and fragments for types that don't exist within the target schema. -* `AddTypenameToAbstract`: Add `__typename` to all abstract types in the document, necessary for type resolution of interfaces within the source schema to work. -* `CheckResultAndHandleErrors`: Given a result from a subschema, propagate errors so that they match the correct subfield. Also provide the correct key if aliases are used. +- `transformSchema`: recieves the original subschema and applies modifications to it, returning a modified wrapper (proxy) schema. This method runs once while initially wrapping the subschema. +- `transformRequest`: recieves each request made to the wrapped schema. The shape of a request matches the wrapper schema, and must be returned in a shape that matches the original subschema. +- `transformResult`: recieves each result returned from the original subschema. The shape of the result matches the original subschema, and must be returned in a shape that matches the wrapper schema. -By passing a custom `transforms` array to `delegateToSchema`, it's possible to run additional operation (request/result) transforms before these default transforms. +The complete transform object API is as follows: -## stitchSchemas (gateway/stitching) transforms +```ts +export interface Transform> { + transformSchema?: SchemaTransform; + transformRequest?: RequestTransform; + transformResult?: ResultTransform; +} -* `AddReplacementSelectionSets(schema: GraphQLSchema, mapping: ReplacementSelectionSetMapping)`: `stitchSchemas` adds selection sets on outgoing requests from the gateway, enabling delegation from fields specified on the gateway using fields obtained from the original requests. The selection sets can be added depending on the presence of fields within the request using the `selectionSet` option within the resolver map. `stitchSchemas` creates the mapping at gateway startup. Selection sets are used instead of fragments as the selections are added prior to transformation in case type names are changed, obviating the need for the fragment name. -* `AddMergedTypeSelectionSets(schema: GraphQLSchema, mapping: Record)`: `stitchSchemas` adds selection sets on outgoing requests from the gateway, enabling type merging from the initial result using any fields initially obtained. The mapping is created at gateway startup. +export type SchemaTransform = ( + originalWrappingSchema: GraphQLSchema, + subschemaConfig: SubschemaConfig, + transformedSchema?: GraphQLSchema +) => GraphQLSchema; + +export type RequestTransform> = ( + originalRequest: Request, + delegationContext: DelegationContext, + transformationContext: T +) => Request; + +export type ResultTransform> = ( + originalResult: ExecutionResult, + delegationContext: DelegationContext, + transformationContext: T +) => ExecutionResult; + +type Request = { + document: DocumentNode; + variables: Record; + extensions?: Record; +}; +``` + +A simple transform that removes types, fields, and arguments prefixed by an underscore might look like this: + +```js +import { wrapSchema } from '@graphql-tools/wrap'; +import { filterSchema, pruneSchema } from '@graphql-tools/utils'; + +class RemovePrivateElementsTransform { + transformSchema(originalWrappingSchema) { + const isPublicName = (name) => !name.startsWith('_'); + + return pruneSchema(filterSchema({ + schema: originalWrappingSchema, + typeFilter: (typeName) => isPublicName(typeName), + rootFieldFilter: (operationName, fieldName) => isPublicName(fieldName), + fieldFilter: (typeName, fieldName) => isPublicName(fieldName), + argumentFilter: (typeName, fieldName, argName) => isPublicName(argName), + })); + } + + // no need for operational transforms +} + +const schema = wrapSchema({ + schema: myRemoteSchema, + transforms: [new RemovePrivateElementsTransform()] +}); +``` + +## Subschema delegation + +The `wrapSchema` method will produce a new schema with all queued `transformSchema` methods applied. Delegating resolvers are automatically generated to map from new schema root fields to old schema root fields. These resolvers should be sufficient for most common case so you don't have to implement your own. + +Delegating resolvers will apply all operation transforms defined by the wrapper's `Transform` objects. Each provided `transformRequest` functions will be applies in reverse order, until the request matches the original schema. The `tranformResult` functions will be applied in the opposite order until the result matches the final gateway schema. + +In advanced cases, transforms may wish to create additional delegating root resolvers (for example, when hoisting a field into a root type). This is also possible. The wrapping schema is actually generated twice -- the first run results in a possibly non-executable version, while the second execution also includes the result of the first one within the `transformedSchema` argument so that an executable version with any new proxying resolvers can be created. + +Remote schemas can also be wrapped! In fact, this is the primary use case. See documentation regarding [remote schemas](/docs/remote-schemas/) for further details about remote schemas. Note that as explained there, when wrapping remote schemas, you will be wrapping a subschema config object, and the array of transforms should be defined on that object rather than as a second argument to `wrapSchema`. diff --git a/website/docs/stitch-combining-schemas.md b/website/docs/stitch-combining-schemas.md index f84d4fe81bd..b311cf9b4d7 100644 --- a/website/docs/stitch-combining-schemas.md +++ b/website/docs/stitch-combining-schemas.md @@ -136,33 +136,26 @@ Stitching has two strategies for handling types duplicated across subschemas: an ### Automatic merge -Types with the same name are automatically merged by default in GraphQL Tools v7. That means objects, interfaces, and input objects with the same name will have their fields consolidated from across subschemas, and unions/enums will consolidate all members. The combined gateway schema will then smartly delegate portions of a request to the proper origin subschema(s). See [type merging guide](/docs/stitch-type-merging/) for a comprehensive overview. +Types with the same name are automatically merged by default in GraphQL Tools v7. That means objects, interfaces, and input objects with the same name will consolidate their fields across subschemas, and unions/enums will consolidate all their members. The combined gateway schema will then smartly delegate portions of a request to the proper origin subschema(s). See [type merging guide](/docs/stitch-type-merging/) for a comprehensive overview. -Automatic merging will only encounter conflicts on fields and type descriptions. By default, the final definition of a field or type description found in the subschemas array is used. You may customize this selection logic in `typeMergingOptions`: +Automatic merging will only encounter conflicts on fields and type descriptions. By default, the final definition of a field or type description found in the subschemas array is used, or a specific version may be [marked as canonical](/docs/stitch-type-merging#canonical-definitions). You may customize this selection logic using `typeMergingOptions`; the following prefers the _first_ definition of each conflicting element found in the subschemas array: ```js const gatewaySchema = stitchSchemas({ subschemas: [...], - mergeTypes: true, // << optional in v7 + mergeTypes: true, // << default in v7 typeMergingOptions: { - typeDescriptionsMerger(candidates) { - const candidate = candidates.find(({ type }) => !!type.description) || candidates.pop(); - return candidate.type.description; - }, - fieldConfigMerger(candidates) { - const configs = candidates.map(c => c.fieldConfig); - return configs.find(({ description }) => !!description) || configs.pop(); - }, - inputFieldConfigMerger(candidates) { - const configs = candidates.map(c => c.inputFieldConfig); - return configs.find(({ description }) => !!description) || configs.pop(); - } + // select a preferred candidate: + selectCanonicalTypeCandidate: (candidates) => candidate[0], + // and/or, itemize specific element definitions: + typeDescriptionsMerger: (candidates) => candidate[0].type.description, + fieldConfigMerger: (candidates) => candidate[0].fieldConfig, + inputFieldConfigMerger: (candidates) => candidate[0].inputFieldConfig, + enumValueConfigMerger: (candidates) => candidate[0].enumValueConfig, }, }); ``` -In the example above, the first non-blank description encountered for each type and field in the subschemas array will be used. - ### Manual resolution By setting `mergeTypes: false`, only the final description and fields for a type found in the subschemas array will be used. You may manually resolve differences between conflicting types with an `onTypeConflict` handler: diff --git a/website/docs/stitch-directives-sdl.md b/website/docs/stitch-directives-sdl.md index 1a3e5753826..7e03af32b34 100644 --- a/website/docs/stitch-directives-sdl.md +++ b/website/docs/stitch-directives-sdl.md @@ -47,25 +47,28 @@ In the above example, the Users and Posts schemas will be combined in the stitch By default, stitching directives use the following definitions (though the names of these directives [may be customized](#customizing-names)): ```graphql -directive @key(selectionSet: String!) on OBJECT directive @merge(keyField: String, keyArg: String, additionalArgs: String, key: [String!], argsExpr: String) on FIELD_DEFINITION +directive @key(selectionSet: String!) on OBJECT directive @computed(selectionSet: String!) on FIELD_DEFINITION +directive @canonical on OBJECT | INTERFACE | INPUT_OBJECT | UNION | ENUM | SCALAR | FIELD_DEFINITION | INPUT_FIELD_DEFINITION ``` The function of these directives are: -* **`@key`:** specifies a base selection set needed to merge the annotated type across subschemas. Analogous to the `selectionSet` setting specified in [merged type configuration](/docs/stitch-type-merging#basic-example). - * **`@merge`:** denotes a root field used to query a merged type across services. The marked field's name is analogous to the `fieldName` setting in [merged type configuration](/docs/stitch-type-merging#basic-example), while the field's arguments and return types automatically configure merging. Additional arguments may tune the merge behavior (see [example recipes](#recipes)): - * `keyField`: specifies the name of a field to pick off origin objects as the key value. Omitting this option yields an [object key](#object-keys) that includes all selectionSet fields. + * `keyField`: specifies the name of a field to pick off origin objects as the key value. Omitting this option requires specification of an [object key](#object-keys) using the `@key` directive. * `keyArg`: specifies which field argument receives the merge key. This may be omitted for fields with only one argument where the key recipient can be inferred. * `additionalArgs`: specifies a string of additional keys and values to apply to other arguments, formatted as `""" arg1: "value", arg2: "value" """`. * _`key`: advanced use only; builds a custom key._ * _`argsExpr`: advanced use only; builds a custom args object._ +* **`@key`:** specifies a base selection set needed to merge the annotated type across subschemas. Analogous to the `selectionSet` setting specified in [merged type configuration](/docs/stitch-type-merging#basic-example). + * **`@computed`:** specifies a selection of fields required from other services to compute the value of this field. These additional fields are only selected when the computed field is requested. Analogous to [computed field](/docs/stitch-type-merging#computed-fields) in merged type configuration. Computed field dependencies must be sent into the subservice using an [object key](#object-keys). +* **`@canonical`:** identifies types and fields that provide a [canonical definition](/docs/stitch-type-merging#canonical-definitions) to be built into the combined gateway schema. Useful when the same types appear across multiple subschemas and a specific definition should be preferred. + #### Customizing names You may use the `stitchingDirectives` helper to build your own type definitions and validator with custom names. For example, the configuration below creates the resources for `@myKey`, `@myMerge`, and `@myComputed` directives: @@ -180,11 +183,11 @@ async function fetchRemoteSchema(executor) { The simplest merge pattern picks a key field from origin objects: ```graphql -type User @key(selectionSet: "{ id }") { +type User { # ... } -type Product @key(selectionSet: "{ upc }") { +type Product { # ... } @@ -214,14 +217,14 @@ merge: { } ``` -Here the `@key` directive specifies a base selection set for each merged type, and the `@merge` directive marks each type's merge query—then `keyField` specifies a field to be picked from each original object as the query argument value. +Here, the `@merge` directive marks each type's merge query—then `keyField` specifies a field to be picked from each original object as the query argument value. ### Multiple arguments This pattern configures a merge query that receives multiple arguments: ```graphql -type User @key(selectionSet: "{ id }") { +type User { # ... } @@ -247,7 +250,7 @@ merge: { } ``` -Because the merge field recieves multiple arguments, the `keyArg` parameter is required to specify which argument recieves the key(s). The `additionalArgs` parameter may then be used to provide static values for the other arguments. +Because the merge field receives multiple arguments, the `keyArg` parameter is required to specify which argument receives the key(s). The `additionalArgs` parameter may then be used to provide static values for the other arguments. ### Object keys @@ -372,14 +375,13 @@ type Query { ## Versioning & release -Once all schemas and their merge configurations are defined together as annotated SDL documents, new versions of these documents can be pushed up to the gateway to trigger a "hot" reload—or, a reload of the gateway schema with server deployment and a restart. See related [handbook example](https://github.com/gmac/schema-stitching-handbook/tree/master/hot-schema-reloading) demonstrating a basic reload. +Once subschemas and their merge configurations are defined as annotated SDLs, new versions of these documents can be pushed to the gateway to trigger a ["hot" reload](https://github.com/gmac/schema-stitching-handbook/tree/master/hot-schema-reloading)—or, a reload of the gateway schema without restarting its server. -However, pushing new SDL versions directly to the gateway is a risky proposition given the potential for incompatible subschema versions to be mixed. Therefore, a formal versioning, testing, and release strategy is necessary for long-term stability. The general process is as follows: +However, pushing untested SDLs directly to the gateway is risky due to the potential for incompatible subschema versions to be mixed. Therefore, a formal versioning, testing, and release strategy is necessary for long-term stability. See the [versioning handbook example](https://github.com/gmac/schema-stitching-handbook/tree/master/versioning-schema-releases) that demonstrates using the GitHub API to turn a basic Git repo into a schema registry that manages versioning and release. -1. Subservice schemas are comitted to a central schema registry where they are all versioned together. -2. Continuous integration tests run on the latest versions of all subservice schemas composed together _before_ any schemas are released. -3. Once a composed release of new subschemas passes integration tests, their underlying subservices are deployed. -4. New subservices should quietly activate new schemas behind the gateway proxy layer without breaking any existing schemas. Breaking changes should always be rolled out during a maintenance window. -5. After all new subservices have been rolled out, the revised schemas may be activated within the gateway proxy layer. +The general process for zero-downtime rollouts is: -While that process sounds fairly involved (in many ways, it is), you can tune your workflow to naturally build around this process. See our [handbook example](https://github.com/gmac/schema-stitching-handbook/tree/master/versioning-schema-releases) that demonstrates using the GitHub API to turn a basic repo into a schema registry in charge of versioning and releases. +1. Compose and test all subschema head versions together to verify their combined stability prior to release. +1. Deploy all updated subservice applications while keeping their existing subschema features operational. +1. Push all updated subschema SDLs to the gateway as a single cutover. +1. Decommission old subservices, and/or outdated subservice features. diff --git a/website/docs/stitch-schema-extensions.md b/website/docs/stitch-schema-extensions.md index c7b8668a5d7..abd39e3543d 100644 --- a/website/docs/stitch-schema-extensions.md +++ b/website/docs/stitch-schema-extensions.md @@ -148,7 +148,7 @@ Post: { }, ``` -The `selectionSet` specifies the key field(s) needed from an object to query for its associations. For example, `Post.user` will require that a Post provide its `userId`. Rather than relying on incoming queries to manually request this key for the association, the selection set will automatically be included in subschema requests to guarentee that these fields are fetched. Dynamic selection sets are also possible by providing a function that recieves a GraphQL `FieldNode` (the gateway field) and returns a `SelectionSetNode`. +The `selectionSet` specifies the key field(s) needed from an object to query for its associations. For example, `Post.user` will require that a Post provide its `userId`. Rather than relying on incoming queries to manually request this key for the association, the selection set will automatically be included in subschema requests to guarentee that these fields are fetched. Dynamic selection sets are also possible by providing a function that receives a GraphQL `FieldNode` (the gateway field) and returns a `SelectionSetNode`. ### resolve diff --git a/website/docs/stitch-type-merging.md b/website/docs/stitch-type-merging.md index d82965cfffa..682919ebc0f 100644 --- a/website/docs/stitch-type-merging.md +++ b/website/docs/stitch-type-merging.md @@ -8,8 +8,6 @@ Type merging allows _partial definitions_ of a type to exist in any subschema, a Type merging is now the preferred method of including GraphQL types across subschemas, replacing the need for [schema extensions](/docs/stitch-schema-extensions) (though does not preclude their use). To migrate from schema extensions, simply enable type merging and then start replacing extensions one by one with merges. -
- ## Basic example Type merging allows each subschema to provide subsets of a type that it has data for. For example: @@ -84,7 +82,7 @@ const gatewaySchema = stitchSchemas({ } }, ], - mergeTypes: true // << optional in v7 + mergeTypes: true // << default in v7 }); ``` @@ -169,7 +167,7 @@ The above example will always resolve a stubbed `User` record for _any_ requeste { id: '7', posts: [] } ``` -This fabricated record fulfills the not-null requirement of the `posts:[Post]!` field. However, it also makes the posts service awkwardly responsible for data it knows only by omission. A cleaner solution may be to loosen schema nullability down to `posts:[Post]`, and then return `null` for unknown user IDs without associated posts. Null is a valid mergable object as long as the unique fields it fulfills are nullable. +This fabricated record fulfills the not-null requirement of the `posts:[Post]!` field. However, it also makes the posts service awkwardly responsible for data it knows only by omission. A cleaner solution may be to loosen schema nullability down to `posts:[Post]`, and then return `null` for unknown user IDs without associated posts. Null is a valid mergable object as long as the unique fields it fulfills are nullable. See the related [handbook example](https://github.com/gmac/schema-stitching-handbook/tree/master/type-merging-nullables) for a detailed explanation. ## Merging flow @@ -365,7 +363,7 @@ const layoutsSchema = makeExecutableSchema({ }); ``` -In the above, both `Post` and `Section` will have a common interface of `{ id title url }` in the gateway schema. The difference in interface fields between the gateway schema and the layouts subschema will be translated automatically during delegation. See related [handbook example](https://github.com/gmac/schema-stitching-handbook/tree/master/type-merging-interfaces) for a working demonstration. +In the above, both `Post` and `Section` will have a common interface of `{ id title url }` in the gateway schema. The difference in interface fields between the gateway schema and the layouts subschema will automatically be expanded into typed fragments for compatibility. See related [handbook example](https://github.com/gmac/schema-stitching-handbook/tree/master/type-merging-interfaces) for a working demonstration. ## Computed fields @@ -472,38 +470,100 @@ The `@computed` SDL directive is a convenience syntax for static configuration t } ``` -The main disadvantage of computed fields is that they cannot be resolved independently from the stitched gateway. Tolerance for this subservice inconsistency is largely dependent on your own service architecture. An imperfect solution is to deprecate all computed fields within a subschema, and then normalize their behavior in the gateway schema using the [`RemoveObjectFieldDeprecations`](https://github.com/ardatan/graphql-tools/blob/master/packages/wrap/tests/transformRemoveObjectFieldDeprecations.test.ts) transform. +The main disadvantage of computed fields is that they cannot be resolved independently from the stitched gateway. Tolerance for this subservice inconsistency is largely dependent on your own service architecture. An imperfect solution is to deprecate all computed fields within a subschema, and then normalize their behavior in the gateway schema with a [`RemoveObjectFieldDeprecations`](/docs/schema-wrapping#grooming) transform. See related [handbook example](https://github.com/gmac/schema-stitching-handbook/tree/master/computed-fields). > **Implementation note:** to facilitate field-level dependencies, computed and non-computed fields of a type in the same subservice are automatically split apart into separate schemas. This assures that computed fields are always requested directly by the gateway with their dependencies provided. However, it also means that computed and non-computed fields may require separate resolution steps. You may enable [query batching](#batching) to consolidate requests whenever possible. ## Federation services -If you're familiar with [Apollo Federation](https://www.apollographql.com/docs/apollo-server/federation/introduction/), then you may notice that the above pattern of computed fields looks similar to the `_entities` service design of the [Apollo Federation specification](https://www.apollographql.com/docs/apollo-server/federation/federation-spec/). +If you're familiar with [Apollo Federation](https://www.apollographql.com/docs/apollo-server/federation/introduction/), then you may notice that the above pattern of computed fields looks similar to the `_entities` service design of the [Apollo Federation specification](https://www.apollographql.com/docs/apollo-server/federation/federation-spec/). Federation resources can be included in a stitched gateway when integrating with third-party services or in the process of a migration. See related [handbook example](https://github.com/gmac/schema-stitching-handbook/tree/master/federation-services) for specifics. + +## Canonical definitions + +As the same types are introduced across services, managing the gateway schema definitions for each GraphQL element becomes challenging. Element definitions may specify: -While type merging offers [simpler patterns](#unidirectional-merges) with [comparable performance](#batching), it can also interface with Apollo Federation services when needed by sending appropraitely formatted representations to the `_entities` query: +- Descriptions (i.e.: doc strings) +- Metadata (custom directives) +- Field nullability +- Field deprecations + +By default, the final definition of each type and field found in the stitched `subschemas` array provides the element's gateway definition. However, this strategy alone can be cumbersome when changes in subschema order cause preferred definitions to be deprioritized. + +The `canonical` setting allows you to specify preferred type and field definitions that should be built into the gateway schema: ```js -{ - schema: storefrontsSchema, - merge: { - Product: { - selectionSet: '{ id price weight }', - fieldName: '_entities', - key: ({ id, price, weight }) => ({ __typename: 'Product', id, price, weight }), - argsFromKeys: (representations) => ({ representations }), +let usersSchema = makeExecutableSchema({ + typeDefs: ` + "Represents an authenticated user" + type User @canonical { + "The primary key of this user record" + id: ID! @mydir(schema: "users") + "ignore this description" + field: String! } - } + ` +}); + +let postsSchema = makeExecutableSchema({ + typeDefs: ` + type Post { + id: ID! + } + + "ignore this description" + type User { + "ignore this description" + id: ID! @mydir(schema: "posts") + "Preferred description for this field" + field: String @canonical + "Posts authored by this user" + posts: [Post!] + } + ` +}); +``` + +The above example uses [stitching directives](/docs/stitch-directives-sdl) to mark schema elements as `@canonical`. A type marked as canonical will provide it's definition and that of all of its fields to the combined gateway schema. In the uncommon scenario where an overlapping field in another subschema provides a more robust definition, that field may be marked as canonical to override the base type. Fields that are unique to a given service (such as `User.posts` above) have no competing definition so are canonical by default. The above User types and ASTs will merge into: + +```graphql +"Represents an authenticated user" +type User { + "The primary key of this user record" + id: ID! @mydir(schema: "users") + "Preferred description for this field" + field: String + "Posts authored by this user" + posts: [Post!] } ``` -Type merging generally maps to Federation concepts as follows: +The above SDL directives can also be written as static configuration: -- `@key`: type merging's closest analog is the type-level `selectionSet` specified in merged type configuration. Unlike Federation though, merging is fully decentralized with no concept of an "origin" service. -- `@requires`: directly comparable to type merging's `@computed` directive. However, merging is decentralized and may resolve computed fields from any number of services. -- `@external`: type merging implicitly expects types in each service to only implement the fields they provide. -- `@provides`: type merging implicitly handles multiple services that implement the same fields, and automatically selects as many requested fields as possible from as few services as possible during each execution cycle. +```js +const gatewaySchema = stitchSchemas({ + subschemas: [{ + schema: usersSchema, + merge: { + User: { + // ... + canonical: true + } + } + }, { + schema: postsSchema, + merge: { + User: { + // ... + fields: { + email: { canonical: true } + } + } + } + }] +}); +``` -See related [handbook example](https://github.com/gmac/schema-stitching-handbook/tree/master/type-merging-interfaces) for a demonstration of federation services used in a stitched gateway. +> **Implementation note:** canonical settings are only used while building the combined gateway schema; they are given no special priority in runtime query planning. You may override the assembly of canonical definitions using [`typeMergingOptions`](/docs/stitch-combining-schemas#automatic-merge). ## Type resolvers diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index 83f0d4cab08..b0f7a863e1a 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -101,7 +101,7 @@ module.exports = { '@docusaurus/preset-classic', { docs: { - sidebarPath: require.resolve('./sidebars.json'), + sidebarPath: require.resolve('./sidebars.js'), editUrl: 'https://github.com/ardatan/graphql-tools/edit/master/website/', }, theme: { diff --git a/website/package.json b/website/package.json index 0f4f33cd0b3..9fe7a3981f2 100644 --- a/website/package.json +++ b/website/package.json @@ -9,8 +9,8 @@ "deploy": "docusaurus deploy" }, "dependencies": { - "@docusaurus/core": "2.0.0-alpha.6703f8420", - "@docusaurus/preset-classic": "2.0.0-alpha.6703f8420", + "@docusaurus/core": "2.0.0-alpha.f48d435ce", + "@docusaurus/preset-classic": "2.0.0-alpha.f48d435ce", "classnames": "2.2.6", "react": "17.0.1", "react-dom": "17.0.1" diff --git a/website/sidebars.js b/website/sidebars.js new file mode 100644 index 00000000000..10f25f94a8d --- /dev/null +++ b/website/sidebars.js @@ -0,0 +1,46 @@ +module.exports = { + "someSidebar": [ + "introduction", + { + "Guides": [ + "generate-schema", + "resolvers", + "resolvers-composition", + "scalars", + "mocking", + "connectors", + "schema-directives", + "directive-resolvers", + "schema-delegation", + "remote-schemas", + "schema-wrapping", + "schema-merging", + { + "Schema stitching": [ + "stitch-combining-schemas", + "stitch-type-merging", + "stitch-directives-sdl", + "stitch-schema-extensions", + "stitch-api" + ] + }, + "server-setup", + "schema-loading", + "documents-loading", + "graphql-tag-pluck", + "relay-operation-optimizer", + { + "Migration": [ + "migration-from-tools", + "migration-from-toolkit", + "migration-from-merge-graphql-schemas", + "migration-from-import" + ] + } + ] + }, + { + "API Reference": require('./api-sidebar.json') + } + ] +} diff --git a/website/sidebars.template.json b/website/sidebars.json similarity index 100% rename from website/sidebars.template.json rename to website/sidebars.json diff --git a/yarn.lock b/yarn.lock index 69dd3603d6c..1efe604cd07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -626,7 +626,7 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@7.12.11", "@babel/parser@^7.12.11": +"@babel/parser@7.12.11", "@babel/parser@^7.12.0", "@babel/parser@^7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.11.tgz#9ce3595bcd74bc5c466905e86c535b8b25011e79" integrity sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg== @@ -1559,7 +1559,7 @@ globals "^11.1.0" lodash "^4.17.19" -"@babel/types@7.12.12", "@babel/types@^7.12.11", "@babel/types@^7.12.12": +"@babel/types@7.12.12", "@babel/types@^7.12.0", "@babel/types@^7.12.11", "@babel/types@^7.12.12": version "7.12.12" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.12.tgz#4608a6ec313abbd87afa55004d373ad04a96c299" integrity sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ== @@ -1819,10 +1819,10 @@ "@docsearch/css" "3.0.0-alpha.32" algoliasearch "^4.0.0" -"@docusaurus/core@2.0.0-alpha.6703f8420": - version "2.0.0-alpha.6703f8420" - resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-2.0.0-alpha.6703f8420.tgz#a1602920f93320dca79479c5c991cb4de784e9df" - integrity sha512-692pFc/UbR0LrL5o6IS/lFWU4yCyMa4c84cwRw5Yp7NKhZibU55lTASzLDsYJjCQ7NoajBEwRKsIm/5+3zKSVg== +"@docusaurus/core@2.0.0-alpha.f48d435ce": + version "2.0.0-alpha.f48d435ce" + resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-2.0.0-alpha.f48d435ce.tgz#7df395f0a1d58f23feab95697df679c0f5c3f676" + integrity sha512-mrrwe9T8aCYP48f4fEFmkktK9JvV55Re38OF0SZS9NCIuPqsYfi089P7kWokS/NGwrhW7YLsB+CiZEzUqpmi+Q== dependencies: "@babel/core" "^7.12.3" "@babel/generator" "^7.12.5" @@ -1836,10 +1836,10 @@ "@babel/runtime" "^7.12.5" "@babel/runtime-corejs3" "^7.12.5" "@babel/traverse" "^7.12.5" - "@docusaurus/cssnano-preset" "2.0.0-alpha.6703f8420" - "@docusaurus/types" "2.0.0-alpha.6703f8420" - "@docusaurus/utils" "2.0.0-alpha.6703f8420" - "@docusaurus/utils-validation" "2.0.0-alpha.6703f8420" + "@docusaurus/cssnano-preset" "2.0.0-alpha.f48d435ce" + "@docusaurus/types" "2.0.0-alpha.f48d435ce" + "@docusaurus/utils" "2.0.0-alpha.f48d435ce" + "@docusaurus/utils-validation" "2.0.0-alpha.f48d435ce" "@endiliey/static-site-generator-webpack-plugin" "^4.0.0" "@svgr/webpack" "^5.4.0" babel-loader "^8.2.1" @@ -1902,25 +1902,25 @@ webpack-merge "^4.2.2" webpackbar "^4.0.0" -"@docusaurus/cssnano-preset@2.0.0-alpha.6703f8420": - version "2.0.0-alpha.6703f8420" - resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-2.0.0-alpha.6703f8420.tgz#ec5c5d164f80fbb16f8e647f37774501f2071359" - integrity sha512-i4pSbdwAB7Ae+XJg6AEvGPgjUkMTAe7J8hlFV3hn2qu6jsGiP57XTP6chbVDJO4lhHuAi5WQOlaSWLL+Tgwh8w== +"@docusaurus/cssnano-preset@2.0.0-alpha.f48d435ce": + version "2.0.0-alpha.f48d435ce" + resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-2.0.0-alpha.f48d435ce.tgz#b75304e058d0895df83458199f6df7ab2de79da7" + integrity sha512-Ugw8FqSm8AGfs2FtJZ7o/nJNDfEpuvUgsaK1JGkUGcsiikR70xoGk9jlWNquLyWV9I8Ri+y4ZN9gYhmpltvFxQ== dependencies: cssnano-preset-advanced "^4.0.7" postcss "^7.0.2" postcss-combine-duplicated-selectors "^9.1.0" postcss-sort-media-queries "^1.7.26" -"@docusaurus/mdx-loader@2.0.0-alpha.6703f8420": - version "2.0.0-alpha.6703f8420" - resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-2.0.0-alpha.6703f8420.tgz#78008de8c747363807cbdb882a0fe8c3312599cb" - integrity sha512-CGH1bM5glwyPi1hEEeCHqqg8KHeJYi9+qfxP75GsL91K7/LhM8ok2+Qxaozbwur4toTXSpmQBMW8ExbUsS/MmA== +"@docusaurus/mdx-loader@2.0.0-alpha.f48d435ce": + version "2.0.0-alpha.f48d435ce" + resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-2.0.0-alpha.f48d435ce.tgz#39e7b55c72809f0b9b9b3b03a14603b9e55b1fab" + integrity sha512-efuMST0XTmUykitTGR6FOhxduakiif+k6QGDxKP/yU4vojgKQrNj8tGBx+NP27E8mTQ4YnWH2C8HOzm+t+Psew== dependencies: "@babel/parser" "^7.12.5" "@babel/traverse" "^7.12.5" - "@docusaurus/core" "2.0.0-alpha.6703f8420" - "@docusaurus/utils" "2.0.0-alpha.6703f8420" + "@docusaurus/core" "2.0.0-alpha.f48d435ce" + "@docusaurus/utils" "2.0.0-alpha.f48d435ce" "@mdx-js/mdx" "^1.6.21" "@mdx-js/react" "^1.6.21" escape-html "^1.0.3" @@ -1936,16 +1936,16 @@ url-loader "^4.1.1" webpack "^4.44.1" -"@docusaurus/plugin-content-blog@2.0.0-alpha.6703f8420": - version "2.0.0-alpha.6703f8420" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-2.0.0-alpha.6703f8420.tgz#cb525a37f9c7e75474a549f424b98c49143e8f16" - integrity sha512-WVdUs3GRKWRiF9BrXVY7sZSFYM9Od7ocPTgEH/PmFm90xNDkWZecNMyxInVyazw01XtEIwuOcXskp7AYG9beWg== +"@docusaurus/plugin-content-blog@2.0.0-alpha.f48d435ce": + version "2.0.0-alpha.f48d435ce" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-2.0.0-alpha.f48d435ce.tgz#86050c158fb69948bc879faca1c35ee7a647f1c2" + integrity sha512-X83x3OGlzlnNFr9MQp+GjcHOOOQy0uOYL2c7T4DZmxLY04SRFZYTyRNwLHSQ4bzAkUT2b42cLeyqTTmw6l+F9Q== dependencies: - "@docusaurus/core" "2.0.0-alpha.6703f8420" - "@docusaurus/mdx-loader" "2.0.0-alpha.6703f8420" - "@docusaurus/types" "2.0.0-alpha.6703f8420" - "@docusaurus/utils" "2.0.0-alpha.6703f8420" - "@docusaurus/utils-validation" "2.0.0-alpha.6703f8420" + "@docusaurus/core" "2.0.0-alpha.f48d435ce" + "@docusaurus/mdx-loader" "2.0.0-alpha.f48d435ce" + "@docusaurus/types" "2.0.0-alpha.f48d435ce" + "@docusaurus/utils" "2.0.0-alpha.f48d435ce" + "@docusaurus/utils-validation" "2.0.0-alpha.f48d435ce" chalk "^3.0.0" feed "^4.2.1" fs-extra "^9.0.1" @@ -1957,16 +1957,16 @@ remark-admonitions "^1.2.1" webpack "^4.44.1" -"@docusaurus/plugin-content-docs@2.0.0-alpha.6703f8420": - version "2.0.0-alpha.6703f8420" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-2.0.0-alpha.6703f8420.tgz#c6a5e5c816829304489f54b7f4c6c5495a8e6701" - integrity sha512-KK04y93Tpo5kpoKNIMaNnSwfqMjJMgyhwmALLzjhf30+W68X5gDmNypUUIV3890PEebWtNoV0VwCivRg4s5jcg== +"@docusaurus/plugin-content-docs@2.0.0-alpha.f48d435ce": + version "2.0.0-alpha.f48d435ce" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-2.0.0-alpha.f48d435ce.tgz#66642542ba4d686e296f829c7bfc8d9a4400b855" + integrity sha512-GnOszH60rbZQpenFcVFNz8shJLAFnOeDlG5Hy6KtPPUArmou/MIYHnjK3oNWVQcJA8VwP5Z90eIjWB2C7uNByw== dependencies: - "@docusaurus/core" "2.0.0-alpha.6703f8420" - "@docusaurus/mdx-loader" "2.0.0-alpha.6703f8420" - "@docusaurus/types" "2.0.0-alpha.6703f8420" - "@docusaurus/utils" "2.0.0-alpha.6703f8420" - "@docusaurus/utils-validation" "2.0.0-alpha.6703f8420" + "@docusaurus/core" "2.0.0-alpha.f48d435ce" + "@docusaurus/mdx-loader" "2.0.0-alpha.f48d435ce" + "@docusaurus/types" "2.0.0-alpha.f48d435ce" + "@docusaurus/utils" "2.0.0-alpha.f48d435ce" + "@docusaurus/utils-validation" "2.0.0-alpha.f48d435ce" chalk "^3.0.0" execa "^3.4.0" fs-extra "^9.0.1" @@ -1985,16 +1985,16 @@ utility-types "^3.10.0" webpack "^4.44.1" -"@docusaurus/plugin-content-pages@2.0.0-alpha.6703f8420": - version "2.0.0-alpha.6703f8420" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-2.0.0-alpha.6703f8420.tgz#f753eb6c92e43c6120211ab427c7f16a571749c0" - integrity sha512-hoSl+Fy8HppVBSsKmRsPk6SspY7SFck42C2bVcEPq4wCVn4xdXcH0OovSB/VVmIMD6/at47EKAeQklX9nMNV+w== +"@docusaurus/plugin-content-pages@2.0.0-alpha.f48d435ce": + version "2.0.0-alpha.f48d435ce" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-2.0.0-alpha.f48d435ce.tgz#767904266612fe3ee60e6fef9fd43f9c72f1944f" + integrity sha512-/xNh66kNBRlecSTYlE4HEkhFc3Hpa6gtLRNJxwsUFP0sP9iqoKk29vRP3VLgZNzB5lb3bv9nfz1+bj4tD730kg== dependencies: - "@docusaurus/core" "2.0.0-alpha.6703f8420" - "@docusaurus/mdx-loader" "2.0.0-alpha.6703f8420" - "@docusaurus/types" "2.0.0-alpha.6703f8420" - "@docusaurus/utils" "2.0.0-alpha.6703f8420" - "@docusaurus/utils-validation" "2.0.0-alpha.6703f8420" + "@docusaurus/core" "2.0.0-alpha.f48d435ce" + "@docusaurus/mdx-loader" "2.0.0-alpha.f48d435ce" + "@docusaurus/types" "2.0.0-alpha.f48d435ce" + "@docusaurus/utils" "2.0.0-alpha.f48d435ce" + "@docusaurus/utils-validation" "2.0.0-alpha.f48d435ce" globby "^10.0.1" joi "^17.2.1" loader-utils "^1.2.3" @@ -2004,70 +2004,70 @@ slash "^3.0.0" webpack "^4.44.1" -"@docusaurus/plugin-debug@2.0.0-alpha.6703f8420": - version "2.0.0-alpha.6703f8420" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-2.0.0-alpha.6703f8420.tgz#fac7fd5058bd4d1fe47a525b1e479df3fca400aa" - integrity sha512-71si6lkImDsfVQxNL339o2XABlMKb+NS0KN1VRPYpl7s/mqtKU5jT7AJcIf+xE/mU80yjlaqhcIFjpl1UJljGQ== +"@docusaurus/plugin-debug@2.0.0-alpha.f48d435ce": + version "2.0.0-alpha.f48d435ce" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-2.0.0-alpha.f48d435ce.tgz#2b88c3ffb72d521b02219771537572ed2aa1e5e6" + integrity sha512-m4DfocHpeNXMG+Fo693Ap3zEeYEDAwM2tYAuuspnEhmCcrFqyB04XThFRgmdp2YFpraPufqEhYkURd1l9z6Z3w== dependencies: - "@docusaurus/core" "2.0.0-alpha.6703f8420" - "@docusaurus/types" "2.0.0-alpha.6703f8420" - "@docusaurus/utils" "2.0.0-alpha.6703f8420" + "@docusaurus/core" "2.0.0-alpha.f48d435ce" + "@docusaurus/types" "2.0.0-alpha.f48d435ce" + "@docusaurus/utils" "2.0.0-alpha.f48d435ce" react-json-view "^1.19.1" -"@docusaurus/plugin-google-analytics@2.0.0-alpha.6703f8420": - version "2.0.0-alpha.6703f8420" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-2.0.0-alpha.6703f8420.tgz#83ec600dc31a242bdfa11109a8ab13b8f21462d4" - integrity sha512-1NKji8iuV0r06kS9rN3h0JWPcWeOvAOnqp99ejonCzcBP1PpT2mZ192jTBFRDApboSGr91dlzH4fP+khYPUFYA== +"@docusaurus/plugin-google-analytics@2.0.0-alpha.f48d435ce": + version "2.0.0-alpha.f48d435ce" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-2.0.0-alpha.f48d435ce.tgz#bf31198d23d477c636bd310dfd6f652a86fa1289" + integrity sha512-b3lHD5rYSTCuuxMwfEOyaoak3GKSFVegawRqVHEqQ7BbBAw/DYfAbQfxgzjyV/2WQ7BtDBW7W4Vh4TI+jrsrOw== dependencies: - "@docusaurus/core" "2.0.0-alpha.6703f8420" + "@docusaurus/core" "2.0.0-alpha.f48d435ce" -"@docusaurus/plugin-google-gtag@2.0.0-alpha.6703f8420": - version "2.0.0-alpha.6703f8420" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-2.0.0-alpha.6703f8420.tgz#8b13afb387226a3f1797d5666a1c56da14b0d590" - integrity sha512-RV5UaHerFzM0EayD6ZZSqmuN5U0c81fwKolgaR1Sclwa87hiy0UQ+ixXhCWy//sB1SqMgW8VrX0A/WdOiIZbsA== +"@docusaurus/plugin-google-gtag@2.0.0-alpha.f48d435ce": + version "2.0.0-alpha.f48d435ce" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-2.0.0-alpha.f48d435ce.tgz#fe92bd4c0dd2a613dfa02c911a34d3b914eb294f" + integrity sha512-C0NmUiZ8pXfciq/BqHRDu8bkDraNhkPmKgQksCfztASkMaLHVqCaJmlR38KqA2w4vsWtXiu/V+8+/CICxMk8TQ== dependencies: - "@docusaurus/core" "2.0.0-alpha.6703f8420" + "@docusaurus/core" "2.0.0-alpha.f48d435ce" -"@docusaurus/plugin-sitemap@2.0.0-alpha.6703f8420": - version "2.0.0-alpha.6703f8420" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-2.0.0-alpha.6703f8420.tgz#a5ee3150d21af548441ec0f9603287d1d358850d" - integrity sha512-nl4fUCzyeD8TgVnKLdXRlyKZYLD3sITUofXG9nApgfLLIunkKyi7dcTB2+Hew+3NNrdSaBP7AJJKWzVRXAah3g== +"@docusaurus/plugin-sitemap@2.0.0-alpha.f48d435ce": + version "2.0.0-alpha.f48d435ce" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-2.0.0-alpha.f48d435ce.tgz#9d4843a0b68138df2d841492e1059338d53b7393" + integrity sha512-vkZtLKUesRPAkyVzdJeNAb2p3HDc22EFsCNtzGWCtUHy5gDCJZo4KCaYKbjWpOcR73xrRvXIkApH0kg3s1MiSA== dependencies: - "@docusaurus/core" "2.0.0-alpha.6703f8420" - "@docusaurus/types" "2.0.0-alpha.6703f8420" + "@docusaurus/core" "2.0.0-alpha.f48d435ce" + "@docusaurus/types" "2.0.0-alpha.f48d435ce" fs-extra "^9.0.1" joi "^17.2.1" sitemap "^3.2.2" -"@docusaurus/preset-classic@2.0.0-alpha.6703f8420": - version "2.0.0-alpha.6703f8420" - resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-2.0.0-alpha.6703f8420.tgz#49658c3ee684086641ce5dfbf229ebd6c2a5ccc8" - integrity sha512-yFgPIwc+zfL8QbFxzmjQpUjNe1fxLk5cjG8oKeSJjuRcoAi7HGcup1OUDZ/THZO5kIJwLx07uuqH8lZF+t3OaQ== - dependencies: - "@docusaurus/core" "2.0.0-alpha.6703f8420" - "@docusaurus/plugin-content-blog" "2.0.0-alpha.6703f8420" - "@docusaurus/plugin-content-docs" "2.0.0-alpha.6703f8420" - "@docusaurus/plugin-content-pages" "2.0.0-alpha.6703f8420" - "@docusaurus/plugin-debug" "2.0.0-alpha.6703f8420" - "@docusaurus/plugin-google-analytics" "2.0.0-alpha.6703f8420" - "@docusaurus/plugin-google-gtag" "2.0.0-alpha.6703f8420" - "@docusaurus/plugin-sitemap" "2.0.0-alpha.6703f8420" - "@docusaurus/theme-classic" "2.0.0-alpha.6703f8420" - "@docusaurus/theme-search-algolia" "2.0.0-alpha.6703f8420" - -"@docusaurus/theme-classic@2.0.0-alpha.6703f8420": - version "2.0.0-alpha.6703f8420" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-2.0.0-alpha.6703f8420.tgz#dc2444db2e45c1f1726f2c41041d099eea442248" - integrity sha512-nOYMhSQw3LOMJZKqlHU0VBf8XzNkpxmhcskq0eaPeSOGPrt7oDuReKX7//nYo5Ra4qtR6SCf2sWkur7SHjVblQ== - dependencies: - "@docusaurus/core" "2.0.0-alpha.6703f8420" - "@docusaurus/plugin-content-blog" "2.0.0-alpha.6703f8420" - "@docusaurus/plugin-content-docs" "2.0.0-alpha.6703f8420" - "@docusaurus/plugin-content-pages" "2.0.0-alpha.6703f8420" - "@docusaurus/theme-common" "2.0.0-alpha.6703f8420" - "@docusaurus/types" "2.0.0-alpha.6703f8420" - "@docusaurus/utils" "2.0.0-alpha.6703f8420" - "@docusaurus/utils-validation" "2.0.0-alpha.6703f8420" +"@docusaurus/preset-classic@2.0.0-alpha.f48d435ce": + version "2.0.0-alpha.f48d435ce" + resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-2.0.0-alpha.f48d435ce.tgz#5627a5a16d7858015bf27a2102105d2568a327be" + integrity sha512-n6vnzbXN7Grzqu9fMCLNnAcrPhdjtthDdr8WM8AlA/flBOVQNQbljq5WpWR3sDFwlmfmdE0bft3LB60URWIxog== + dependencies: + "@docusaurus/core" "2.0.0-alpha.f48d435ce" + "@docusaurus/plugin-content-blog" "2.0.0-alpha.f48d435ce" + "@docusaurus/plugin-content-docs" "2.0.0-alpha.f48d435ce" + "@docusaurus/plugin-content-pages" "2.0.0-alpha.f48d435ce" + "@docusaurus/plugin-debug" "2.0.0-alpha.f48d435ce" + "@docusaurus/plugin-google-analytics" "2.0.0-alpha.f48d435ce" + "@docusaurus/plugin-google-gtag" "2.0.0-alpha.f48d435ce" + "@docusaurus/plugin-sitemap" "2.0.0-alpha.f48d435ce" + "@docusaurus/theme-classic" "2.0.0-alpha.f48d435ce" + "@docusaurus/theme-search-algolia" "2.0.0-alpha.f48d435ce" + +"@docusaurus/theme-classic@2.0.0-alpha.f48d435ce": + version "2.0.0-alpha.f48d435ce" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-2.0.0-alpha.f48d435ce.tgz#967e697b1fbb2dffcb82312c5218d5d518cdec4e" + integrity sha512-Pazgw8a9RG8wA4c7JiyVLu7RfNEWDo6dpzPJD0wDD2tKRe5KPkZdUKx1FVGQKvfkMPmMkg6k1TKazukeS66hZA== + dependencies: + "@docusaurus/core" "2.0.0-alpha.f48d435ce" + "@docusaurus/plugin-content-blog" "2.0.0-alpha.f48d435ce" + "@docusaurus/plugin-content-docs" "2.0.0-alpha.f48d435ce" + "@docusaurus/plugin-content-pages" "2.0.0-alpha.f48d435ce" + "@docusaurus/theme-common" "2.0.0-alpha.f48d435ce" + "@docusaurus/types" "2.0.0-alpha.f48d435ce" + "@docusaurus/utils" "2.0.0-alpha.f48d435ce" + "@docusaurus/utils-validation" "2.0.0-alpha.f48d435ce" "@mdx-js/mdx" "^1.6.21" "@mdx-js/react" "^1.6.21" "@types/react-toggle" "^4.0.2" @@ -2083,26 +2083,26 @@ react-router-dom "^5.2.0" react-toggle "^4.1.1" -"@docusaurus/theme-common@2.0.0-alpha.6703f8420": - version "2.0.0-alpha.6703f8420" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-2.0.0-alpha.6703f8420.tgz#e5d156c2cb2615f8cb9ff98d35b46cc406c19e5a" - integrity sha512-r24TTAPBCOLIm236W3sQLRldMPCLeHJ2SJLD+3T52YiARqfuPnCEici82cFqA71Nz2ksc/SVKglrK4iPi2d4CQ== +"@docusaurus/theme-common@2.0.0-alpha.f48d435ce": + version "2.0.0-alpha.f48d435ce" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-2.0.0-alpha.f48d435ce.tgz#21b954cdc90858d32d5214bb14b3a4f8bd1b496c" + integrity sha512-ym3i3yUTRxtm92kk/tE/P683wduxrX5zGo4D6ds2aChtx9iJf+KMx69TAUmreBvR9aKhQHGX1eFHxnyP/1YJXw== dependencies: - "@docusaurus/core" "2.0.0-alpha.6703f8420" - "@docusaurus/plugin-content-blog" "2.0.0-alpha.6703f8420" - "@docusaurus/plugin-content-docs" "2.0.0-alpha.6703f8420" - "@docusaurus/plugin-content-pages" "2.0.0-alpha.6703f8420" - "@docusaurus/types" "2.0.0-alpha.6703f8420" + "@docusaurus/core" "2.0.0-alpha.f48d435ce" + "@docusaurus/plugin-content-blog" "2.0.0-alpha.f48d435ce" + "@docusaurus/plugin-content-docs" "2.0.0-alpha.f48d435ce" + "@docusaurus/plugin-content-pages" "2.0.0-alpha.f48d435ce" + "@docusaurus/types" "2.0.0-alpha.f48d435ce" -"@docusaurus/theme-search-algolia@2.0.0-alpha.6703f8420": - version "2.0.0-alpha.6703f8420" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.0.0-alpha.6703f8420.tgz#a574c74e0756b63bf6bed63a8cdca969ed65a80a" - integrity sha512-wRXTZouL3gy2TO6qb4EHxY5HN1JtPmG/i3tyKDdHtmF53Xb3kORtXu+4BYCVowKYnJGMc6DnTjnZgcKdc11pNw== +"@docusaurus/theme-search-algolia@2.0.0-alpha.f48d435ce": + version "2.0.0-alpha.f48d435ce" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.0.0-alpha.f48d435ce.tgz#854bd5dc9c1b22ed53ed0d1a86481405e1441fad" + integrity sha512-XPpfsPXrBc3TTlNmqxrPH/ahKjPCnfYR8mAKlR0hrydqIw/TqWg7M2dLvC+5VH+oTyBWpAnMtseuxI+KPY+Ing== dependencies: "@docsearch/react" "^3.0.0-alpha.31" - "@docusaurus/core" "2.0.0-alpha.6703f8420" - "@docusaurus/theme-common" "2.0.0-alpha.6703f8420" - "@docusaurus/utils" "2.0.0-alpha.6703f8420" + "@docusaurus/core" "2.0.0-alpha.f48d435ce" + "@docusaurus/theme-common" "2.0.0-alpha.f48d435ce" + "@docusaurus/utils" "2.0.0-alpha.f48d435ce" algoliasearch "^4.0.0" algoliasearch-helper "^3.1.1" clsx "^1.1.1" @@ -2110,31 +2110,31 @@ joi "^17.2.1" lodash "^4.17.19" -"@docusaurus/types@2.0.0-alpha.6703f8420": - version "2.0.0-alpha.6703f8420" - resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-2.0.0-alpha.6703f8420.tgz#130ce53bd7b7a19672861b650b3a63dab732546e" - integrity sha512-E3hfWT8SD64VXmzlCP73k361KtDDW/d5Rq7flGtVNhExc/vR26oR63Hy+hlMoggg5NXO/z1t5QlBTHEzJ3GxBA== +"@docusaurus/types@2.0.0-alpha.f48d435ce": + version "2.0.0-alpha.f48d435ce" + resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-2.0.0-alpha.f48d435ce.tgz#83afe56b34f518cf68e61948f2779ea2a78f50e5" + integrity sha512-aUhd2cgEhFDlsUInS2vQIjrKZWKRfTTRgV9JQGJQTm+ghNwv/HA166LAfJBjS1vt/04jKYNG9KXwwuySuWrDZw== dependencies: "@types/webpack" "^4.41.0" commander "^4.0.1" querystring "0.2.0" webpack-merge "^4.2.2" -"@docusaurus/utils-validation@2.0.0-alpha.6703f8420": - version "2.0.0-alpha.6703f8420" - resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-2.0.0-alpha.6703f8420.tgz#3a2dba5adc0dad2229393f850bf08b8f3cde7b30" - integrity sha512-O2G/whQfyD6UtEdNV4JFcvih5LNw0iklAENuisEHLaW7fWvmcx4WTee82RQ5CcAa6XXuvj6v6ciXmHpjdH023g== +"@docusaurus/utils-validation@2.0.0-alpha.f48d435ce": + version "2.0.0-alpha.f48d435ce" + resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-2.0.0-alpha.f48d435ce.tgz#2b877b413de29db2213b315bdf40af8536f722fe" + integrity sha512-MDhMMUELfZi2swtDznH298lTZJTh3gMv9K5kINpi1lZHFCbiFSm48vvsb33/w2LUBptTAyjyqFsLQWScrF8Rbw== dependencies: - "@docusaurus/utils" "2.0.0-alpha.6703f8420" + "@docusaurus/utils" "2.0.0-alpha.f48d435ce" chalk "^3.0.0" joi "^17.2.1" -"@docusaurus/utils@2.0.0-alpha.6703f8420": - version "2.0.0-alpha.6703f8420" - resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-2.0.0-alpha.6703f8420.tgz#21ecf60a00d25302216f2c05d5c5a24e8f453650" - integrity sha512-RVHAS9To99LbB2hUiu8yQhQrNbr5j2uB+mLeoBsrLOfIPkE9GGMUTWJe/P4tsr9v5hEX5adDNzBmCoznR/cMFQ== +"@docusaurus/utils@2.0.0-alpha.f48d435ce": + version "2.0.0-alpha.f48d435ce" + resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-2.0.0-alpha.f48d435ce.tgz#029135ac599df8046912e8190928332df521ff3b" + integrity sha512-cJ7zhQixYhbrw2zrev6APeIylPzuYz9yOpb8R6glOxhzjzvnCuxiyEQBYQRrFFrkUPRoXzNV+xUEt0fD9CkTTg== dependencies: - "@docusaurus/types" "2.0.0-alpha.6703f8420" + "@docusaurus/types" "2.0.0-alpha.f48d435ce" chalk "^3.0.0" escape-string-regexp "^2.0.0" fs-extra "^9.0.1" @@ -2865,10 +2865,10 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@26.0.19": - version "26.0.19" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.19.tgz#e6fa1e3def5842ec85045bd5210e9bb8289de790" - integrity sha512-jqHoirTG61fee6v6rwbnEuKhpSKih0tuhqeFbCmMmErhtu3BYlOZaXWjffgOstMM4S/3iQD31lI5bGLTrs97yQ== +"@types/jest@26.0.20": + version "26.0.20" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.20.tgz#cd2f2702ecf69e86b586e1f5223a60e454056307" + integrity sha512-9zi2Y+5USJRxd0FsahERhBwlcvFh6D2GLQnY2FH2BzK8J9s9omvNHIbvABwIluXa0fD8XVKMLTO0aOEuUfACAA== dependencies: jest-diff "^26.0.0" pretty-format "^26.0.0" @@ -2881,10 +2881,10 @@ jest-diff "^26.0.0" pretty-format "^26.0.0" -"@types/js-yaml@^3.12.5": - version "3.12.5" - resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.5.tgz#136d5e6a57a931e1cce6f9d8126aa98a9c92a6bb" - integrity sha512-JCcp6J0GV66Y4ZMDAQCXot4xprYB+Zfd3meK9+INSJeVZwJmHAW30BBEEkPzXswMXuiyReUGOP3GxrADc9wPww== +"@types/js-yaml@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.0.tgz#d1a11688112091f2c711674df3a65ea2f47b5dfb" + integrity sha512-4vlpCM5KPCL5CfGmTbpjwVKbISRYhduEJvvUWsH5EB7QInhEj94XPZ3ts/9FPiLZFqYO0xoW4ZL8z2AabTGgJA== "@types/json-schema@^7.0.3", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6": version "7.0.6" @@ -2934,10 +2934,10 @@ "@types/koa-compose" "*" "@types/node" "*" -"@types/lodash@4.14.166": - version "4.14.166" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.166.tgz#07e7f2699a149219dbc3c35574f126ec8737688f" - integrity sha512-A3YT/c1oTlyvvW/GQqG86EyqWNrT/tisOIh2mW3YCgcx71TNjiTZA3zYZWA5BCmtsOTXjhliy4c4yEkErw6njA== +"@types/lodash@4.14.167": + version "4.14.167" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.167.tgz#ce7d78553e3c886d4ea643c37ec7edc20f16765e" + integrity sha512-w7tQPjARrvdeBkX/Rwg95S592JwxqOjmms3zWQ0XZgSyxSLdzWaYH3vErBhdVS/lRBX7F8aBYcYJYTr5TMGOzw== "@types/mdast@^3.0.0": version "3.0.3" @@ -2966,10 +2966,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.9.tgz#04afc9a25c6ff93da14deabd65dc44485b53c8d6" integrity sha512-JsoLXFppG62tWTklIoO4knA+oDTYsmqWxHRvd4lpmfQRNhX6osheUOWETP2jMoV/2bEHuMra8Pp3Dmo/stBFcw== -"@types/node@14.14.17": - version "14.14.17" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.17.tgz#29fab92f3986c0e379968ad3c2043683d8020dbb" - integrity sha512-G0lD1/7qD60TJ/mZmhog76k7NcpLWkPVGgzkRy3CTlnFu4LUQh5v2Wa661z6vnXmD8EQrnALUyf0VRtrACYztw== +"@types/node@14.14.20": + version "14.14.20" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.20.tgz#f7974863edd21d1f8a494a73e8e2b3658615c340" + integrity sha512-Y93R97Ouif9JEOWPIUyU+eyIdyRqQR0I8Ez1dzku4hDx34NWh4HbtIc3WNzwB1Y9ULvNGeu5B8h8bVL5cAk4/A== "@types/node@^12.7.1": version "12.12.62" @@ -3154,61 +3154,61 @@ resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.1.tgz#5668c0bce55a91f2b9566b1d8a4c0a8dbbc79764" integrity sha512-wmk0xQI6Yy7Fs/il4EpOcflG4uonUpYGqvZARESLc2oy4u69fkatFLbJOeW4Q6awO15P4rduAe6xkwHevpXcUQ== -"@typescript-eslint/eslint-plugin@4.11.1": - version "4.11.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.11.1.tgz#7579c6d17ad862154c10bc14b40e5427b729e209" - integrity sha512-fABclAX2QIEDmTMk6Yd7Muv1CzFLwWM4505nETzRHpP3br6jfahD9UUJkhnJ/g2m7lwfz8IlswcwGGPGiq9exw== +"@typescript-eslint/eslint-plugin@4.12.0": + version "4.12.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.12.0.tgz#00d1b23b40b58031e6d7c04a5bc6c1a30a2e834a" + integrity sha512-wHKj6q8s70sO5i39H2g1gtpCXCvjVszzj6FFygneNFyIAxRvNSVz9GML7XpqrB9t7hNutXw+MHnLN/Ih6uyB8Q== dependencies: - "@typescript-eslint/experimental-utils" "4.11.1" - "@typescript-eslint/scope-manager" "4.11.1" + "@typescript-eslint/experimental-utils" "4.12.0" + "@typescript-eslint/scope-manager" "4.12.0" debug "^4.1.1" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@4.11.1": - version "4.11.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.11.1.tgz#2dad3535b878c25c7424e40bfa79d899f3f485bc" - integrity sha512-mAlWowT4A6h0TC9F+J5pdbEhjNiEMO+kqPKQ4sc3fVieKL71dEqfkKgtcFVSX3cjSBwYwhImaQ/mXQF0oaI38g== +"@typescript-eslint/experimental-utils@4.12.0": + version "4.12.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.12.0.tgz#372838e76db76c9a56959217b768a19f7129546b" + integrity sha512-MpXZXUAvHt99c9ScXijx7i061o5HEjXltO+sbYfZAAHxv3XankQkPaNi5myy0Yh0Tyea3Hdq1pi7Vsh0GJb0fA== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/scope-manager" "4.11.1" - "@typescript-eslint/types" "4.11.1" - "@typescript-eslint/typescript-estree" "4.11.1" + "@typescript-eslint/scope-manager" "4.12.0" + "@typescript-eslint/types" "4.12.0" + "@typescript-eslint/typescript-estree" "4.12.0" eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/parser@4.11.1": - version "4.11.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.11.1.tgz#981e18de2e019d6ca312596615f92e8f6f6598ed" - integrity sha512-BJ3jwPQu1jeynJ5BrjLuGfK/UJu6uwHxJ/di7sanqmUmxzmyIcd3vz58PMR7wpi8k3iWq2Q11KMYgZbUpRoIPw== +"@typescript-eslint/parser@4.12.0": + version "4.12.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.12.0.tgz#e1cf30436e4f916c31fcc962158917bd9e9d460a" + integrity sha512-9XxVADAo9vlfjfoxnjboBTxYOiNY93/QuvcPgsiKvHxW6tOZx1W4TvkIQ2jB3k5M0pbFP5FlXihLK49TjZXhuQ== dependencies: - "@typescript-eslint/scope-manager" "4.11.1" - "@typescript-eslint/types" "4.11.1" - "@typescript-eslint/typescript-estree" "4.11.1" + "@typescript-eslint/scope-manager" "4.12.0" + "@typescript-eslint/types" "4.12.0" + "@typescript-eslint/typescript-estree" "4.12.0" debug "^4.1.1" -"@typescript-eslint/scope-manager@4.11.1": - version "4.11.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.11.1.tgz#72dc2b60b0029ab0888479b12bf83034920b4b69" - integrity sha512-Al2P394dx+kXCl61fhrrZ1FTI7qsRDIUiVSuN6rTwss6lUn8uVO2+nnF4AvO0ug8vMsy3ShkbxLu/uWZdTtJMQ== +"@typescript-eslint/scope-manager@4.12.0": + version "4.12.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.12.0.tgz#beeb8beca895a07b10c593185a5612f1085ef279" + integrity sha512-QVf9oCSVLte/8jvOsxmgBdOaoe2J0wtEmBr13Yz0rkBNkl5D8bfnf6G4Vhox9qqMIoG7QQoVwd2eG9DM/ge4Qg== dependencies: - "@typescript-eslint/types" "4.11.1" - "@typescript-eslint/visitor-keys" "4.11.1" + "@typescript-eslint/types" "4.12.0" + "@typescript-eslint/visitor-keys" "4.12.0" -"@typescript-eslint/types@4.11.1": - version "4.11.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.11.1.tgz#3ba30c965963ef9f8ced5a29938dd0c465bd3e05" - integrity sha512-5kvd38wZpqGY4yP/6W3qhYX6Hz0NwUbijVsX2rxczpY6OXaMxh0+5E5uLJKVFwaBM7PJe1wnMym85NfKYIh6CA== +"@typescript-eslint/types@4.12.0": + version "4.12.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.12.0.tgz#fb891fe7ccc9ea8b2bbd2780e36da45d0dc055e5" + integrity sha512-N2RhGeheVLGtyy+CxRmxdsniB7sMSCfsnbh8K/+RUIXYYq3Ub5+sukRCjVE80QerrUBvuEvs4fDhz5AW/pcL6g== -"@typescript-eslint/typescript-estree@4.11.1": - version "4.11.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.11.1.tgz#a4416b4a65872a48773b9e47afabdf7519eb10bc" - integrity sha512-tC7MKZIMRTYxQhrVAFoJq/DlRwv1bnqA4/S2r3+HuHibqvbrPcyf858lNzU7bFmy4mLeIHFYr34ar/1KumwyRw== +"@typescript-eslint/typescript-estree@4.12.0": + version "4.12.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.12.0.tgz#3963418c850f564bdab3882ae23795d115d6d32e" + integrity sha512-gZkFcmmp/CnzqD2RKMich2/FjBTsYopjiwJCroxqHZIY11IIoN0l5lKqcgoAPKHt33H2mAkSfvzj8i44Jm7F4w== dependencies: - "@typescript-eslint/types" "4.11.1" - "@typescript-eslint/visitor-keys" "4.11.1" + "@typescript-eslint/types" "4.12.0" + "@typescript-eslint/visitor-keys" "4.12.0" debug "^4.1.1" globby "^11.0.1" is-glob "^4.0.1" @@ -3216,12 +3216,12 @@ semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/visitor-keys@4.11.1": - version "4.11.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.11.1.tgz#4c050a4c1f7239786e2dd4e69691436143024e05" - integrity sha512-IrlBhD9bm4bdYcS8xpWarazkKXlE7iYb1HzRuyBP114mIaj5DJPo11Us1HgH60dTt41TCZXMaTCAW+OILIYPOg== +"@typescript-eslint/visitor-keys@4.12.0": + version "4.12.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.12.0.tgz#a470a79be6958075fa91c725371a83baf428a67a" + integrity sha512-hVpsLARbDh4B9TKYz5cLbcdMIOAoBYgFPCSP9FFS/liSF+b33gVNq8JHY3QGhHNVz85hObvL7BEYLlgx553WCw== dependencies: - "@typescript-eslint/types" "4.11.1" + "@typescript-eslint/types" "4.12.0" eslint-visitor-keys "^2.0.0" "@ungap/global-this@^0.4.2": @@ -3229,6 +3229,60 @@ resolved "https://registry.yarnpkg.com/@ungap/global-this/-/global-this-0.4.2.tgz#bc59a79799862e3e0cf647c090fd3f3f2285cf56" integrity sha512-uFg7Kz+E12RBlgBLMlWVjmn2OIeE2J1Lzij0RseNcCVsrJX+LEB4fQ9MnoPXkXJmO5cHtTEzI5ATtb3IJfQ9tQ== +"@vue/compiler-core@3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.0.5.tgz#a6e54cabe9536e74c6513acd2649f311af1d43ac" + integrity sha512-iFXwk2gmU/GGwN4hpBwDWWMLvpkIejf/AybcFtlQ5V1ur+5jwfBaV0Y1RXoR6ePfBPJixtKZ3PmN+M+HgMAtfQ== + dependencies: + "@babel/parser" "^7.12.0" + "@babel/types" "^7.12.0" + "@vue/shared" "3.0.5" + estree-walker "^2.0.1" + source-map "^0.6.1" + +"@vue/compiler-dom@3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.0.5.tgz#7885a13e6d18f64dde8ebceec052ed2c102696c2" + integrity sha512-HSOSe2XSPuCkp20h4+HXSiPH9qkhz6YbW9z9ZtL5vef2T2PMugH7/osIFVSrRZP/Ul5twFZ7MIRlp8tPX6e4/g== + dependencies: + "@vue/compiler-core" "3.0.5" + "@vue/shared" "3.0.5" + +"@vue/compiler-sfc@^3.0.4": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.0.5.tgz#3ae08e60244a72faf9598361874fb7bdb5b1d37c" + integrity sha512-uOAC4X0Gx3SQ9YvDC7YMpbDvoCmPvP0afVhJoxRotDdJ+r8VO3q4hFf/2f7U62k4Vkdftp6DVni8QixrfYzs+w== + dependencies: + "@babel/parser" "^7.12.0" + "@babel/types" "^7.12.0" + "@vue/compiler-core" "3.0.5" + "@vue/compiler-dom" "3.0.5" + "@vue/compiler-ssr" "3.0.5" + "@vue/shared" "3.0.5" + consolidate "^0.16.0" + estree-walker "^2.0.1" + hash-sum "^2.0.0" + lru-cache "^5.1.1" + magic-string "^0.25.7" + merge-source-map "^1.1.0" + postcss "^7.0.32" + postcss-modules "^3.2.2" + postcss-selector-parser "^6.0.4" + source-map "^0.6.1" + +"@vue/compiler-ssr@3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.0.5.tgz#7661ad891a0be948726c7f7ad1e425253c587b83" + integrity sha512-Wm//Kuxa1DpgjE4P9W0coZr8wklOfJ35Jtq61CbU+t601CpPTK4+FL2QDBItaG7aoUUDCWL5nnxMkuaOgzTBKg== + dependencies: + "@vue/compiler-dom" "3.0.5" + "@vue/shared" "3.0.5" + +"@vue/shared@3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.0.5.tgz#c131d88bd6713cc4d93b3bb1372edb1983225ff0" + integrity sha512-gYsNoGkWejBxNO6SNRjOh/xKeZ0H0V+TFzaPzODfBjkAIb0aQgBuixC1brandC/CDJy1wYPwSoYrXpvul7m6yw== + "@webassemblyjs/ast@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" @@ -3635,6 +3689,11 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" @@ -5013,6 +5072,13 @@ console-browserify@^1.1.0: resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== +consolidate@^0.16.0: + version "0.16.0" + resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.16.0.tgz#a11864768930f2f19431660a65906668f5fbdc16" + integrity sha512-Nhl1wzCslqXYTJVDyJCu3ODohy9OfBMB5uD2BiBTzd7w+QY0lBzafkR8y8755yMYHAaMD4NuzbAw03/xzfw+eQ== + dependencies: + bluebird "^3.7.2" + constants-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" @@ -5539,11 +5605,6 @@ dateformat@4.4.1: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.4.1.tgz#c6b3b821588f36826735c346148424d7a39007c3" integrity sha512-3V9b/50QBYmFtd2c3cPOmdr2xNfnDphoBLxh/UVBoPIsylWkbUYGq3f4EQYuEaK7Mq4vcIpQCmOyJ37pqW/Uug== -de-indent@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" - integrity sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0= - debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -6291,10 +6352,10 @@ eslint-visitor-keys@^2.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== -eslint@7.16.0: - version "7.16.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.16.0.tgz#a761605bf9a7b32d24bb7cde59aeb0fd76f06092" - integrity sha512-iVWPS785RuDA4dWuhhgXTNrGxHHK3a8HLSMBgbbU59ruJDubUraXN8N5rn7kb8tG6sjg74eE0RA3YWT51eusEw== +eslint@7.17.0: + version "7.17.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.17.0.tgz#4ccda5bf12572ad3bf760e6f195886f50569adb0" + integrity sha512-zJk08MiBgwuGoxes5sSQhOtibZ75pz0J35XTRlZOk9xMffhpA9BTbQZxoXZzOl5zMbleShbGwtw+1kGferfFwQ== dependencies: "@babel/code-frame" "^7.0.0" "@eslint/eslintrc" "^0.2.2" @@ -6386,6 +6447,11 @@ estree-walker@^1.0.1: resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== +estree-walker@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" @@ -6878,12 +6944,12 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" -find-versions@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/find-versions/-/find-versions-3.2.0.tgz#10297f98030a786829681690545ef659ed1d254e" - integrity sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww== +find-versions@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/find-versions/-/find-versions-4.0.0.tgz#3c57e573bf97769b8cb8df16934b627915da4965" + integrity sha512-wgpWy002tA+wgmO27buH/9KzyEOQnKsG/R0yrcjPT9BOFm0zRBVQbZ95nRGXWMywS8YR5knRbpohio0bcJABxQ== dependencies: - semver-regex "^2.0.0" + semver-regex "^3.1.2" find-yarn-workspace-root2@1.2.16: version "1.2.16" @@ -7132,6 +7198,13 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +generic-names@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/generic-names/-/generic-names-2.0.1.tgz#f8a378ead2ccaa7a34f0317b05554832ae41b872" + integrity sha512-kPCHWa1m9wGG/OwQpeweTwM/PYiQLrUIxXbt/P4Nic3LbGjCP0YwrALHW1uNLKZ0LIMg+RF+XRlj2ekT9ZlZAQ== + dependencies: + loader-utils "^1.1.0" + gensync@^1.0.0-beta.1: version "1.0.0-beta.1" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" @@ -7269,10 +7342,10 @@ globby@11.0.0: merge2 "^1.3.0" slash "^3.0.0" -globby@11.0.1, globby@^11.0.0, globby@^11.0.1: - version "11.0.1" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357" - integrity sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ== +globby@11.0.2: + version "11.0.2" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.2.tgz#1af538b766a3b540ebfb58a32b2e2d5897321d83" + integrity sha512-2ZThXDvvV8fYFRVIxnrMQBipZQDr7MxKAmQK1vujaj9/7eF0efG7BPUKJ7jP7G5SLF37xKDXvO4S/KKLj/Z0og== dependencies: array-union "^2.1.0" dir-glob "^3.0.1" @@ -7308,6 +7381,18 @@ globby@^10.0.1: merge2 "^1.2.3" slash "^3.0.0" +globby@^11.0.0, globby@^11.0.1: + version "11.0.1" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357" + integrity sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.1.1" + ignore "^5.1.4" + merge2 "^1.3.0" + slash "^3.0.0" + globby@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" @@ -7353,10 +7438,10 @@ grapheme-splitter@^1.0.4: resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== -graphql-helix@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/graphql-helix/-/graphql-helix-1.2.0.tgz#263dba445d1eb668d44ddf592a4a6423f197dcb5" - integrity sha512-oOdJc9xvbwr5xe0zPublElbE8BUXGJhhtemJbcN8W7+TPEmZm8At1YcRCoazrdrNyd2gPkOCODaFNd9JnByNjA== +graphql-helix@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/graphql-helix/-/graphql-helix-1.2.1.tgz#b0c9255fbf05fe84a2b8a0c33053d625a6c4dc53" + integrity sha512-pnlXKSW6qrU/SLfKHvoWicYsmTk2IDjANv/Kdq9kDpT3e0Ajl+fdG+xmNhpjieHPzoOry97CBS2/2qT3qspcsQ== graphql-request@^3.3.0: version "3.3.0" @@ -7544,6 +7629,11 @@ hash-base@^3.0.0: readable-stream "^3.6.0" safe-buffer "^5.2.0" +hash-sum@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-2.0.0.tgz#81d01bb5de8ea4a214ad5d6ead1b523460b0b45a" + integrity sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg== + hash.js@^1.0.0, hash.js@^1.0.3: version "1.1.7" resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" @@ -7630,7 +7720,7 @@ hastscript@^5.0.0: property-information "^5.0.0" space-separated-tokens "^1.0.0" -he@^1.1.0, he@^1.2.0: +he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== @@ -7888,18 +7978,18 @@ human-signals@^1.1.1: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== -husky@4.3.6: - version "4.3.6" - resolved "https://registry.yarnpkg.com/husky/-/husky-4.3.6.tgz#ebd9dd8b9324aa851f1587318db4cccb7665a13c" - integrity sha512-o6UjVI8xtlWRL5395iWq9LKDyp/9TE7XMOTvIpEVzW638UcGxTmV5cfel6fsk/jbZSTlvfGVJf2svFtybcIZag== +husky@4.3.7: + version "4.3.7" + resolved "https://registry.yarnpkg.com/husky/-/husky-4.3.7.tgz#ca47bbe6213c1aa8b16bbd504530d9600de91e88" + integrity sha512-0fQlcCDq/xypoyYSJvEuzbDPHFf8ZF9IXKJxlrnvxABTSzK1VPT2RKYQKrcgJ+YD39swgoB6sbzywUqFxUiqjw== dependencies: chalk "^4.0.0" ci-info "^2.0.0" compare-versions "^3.6.0" cosmiconfig "^7.0.0" - find-versions "^3.2.0" + find-versions "^4.0.0" opencollective-postinstall "^2.0.2" - pkg-dir "^4.2.0" + pkg-dir "^5.0.0" please-upgrade-node "^3.2.0" slash "^3.0.0" which-pm-runs "^1.0.0" @@ -7918,6 +8008,11 @@ iconv-lite@^0.6.2: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" +icss-replace-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" + integrity sha1-Bupvg2ead0njhs/h/oEq5dsiPe0= + icss-utils@^4.0.0, icss-utils@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467" @@ -9062,7 +9157,7 @@ js-tokens@^3.0.2: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= -js-yaml@^3.11.0, js-yaml@^3.13.0, js-yaml@^3.13.1, js-yaml@^3.14.0, js-yaml@^3.6.1: +js-yaml@^3.11.0, js-yaml@^3.13.0, js-yaml@^3.13.1, js-yaml@^3.6.1: version "3.14.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== @@ -9070,6 +9165,13 @@ js-yaml@^3.11.0, js-yaml@^3.13.0, js-yaml@^3.13.1, js-yaml@^3.14.0, js-yaml@^3.6 argparse "^1.0.7" esprima "^4.0.0" +js-yaml@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.0.0.tgz#f426bc0ff4b4051926cd588c71113183409a121f" + integrity sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q== + dependencies: + argparse "^2.0.1" + jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" @@ -9760,6 +9862,13 @@ lunr@^2.3.9: resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow== +magic-string@^0.25.7: + version "0.25.7" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" + integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== + dependencies: + sourcemap-codec "^1.4.4" + make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -9928,6 +10037,13 @@ merge-descriptors@1.0.1: resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= +merge-source-map@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646" + integrity sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw== + dependencies: + source-map "^0.6.1" + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -11124,6 +11240,13 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +pkg-dir@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-5.0.0.tgz#a02d6aebe6ba133a928f74aec20bafdfe6b8e760" + integrity sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA== + dependencies: + find-up "^5.0.0" + pkg-up@3.1.0, pkg-up@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" @@ -11519,6 +11642,21 @@ postcss-modules-values@^3.0.0: icss-utils "^4.0.0" postcss "^7.0.6" +postcss-modules@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/postcss-modules/-/postcss-modules-3.2.2.tgz#ee390de0f9f18e761e1778dfb9be26685c02c51f" + integrity sha512-JQ8IAqHELxC0N6tyCg2UF40pACY5oiL6UpiqqcIFRWqgDYO8B0jnxzoQ0EOpPrWXvcpu6BSbQU/3vSiq7w8Nhw== + dependencies: + generic-names "^2.0.1" + icss-replace-symbols "^1.1.0" + lodash.camelcase "^4.3.0" + postcss "^7.0.32" + postcss-modules-extract-imports "^2.0.0" + postcss-modules-local-by-default "^3.0.2" + postcss-modules-scope "^2.2.0" + postcss-modules-values "^3.0.0" + string-hash "^1.1.1" + postcss-nesting@^7.0.0: version "7.0.1" resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-7.0.1.tgz#b50ad7b7f0173e5b5e3880c3501344703e04c052" @@ -11758,7 +11896,7 @@ postcss-selector-parser@^5.0.0-rc.3, postcss-selector-parser@^5.0.0-rc.4: indexes-of "^1.0.1" uniq "^1.0.1" -postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2: +postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: version "6.0.4" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3" integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw== @@ -13049,10 +13187,10 @@ semver-diff@^3.1.1: dependencies: semver "^6.3.0" -semver-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-2.0.0.tgz#a93c2c5844539a770233379107b38c7b4ac9d338" - integrity sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw== +semver-regex@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-3.1.2.tgz#34b4c0d361eef262e07199dbef316d0f2ab11807" + integrity sha512-bXWyL6EAKOJa81XG1OZ/Yyuq+oT0b2YLlxx7c+mrdYPaPbnj6WgVULXhinMIeZGufuUBu/eVRqXEhiv4imfwxA== "semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0: version "5.7.1" @@ -13455,6 +13593,11 @@ source-map@^0.7.3, source-map@~0.7.2: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== +sourcemap-codec@^1.4.4: + version "1.4.8" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + space-separated-tokens@^1.0.0: version "1.1.5" resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899" @@ -13659,6 +13802,11 @@ string-argv@0.3.1: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== +string-hash@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b" + integrity sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs= + string-length@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.1.tgz#4a973bf31ef77c4edbceadd6af2611996985f8a1" @@ -14236,6 +14384,11 @@ tslib@^2.0.3, tslib@~2.0.1, tslib@~2.0.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c" integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ== +tslib@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" + integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== + tsutils@^3.17.1: version "3.17.1" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" @@ -14341,17 +14494,17 @@ typedoc-default-themes@0.12.0: resolved "https://registry.yarnpkg.com/typedoc-default-themes/-/typedoc-default-themes-0.12.0.tgz#42451948e55a148c1350eb2aa68829be5c2b06b3" integrity sha512-0hHBxwmfxE0rkIslOiO39fJyYwaScQEhUIxcpqx3uS1BL3zhFW5oQfUaPx2cv2XLL/GXhYFxhdFLoVmNptbxEQ== -typedoc-plugin-markdown@3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/typedoc-plugin-markdown/-/typedoc-plugin-markdown-3.2.1.tgz#c11d1107893811b4d4d8f9443885a34d2a53e502" - integrity sha512-gepVk2zFFrTGaKywLEgwz6EARYjOGcx9rHF8M8a+fqz/iTp6Zobvw+7x01BJ9V4tbuXI3M9Y2/wMYwC378/msg== +typedoc-plugin-markdown@3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/typedoc-plugin-markdown/-/typedoc-plugin-markdown-3.4.0.tgz#5634f2aa74053f514b882f36782769c157c3a9aa" + integrity sha512-aHLWI4jeSpSDgMbRByinRp+b2u4kHXySiccZc7lKSExH4Md44ds21oH0g+xZ5lBv9dhZdTz7mhTCrbAm5Nh24w== dependencies: handlebars "^4.7.6" -typedoc@0.20.5: - version "0.20.5" - resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.20.5.tgz#09f76d6b7d7ae647e44cdfa0ed4a507836d70b22" - integrity sha512-gx9DHzdRz+b7jZvVXx6+RTrf8HssL3DcZjCEShgMhYttXuZ/6gkQWF2gYmBQ/65L33eS9Bgzwi2RR4bN2h7elg== +typedoc@0.20.13: + version "0.20.13" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.20.13.tgz#9d56df76d0b421a71a33336dc64b1a466dc8f2fd" + integrity sha512-SJVFn6NJd5bWJHMPgEkDUrKIEfMbja6ftIJv/tgd0xQZp5cWxGTdEnmRr56+egIQZkAJFB39eDvmNV4Lqqy4Gw== dependencies: colors "^1.4.0" fs-extra "^9.0.1" @@ -14793,14 +14946,6 @@ vscode-textmate@^5.2.0: resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.2.0.tgz#01f01760a391e8222fe4f33fbccbd1ad71aed74e" integrity sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ== -vue-template-compiler@^2.6.12: - version "2.6.12" - resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.12.tgz#947ed7196744c8a5285ebe1233fe960437fcc57e" - integrity sha512-OzzZ52zS41YUbkCBfdXShQTe69j1gQDZ9HIX8miuC9C3rBCk9wIRjLiZZLrmX9V+Ftq/YEyv1JaVr5Y/hNtByg== - dependencies: - de-indent "^1.0.2" - he "^1.1.0" - w3c-hr-time@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" From 2749b0bafd70fc7328530a5151bf509ac6d91eb5 Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Sat, 9 Jan 2021 13:16:47 -0500 Subject: [PATCH 02/14] shrink diff. --- packages/stitch/src/mergeCandidates.ts | 12 ++++--- website/docs/stitch-combining-schemas.md | 2 +- website/docs/stitch-directives-sdl.md | 2 +- website/sidebars.json | 44 ------------------------ 4 files changed, 9 insertions(+), 51 deletions(-) delete mode 100644 website/sidebars.json diff --git a/packages/stitch/src/mergeCandidates.ts b/packages/stitch/src/mergeCandidates.ts index 51e11accee4..2eaac32a62c 100644 --- a/packages/stitch/src/mergeCandidates.ts +++ b/packages/stitch/src/mergeCandidates.ts @@ -92,7 +92,7 @@ function mergeObjectTypeCandidates( const astNodes = pluck('astNode', candidates); const fieldAstNodes = Object.values(fields) - .map(({ astNode }) => astNode) + .map(f => f.astNode) .filter(n => n != null); if (astNodes.length > 1 && fieldAstNodes.length) { @@ -110,6 +110,7 @@ function mergeObjectTypeCandidates( ); const extensionASTNodes = [].concat(pluck>('extensionASTNodes', candidates)); + const extensions = Object.assign({}, ...pluck>('extensions', candidates)); const typeConfig = { @@ -137,7 +138,7 @@ function mergeInputObjectTypeCandidates( const astNodes = pluck('astNode', candidates); const fieldAstNodes = Object.values(fields) - .map(({ astNode }) => astNode) + .map(f => f.astNode) .filter(n => n != null); if (astNodes.length > 1 && fieldAstNodes.length) { @@ -198,7 +199,7 @@ function mergeInterfaceTypeCandidates( const astNodes = pluck('astNode', candidates); const fieldAstNodes = Object.values(fields) - .map(({ astNode }) => astNode) + .map(f => f.astNode) .filter(n => n != null); if (astNodes.length > 1 && fieldAstNodes.length) { @@ -239,6 +240,7 @@ function mergeUnionTypeCandidates( ): GraphQLUnionType { candidates = orderedTypeCandidates(candidates, typeMergingOptions); const description = mergeTypeDescriptions(candidates, typeMergingOptions); + const typeConfigs = candidates.map(candidate => (candidate.type as GraphQLUnionType).toConfig()); const typeMap = typeConfigs.reduce((acc, typeConfig) => { typeConfig.types.forEach(type => { @@ -284,7 +286,7 @@ function mergeEnumTypeCandidates( const astNodes = pluck('astNode', candidates); const valueAstNodes = Object.values(values) - .map(({ astNode }) => astNode) + .map(v => v.astNode) .filter(n => n != null); if (astNodes.length > 1 && valueAstNodes.length) { @@ -346,7 +348,7 @@ function enumValueConfigMapFromTypeCandidates( enumValueConfigMap[enumValue] = enumValueConfigMerger(enumValueConfigCandidatesMap[enumValue]); }); - return JSON.parse(JSON.stringify(enumValueConfigMap)) as GraphQLEnumValueConfigMap; + return enumValueConfigMap; } function defaultEnumValueConfigMerger(candidates: Array) { diff --git a/website/docs/stitch-combining-schemas.md b/website/docs/stitch-combining-schemas.md index b311cf9b4d7..b57f7a11747 100644 --- a/website/docs/stitch-combining-schemas.md +++ b/website/docs/stitch-combining-schemas.md @@ -147,7 +147,7 @@ const gatewaySchema = stitchSchemas({ typeMergingOptions: { // select a preferred candidate: selectCanonicalTypeCandidate: (candidates) => candidate[0], - // and/or, itemize specific element definitions: + // and/or itemize the selection of specific definitions: typeDescriptionsMerger: (candidates) => candidate[0].type.description, fieldConfigMerger: (candidates) => candidate[0].fieldConfig, inputFieldConfigMerger: (candidates) => candidate[0].inputFieldConfig, diff --git a/website/docs/stitch-directives-sdl.md b/website/docs/stitch-directives-sdl.md index 7e03af32b34..1084d68ea9b 100644 --- a/website/docs/stitch-directives-sdl.md +++ b/website/docs/stitch-directives-sdl.md @@ -67,7 +67,7 @@ The function of these directives are: * **`@computed`:** specifies a selection of fields required from other services to compute the value of this field. These additional fields are only selected when the computed field is requested. Analogous to [computed field](/docs/stitch-type-merging#computed-fields) in merged type configuration. Computed field dependencies must be sent into the subservice using an [object key](#object-keys). -* **`@canonical`:** identifies types and fields that provide a [canonical definition](/docs/stitch-type-merging#canonical-definitions) to be built into the combined gateway schema. Useful when the same types appear across multiple subschemas and a specific definition should be preferred. +* **`@canonical`:** identifies types and fields that provide a [canonical definition](/docs/stitch-type-merging#canonical-definitions) to be built into the combined gateway schema. Useful when the same types appear across multiple subschemas and a specific element definition should be preferred. #### Customizing names diff --git a/website/sidebars.json b/website/sidebars.json deleted file mode 100644 index 6204f7b9549..00000000000 --- a/website/sidebars.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "someSidebar": [ - "introduction", - { - "Guides": [ - "generate-schema", - "resolvers", - "resolvers-composition", - "scalars", - "mocking", - "connectors", - "schema-directives", - "directive-resolvers", - "schema-delegation", - "remote-schemas", - "schema-wrapping", - "schema-merging", - { - "Schema stitching": [ - "stitch-combining-schemas", - "stitch-type-merging", - "stitch-directives-sdl", - "stitch-schema-extensions", - "stitch-api" - ] - }, - "server-setup", - "schema-loading", - "documents-loading", - "graphql-tag-pluck", - "relay-operation-optimizer", - { - "Migration": [ - "migration-from-tools", - "migration-from-toolkit", - "migration-from-merge-graphql-schemas", - "migration-from-import" - ] - } - ] - }, - { "API Reference": [] } - ] -} From 748f9e5eaccd0bff83ad7d5a1497a2814f1f817f Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Sat, 9 Jan 2021 13:18:29 -0500 Subject: [PATCH 03/14] shrink diff. --- packages/merge/src/typedefs-mergers/enum-values.ts | 2 +- packages/merge/src/typedefs-mergers/fields.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/merge/src/typedefs-mergers/enum-values.ts b/packages/merge/src/typedefs-mergers/enum-values.ts index 57b7750182a..64d5e977bab 100644 --- a/packages/merge/src/typedefs-mergers/enum-values.ts +++ b/packages/merge/src/typedefs-mergers/enum-values.ts @@ -6,7 +6,7 @@ import { compareNodes } from '@graphql-tools/utils'; export function mergeEnumValues( first: ReadonlyArray, second: ReadonlyArray, - config?: Config + config: Config ): EnumValueDefinitionNode[] { const enumValueMap = new Map(); for (const firstValue of first) { diff --git a/packages/merge/src/typedefs-mergers/fields.ts b/packages/merge/src/typedefs-mergers/fields.ts index 7f6d49d3373..6591156edb8 100644 --- a/packages/merge/src/typedefs-mergers/fields.ts +++ b/packages/merge/src/typedefs-mergers/fields.ts @@ -26,7 +26,7 @@ export function mergeFields, f2: ReadonlyArray, - config?: Config + config: Config ): T[] { const result: T[] = [...f2]; From a1de9390893a913f98870c1d0eb61118e240c4ce Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Sat, 9 Jan 2021 13:22:32 -0500 Subject: [PATCH 04/14] fix ts error. --- packages/stitch/src/mergeCandidates.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/stitch/src/mergeCandidates.ts b/packages/stitch/src/mergeCandidates.ts index 2eaac32a62c..0899a061e03 100644 --- a/packages/stitch/src/mergeCandidates.ts +++ b/packages/stitch/src/mergeCandidates.ts @@ -16,7 +16,6 @@ import { GraphQLInputFieldConfig, GraphQLInputFieldConfigMap, ObjectTypeDefinitionNode, - FieldDefinitionNode, InputObjectTypeDefinitionNode, InterfaceTypeDefinitionNode, UnionTypeDefinitionNode, From 5c42631a0c46eccea1fc7a8efc75f47e5861db18 Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Sat, 9 Jan 2021 15:23:09 -0500 Subject: [PATCH 05/14] rename candidate merger. --- packages/stitch/src/mergeCandidates.ts | 7 +++---- packages/stitch/src/types.ts | 2 +- website/docs/stitch-combining-schemas.md | 4 ++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/stitch/src/mergeCandidates.ts b/packages/stitch/src/mergeCandidates.ts index 0899a061e03..66d7420f00c 100644 --- a/packages/stitch/src/mergeCandidates.ts +++ b/packages/stitch/src/mergeCandidates.ts @@ -404,13 +404,12 @@ function orderedTypeCandidates( candidates: Array, typeMergingOptions: TypeMergingOptions ): Array { - const selectCanonicalTypeCandidate = - typeMergingOptions?.selectCanonicalTypeCandidate ?? defaultSelectCanonicalTypeCandidate; - const candidate = selectCanonicalTypeCandidate(candidates); + const typeCandidateMerger = typeMergingOptions?.typeCandidateMerger ?? defaultTypeCandidateMerger; + const candidate = typeCandidateMerger(candidates); return candidates.sort((_a, b) => (b === candidate ? -1 : 0)); } -function defaultSelectCanonicalTypeCandidate(candidates: Array): MergeTypeCandidate { +function defaultTypeCandidateMerger(candidates: Array): MergeTypeCandidate { const canonical: Array = candidates.filter(({ type, subschema }) => isSubschemaConfig(subschema) ? subschema.merge?.[type.name]?.canonical : false ); diff --git a/packages/stitch/src/types.ts b/packages/stitch/src/types.ts index 07ea4ec43fb..6532b7ffb24 100644 --- a/packages/stitch/src/types.ts +++ b/packages/stitch/src/types.ts @@ -80,7 +80,7 @@ export interface IStitchSchemasOptions extends Omit SubschemaConfig; export interface TypeMergingOptions { - selectCanonicalTypeCandidate?: (candidates: Array) => MergeTypeCandidate; + typeCandidateMerger?: (candidates: Array) => MergeTypeCandidate; typeDescriptionsMerger?: (candidates: Array) => string; fieldConfigMerger?: (candidates: Array) => GraphQLFieldConfig; inputFieldConfigMerger?: (candidates: Array) => GraphQLInputFieldConfig; diff --git a/website/docs/stitch-combining-schemas.md b/website/docs/stitch-combining-schemas.md index b57f7a11747..a3f324532ee 100644 --- a/website/docs/stitch-combining-schemas.md +++ b/website/docs/stitch-combining-schemas.md @@ -146,8 +146,8 @@ const gatewaySchema = stitchSchemas({ mergeTypes: true, // << default in v7 typeMergingOptions: { // select a preferred candidate: - selectCanonicalTypeCandidate: (candidates) => candidate[0], - // and/or itemize the selection of specific definitions: + typeCandidateMerger: (candidates) => candidate[0], + // and/or itemize the selection of other specific definitions: typeDescriptionsMerger: (candidates) => candidate[0].type.description, fieldConfigMerger: (candidates) => candidate[0].fieldConfig, inputFieldConfigMerger: (candidates) => candidate[0].inputFieldConfig, From bdef2fb33e494fe56bc47ed7386f268510b8cca4 Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Sat, 9 Jan 2021 21:42:29 -0500 Subject: [PATCH 06/14] try without sorting? --- packages/stitch/src/mergeCandidates.ts | 2 +- .../tests/{mergeCanonical.test.ts => mergeDefinitions.test.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/stitch/tests/{mergeCanonical.test.ts => mergeDefinitions.test.ts} (100%) diff --git a/packages/stitch/src/mergeCandidates.ts b/packages/stitch/src/mergeCandidates.ts index 66d7420f00c..73e88cb9471 100644 --- a/packages/stitch/src/mergeCandidates.ts +++ b/packages/stitch/src/mergeCandidates.ts @@ -406,7 +406,7 @@ function orderedTypeCandidates( ): Array { const typeCandidateMerger = typeMergingOptions?.typeCandidateMerger ?? defaultTypeCandidateMerger; const candidate = typeCandidateMerger(candidates); - return candidates.sort((_a, b) => (b === candidate ? -1 : 0)); + return candidates.filter(c => c !== candidate).concat([candidate]); } function defaultTypeCandidateMerger(candidates: Array): MergeTypeCandidate { diff --git a/packages/stitch/tests/mergeCanonical.test.ts b/packages/stitch/tests/mergeDefinitions.test.ts similarity index 100% rename from packages/stitch/tests/mergeCanonical.test.ts rename to packages/stitch/tests/mergeDefinitions.test.ts From f1b6d0f685e9007d6d686d204b190454fb62567b Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Sat, 9 Jan 2021 22:47:28 -0500 Subject: [PATCH 07/14] updated docs. --- website/docs/stitch-combining-schemas.md | 6 ++-- website/docs/stitch-directives-sdl.md | 44 +++++++++++------------- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/website/docs/stitch-combining-schemas.md b/website/docs/stitch-combining-schemas.md index a3f324532ee..fa88df81fcd 100644 --- a/website/docs/stitch-combining-schemas.md +++ b/website/docs/stitch-combining-schemas.md @@ -102,7 +102,7 @@ Also note that these subschema config objects may need to be referenced again in ## Stitching remote schemas -To include a remote schema in the combined gateway, you must provide at least the `schema` and `executor` subschema config options: +To include a remote schema in the combined gateway, you must provide at least the `schema` and `executor` subschema config options, and an optional `subscriber` for subscriptions: ```js import { introspectSchema } from '@graphql-tools/wrap'; @@ -138,14 +138,14 @@ Stitching has two strategies for handling types duplicated across subschemas: an Types with the same name are automatically merged by default in GraphQL Tools v7. That means objects, interfaces, and input objects with the same name will consolidate their fields across subschemas, and unions/enums will consolidate all their members. The combined gateway schema will then smartly delegate portions of a request to the proper origin subschema(s). See [type merging guide](/docs/stitch-type-merging/) for a comprehensive overview. -Automatic merging will only encounter conflicts on fields and type descriptions. By default, the final definition of a field or type description found in the subschemas array is used, or a specific version may be [marked as canonical](/docs/stitch-type-merging#canonical-definitions). You may customize this selection logic using `typeMergingOptions`; the following prefers the _first_ definition of each conflicting element found in the subschemas array: +Automatic merging will only encounter conflicts on type descriptions and fields. By default, the final definition of a type or field found in the subschemas array is used, or a specific definition may be [marked as canonical](/docs/stitch-type-merging#canonical-definitions). You may customize all selection logic using `typeMergingOptions`; the following prefers the _first_ definition of each conflicting element found in the subschemas array: ```js const gatewaySchema = stitchSchemas({ subschemas: [...], mergeTypes: true, // << default in v7 typeMergingOptions: { - // select a preferred candidate: + // select a preferred type candidate that provides definitions: typeCandidateMerger: (candidates) => candidate[0], // and/or itemize the selection of other specific definitions: typeDescriptionsMerger: (candidates) => candidate[0].type.description, diff --git a/website/docs/stitch-directives-sdl.md b/website/docs/stitch-directives-sdl.md index 1084d68ea9b..4fee9ba8f7b 100644 --- a/website/docs/stitch-directives-sdl.md +++ b/website/docs/stitch-directives-sdl.md @@ -12,7 +12,7 @@ Using SDL directives, a subservice may express its complete schema _and type mer ```graphql # --- Users schema --- -type User @key(selectionSet: "{ id }") { +type User { id: ID! username: String! email: String! @@ -29,7 +29,7 @@ type Post { author: User } -type User @key(selectionSet: "{ id }") { +type User { id: ID! posts: [Post] } @@ -44,7 +44,7 @@ In the above example, the Users and Posts schemas will be combined in the stitch ## Directives glossary -By default, stitching directives use the following definitions (though the names of these directives [may be customized](#customizing-names)): +By default, stitching directives use the following definitions (though the names of these directives [may be customized](#customizing-directive-names)): ```graphql directive @merge(keyField: String, keyArg: String, additionalArgs: String, key: [String!], argsExpr: String) on FIELD_DEFINITION @@ -55,10 +55,10 @@ directive @canonical on OBJECT | INTERFACE | INPUT_OBJECT | UNION | ENUM | SCALA The function of these directives are: -* **`@merge`:** denotes a root field used to query a merged type across services. The marked field's name is analogous to the `fieldName` setting in [merged type configuration](/docs/stitch-type-merging#basic-example), while the field's arguments and return types automatically configure merging. Additional arguments may tune the merge behavior (see [example recipes](#recipes)): +* **`@merge`:** denotes a root field used to query a merged type across services. The marked field's name is analogous to the `fieldName` setting in [merged type configuration](/docs/stitch-type-merging#basic-example), while the field's arguments and return type are used to infer merge configuration. Directive arguments tune the merge behavior (see [example recipes](#recipes)): - * `keyField`: specifies the name of a field to pick off origin objects as the key value. Omitting this option requires specification of an [object key](#object-keys) using the `@key` directive. - * `keyArg`: specifies which field argument receives the merge key. This may be omitted for fields with only one argument where the key recipient can be inferred. + * `keyField`: specifies the name of a field to pick off origin objects as the key value. When omitted, a `@key` directive must be included on the return type's definition to be built into an [object key](#object-keys). + * `keyArg`: specifies which field argument receives the merge key. This may be omitted for fields with only one argument where the recipient can be inferred. * `additionalArgs`: specifies a string of additional keys and values to apply to other arguments, formatted as `""" arg1: "value", arg2: "value" """`. * _`key`: advanced use only; builds a custom key._ * _`argsExpr`: advanced use only; builds a custom args object._ @@ -67,9 +67,9 @@ The function of these directives are: * **`@computed`:** specifies a selection of fields required from other services to compute the value of this field. These additional fields are only selected when the computed field is requested. Analogous to [computed field](/docs/stitch-type-merging#computed-fields) in merged type configuration. Computed field dependencies must be sent into the subservice using an [object key](#object-keys). -* **`@canonical`:** identifies types and fields that provide a [canonical definition](/docs/stitch-type-merging#canonical-definitions) to be built into the combined gateway schema. Useful when the same types appear across multiple subschemas and a specific element definition should be preferred. +* **`@canonical`:** identifies types and fields that provide a [canonical definition](/docs/stitch-type-merging#canonical-definitions) to be built into the combined gateway schema. Useful when the same types appear across multiple subschemas and a specific definition should be promoted. -#### Customizing names +#### Customizing directive names You may use the `stitchingDirectives` helper to build your own type definitions and validator with custom names. For example, the configuration below creates the resources for `@myKey`, `@myMerge`, and `@myComputed` directives: @@ -184,11 +184,11 @@ The simplest merge pattern picks a key field from origin objects: ```graphql type User { - # ... + id: ID! } type Product { - # ... + upc: ID! } type Query { @@ -197,7 +197,7 @@ type Query { } ``` -This SDL translates into the following merge config: +Here, the `@merge` directive marks each type's merge query, and its `keyField` argument specifies a field to be picked from each original object as the query argument value. The above SDL translates into the following merge config: ```js merge: { @@ -217,15 +217,13 @@ merge: { } ``` -Here, the `@merge` directive marks each type's merge query—then `keyField` specifies a field to be picked from each original object as the query argument value. - ### Multiple arguments This pattern configures a merge query that receives multiple arguments: ```graphql type User { - # ... + id: ID! } type Query { @@ -237,7 +235,7 @@ type Query { } ``` -This SDL translates into the following merge config: +Because the merger field receives multiple arguments, the `keyArg` parameter is required to specify which argument receives the key(s). The `additionalArgs` parameter may also be used to provide static values for other arguments. The above SDL translates into the following merge config: ```js merge: { @@ -250,15 +248,13 @@ merge: { } ``` -Because the merge field receives multiple arguments, the `keyArg` parameter is required to specify which argument receives the key(s). The `additionalArgs` parameter may then be used to provide static values for the other arguments. - ### Object keys -In the absence of a `keyField` to pick, keys will assume the shape of an object with a `__typename` and all fields collected for all selectionSets on the type. These object keys may be represented in your schema with a dedicated scalar type, or as an [input object](#typed-inputs): +In the absence of a `keyField` for the merge directive to pick, keys will assume the shape of an object with a `__typename` and all fields collected for utilized selectionSets on the type: ```graphql type Product @key(selectionSet: "{ upc }") { - # ... + upc: ID! shippingEstimate: Int @computed(selectionSet: "{ price weight }") } @@ -269,7 +265,7 @@ type Query { } ``` -You may use any name for the key scalar, here we're calling it `_Key`. This SDL translates into the following merge config: +The above SDL specifies a type-level selectionSet using the `@key` directive, and a field-level selectionSet using the `@computed` directive. The `@merge` directive takes no arguments here, and will build object keys with fields collected from all utilized selectionSets. These object keys are passed to the merger field as a custom scalar (here called `_Key`), or as an [input object](#typed-inputs). This SDL translates into the following merge config: ```js // assume "pick" works like the lodash method... @@ -310,7 +306,7 @@ Similar to the [object keys](#object-keys) discussed above, an input object type ```graphql type Product @key(selectionSet: "{ upc }") { - # ... + upc: ID! shippingEstimate: Int @computed(selectionSet: "{ price weight }") } @@ -357,7 +353,7 @@ More advanced cases may need to interface with complex inputs. In these cases, t ```graphql type Product @key(selectionSet: "{ upc }") { - # ... + upc: ID! } input ProductKey { @@ -377,9 +373,9 @@ type Query { Once subschemas and their merge configurations are defined as annotated SDLs, new versions of these documents can be pushed to the gateway to trigger a ["hot" reload](https://github.com/gmac/schema-stitching-handbook/tree/master/hot-schema-reloading)—or, a reload of the gateway schema without restarting its server. -However, pushing untested SDLs directly to the gateway is risky due to the potential for incompatible subschema versions to be mixed. Therefore, a formal versioning, testing, and release strategy is necessary for long-term stability. See the [versioning handbook example](https://github.com/gmac/schema-stitching-handbook/tree/master/versioning-schema-releases) that demonstrates using the GitHub API to turn a basic Git repo into a schema registry that manages versioning and release. +However, pushing untested SDLs directly to the gateway is risky due to the potential for incompatible subschema versions to be mixed. Therefore, a formal versioning, testing, and release strategy is necessary for long-term stability. See the [handbook's versioning example](https://github.com/gmac/schema-stitching-handbook/tree/master/versioning-schema-releases) that demonstrates turning a basic Git repo into a schema registry that manages versioning and release. -The general process for zero-downtime rollouts is: +**The general process for zero-downtime rollouts is:** 1. Compose and test all subschema head versions together to verify their combined stability prior to release. 1. Deploy all updated subservice applications while keeping their existing subschema features operational. From f7985739790eba90452af3628bd6d36a436592f5 Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Sun, 10 Jan 2021 12:52:15 -0500 Subject: [PATCH 08/14] update SDL example. --- website/docs/stitch-type-merging.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/stitch-type-merging.md b/website/docs/stitch-type-merging.md index 682919ebc0f..105ecfddb5a 100644 --- a/website/docs/stitch-type-merging.md +++ b/website/docs/stitch-type-merging.md @@ -92,10 +92,10 @@ That's it! Under the subschema config `merge` option, each merged type provides - `selectionSet` specifies one or more key fields required from other services to perform this query. Query planning will automatically resolve these fields from other subschemas in dependency order. - `args` formats the initial object representation into query arguments. -See related [handbook example](https://github.com/gmac/schema-stitching-handbook/tree/master/type-merging-single-records) for a working demonstration of this setup. This JavaScript-based syntax may also be written directly into schema type definitions using the [stitching directives SDL](/docs/stitch-directives-sdl): +See related [handbook example](https://github.com/gmac/schema-stitching-handbook/tree/master/type-merging-single-records) for a working demonstration of this setup. This JavaScript-based syntax may also be written directly into schema type definitions using the `@merge` directive of the [stitching SDL](/docs/stitch-directives-sdl): ```graphql -type User @key(selectionSet: "{ id }") { +type User { id: ID! email: String! } From 58e476ce8126eda525e0dbe23cebbd49300b6f9b Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Sun, 10 Jan 2021 21:49:14 -0500 Subject: [PATCH 09/14] Create quick-masks-hang.md --- .changeset/quick-masks-hang.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/quick-masks-hang.md diff --git a/.changeset/quick-masks-hang.md b/.changeset/quick-masks-hang.md new file mode 100644 index 00000000000..e39fc70a2bb --- /dev/null +++ b/.changeset/quick-masks-hang.md @@ -0,0 +1,9 @@ +--- +"@graphql-tools/delegate": patch +"@graphql-tools/merge": patch +"@graphql-tools/stitch": minor +"@graphql-tools/stitching-directives": minor +"@graphql-tools/website": patch +--- + +enhance(stitch) canonical merged type and field definitions. Use the @canonical directive to promote preferred type and field descriptions into the combined gateway schema. From 04c209add3f946248e862078640cf9247982e6fc Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Mon, 11 Jan 2021 09:19:53 -0500 Subject: [PATCH 10/14] only re-merge override fields. --- packages/stitch/src/mergeCandidates.ts | 39 +++++++++++++----------- website/docs/stitch-combining-schemas.md | 2 +- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/packages/stitch/src/mergeCandidates.ts b/packages/stitch/src/mergeCandidates.ts index 73e88cb9471..72051fd4f09 100644 --- a/packages/stitch/src/mergeCandidates.ts +++ b/packages/stitch/src/mergeCandidates.ts @@ -90,8 +90,8 @@ function mergeObjectTypeCandidates( const interfaces = Object.keys(interfaceMap).map(interfaceName => interfaceMap[interfaceName]); const astNodes = pluck('astNode', candidates); - const fieldAstNodes = Object.values(fields) - .map(f => f.astNode) + const fieldAstNodes = canonicalFieldNamesForType(candidates) + .map(fieldName => fields[fieldName]?.astNode) .filter(n => n != null); if (astNodes.length > 1 && fieldAstNodes.length) { @@ -136,8 +136,8 @@ function mergeInputObjectTypeCandidates( const fields = inputFieldConfigMapFromTypeCandidates(candidates, typeMergingOptions); const astNodes = pluck('astNode', candidates); - const fieldAstNodes = Object.values(fields) - .map(f => f.astNode) + const fieldAstNodes = canonicalFieldNamesForType(candidates) + .map(fieldName => fields[fieldName]?.astNode) .filter(n => n != null); if (astNodes.length > 1 && fieldAstNodes.length) { @@ -197,8 +197,8 @@ function mergeInterfaceTypeCandidates( const interfaces = Object.keys(interfaceMap).map(interfaceName => interfaceMap[interfaceName]); const astNodes = pluck('astNode', candidates); - const fieldAstNodes = Object.values(fields) - .map(f => f.astNode) + const fieldAstNodes = canonicalFieldNamesForType(candidates) + .map(fieldName => fields[fieldName]?.astNode) .filter(n => n != null); if (astNodes.length > 1 && fieldAstNodes.length) { @@ -284,17 +284,6 @@ function mergeEnumTypeCandidates( const values = enumValueConfigMapFromTypeCandidates(candidates, typeMergingOptions); const astNodes = pluck('astNode', candidates); - const valueAstNodes = Object.values(values) - .map(v => v.astNode) - .filter(n => n != null); - - if (astNodes.length > 1 && valueAstNodes.length) { - astNodes.push({ - ...astNodes[astNodes.length - 1], - values: JSON.parse(JSON.stringify(valueAstNodes)), - }); - } - const astNode = astNodes .slice(1) .reduce((acc, astNode) => mergeEnum(astNode, acc as EnumTypeDefinitionNode) as EnumTypeDefinitionNode, astNodes[0]); @@ -553,3 +542,19 @@ function defaultInputFieldConfigMerger(candidates: Array): Array { + const canonicalFieldNames: Record = Object.create(null); + + candidates.forEach(({ type, subschema }) => { + if (isSubschemaConfig(subschema) && subschema.merge?.[type.name]?.fields && !subschema.merge[type.name].canonical) { + Object.entries(subschema.merge[type.name].fields).forEach(([fieldName, mergedFieldConfig]) => { + if (mergedFieldConfig.canonical) { + canonicalFieldNames[fieldName] = true; + } + }); + } + }); + + return Object.keys(canonicalFieldNames); +} diff --git a/website/docs/stitch-combining-schemas.md b/website/docs/stitch-combining-schemas.md index fa88df81fcd..482fccec7ff 100644 --- a/website/docs/stitch-combining-schemas.md +++ b/website/docs/stitch-combining-schemas.md @@ -4,7 +4,7 @@ title: Combining schemas sidebar_label: Combining schemas --- -Schema stitching (`@graphql-tools/stitch`) creates a single GraphQL gateway schema from multiple underlying GraphQL services. Unlike [schema merging](/docs/merge-schemas), which simply combines local schema instances, stitching builds a combined proxy layer that delegates requests through to underlying service APIs. Stitching is a comperable alternative to [Apollo Federation](https://www.apollographql.com/docs/federation/). +Schema stitching (`@graphql-tools/stitch`) creates a single GraphQL gateway schema from multiple underlying GraphQL services. Unlike [schema merging](/docs/merge-schemas), which simply combines local schema instances, stitching builds a combined proxy layer that delegates requests through to underlying service APIs. As of GraphQL Tools v7, stitching is a comperable alternative to [Apollo Federation](https://www.apollographql.com/docs/federation/) with automated query planning, merged types, and declarative schema directives. ## Why stitching? From a0a626f777221cb3d39a1b7117a290b4601b91f1 Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Mon, 11 Jan 2021 09:43:03 -0500 Subject: [PATCH 11/14] e, a... whats the difference? --- website/docs/stitch-combining-schemas.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/stitch-combining-schemas.md b/website/docs/stitch-combining-schemas.md index 482fccec7ff..f8289fe347a 100644 --- a/website/docs/stitch-combining-schemas.md +++ b/website/docs/stitch-combining-schemas.md @@ -4,7 +4,7 @@ title: Combining schemas sidebar_label: Combining schemas --- -Schema stitching (`@graphql-tools/stitch`) creates a single GraphQL gateway schema from multiple underlying GraphQL services. Unlike [schema merging](/docs/merge-schemas), which simply combines local schema instances, stitching builds a combined proxy layer that delegates requests through to underlying service APIs. As of GraphQL Tools v7, stitching is a comperable alternative to [Apollo Federation](https://www.apollographql.com/docs/federation/) with automated query planning, merged types, and declarative schema directives. +Schema stitching (`@graphql-tools/stitch`) creates a single GraphQL gateway schema from multiple underlying GraphQL services. Unlike [schema merging](/docs/merge-schemas), which simply combines local schema instances, stitching builds a combined proxy layer that delegates requests through to underlying service APIs. As of GraphQL Tools v7, stitching is a comparable alternative to [Apollo Federation](https://www.apollographql.com/docs/federation/) with automated query planning, merged types, and declarative schema directives. ## Why stitching? From 1a1b33283ee849cf2658d4222afa22e8c6596376 Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Sat, 16 Jan 2021 14:44:34 -0500 Subject: [PATCH 12/14] well, that seems to work! --- .../stitch/tests/mergeDefinitions.test.ts | 69 +++++++++++++++++-- 1 file changed, 65 insertions(+), 4 deletions(-) diff --git a/packages/stitch/tests/mergeDefinitions.test.ts b/packages/stitch/tests/mergeDefinitions.test.ts index aeb42953613..76356a2fb30 100644 --- a/packages/stitch/tests/mergeDefinitions.test.ts +++ b/packages/stitch/tests/mergeDefinitions.test.ts @@ -1,7 +1,15 @@ import { makeExecutableSchema } from '@graphql-tools/schema'; import { stitchSchemas } from '@graphql-tools/stitch'; import { getDirectives } from '@graphql-tools/utils'; -import { GraphQLObjectType, GraphQLInterfaceType, GraphQLInputObjectType, GraphQLEnumType, GraphQLUnionType, GraphQLScalarType } from 'graphql'; +import { + GraphQLObjectType, + GraphQLInterfaceType, + GraphQLInputObjectType, + GraphQLEnumType, + GraphQLUnionType, + GraphQLScalarType, + graphql +} from 'graphql'; describe('merge canonical types', () => { const firstSchema = makeExecutableSchema({ @@ -45,7 +53,21 @@ describe('merge canonical types', () => { "first" scalar ProductScalar @mydir(value: "first") - ` + + "first" + type Query @mydir(value: "first") { + "first" + field1: String @mydir(value: "first") + "first" + field2: String @mydir(value: "first") + } + `, + resolvers: { + Query: { + field1: () => 'first', + field2: () => 'first', + } + } }); const secondSchema = makeExecutableSchema({ @@ -91,7 +113,21 @@ describe('merge canonical types', () => { "second" scalar ProductScalar @mydir(value: "second") - ` + + "second" + type Query @mydir(value: "second") { + "second" + field1: String @mydir(value: "second") + "second" + field2: String @mydir(value: "second") + } + `, + resolvers: { + Query: { + field1: () => 'second', + field2: () => 'second', + } + } }); const gatewaySchema = stitchSchemas({ @@ -120,6 +156,9 @@ describe('merge canonical types', () => { ProductScalar: { canonical: true, }, + Query: { + canonical: true, + }, } }, { @@ -142,7 +181,12 @@ describe('merge canonical types', () => { fields: { url: { canonical: true }, } - } + }, + Query: { + fields: { + field2: { canonical: true }, + } + }, } }, ], @@ -190,6 +234,7 @@ describe('merge canonical types', () => { }); it('merges prioritized descriptions', () => { + expect(gatewaySchema.getQueryType().description).toEqual('first'); expect(gatewaySchema.getType('Product').description).toEqual('first'); expect(gatewaySchema.getType('IProduct').description).toEqual('first'); expect(gatewaySchema.getType('ProductInput').description).toEqual('first'); @@ -197,11 +242,15 @@ describe('merge canonical types', () => { expect(gatewaySchema.getType('ProductUnion').description).toEqual('first'); expect(gatewaySchema.getType('ProductScalar').description).toEqual('first'); + const queryType = gatewaySchema.getQueryType(); const objectType = gatewaySchema.getType('Product') as GraphQLObjectType; const interfaceType = gatewaySchema.getType('IProduct') as GraphQLInterfaceType; const inputType = gatewaySchema.getType('ProductInput') as GraphQLInputObjectType; const enumType = gatewaySchema.getType('ProductEnum') as GraphQLEnumType; + expect(queryType.getFields().field1.description).toEqual('first'); + expect(queryType.getFields().field2.description).toEqual('second'); + expect(objectType.getFields().id.description).toEqual('first'); expect(interfaceType.getFields().id.description).toEqual('first'); expect(inputType.getFields().id.description).toEqual('first'); @@ -216,6 +265,7 @@ describe('merge canonical types', () => { }); it('merges prioritized ASTs', () => { + const queryType = gatewaySchema.getQueryType(); const objectType = gatewaySchema.getType('Product') as GraphQLObjectType; const interfaceType = gatewaySchema.getType('IProduct') as GraphQLInterfaceType; const inputType = gatewaySchema.getType('ProductInput') as GraphQLInputObjectType; @@ -223,6 +273,7 @@ describe('merge canonical types', () => { const unionType = gatewaySchema.getType('ProductUnion') as GraphQLUnionType; const scalarType = gatewaySchema.getType('ProductScalar') as GraphQLScalarType; + expect(getDirectives(firstSchema, queryType.toConfig()).mydir.value).toEqual('first'); expect(getDirectives(firstSchema, objectType.toConfig()).mydir.value).toEqual('first'); expect(getDirectives(firstSchema, interfaceType.toConfig()).mydir.value).toEqual('first'); expect(getDirectives(firstSchema, inputType.toConfig()).mydir.value).toEqual('first'); @@ -230,6 +281,8 @@ describe('merge canonical types', () => { expect(getDirectives(firstSchema, unionType.toConfig()).mydir.value).toEqual('first'); expect(getDirectives(firstSchema, scalarType.toConfig()).mydir.value).toEqual('first'); + expect(getDirectives(firstSchema, queryType.getFields().field1).mydir.value).toEqual('first'); + expect(getDirectives(firstSchema, queryType.getFields().field2).mydir.value).toEqual('second'); expect(getDirectives(firstSchema, objectType.getFields().id).mydir.value).toEqual('first'); expect(getDirectives(firstSchema, objectType.getFields().url).mydir.value).toEqual('second'); expect(getDirectives(firstSchema, interfaceType.getFields().id).mydir.value).toEqual('first'); @@ -250,4 +303,12 @@ describe('merge canonical types', () => { expect(getDirectives(firstSchema, objectType.getFields().id).deprecated.reason).toEqual('first'); expect(getDirectives(firstSchema, objectType.getFields().url).deprecated.reason).toEqual('second'); }); + + it('promotes canonical root field definitions', async () => { + const { data } = await graphql(gatewaySchema, '{ field1 field2 }'); + expect(data).toEqual({ + field1: 'first', + field2: 'second', + }); + }); }); From 4738b19f5264a2d22da632a3ad4b51f4fee83949 Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Sat, 16 Jan 2021 20:56:30 -0500 Subject: [PATCH 13/14] expand landing page. --- website/docs/schema-stitching.md | 91 +++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 2 deletions(-) diff --git a/website/docs/schema-stitching.md b/website/docs/schema-stitching.md index a0ebdacb9a3..8904b3929c7 100644 --- a/website/docs/schema-stitching.md +++ b/website/docs/schema-stitching.md @@ -4,6 +4,93 @@ title: Schema Stitching sidebar_label: Schema Stitching --- -Schema stitching (`@graphql-tools/stitch`) creates a single GraphQL gateway schema from multiple underlying GraphQL services. Unlike [schema merging](/docs/merge-schemas), which simply consolidates local schema instances, stitching builds a combined proxy layer that delegates requests through to many underlying service APIs. +Schema stitching (`@graphql-tools/stitch`) creates a single GraphQL gateway schema from multiple underlying GraphQL services. Unlike [schema merging](/docs/merge-schemas), which simply combines local schema instances, stitching builds a combined proxy layer that delegates requests through to underlying service APIs. As of GraphQL Tools v7, stitching is a comparable alternative to [Apollo Federation](https://www.apollographql.com/docs/federation/) with automated query planning, merged types, and declarative schema directives. -[Learn more about schema stitching](/docs/stitch-combining-schemas) +## Topics + +Browse the following documentation topics to learn about stitching libraries, or review the [Schema Stitching Handbook](https://github.com/gmac/schema-stitching-handbook) for working examples of major stitching features. + +- [Combining multiple schemas](/docs/stitch-combining-schemas) +- [Merging types across schemas](/docs/stitch-type-merging) +- [Schema extensions](/docs/stitch-type-merging) +- [Stitching directives SDL](/docs/stitch-directives-sdl) + +## Basic example + +Given two self-contained subschemas, a single "stitched" schema can be build that delegates (or, proxies) relevant portions of a request to each subservice: + +```js +import { makeExecutableSchema } from '@graphql-tools/schema'; +import { stitchSchemas } from '@graphql-tools/stitch'; +import { stitchingDirectives } from '@graphql-tools/stitching-directives'; + +const postsService = makeExecutableSchema({ + typeDefs: ` + type Post { + id: ID! + message: String! + author: User + } + + type User { + id: ID! + posts: [Post] + } + + type Query { + post(id: ID!): Post + users(ids: [ID!]!): [User]! @merge(keyField: "id") + } + `, + resolvers: { + // ... + } +}); + +const usersService = makeExecutableSchema({ + typeDefs: ` + type User { + id: ID! + username: String! + email: String! + } + + type Query { + users(ids: [ID!]!): [User]! @merge(keyField: "id") @primary + } + `, + resolvers: { + // ... + } +}); + +const { stitchingDirectivesTransformer } = stitchingDirectives({ + // options... +}); + +const gatewaySchema = stitchSchemas({ + subschemaConfigTransforms: [stitchingDirectivesTransformer], + subschemas: [ + { schema: postsSchema, batch: true }, + { schema: usersSchema, batch: true }, + ] +}); +``` + +Using the stitched proxy schema, data may be requested interchangeably from any service in the same request: + +```graphql +query { + users(ids: ["1", "2"]) { + username + email + posts { + message + author { + username + email + } + } + } +} +``` From 1d0138ef695176d98140188560c771e255ef5d31 Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Sat, 16 Jan 2021 23:27:21 -0500 Subject: [PATCH 14/14] cleanup docs. --- website/docs/schema-stitching.md | 4 +- website/docs/stitch-directives-sdl.md | 6 +- website/docs/stitch-type-merging.md | 100 ++++++++++++++++---------- 3 files changed, 66 insertions(+), 44 deletions(-) diff --git a/website/docs/schema-stitching.md b/website/docs/schema-stitching.md index 8904b3929c7..5f302f24847 100644 --- a/website/docs/schema-stitching.md +++ b/website/docs/schema-stitching.md @@ -17,7 +17,7 @@ Browse the following documentation topics to learn about stitching libraries, or ## Basic example -Given two self-contained subschemas, a single "stitched" schema can be build that delegates (or, proxies) relevant portions of a request to each subservice: +Given two self-contained subschemas, a single "stitched" schema can be built that delegates (or, proxies) relevant portions of a request to each subservice: ```js import { makeExecutableSchema } from '@graphql-tools/schema'; @@ -56,7 +56,7 @@ const usersService = makeExecutableSchema({ } type Query { - users(ids: [ID!]!): [User]! @merge(keyField: "id") @primary + users(ids: [ID!]!): [User]! @merge(keyField: "id") @canonical } `, resolvers: { diff --git a/website/docs/stitch-directives-sdl.md b/website/docs/stitch-directives-sdl.md index 4fee9ba8f7b..b2d983c5a2b 100644 --- a/website/docs/stitch-directives-sdl.md +++ b/website/docs/stitch-directives-sdl.md @@ -19,7 +19,7 @@ type User { } type Query { - users(ids: [ID!]!): [User]! @merge(keyField: "id") + users(ids: [ID!]!): [User]! @merge(keyField: "id") @canonical } # --- Posts schema --- @@ -36,7 +36,7 @@ type User { type Query { post(id: ID!): Post - _users(ids: [ID!]!): [User]! @merge(keyField: "id") + users(ids: [ID!]!): [User]! @merge(keyField: "id") } ``` @@ -67,7 +67,7 @@ The function of these directives are: * **`@computed`:** specifies a selection of fields required from other services to compute the value of this field. These additional fields are only selected when the computed field is requested. Analogous to [computed field](/docs/stitch-type-merging#computed-fields) in merged type configuration. Computed field dependencies must be sent into the subservice using an [object key](#object-keys). -* **`@canonical`:** identifies types and fields that provide a [canonical definition](/docs/stitch-type-merging#canonical-definitions) to be built into the combined gateway schema. Useful when the same types appear across multiple subschemas and a specific definition should be promoted. +* **`@canonical`:** specifies types and fields that provide a [canonical definition](/docs/stitch-type-merging#canonical-definitions) to be built into the gateway schema. Useful for selecting preferred characteristics among types and fields that overlap across subschemas. Root fields marked as canonical specify which subschema the field proxies for new queries entering the graph. #### Customizing directive names diff --git a/website/docs/stitch-type-merging.md b/website/docs/stitch-type-merging.md index 105ecfddb5a..c5dd31fe4c9 100644 --- a/website/docs/stitch-type-merging.md +++ b/website/docs/stitch-type-merging.md @@ -476,67 +476,84 @@ The main disadvantage of computed fields is that they cannot be resolved indepen ## Federation services -If you're familiar with [Apollo Federation](https://www.apollographql.com/docs/apollo-server/federation/introduction/), then you may notice that the above pattern of computed fields looks similar to the `_entities` service design of the [Apollo Federation specification](https://www.apollographql.com/docs/apollo-server/federation/federation-spec/). Federation resources can be included in a stitched gateway when integrating with third-party services or in the process of a migration. See related [handbook example](https://github.com/gmac/schema-stitching-handbook/tree/master/federation-services) for specifics. +If you're familiar with [Apollo Federation](https://www.apollographql.com/docs/apollo-server/federation/introduction/), then you may notice that the above pattern of computed fields looks similar to the `_entities` service design of the [Apollo Federation specification](https://www.apollographql.com/docs/apollo-server/federation/federation-spec/). Federation resources may be included in a stitched gateway; this comes in handy when integrating with third-party services or in the process of a migration. See related [handbook example](https://github.com/gmac/schema-stitching-handbook/tree/master/federation-services) for more information. ## Canonical definitions -As the same types are introduced across services, managing the gateway schema definitions for each GraphQL element becomes challenging. Element definitions may specify: +Managing the gateway schema definition of each type and field becomes challenging as the same type names are introduced across subschemas. By default, the final definition of each named GraphQL element found in the stitched `subschemas` array provides its gateway definition. However, preferred definitions may be marked as `canonical` to recieve this final priority. Canonical definitions provide: -- Descriptions (i.e.: doc strings) -- Metadata (custom directives) -- Field nullability -- Field deprecations +- an element's description (doc string). +- an element's final directive values. +- a field's final nullability, arguments, and deprecation reason. +- a root field's default delegation target. -By default, the final definition of each type and field found in the stitched `subschemas` array provides the element's gateway definition. However, this strategy alone can be cumbersome when changes in subschema order cause preferred definitions to be deprioritized. +The following example uses [stitching directives](/docs/stitch-directives-sdl) to mark preferred subschema elements as `@canonical`: -The `canonical` setting allows you to specify preferred type and field definitions that should be built into the gateway schema: +```graphql +# --- Users schema --- -```js -let usersSchema = makeExecutableSchema({ - typeDefs: ` - "Represents an authenticated user" - type User @canonical { - "The primary key of this user record" - id: ID! @mydir(schema: "users") - "ignore this description" - field: String! - } - ` -}); +"Represents an authenticated user" +type User @canonical { + "The primary key of this user record" + id: ID! @mydir(schema: "users") + "other description" + field: String! +} -let postsSchema = makeExecutableSchema({ - typeDefs: ` - type Post { - id: ID! - } +type Query { + "Users schema definition" + user(id: ID!): User @canonical +} - "ignore this description" - type User { - "ignore this description" - id: ID! @mydir(schema: "posts") - "Preferred description for this field" - field: String @canonical - "Posts authored by this user" - posts: [Post!] - } - ` -}); +# --- Posts schema --- + +type Post { + id: ID! +} + +"other description" +type User { + "other description" + id: ID! @mydir(schema: "posts") + "The canonical field description" + field: String @canonical + "Posts authored by this user" + posts: [Post!] +} + +type Query { + "Posts schema definition" + user(id: ID!): User +} ``` -The above example uses [stitching directives](/docs/stitch-directives-sdl) to mark schema elements as `@canonical`. A type marked as canonical will provide it's definition and that of all of its fields to the combined gateway schema. In the uncommon scenario where an overlapping field in another subschema provides a more robust definition, that field may be marked as canonical to override the base type. Fields that are unique to a given service (such as `User.posts` above) have no competing definition so are canonical by default. The above User types and ASTs will merge into: +The above ASTs will merge into the following gateway schema definition, and the root `user` field will proxy the Users subschema by default: ```graphql +# --- Gateway schema --- + "Represents an authenticated user" type User { "The primary key of this user record" id: ID! @mydir(schema: "users") - "Preferred description for this field" + "The canonical field description" field: String "Posts authored by this user" posts: [Post!] } + +type Query { + "Users schema definition" + user(id: ID!): User +} ``` +- **Types** marked as canonical will provide their definition _and that of all of their fields_ to the combined gateway schema. +- **Fields** marked as canonical will override those of a canonical type. +- **Root fields** marked as canonical will specify which subschema the field proxies by default for new queries entering the graph. + +Only one of any given type or field may be made canonical. Fields that are unique to one service (such as `User.posts` above) have no competing definition so are canonical by default. + The above SDL directives can also be written as static configuration: ```js @@ -547,6 +564,11 @@ const gatewaySchema = stitchSchemas({ User: { // ... canonical: true + }, + Query: { + fields: { + user: { canonical: true } + } } } }, { @@ -563,7 +585,7 @@ const gatewaySchema = stitchSchemas({ }); ``` -> **Implementation note:** canonical settings are only used while building the combined gateway schema; they are given no special priority in runtime query planning. You may override the assembly of canonical definitions using [`typeMergingOptions`](/docs/stitch-combining-schemas#automatic-merge). +> **Implementation note:** canonical settings are _only_ used for building the combined gateway schema definition and defaulting root field targets; otherwise, they are given no special priority in runtime query planning (which always selects necessary fields from as few subschemas as possible). You may override the assembly of canonical definitions using [`typeMergingOptions`](/docs/stitch-combining-schemas#automatic-merge). ## Type resolvers