├── .eslintrc.yml ├── .gitattributes ├── .github ├── CODEOWNERS └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .npmrc ├── .travis.yml ├── .vscode └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── netlify.toml ├── package.json ├── renovate.json ├── src ├── Interfaces.ts ├── delegate │ ├── addTypenameToAbstract.ts │ ├── checkResultAndHandleErrors.ts │ ├── createRequest.ts │ ├── delegateToSchema.ts │ └── index.ts ├── generate │ ├── Logger.ts │ ├── SchemaError.ts │ ├── addResolversToSchema.ts │ ├── addSchemaLevelResolver.ts │ ├── assertResolversPresent.ts │ ├── attachConnectorsToContext.ts │ ├── attachDirectiveResolvers.ts │ ├── buildSchemaFromTypeDefinitions.ts │ ├── chainResolvers.ts │ ├── checkForResolveTypeResolver.ts │ ├── concatenateTypeDefs.ts │ ├── decorateWithLogger.ts │ ├── extendResolversFromInterfaces.ts │ ├── extensionDefinitions.ts │ ├── index.ts │ └── makeExecutableSchema.ts ├── index.ts ├── links │ ├── AwaitVariablesLink.ts │ ├── createServerHttpLink.ts │ └── index.ts ├── mock │ └── index.ts ├── polyfills │ ├── buildSchema.ts │ ├── extendSchema.ts │ ├── index.ts │ ├── isSpecifiedScalarType.ts │ └── toConfig.ts ├── scalars │ ├── GraphQLUpload.ts │ └── index.ts ├── stitch │ ├── createMergedResolver.ts │ ├── defaultMergedResolver.ts │ ├── errors.ts │ ├── getResponseKeyFromInfo.ts │ ├── index.ts │ ├── introspectSchema.ts │ ├── linkToFetcher.ts │ ├── makeMergedType.ts │ ├── mapAsyncIterator.ts │ ├── mergeFields.ts │ ├── mergeInfo.ts │ ├── mergeSchemas.ts │ ├── observableToAsyncIterable.ts │ ├── proxiedResult.ts │ ├── resolveFromParentTypename.ts │ └── typeFromAST.ts ├── test │ ├── circularSchemaA.ts │ ├── circularSchemaB.ts │ ├── testAlternateMergeSchemas.ts │ ├── testDataloader.ts │ ├── testDelegateToSchema.ts │ ├── testDirectives.ts │ ├── testErrors.ts │ ├── testExtensionExtraction.ts │ ├── testFragmentsAreNotDuplicated.ts │ ├── testGatsbyTransforms.ts │ ├── testLogger.ts │ ├── testMakeRemoteExecutableSchema.ts │ ├── testMapSchema.ts │ ├── testMergeSchemas.ts │ ├── testMocking.ts │ ├── testResolution.ts │ ├── testSchemaGenerator.ts │ ├── testStitchingFromSubschemas.ts │ ├── testTransforms.ts │ ├── testTypeMerging.ts │ ├── testUpload.ts │ ├── testUtils.ts │ └── testingSchemas.ts ├── utils │ ├── SchemaDirectiveVisitor.ts │ ├── SchemaVisitor.ts │ ├── astFromType.ts │ ├── clone.ts │ ├── each.ts │ ├── fieldNodes.ts │ ├── fields.ts │ ├── filterSchema.ts │ ├── forEachDefaultValue.ts │ ├── forEachField.ts │ ├── fragments.ts │ ├── getResolversFromSchema.ts │ ├── graphqlVersion.ts │ ├── heal.ts │ ├── implementsAbstractType.ts │ ├── index.ts │ ├── isEmptyObject.ts │ ├── map.ts │ ├── mergeDeep.ts │ ├── selectionSets.ts │ ├── stub.ts │ ├── transformInputValue.ts │ ├── updateArgument.ts │ ├── updateEachKey.ts │ ├── valueFromASTUntyped.ts │ └── visitSchema.ts └── wrap │ ├── index.ts │ ├── makeRemoteExecutableSchema.ts │ ├── resolvers.ts │ ├── transformSchema.ts │ ├── transforms.ts │ ├── transforms │ ├── AddArgumentsAsVariables.ts │ ├── AddMergedTypeSelectionSets.ts │ ├── AddReplacementFragments.ts │ ├── AddReplacementSelectionSets.ts │ ├── AddTypenameToAbstract.ts │ ├── CheckResultAndHandleErrors.ts │ ├── ExpandAbstractTypes.ts │ ├── ExtendSchema.ts │ ├── ExtractField.ts │ ├── FilterInterfaceFields.ts │ ├── FilterObjectFields.ts │ ├── FilterRootFields.ts │ ├── FilterToSchema.ts │ ├── FilterTypes.ts │ ├── HoistField.ts │ ├── MapFields.ts │ ├── RenameInterfaceFields.ts │ ├── RenameObjectFields.ts │ ├── RenameRootFields.ts │ ├── RenameRootTypes.ts │ ├── RenameTypes.ts │ ├── ReplaceFieldWithFragment.ts │ ├── TransformCompositeFields.ts │ ├── TransformInterfaceFields.ts │ ├── TransformObjectFields.ts │ ├── TransformQuery.ts │ ├── TransformRootFields.ts │ ├── WrapFields.ts │ ├── WrapQuery.ts │ ├── WrapType.ts │ └── index.ts │ └── wrapSchema.ts ├── tsconfig.json ├── typedoc.json └── typings.d.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @yaacovCR 2 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | TODO: 9 | 10 | - [ ] If this PR is a new feature, reference an issue where a consensus about the design was reached (not necessary for small changes) 11 | - [ ] Make sure all of the significant new logic is covered by tests 12 | - [ ] Rebase your changes on master so that they can be merged easily 13 | - [ ] Make sure all tests and linter rules pass 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | dist/ 4 | typedoc/ 5 | 6 | *.tgz 7 | .DS_Store 8 | 9 | yarn.lock 10 | package-lock.json 11 | 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | .eslintcache 17 | .nyc_output 18 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact = true 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "13" 4 | - "12" 5 | - "10" 6 | 7 | env: 8 | - GRAPHQL_VERSION='0.12' 9 | - GRAPHQL_VERSION='0.13' 10 | - GRAPHQL_VERSION='14.0' 11 | - GRAPHQL_VERSION='14.1' 12 | - GRAPHQL_VERSION='14.2' 13 | - GRAPHQL_VERSION='14.3' 14 | - GRAPHQL_VERSION='14.4' 15 | - GRAPHQL_VERSION='14.5' 16 | - GRAPHQL_VERSION='14.6' 17 | - GRAPHQL_VERSION='rc' 18 | 19 | install: 20 | - npm config set spin=false 21 | - npm install 22 | 23 | script: 24 | - node_version=$(node -v); if [[ ${node_version:1:2} == "13" && $GRAPHQL_VERSION == "14.6" ]]; then 25 | npm run lint; 26 | fi 27 | - node_version=$(node -v); if [[ ${node_version:1:2} == "13" && $GRAPHQL_VERSION == "14.6" ]]; then 28 | npm run prettier:check; 29 | fi 30 | - npm run compile 31 | - npm install graphql@$GRAPHQL_VERSION 32 | - npm run testonly:cover 33 | 34 | after_success: 35 | - npm run coverage 36 | 37 | # Allow Travis tests to run in containers. 38 | sudo: false 39 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "editor.tabSize": 2, 4 | "editor.insertSpaces": true, 5 | "editor.rulers": [80], 6 | "editor.wordWrapColumn": 110, 7 | "prettier.semi": true, 8 | "files.trimTrailingWhitespace": true, 9 | "files.insertFinalNewline": true, 10 | "prettier.singleQuote": true, 11 | "prettier.printWidth": 110, 12 | "files.exclude": { 13 | "**/.git": true, 14 | "**/.DS_Store": true, 15 | "node_modules": true, 16 | "test-lib": true, 17 | "lib": true, 18 | "coverage": true, 19 | "npm": true 20 | }, 21 | "typescript.tsdk": "node_modules/typescript/lib" 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 - 2017 Meteor Development Group, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-tools-fork: keep on stitching 2 | 3 | This fork has been merged into upstream [graphql-tools](https://github.com/apollographql/graphql-tools) v5. 4 | 5 | Thanks for following along! 6 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "typedoc/" 3 | command = "npm install && npm run typedoc" 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-tools-fork", 3 | "version": "9.0.2", 4 | "description": "Forked graphql-tools, still more useful tools to create and manipulate GraphQL schemas.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "files": [ 8 | "/dist", 9 | "!/dist/test" 10 | ], 11 | "sideEffects": false, 12 | "scripts": { 13 | "clean": "rimraf dist", 14 | "build": "npm run compile", 15 | "precompile": "npm run clean", 16 | "compile": "tsc", 17 | "pretest": "npm run clean && npm run compile", 18 | "test": "npm run testonly", 19 | "posttest": "npm run lint && npm run prettier:check", 20 | "lint": "eslint --ext .js,.ts src", 21 | "lint:watch": "esw --watch --cache --ext .js,.ts src", 22 | "watch": "tsc -w", 23 | "testonly": "mocha --reporter spec --full-trace ./dist/test/**.js --require source-map-support/register", 24 | "testonly:cover": "nyc npm run testonly", 25 | "testonly:watch": "mocha -w --reporter spec --full-trace ./dist/test/**.js --require source-map-support/register", 26 | "coverage": "nyc report --reporter=text-lcov | coveralls", 27 | "prepublishOnly": "npm run compile", 28 | "prettier": "prettier --trailing-comma all --single-quote --write src/**/*.ts", 29 | "prettier:check": "prettier --trailing-comma all --single-quote --check src/**/*.ts", 30 | "prerelease": "npm test", 31 | "release": "npm run releaseonly", 32 | "releaseonly": "standard-version", 33 | "typedoc": "typedoc src/delegate/index.ts src/generate/index.ts src/links/index.ts src/polyfills/index.ts src/mock/index.ts src/scalars/index.ts src/stitch/index.ts src/utils/index.ts src/wrap/index.ts src/Interfaces.ts typings.d.ts" 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "git+https://github.com/yaacovCR/graphql-tools-fork.git" 38 | }, 39 | "keywords": [ 40 | "GraphQL", 41 | "Apollo", 42 | "JavaScript", 43 | "TypeScript", 44 | "Mock", 45 | "Schema", 46 | "Schema Language", 47 | "Tools" 48 | ], 49 | "license": "MIT", 50 | "bugs": { 51 | "url": "https://github.com/yaacovCR/graphql-tools-fork/issues" 52 | }, 53 | "homepage": "https://github.com/yaacovCR/graphql-tools-fork#readme", 54 | "dependencies": { 55 | "apollo-link": "^1.2.13", 56 | "apollo-upload-client": "^13.0.0", 57 | "deprecated-decorator": "^0.1.6", 58 | "form-data": "^3.0.0", 59 | "iterall": "^1.3.0", 60 | "node-fetch": "^2.6.0", 61 | "tslib": "^1.11.0", 62 | "uuid": "^7.0.2" 63 | }, 64 | "peerDependencies": { 65 | "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0-rc" 66 | }, 67 | "devDependencies": { 68 | "@types/chai": "4.2.11", 69 | "@types/dateformat": "3.0.1", 70 | "@types/express": "4.17.3", 71 | "@types/graphql-type-json": "0.3.2", 72 | "@types/graphql-upload": "8.0.3", 73 | "@types/mocha": "7.0.2", 74 | "@types/node": "13.9.3", 75 | "@types/node-fetch": "2.5.5", 76 | "@types/uuid": "7.0.2", 77 | "@typescript-eslint/eslint-plugin": "2.25.0", 78 | "@typescript-eslint/parser": "2.25.0", 79 | "babel-eslint": "10.1.0", 80 | "body-parser": "1.19.0", 81 | "chai": "4.2.0", 82 | "coveralls": "3.0.11", 83 | "dataloader": "2.0.0", 84 | "dateformat": "3.0.3", 85 | "eslint": "6.8.0", 86 | "eslint-plugin-import": "2.20.2", 87 | "eslint-watch": "6.0.1", 88 | "express": "4.17.1", 89 | "express-graphql": "0.9.0", 90 | "graphql": "14.6.0", 91 | "graphql-subscriptions": "1.1.0", 92 | "graphql-type-json": "0.3.1", 93 | "graphql-upload": "10.0.0", 94 | "mocha": "7.1.1", 95 | "nyc": "15.0.0", 96 | "prettier": "2.0.2", 97 | "rimraf": "3.0.2", 98 | "source-map-support": "0.5.16", 99 | "standard-version": "7.1.0", 100 | "typescript": "3.8.3", 101 | "zen-observable-ts": "0.8.20" 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseBranches": ["master", "next"], 3 | "packageFiles": ["docs/package.json"], 4 | "pathRules": [ 5 | { 6 | "paths": ["docs/package.json"], 7 | "extends": ["apollo-docs"] 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/delegate/addTypenameToAbstract.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLType, 3 | DocumentNode, 4 | TypeInfo, 5 | visit, 6 | visitWithTypeInfo, 7 | SelectionSetNode, 8 | Kind, 9 | GraphQLSchema, 10 | isAbstractType, 11 | } from 'graphql'; 12 | 13 | export function addTypenameToAbstract( 14 | targetSchema: GraphQLSchema, 15 | document: DocumentNode, 16 | ): DocumentNode { 17 | const typeInfo = new TypeInfo(targetSchema); 18 | return visit( 19 | document, 20 | visitWithTypeInfo(typeInfo, { 21 | [Kind.SELECTION_SET]( 22 | node: SelectionSetNode, 23 | ): SelectionSetNode | null | undefined { 24 | const parentType: GraphQLType = typeInfo.getParentType(); 25 | let selections = node.selections; 26 | if (parentType != null && isAbstractType(parentType)) { 27 | selections = selections.concat({ 28 | kind: Kind.FIELD, 29 | name: { 30 | kind: Kind.NAME, 31 | value: '__typename', 32 | }, 33 | }); 34 | } 35 | 36 | if (selections !== node.selections) { 37 | return { 38 | ...node, 39 | selections, 40 | }; 41 | } 42 | }, 43 | }), 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/generate/Logger.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A very simple class for logging errors 3 | */ 4 | 5 | import { ILogger } from '../Interfaces'; 6 | 7 | export class Logger implements ILogger { 8 | public errors: Array; 9 | public name: string | undefined; 10 | private readonly callback: Function | undefined; 11 | 12 | constructor(name?: string, callback?: Function) { 13 | this.name = name; 14 | this.errors = []; 15 | this.callback = callback; 16 | // TODO: should assert that callback is a function 17 | } 18 | 19 | public log(err: Error) { 20 | this.errors.push(err); 21 | if (typeof this.callback === 'function') { 22 | this.callback(err); 23 | } 24 | } 25 | 26 | public printOneError(e: Error): string { 27 | return e.stack ? e.stack : ''; 28 | } 29 | 30 | public printAllErrors() { 31 | return this.errors.reduce( 32 | (agg: string, e: Error) => `${agg}\n${this.printOneError(e)}`, 33 | '', 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/generate/SchemaError.ts: -------------------------------------------------------------------------------- 1 | // @schemaDefinition: A GraphQL type schema in shorthand 2 | // @resolvers: Definitions for resolvers to be merged with schema 3 | export default class SchemaError extends Error { 4 | public message: string; 5 | 6 | constructor(message: string) { 7 | super(message); 8 | this.message = message; 9 | Error.captureStackTrace(this, this.constructor); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/generate/addSchemaLevelResolver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defaultFieldResolver, 3 | GraphQLSchema, 4 | GraphQLFieldResolver, 5 | } from 'graphql'; 6 | 7 | // wraps all resolvers of query, mutation or subscription fields 8 | // with the provided function to simulate a root schema level resolver 9 | function addSchemaLevelResolver( 10 | schema: GraphQLSchema, 11 | fn: GraphQLFieldResolver, 12 | ): void { 13 | // TODO test that schema is a schema, fn is a function 14 | const rootTypes = [ 15 | schema.getQueryType(), 16 | schema.getMutationType(), 17 | schema.getSubscriptionType(), 18 | ].filter((x) => Boolean(x)); 19 | rootTypes.forEach((type) => { 20 | if (type != null) { 21 | // XXX this should run at most once per request to simulate a true root resolver 22 | // for graphql-js this is an approximation that works with queries but not mutations 23 | const rootResolveFn = runAtMostOncePerRequest(fn); 24 | const fields = type.getFields(); 25 | Object.keys(fields).forEach((fieldName) => { 26 | // XXX if the type is a subscription, a same query AST will be ran multiple times so we 27 | // deactivate here the runOnce if it's a subscription. This may not be optimal though... 28 | if (type === schema.getSubscriptionType()) { 29 | fields[fieldName].resolve = wrapResolver( 30 | fields[fieldName].resolve, 31 | fn, 32 | ); 33 | } else { 34 | fields[fieldName].resolve = wrapResolver( 35 | fields[fieldName].resolve, 36 | rootResolveFn, 37 | ); 38 | } 39 | }); 40 | } 41 | }); 42 | } 43 | 44 | // XXX badly named function. this doesn't really wrap, it just chains resolvers... 45 | function wrapResolver( 46 | innerResolver: GraphQLFieldResolver | undefined, 47 | outerResolver: GraphQLFieldResolver, 48 | ): GraphQLFieldResolver { 49 | return (obj, args, ctx, info) => 50 | Promise.resolve(outerResolver(obj, args, ctx, info)).then((root) => { 51 | if (innerResolver != null) { 52 | return innerResolver(root, args, ctx, info); 53 | } 54 | return defaultFieldResolver(root, args, ctx, info); 55 | }); 56 | } 57 | 58 | // XXX this function only works for resolvers 59 | // XXX very hacky way to remember if the function 60 | // already ran for this request. This will only work 61 | // if people don't actually cache the operation. 62 | // if they do cache the operation, they will have to 63 | // manually remove the __runAtMostOnce before every request. 64 | function runAtMostOncePerRequest( 65 | fn: GraphQLFieldResolver, 66 | ): GraphQLFieldResolver { 67 | let value: any; 68 | const randomNumber = Math.random(); 69 | return (root, args, ctx, info) => { 70 | if (!info.operation['__runAtMostOnce']) { 71 | info.operation['__runAtMostOnce'] = {}; 72 | } 73 | if (!info.operation['__runAtMostOnce'][randomNumber]) { 74 | info.operation['__runAtMostOnce'][randomNumber] = true; 75 | value = fn(root, args, ctx, info); 76 | } 77 | return value; 78 | }; 79 | } 80 | 81 | export default addSchemaLevelResolver; 82 | -------------------------------------------------------------------------------- /src/generate/assertResolversPresent.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLSchema, 3 | GraphQLField, 4 | getNamedType, 5 | isScalarType, 6 | } from 'graphql'; 7 | 8 | import { IResolverValidationOptions } from '../Interfaces'; 9 | import { forEachField } from '../utils/index'; 10 | 11 | import SchemaError from './SchemaError'; 12 | 13 | function assertResolversPresent( 14 | schema: GraphQLSchema, 15 | resolverValidationOptions: IResolverValidationOptions = {}, 16 | ): void { 17 | const { 18 | requireResolversForArgs = false, 19 | requireResolversForNonScalar = false, 20 | requireResolversForAllFields = false, 21 | } = resolverValidationOptions; 22 | 23 | if ( 24 | requireResolversForAllFields && 25 | (requireResolversForArgs || requireResolversForNonScalar) 26 | ) { 27 | throw new TypeError( 28 | 'requireResolversForAllFields takes precedence over the more specific assertions. ' + 29 | 'Please configure either requireResolversForAllFields or requireResolversForArgs / ' + 30 | 'requireResolversForNonScalar, but not a combination of them.', 31 | ); 32 | } 33 | 34 | forEachField(schema, (field, typeName, fieldName) => { 35 | // requires a resolver for *every* field. 36 | if (requireResolversForAllFields) { 37 | expectResolver(field, typeName, fieldName); 38 | } 39 | 40 | // requires a resolver on every field that has arguments 41 | if (requireResolversForArgs && field.args.length > 0) { 42 | expectResolver(field, typeName, fieldName); 43 | } 44 | 45 | // requires a resolver on every field that returns a non-scalar type 46 | if ( 47 | requireResolversForNonScalar && 48 | !isScalarType(getNamedType(field.type)) 49 | ) { 50 | expectResolver(field, typeName, fieldName); 51 | } 52 | }); 53 | } 54 | 55 | function expectResolver( 56 | field: GraphQLField, 57 | typeName: string, 58 | fieldName: string, 59 | ) { 60 | if (!field.resolve) { 61 | // eslint-disable-next-line no-console 62 | console.warn( 63 | `Resolver missing for "${typeName}.${fieldName}". To disable this warning check https://github.com/apollostack/graphql-tools/issues/131`, 64 | ); 65 | return; 66 | } 67 | if (typeof field.resolve !== 'function') { 68 | throw new SchemaError( 69 | `Resolver "${typeName}.${fieldName}" must be a function`, 70 | ); 71 | } 72 | } 73 | 74 | export default assertResolversPresent; 75 | -------------------------------------------------------------------------------- /src/generate/buildSchemaFromTypeDefinitions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | parse, 3 | extendSchema, 4 | buildASTSchema, 5 | GraphQLSchema, 6 | DocumentNode, 7 | } from 'graphql'; 8 | 9 | import { ITypeDefinitions, GraphQLParseOptions, isASTNode } from '../Interfaces'; 10 | 11 | import { 12 | extractExtensionDefinitions, 13 | filterExtensionDefinitions, 14 | } from './extensionDefinitions'; 15 | import concatenateTypeDefs from './concatenateTypeDefs'; 16 | import SchemaError from './SchemaError'; 17 | 18 | function buildSchemaFromTypeDefinitions( 19 | typeDefinitions: ITypeDefinitions, 20 | parseOptions?: GraphQLParseOptions, 21 | ): GraphQLSchema { 22 | // TODO: accept only array here, otherwise interfaces get confusing. 23 | let document: DocumentNode; 24 | 25 | if (isASTNode(typeDefinitions)) { 26 | document = typeDefinitions; 27 | } else if (typeof typeDefinitions === 'string') { 28 | document = parse(typeDefinitions, parseOptions); 29 | } else { 30 | if (!Array.isArray(typeDefinitions)) { 31 | throw new SchemaError( 32 | `typeDefs must be a string, array or schema AST, got ${typeof typeDefinitions}`, 33 | ); 34 | } 35 | document = parse(concatenateTypeDefs(typeDefinitions), parseOptions); 36 | } 37 | 38 | const typesAst = filterExtensionDefinitions(document); 39 | 40 | const backcompatOptions = { commentDescriptions: true }; 41 | let schema: GraphQLSchema = buildASTSchema(typesAst, backcompatOptions); 42 | 43 | const extensionsAst = extractExtensionDefinitions(document); 44 | if (extensionsAst.definitions.length > 0) { 45 | schema = extendSchema(schema, extensionsAst, backcompatOptions); 46 | } 47 | 48 | return schema; 49 | } 50 | 51 | export default buildSchemaFromTypeDefinitions; 52 | -------------------------------------------------------------------------------- /src/generate/chainResolvers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defaultFieldResolver, 3 | GraphQLResolveInfo, 4 | GraphQLFieldResolver, 5 | } from 'graphql'; 6 | 7 | export function chainResolvers( 8 | resolvers: Array>, 9 | ) { 10 | return ( 11 | root: any, 12 | args: { [argName: string]: any }, 13 | ctx: any, 14 | info: GraphQLResolveInfo, 15 | ) => 16 | resolvers.reduce((prev, curResolver) => { 17 | if (curResolver != null) { 18 | return curResolver(prev, args, ctx, info); 19 | } 20 | 21 | return defaultFieldResolver(prev, args, ctx, info); 22 | }, root); 23 | } 24 | -------------------------------------------------------------------------------- /src/generate/checkForResolveTypeResolver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLInterfaceType, 3 | GraphQLUnionType, 4 | GraphQLSchema, 5 | isAbstractType, 6 | } from 'graphql'; 7 | 8 | import SchemaError from './SchemaError'; 9 | 10 | // If we have any union or interface types throw if no there is no resolveType or isTypeOf resolvers 11 | function checkForResolveTypeResolver( 12 | schema: GraphQLSchema, 13 | requireResolversForResolveType?: boolean, 14 | ) { 15 | Object.keys(schema.getTypeMap()) 16 | .map((typeName) => schema.getType(typeName)) 17 | .forEach((type: GraphQLUnionType | GraphQLInterfaceType) => { 18 | if (!isAbstractType(type)) { 19 | return; 20 | } 21 | if (!type.resolveType) { 22 | if (!requireResolversForResolveType) { 23 | return; 24 | } 25 | throw new SchemaError( 26 | `Type "${type.name}" is missing a "__resolveType" resolver. Pass false into ` + 27 | '"resolverValidationOptions.requireResolversForResolveType" to disable this error.', 28 | ); 29 | } 30 | }); 31 | } 32 | export default checkForResolveTypeResolver; 33 | -------------------------------------------------------------------------------- /src/generate/concatenateTypeDefs.ts: -------------------------------------------------------------------------------- 1 | import { print } from 'graphql'; 2 | 3 | import { ITypedef, isASTNode } from '../Interfaces'; 4 | 5 | import SchemaError from './SchemaError'; 6 | 7 | function concatenateTypeDefs( 8 | typeDefinitionsAry: Array, 9 | calledFunctionRefs = [] as any, 10 | ): string { 11 | let resolvedTypeDefinitions: Array = []; 12 | typeDefinitionsAry.forEach((typeDef: ITypedef) => { 13 | if (typeof typeDef === 'function') { 14 | if (calledFunctionRefs.indexOf(typeDef) === -1) { 15 | calledFunctionRefs.push(typeDef); 16 | resolvedTypeDefinitions = resolvedTypeDefinitions.concat( 17 | concatenateTypeDefs(typeDef(), calledFunctionRefs), 18 | ); 19 | } 20 | } else if (typeof typeDef === 'string') { 21 | resolvedTypeDefinitions.push(typeDef.trim()); 22 | } else if (isASTNode(typeDef)) { 23 | resolvedTypeDefinitions.push(print(typeDef).trim()); 24 | } else { 25 | throw new SchemaError( 26 | `typeDef array must contain only strings, ASTs or functions, got ${typeof typeDef}`, 27 | ); 28 | } 29 | }); 30 | return uniq(resolvedTypeDefinitions.map((x) => x.trim())).join('\n'); 31 | } 32 | 33 | function uniq(array: Array): Array { 34 | return array.reduce( 35 | (accumulator, currentValue) => 36 | accumulator.indexOf(currentValue) === -1 37 | ? [...accumulator, currentValue] 38 | : accumulator, 39 | [], 40 | ); 41 | } 42 | 43 | export default concatenateTypeDefs; 44 | -------------------------------------------------------------------------------- /src/generate/decorateWithLogger.ts: -------------------------------------------------------------------------------- 1 | import { defaultFieldResolver, GraphQLFieldResolver } from 'graphql'; 2 | 3 | import { ILogger } from '../Interfaces'; 4 | 5 | /* 6 | * fn: The function to decorate with the logger 7 | * logger: an object instance of type Logger 8 | * hint: an optional hint to add to the error's message 9 | */ 10 | function decorateWithLogger( 11 | fn: GraphQLFieldResolver, 12 | logger: ILogger, 13 | hint: string, 14 | ): GraphQLFieldResolver { 15 | const resolver = fn != null ? fn : defaultFieldResolver; 16 | 17 | const logError = (e: Error) => { 18 | // TODO: clone the error properly 19 | const newE = new Error(); 20 | newE.stack = e.stack; 21 | /* istanbul ignore else: always get the hint from addErrorLoggingToSchema */ 22 | if (hint) { 23 | newE['originalMessage'] = e.message; 24 | newE['message'] = `Error in resolver ${hint}\n${e.message}`; 25 | } 26 | logger.log(newE); 27 | }; 28 | 29 | return (root, args, ctx, info) => { 30 | try { 31 | const result = resolver(root, args, ctx, info); 32 | // If the resolver returns a Promise log any Promise rejects. 33 | if ( 34 | result && 35 | typeof result.then === 'function' && 36 | typeof result.catch === 'function' 37 | ) { 38 | result.catch((reason: Error | string) => { 39 | // make sure that it's an error we're logging. 40 | const error = reason instanceof Error ? reason : new Error(reason); 41 | logError(error); 42 | 43 | // We don't want to leave an unhandled exception so pass on error. 44 | return reason; 45 | }); 46 | } 47 | return result; 48 | } catch (e) { 49 | logError(e); 50 | // we want to pass on the error, just in case. 51 | throw e; 52 | } 53 | }; 54 | } 55 | 56 | export default decorateWithLogger; 57 | -------------------------------------------------------------------------------- /src/generate/extendResolversFromInterfaces.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLSchema, 4 | isObjectType, 5 | isInterfaceType, 6 | } from 'graphql'; 7 | 8 | import { IResolvers } from '../Interfaces'; 9 | import { graphqlVersion } from '../utils/index'; 10 | 11 | function extendResolversFromInterfaces( 12 | schema: GraphQLSchema, 13 | resolvers: IResolvers, 14 | ) { 15 | const typeNames = Object.keys({ 16 | ...schema.getTypeMap(), 17 | ...resolvers, 18 | }); 19 | 20 | const extendedResolvers: IResolvers = {}; 21 | typeNames.forEach((typeName) => { 22 | const typeResolvers = resolvers[typeName]; 23 | const type = schema.getType(typeName); 24 | if ( 25 | isObjectType(type) || 26 | (graphqlVersion() >= 15 && isInterfaceType(type)) 27 | ) { 28 | const interfaceResolvers = (type as GraphQLObjectType) 29 | .getInterfaces() 30 | .map((iFace) => resolvers[iFace.name]); 31 | extendedResolvers[typeName] = Object.assign( 32 | {}, 33 | ...interfaceResolvers, 34 | typeResolvers, 35 | ); 36 | } else if (typeResolvers != null) { 37 | extendedResolvers[typeName] = typeResolvers; 38 | } 39 | }); 40 | 41 | return extendedResolvers; 42 | } 43 | 44 | export default extendResolversFromInterfaces; 45 | -------------------------------------------------------------------------------- /src/generate/extensionDefinitions.ts: -------------------------------------------------------------------------------- 1 | import { DocumentNode, DefinitionNode, Kind } from 'graphql'; 2 | 3 | import { graphqlVersion } from '../utils/index'; 4 | 5 | export function extractExtensionDefinitions(ast: DocumentNode) { 6 | const extensionDefs = ast.definitions.filter( 7 | (def: DefinitionNode) => 8 | def.kind === Kind.OBJECT_TYPE_EXTENSION || 9 | (graphqlVersion() >= 13 && def.kind === Kind.INTERFACE_TYPE_EXTENSION) || 10 | def.kind === Kind.INPUT_OBJECT_TYPE_EXTENSION || 11 | def.kind === Kind.UNION_TYPE_EXTENSION || 12 | def.kind === Kind.ENUM_TYPE_EXTENSION || 13 | def.kind === Kind.SCALAR_TYPE_EXTENSION || 14 | def.kind === Kind.SCHEMA_EXTENSION, 15 | ); 16 | 17 | return { 18 | ...ast, 19 | definitions: extensionDefs, 20 | }; 21 | } 22 | 23 | export function filterExtensionDefinitions(ast: DocumentNode) { 24 | const extensionDefs = ast.definitions.filter( 25 | (def: DefinitionNode) => 26 | def.kind !== Kind.OBJECT_TYPE_EXTENSION && 27 | def.kind !== Kind.INTERFACE_TYPE_EXTENSION && 28 | def.kind !== Kind.INPUT_OBJECT_TYPE_EXTENSION && 29 | def.kind !== Kind.UNION_TYPE_EXTENSION && 30 | def.kind !== Kind.ENUM_TYPE_EXTENSION && 31 | def.kind !== Kind.SCALAR_TYPE_EXTENSION && 32 | def.kind !== Kind.SCHEMA_EXTENSION, 33 | ); 34 | 35 | return { 36 | ...ast, 37 | definitions: extensionDefs, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/generate/makeExecutableSchema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defaultFieldResolver, 3 | GraphQLSchema, 4 | GraphQLFieldResolver, 5 | } from 'graphql'; 6 | 7 | import { IExecutableSchemaDefinition, ILogger } from '../Interfaces'; 8 | import { 9 | SchemaDirectiveVisitor, 10 | forEachField, 11 | mergeDeep, 12 | } from '../utils/index'; 13 | 14 | import attachDirectiveResolvers from './attachDirectiveResolvers'; 15 | import assertResolversPresent from './assertResolversPresent'; 16 | import addResolversToSchema from './addResolversToSchema'; 17 | import attachConnectorsToContext from './attachConnectorsToContext'; 18 | import addSchemaLevelResolver from './addSchemaLevelResolver'; 19 | import buildSchemaFromTypeDefinitions from './buildSchemaFromTypeDefinitions'; 20 | import decorateWithLogger from './decorateWithLogger'; 21 | import SchemaError from './SchemaError'; 22 | 23 | export function makeExecutableSchema({ 24 | typeDefs, 25 | resolvers = {}, 26 | connectors, 27 | logger, 28 | allowUndefinedInResolve = true, 29 | resolverValidationOptions = {}, 30 | directiveResolvers, 31 | schemaDirectives, 32 | parseOptions = {}, 33 | inheritResolversFromInterfaces = false, 34 | }: IExecutableSchemaDefinition) { 35 | // Validate and clean up arguments 36 | if (typeof resolverValidationOptions !== 'object') { 37 | throw new SchemaError( 38 | 'Expected `resolverValidationOptions` to be an object', 39 | ); 40 | } 41 | 42 | if (!typeDefs) { 43 | throw new SchemaError('Must provide typeDefs'); 44 | } 45 | 46 | // We allow passing in an array of resolver maps, in which case we merge them 47 | const resolverMap = Array.isArray(resolvers) 48 | ? resolvers 49 | .filter((resolverObj) => typeof resolverObj === 'object') 50 | .reduce(mergeDeep, {}) 51 | : resolvers; 52 | 53 | // Arguments are now validated and cleaned up 54 | 55 | const schema = buildSchemaFromTypeDefinitions(typeDefs, parseOptions); 56 | 57 | addResolversToSchema({ 58 | schema, 59 | resolvers: resolverMap, 60 | resolverValidationOptions, 61 | inheritResolversFromInterfaces, 62 | }); 63 | 64 | assertResolversPresent(schema, resolverValidationOptions); 65 | 66 | if (!allowUndefinedInResolve) { 67 | addCatchUndefinedToSchema(schema); 68 | } 69 | 70 | if (logger != null) { 71 | addErrorLoggingToSchema(schema, logger); 72 | } 73 | 74 | if (typeof resolvers['__schema'] === 'function') { 75 | // TODO a bit of a hack now, better rewrite generateSchema to attach it there. 76 | // not doing that now, because I'd have to rewrite a lot of tests. 77 | addSchemaLevelResolver( 78 | schema, 79 | resolvers['__schema'] as GraphQLFieldResolver, 80 | ); 81 | } 82 | 83 | if (connectors != null) { 84 | // connectors are optional, at least for now. That means you can just import them in the resolve 85 | // function if you want. 86 | attachConnectorsToContext(schema, connectors); 87 | } 88 | 89 | if (directiveResolvers != null) { 90 | attachDirectiveResolvers(schema, directiveResolvers); 91 | } 92 | 93 | if (schemaDirectives != null) { 94 | SchemaDirectiveVisitor.visitSchemaDirectives(schema, schemaDirectives); 95 | } 96 | 97 | return schema; 98 | } 99 | 100 | function decorateToCatchUndefined( 101 | fn: GraphQLFieldResolver, 102 | hint: string, 103 | ): GraphQLFieldResolver { 104 | const resolve = fn == null ? defaultFieldResolver : fn; 105 | return (root, args, ctx, info) => { 106 | const result = resolve(root, args, ctx, info); 107 | if (typeof result === 'undefined') { 108 | throw new Error(`Resolver for "${hint}" returned undefined`); 109 | } 110 | return result; 111 | }; 112 | } 113 | 114 | export function addCatchUndefinedToSchema(schema: GraphQLSchema): void { 115 | forEachField(schema, (field, typeName, fieldName) => { 116 | const errorHint = `${typeName}.${fieldName}`; 117 | field.resolve = decorateToCatchUndefined(field.resolve, errorHint); 118 | }); 119 | } 120 | 121 | export function addErrorLoggingToSchema( 122 | schema: GraphQLSchema, 123 | logger?: ILogger, 124 | ): void { 125 | if (!logger) { 126 | throw new Error('Must provide a logger'); 127 | } 128 | if (typeof logger.log !== 'function') { 129 | throw new Error('Logger.log must be a function'); 130 | } 131 | forEachField(schema, (field, typeName, fieldName) => { 132 | const errorHint = `${typeName}.${fieldName}`; 133 | field.resolve = decorateWithLogger(field.resolve, logger, errorHint); 134 | }); 135 | } 136 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Interfaces'; 2 | export * from './delegate'; 3 | export * from './generate'; 4 | export * from './links'; 5 | export * from './mock'; 6 | export * from './polyfills'; 7 | export * from './scalars'; 8 | export * from './stitch'; 9 | export * from './wrap'; 10 | export * from './utils'; 11 | -------------------------------------------------------------------------------- /src/links/AwaitVariablesLink.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApolloLink, 3 | Operation, 4 | NextLink, 5 | Observable, 6 | FetchResult, 7 | } from 'apollo-link'; 8 | 9 | function getFinalPromise(object: any): Promise { 10 | return Promise.resolve(object).then((resolvedObject) => { 11 | if (resolvedObject == null) { 12 | return resolvedObject; 13 | } 14 | 15 | if (Array.isArray(resolvedObject)) { 16 | return Promise.all(resolvedObject.map((o) => getFinalPromise(o))); 17 | } else if (typeof resolvedObject === 'object') { 18 | const keys = Object.keys(resolvedObject); 19 | return Promise.all( 20 | keys.map((key) => getFinalPromise(resolvedObject[key])), 21 | ).then((awaitedValues) => { 22 | for (let i = 0; i < keys.length; i++) { 23 | resolvedObject[keys[i]] = awaitedValues[i]; 24 | } 25 | return resolvedObject; 26 | }); 27 | } 28 | 29 | return resolvedObject; 30 | }); 31 | } 32 | 33 | class AwaitVariablesLink extends ApolloLink { 34 | request(operation: Operation, forward: NextLink): Observable { 35 | return new Observable((observer) => { 36 | let subscription: any; 37 | getFinalPromise(operation.variables) 38 | .then((resolvedVariables) => { 39 | operation.variables = resolvedVariables; 40 | subscription = forward(operation).subscribe({ 41 | next: observer.next.bind(observer), 42 | error: observer.error.bind(observer), 43 | complete: observer.complete.bind(observer), 44 | }); 45 | }) 46 | .catch(observer.error.bind(observer)); 47 | 48 | return () => { 49 | if (subscription != null) { 50 | subscription.unsubscribe(); 51 | } 52 | }; 53 | }); 54 | } 55 | } 56 | 57 | export { AwaitVariablesLink }; 58 | -------------------------------------------------------------------------------- /src/links/createServerHttpLink.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-nodejs-modules */ 2 | 3 | import { concat } from 'apollo-link'; 4 | import { 5 | createUploadLink, 6 | formDataAppendFile, 7 | isExtractableFile, 8 | } from 'apollo-upload-client'; 9 | import FormData, { AppendOptions } from 'form-data'; 10 | import fetch from 'node-fetch'; 11 | 12 | import { AwaitVariablesLink } from './AwaitVariablesLink'; 13 | 14 | const hasOwn = Object.prototype.hasOwnProperty; 15 | 16 | class FormDataWithStreamSupport extends FormData { 17 | private hasUnknowableLength: boolean; 18 | 19 | constructor(options?: any) { 20 | super(options); 21 | this.hasUnknowableLength = false; 22 | } 23 | 24 | public append( 25 | key: string, 26 | value: any, 27 | optionsOrFilename: AppendOptions | string = {}, 28 | ): void { 29 | // allow filename as single option 30 | const options: AppendOptions = 31 | typeof optionsOrFilename === 'string' 32 | ? { filename: optionsOrFilename } 33 | : optionsOrFilename; 34 | 35 | // empty or either doesn't have path or not an http response 36 | if ( 37 | !options.knownLength && 38 | !Buffer.isBuffer(value) && 39 | typeof value !== 'string' && 40 | !value.path && 41 | !(value.readable && hasOwn.call(value, 'httpVersion')) 42 | ) { 43 | this.hasUnknowableLength = true; 44 | } 45 | 46 | super.append(key, value, options); 47 | } 48 | 49 | public getLength( 50 | callback: (err: Error | null, length: number) => void, 51 | ): void { 52 | if (this.hasUnknowableLength) { 53 | return null; 54 | } 55 | 56 | return super.getLength(callback); 57 | } 58 | 59 | public getLengthSync(): number { 60 | if (this.hasUnknowableLength) { 61 | return null; 62 | } 63 | 64 | // eslint-disable-next-line no-sync 65 | return super.getLengthSync(); 66 | } 67 | } 68 | 69 | export const createServerHttpLink = (options: any) => 70 | concat( 71 | new AwaitVariablesLink(), 72 | createUploadLink({ 73 | ...options, 74 | fetch, 75 | FormData: FormDataWithStreamSupport, 76 | isExtractableFile: (value: any) => 77 | isExtractableFile(value) || value?.createReadStream, 78 | formDataAppendFile: (form: FormData, index: string, file: any) => { 79 | if (file.createReadStream != null) { 80 | form.append(index, file.createReadStream(), { 81 | filename: file.filename, 82 | contentType: file.mimetype, 83 | }); 84 | } else { 85 | formDataAppendFile(form, index, file); 86 | } 87 | }, 88 | }), 89 | ); 90 | -------------------------------------------------------------------------------- /src/links/index.ts: -------------------------------------------------------------------------------- 1 | import { createServerHttpLink } from './createServerHttpLink'; 2 | import { AwaitVariablesLink } from './AwaitVariablesLink'; 3 | 4 | export { createServerHttpLink, AwaitVariablesLink }; 5 | -------------------------------------------------------------------------------- /src/polyfills/buildSchema.ts: -------------------------------------------------------------------------------- 1 | import { Source, buildASTSchema, parse, BuildSchemaOptions } from 'graphql'; 2 | 3 | /** 4 | * Polyfill for graphql prior to v13 which do not pass options to buildASTSchema 5 | */ 6 | export function buildSchema( 7 | ast: string | Source, 8 | buildSchemaOptions: BuildSchemaOptions, 9 | ) { 10 | return buildASTSchema(parse(ast), buildSchemaOptions); 11 | } 12 | -------------------------------------------------------------------------------- /src/polyfills/extendSchema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DocumentNode, 3 | GraphQLSchema, 4 | extendSchema as graphqlExtendSchema, 5 | } from 'graphql'; 6 | 7 | import { getResolversFromSchema } from '../utils/index'; 8 | import { IResolverOptions } from '../Interfaces'; 9 | 10 | /** 11 | * Polyfill for graphql < v14.2 which does not support subscriptions 12 | */ 13 | export function extendSchema( 14 | schema: GraphQLSchema, 15 | extension: DocumentNode, 16 | options: any, 17 | ): GraphQLSchema { 18 | const subscriptionType = schema.getSubscriptionType(); 19 | if (subscriptionType == null) { 20 | return graphqlExtendSchema(schema, extension, options); 21 | } 22 | 23 | const resolvers = getResolversFromSchema(schema); 24 | 25 | const subscriptionTypeName = subscriptionType.name; 26 | const subscriptionResolvers = resolvers[ 27 | subscriptionTypeName 28 | ] as IResolverOptions; 29 | 30 | const extendedSchema = graphqlExtendSchema(schema, extension, options); 31 | 32 | const fields = extendedSchema.getSubscriptionType().getFields(); 33 | Object.keys(subscriptionResolvers).forEach((fieldName) => { 34 | fields[fieldName].subscribe = subscriptionResolvers[fieldName].subscribe; 35 | }); 36 | 37 | return extendedSchema; 38 | } 39 | -------------------------------------------------------------------------------- /src/polyfills/index.ts: -------------------------------------------------------------------------------- 1 | export { isSpecifiedScalarType } from './isSpecifiedScalarType'; 2 | 3 | export { buildSchema } from './buildSchema'; 4 | 5 | export { extendSchema } from './extendSchema'; 6 | 7 | export { 8 | toConfig, 9 | schemaToConfig, 10 | typeToConfig, 11 | objectTypeToConfig, 12 | interfaceTypeToConfig, 13 | unionTypeToConfig, 14 | enumTypeToConfig, 15 | scalarTypeToConfig, 16 | inputObjectTypeToConfig, 17 | directiveToConfig, 18 | inputFieldMapToConfig, 19 | inputFieldToConfig, 20 | fieldMapToConfig, 21 | fieldToConfig, 22 | argumentMapToConfig, 23 | argumentToConfig, 24 | } from './toConfig'; 25 | -------------------------------------------------------------------------------- /src/polyfills/isSpecifiedScalarType.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLString, 3 | GraphQLInt, 4 | GraphQLFloat, 5 | GraphQLBoolean, 6 | GraphQLID, 7 | GraphQLScalarType, 8 | isNamedType, 9 | } from 'graphql'; 10 | 11 | // FIXME: Replace with https://github.com/graphql/graphql-js/blob/master/src/type/scalars.js#L139 12 | // Blocked by https://github.com/graphql/graphql-js/issues/2153 13 | 14 | export const specifiedScalarTypes: Array = [ 15 | GraphQLString, 16 | GraphQLInt, 17 | GraphQLFloat, 18 | GraphQLBoolean, 19 | GraphQLID, 20 | ]; 21 | 22 | export function isSpecifiedScalarType(type: any): boolean { 23 | return ( 24 | isNamedType(type) && 25 | // Would prefer to use specifiedScalarTypes.some(), however %checks needs 26 | // a simple expression. 27 | (type.name === GraphQLString.name || 28 | type.name === GraphQLInt.name || 29 | type.name === GraphQLFloat.name || 30 | type.name === GraphQLBoolean.name || 31 | type.name === GraphQLID.name) 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/scalars/GraphQLUpload.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLScalarType, GraphQLError } from 'graphql'; 2 | 3 | /** 4 | * A scalar that supports file uploads with support for schema proxying. 5 | */ 6 | const GraphQLUpload = new GraphQLScalarType({ 7 | name: 'Upload', 8 | description: 'The `Upload` scalar type represents a file upload.', 9 | parseValue: (value) => { 10 | if (value != null && value.promise instanceof Promise) { 11 | // graphql-upload v10 12 | return value.promise; 13 | } else if (value instanceof Promise) { 14 | // graphql-upload v9 15 | return value; 16 | } 17 | throw new GraphQLError('Upload value invalid.'); 18 | }, 19 | // serialization requires to support schema stitching 20 | serialize: (value) => value, 21 | parseLiteral: (ast) => { 22 | throw new GraphQLError('Upload literal unsupported.', ast); 23 | }, 24 | }); 25 | 26 | export { GraphQLUpload }; 27 | -------------------------------------------------------------------------------- /src/scalars/index.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLUpload } from './GraphQLUpload'; 2 | 3 | export { GraphQLUpload }; 4 | -------------------------------------------------------------------------------- /src/stitch/createMergedResolver.ts: -------------------------------------------------------------------------------- 1 | import { IFieldResolver } from '../Interfaces'; 2 | 3 | import { unwrapResult, dehoistResult } from './proxiedResult'; 4 | import defaultMergedResolver from './defaultMergedResolver'; 5 | 6 | export function createMergedResolver({ 7 | fromPath, 8 | dehoist, 9 | delimeter = '__gqltf__', 10 | }: { 11 | fromPath?: Array; 12 | dehoist?: boolean; 13 | delimeter?: string; 14 | }): IFieldResolver { 15 | const parentErrorResolver: IFieldResolver = ( 16 | parent, 17 | args, 18 | context, 19 | info, 20 | ) => 21 | parent instanceof Error 22 | ? parent 23 | : defaultMergedResolver(parent, args, context, info); 24 | 25 | const unwrappingResolver: IFieldResolver = 26 | fromPath != null 27 | ? (parent, args, context, info) => 28 | parentErrorResolver( 29 | unwrapResult(parent, info, fromPath), 30 | args, 31 | context, 32 | info, 33 | ) 34 | : parentErrorResolver; 35 | 36 | const dehoistingResolver: IFieldResolver = dehoist 37 | ? (parent, args, context, info) => 38 | unwrappingResolver( 39 | dehoistResult(parent, delimeter), 40 | args, 41 | context, 42 | info, 43 | ) 44 | : unwrappingResolver; 45 | 46 | const noParentResolver: IFieldResolver = ( 47 | parent, 48 | args, 49 | context, 50 | info, 51 | ) => (parent ? dehoistingResolver(parent, args, context, info) : {}); 52 | 53 | return noParentResolver; 54 | } 55 | -------------------------------------------------------------------------------- /src/stitch/defaultMergedResolver.ts: -------------------------------------------------------------------------------- 1 | import { defaultFieldResolver } from 'graphql'; 2 | 3 | import { IGraphQLToolsResolveInfo } from '../Interfaces'; 4 | import { handleResult } from '../delegate/checkResultAndHandleErrors'; 5 | 6 | import { getErrors, getSubschema } from './proxiedResult'; 7 | import { getResponseKeyFromInfo } from './getResponseKeyFromInfo'; 8 | 9 | /** 10 | * Resolver that knows how to: 11 | * a) handle aliases for proxied schemas 12 | * b) handle errors from proxied schemas 13 | * c) handle external to internal enum coversion 14 | */ 15 | export default function defaultMergedResolver( 16 | parent: Record, 17 | args: Record, 18 | context: Record, 19 | info: IGraphQLToolsResolveInfo, 20 | ) { 21 | if (!parent) { 22 | return null; 23 | } 24 | 25 | const responseKey = getResponseKeyFromInfo(info); 26 | const errors = getErrors(parent, responseKey); 27 | 28 | // check to see if parent is not a proxied result, i.e. if parent resolver was manually overwritten 29 | // See https://github.com/apollographql/graphql-tools/issues/967 30 | if (!errors) { 31 | return defaultFieldResolver(parent, args, context, info); 32 | } 33 | 34 | const result = parent[responseKey]; 35 | const subschema = getSubschema(parent, responseKey); 36 | 37 | return handleResult(result, errors, subschema, context, info); 38 | } 39 | -------------------------------------------------------------------------------- /src/stitch/errors.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError, ASTNode } from 'graphql'; 2 | 3 | export function relocatedError( 4 | originalError: Error | GraphQLError, 5 | nodes: ReadonlyArray, 6 | path: ReadonlyArray, 7 | ): GraphQLError { 8 | if (Array.isArray((originalError as GraphQLError).path)) { 9 | return new GraphQLError( 10 | (originalError as GraphQLError).message, 11 | (originalError as GraphQLError).nodes, 12 | (originalError as GraphQLError).source, 13 | (originalError as GraphQLError).positions, 14 | path != null ? path : (originalError as GraphQLError).path, 15 | (originalError as GraphQLError).originalError, 16 | (originalError as GraphQLError).extensions, 17 | ); 18 | } 19 | 20 | if (originalError == null) { 21 | return new GraphQLError( 22 | undefined, 23 | nodes, 24 | undefined, 25 | undefined, 26 | path, 27 | originalError, 28 | ); 29 | } 30 | 31 | return new GraphQLError( 32 | originalError.message, 33 | (originalError as GraphQLError).nodes != null 34 | ? (originalError as GraphQLError).nodes 35 | : nodes, 36 | (originalError as GraphQLError).source, 37 | (originalError as GraphQLError).positions, 38 | path, 39 | originalError, 40 | ); 41 | } 42 | 43 | export function slicedError(originalError: GraphQLError) { 44 | return relocatedError( 45 | originalError, 46 | originalError.nodes, 47 | originalError.path != null ? originalError.path.slice(1) : undefined, 48 | ); 49 | } 50 | 51 | export function getErrorsByPathSegment( 52 | errors: ReadonlyArray, 53 | ): Record> { 54 | const record = Object.create(null); 55 | errors.forEach((error) => { 56 | if (!error.path || error.path.length < 2) { 57 | return; 58 | } 59 | 60 | const pathSegment = error.path[1]; 61 | 62 | const current = record[pathSegment] != null ? record[pathSegment] : []; 63 | current.push(slicedError(error)); 64 | record[pathSegment] = current; 65 | }); 66 | 67 | return record; 68 | } 69 | 70 | class CombinedError extends Error { 71 | public errors: ReadonlyArray; 72 | constructor(message: string, errors: ReadonlyArray) { 73 | super(message); 74 | this.errors = errors; 75 | } 76 | } 77 | 78 | export function combineErrors( 79 | errors: ReadonlyArray, 80 | ): GraphQLError | CombinedError { 81 | if (errors.length === 1) { 82 | return new GraphQLError( 83 | errors[0].message, 84 | errors[0].nodes, 85 | errors[0].source, 86 | errors[0].positions, 87 | errors[0].path, 88 | errors[0].originalError, 89 | errors[0].extensions, 90 | ); 91 | } 92 | 93 | return new CombinedError( 94 | errors.map((error) => error.message).join('\n'), 95 | errors, 96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /src/stitch/getResponseKeyFromInfo.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLResolveInfo } from 'graphql'; 2 | 3 | /** 4 | * Get the key under which the result of this resolver will be placed in the response JSON. Basically, just 5 | * resolves aliases. 6 | * @param info The info argument to the resolver. 7 | */ 8 | export function getResponseKeyFromInfo(info: GraphQLResolveInfo) { 9 | return info.fieldNodes[0].alias != null 10 | ? info.fieldNodes[0].alias.value 11 | : info.fieldName; 12 | } 13 | -------------------------------------------------------------------------------- /src/stitch/introspectSchema.ts: -------------------------------------------------------------------------------- 1 | import { ApolloLink } from 'apollo-link'; 2 | import { 3 | GraphQLSchema, 4 | DocumentNode, 5 | getIntrospectionQuery, 6 | buildClientSchema, 7 | parse, 8 | } from 'graphql'; 9 | 10 | import { Fetcher } from '../Interfaces'; 11 | 12 | import { combineErrors } from './errors'; 13 | import linkToFetcher from './linkToFetcher'; 14 | 15 | const parsedIntrospectionQuery: DocumentNode = parse(getIntrospectionQuery()); 16 | 17 | export default function introspectSchema( 18 | linkOrFetcher: ApolloLink | Fetcher, 19 | linkContext?: { [key: string]: any }, 20 | ): Promise { 21 | const fetcher = 22 | linkOrFetcher instanceof ApolloLink 23 | ? linkToFetcher(linkOrFetcher) 24 | : linkOrFetcher; 25 | 26 | return fetcher({ 27 | query: parsedIntrospectionQuery, 28 | context: linkContext, 29 | }).then((introspectionResult) => { 30 | if ( 31 | (Array.isArray(introspectionResult.errors) && 32 | introspectionResult.errors.length) || 33 | !introspectionResult.data.__schema 34 | ) { 35 | if (Array.isArray(introspectionResult.errors)) { 36 | const combinedError: Error = combineErrors(introspectionResult.errors); 37 | throw combinedError; 38 | } else { 39 | throw new Error( 40 | 'Could not obtain introspection result, received: ' + 41 | JSON.stringify(introspectionResult), 42 | ); 43 | } 44 | } else { 45 | const schema = buildClientSchema( 46 | introspectionResult.data as { 47 | __schema: any; 48 | }, 49 | ); 50 | return schema; 51 | } 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /src/stitch/linkToFetcher.ts: -------------------------------------------------------------------------------- 1 | import { ApolloLink, toPromise, execute, ExecutionResult } from 'apollo-link'; 2 | 3 | import { Fetcher, IFetcherOperation } from '../Interfaces'; 4 | 5 | export { execute } from 'apollo-link'; 6 | 7 | export default function linkToFetcher(link: ApolloLink): Fetcher { 8 | return (fetcherOperation: IFetcherOperation): Promise => 9 | toPromise(execute(link, fetcherOperation)); 10 | } 11 | -------------------------------------------------------------------------------- /src/stitch/makeMergedType.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLType, isAbstractType, isObjectType } from 'graphql'; 2 | 3 | import defaultMergedResolver from './defaultMergedResolver'; 4 | import resolveFromParentTypename from './resolveFromParentTypename'; 5 | 6 | export function makeMergedType(type: GraphQLType): void { 7 | if (isObjectType(type)) { 8 | type.isTypeOf = undefined; 9 | 10 | const fieldMap = type.getFields(); 11 | Object.keys(fieldMap).forEach((fieldName) => { 12 | fieldMap[fieldName].resolve = defaultMergedResolver; 13 | fieldMap[fieldName].subscribe = null; 14 | }); 15 | } else if (isAbstractType(type)) { 16 | type.resolveType = (parent) => resolveFromParentTypename(parent); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/stitch/mapAsyncIterator.ts: -------------------------------------------------------------------------------- 1 | import { $$asyncIterator } from 'iterall'; 2 | 3 | /** 4 | * Given an AsyncIterable and a callback function, return an AsyncIterator 5 | * which produces values mapped via calling the callback function. 6 | */ 7 | export default function mapAsyncIterator( 8 | iterator: AsyncIterator, 9 | callback: (value: T) => Promise | U, 10 | rejectCallback?: any, 11 | ): AsyncIterator { 12 | let $return: any; 13 | let abruptClose: any; 14 | 15 | if (typeof iterator.return === 'function') { 16 | $return = iterator.return; 17 | abruptClose = (error: any) => { 18 | const rethrow = () => Promise.reject(error); 19 | return $return.call(iterator).then(rethrow, rethrow); 20 | }; 21 | } 22 | 23 | function mapResult(result: any) { 24 | return result.done 25 | ? result 26 | : asyncMapValue(result.value, callback).then(iteratorResult, abruptClose); 27 | } 28 | 29 | let mapReject: any; 30 | if (rejectCallback) { 31 | // Capture rejectCallback to ensure it cannot be null. 32 | const reject = rejectCallback; 33 | mapReject = (error: any) => 34 | asyncMapValue(error, reject).then(iteratorResult, abruptClose); 35 | } 36 | 37 | return { 38 | next() { 39 | return iterator.next().then(mapResult, mapReject); 40 | }, 41 | return() { 42 | return $return 43 | ? $return.call(iterator).then(mapResult, mapReject) 44 | : Promise.resolve({ value: undefined, done: true }); 45 | }, 46 | throw(error: any) { 47 | if (typeof iterator.throw === 'function') { 48 | return iterator.throw(error).then(mapResult, mapReject); 49 | } 50 | return Promise.reject(error).catch(abruptClose); 51 | }, 52 | [$$asyncIterator]() { 53 | return this; 54 | }, 55 | } as any; 56 | } 57 | 58 | function asyncMapValue( 59 | value: T, 60 | callback: (value: T) => Promise | U, 61 | ): Promise { 62 | return new Promise((resolve) => resolve(callback(value))); 63 | } 64 | 65 | function iteratorResult(value: T): IteratorResult { 66 | return { value, done: false }; 67 | } 68 | -------------------------------------------------------------------------------- /src/stitch/mergeFields.ts: -------------------------------------------------------------------------------- 1 | import { FieldNode, SelectionNode, Kind } from 'graphql'; 2 | 3 | import { 4 | SubschemaConfig, 5 | IGraphQLToolsResolveInfo, 6 | MergedTypeInfo, 7 | } from '../Interfaces'; 8 | 9 | import { mergeProxiedResults } from './proxiedResult'; 10 | 11 | function buildDelegationPlan( 12 | mergedTypeInfo: MergedTypeInfo, 13 | originalSelections: Array, 14 | sourceSubschemas: Array, 15 | targetSubschemas: Array, 16 | ): { 17 | delegationMap: Map>; 18 | unproxiableSelections: Array; 19 | proxiableSubschemas: Array; 20 | nonProxiableSubschemas: Array; 21 | } { 22 | // 1. calculate if possible to delegate to given subschema 23 | // TODO: change logic so that required selection set can be spread across multiple subschemas? 24 | 25 | const proxiableSubschemas: Array = []; 26 | const nonProxiableSubschemas: Array = []; 27 | 28 | targetSubschemas.forEach((t) => { 29 | if ( 30 | sourceSubschemas.some((s) => { 31 | const selectionSet = mergedTypeInfo.selectionSets.get(t); 32 | return mergedTypeInfo.containsSelectionSet.get(s).get(selectionSet); 33 | }) 34 | ) { 35 | proxiableSubschemas.push(t); 36 | } else { 37 | nonProxiableSubschemas.push(t); 38 | } 39 | }); 40 | 41 | const { uniqueFields, nonUniqueFields } = mergedTypeInfo; 42 | const unproxiableSelections: Array = []; 43 | 44 | // 2. for each selection: 45 | 46 | const delegationMap: Map> = new Map(); 47 | originalSelections.forEach((selection) => { 48 | // 2a. use uniqueFields map to assign fields to subschema if one of possible subschemas 49 | 50 | const uniqueSubschema: SubschemaConfig = uniqueFields[selection.name.value]; 51 | if (uniqueSubschema != null) { 52 | if (proxiableSubschemas.includes(uniqueSubschema)) { 53 | const existingSubschema = delegationMap.get(uniqueSubschema); 54 | if (existingSubschema != null) { 55 | existingSubschema.push(selection); 56 | } else { 57 | delegationMap.set(uniqueSubschema, [selection]); 58 | } 59 | } else { 60 | unproxiableSelections.push(selection); 61 | } 62 | } else { 63 | // 2b. use nonUniqueFields to assign to a possible subschema, 64 | // preferring one of the subschemas already targets of delegation 65 | 66 | let nonUniqueSubschemas: Array = 67 | nonUniqueFields[selection.name.value]; 68 | nonUniqueSubschemas = nonUniqueSubschemas.filter((s) => 69 | proxiableSubschemas.includes(s), 70 | ); 71 | if (nonUniqueSubschemas != null) { 72 | const subschemas: Array = Array.from( 73 | delegationMap.keys(), 74 | ); 75 | const existingSubschema = nonUniqueSubschemas.find((s) => 76 | subschemas.includes(s), 77 | ); 78 | if (existingSubschema != null) { 79 | delegationMap.get(existingSubschema).push(selection); 80 | } else { 81 | delegationMap.set(nonUniqueSubschemas[0], [selection]); 82 | } 83 | } else { 84 | unproxiableSelections.push(selection); 85 | } 86 | } 87 | }); 88 | 89 | return { 90 | delegationMap, 91 | unproxiableSelections, 92 | proxiableSubschemas, 93 | nonProxiableSubschemas, 94 | }; 95 | } 96 | 97 | export function mergeFields( 98 | mergedTypeInfo: MergedTypeInfo, 99 | typeName: string, 100 | object: any, 101 | originalSelections: Array, 102 | sourceSubschemas: Array, 103 | targetSubschemas: Array, 104 | context: Record, 105 | info: IGraphQLToolsResolveInfo, 106 | ): any { 107 | if (!originalSelections.length) { 108 | return object; 109 | } 110 | 111 | const { 112 | delegationMap, 113 | unproxiableSelections, 114 | proxiableSubschemas, 115 | nonProxiableSubschemas, 116 | } = buildDelegationPlan( 117 | mergedTypeInfo, 118 | originalSelections, 119 | sourceSubschemas, 120 | targetSubschemas, 121 | ); 122 | 123 | if (!delegationMap.size) { 124 | return object; 125 | } 126 | 127 | const maybePromises: Promise | any = []; 128 | delegationMap.forEach( 129 | (selections: Array, s: SubschemaConfig) => { 130 | const maybePromise = s.merge[typeName].resolve(object, context, info, s, { 131 | kind: Kind.SELECTION_SET, 132 | selections, 133 | }); 134 | maybePromises.push(maybePromise); 135 | }, 136 | ); 137 | 138 | let containsPromises = false; 139 | for (const maybePromise of maybePromises) { 140 | if (maybePromise instanceof Promise) { 141 | containsPromises = true; 142 | break; 143 | } 144 | } 145 | 146 | return containsPromises 147 | ? Promise.all(maybePromises).then((results) => 148 | mergeFields( 149 | mergedTypeInfo, 150 | typeName, 151 | mergeProxiedResults(object, ...results), 152 | unproxiableSelections, 153 | sourceSubschemas.concat(proxiableSubschemas), 154 | nonProxiableSubschemas, 155 | context, 156 | info, 157 | ), 158 | ) 159 | : mergeFields( 160 | mergedTypeInfo, 161 | typeName, 162 | mergeProxiedResults(object, ...maybePromises), 163 | unproxiableSelections, 164 | sourceSubschemas.concat(proxiableSubschemas), 165 | nonProxiableSubschemas, 166 | context, 167 | info, 168 | ); 169 | } 170 | -------------------------------------------------------------------------------- /src/stitch/observableToAsyncIterable.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'apollo-link'; 2 | import { $$asyncIterator } from 'iterall'; 3 | 4 | type Callback = (value?: any) => any; 5 | 6 | export function observableToAsyncIterable( 7 | observable: Observable, 8 | ): AsyncIterator & { 9 | [$$asyncIterator]: () => AsyncIterator; 10 | } { 11 | const pullQueue: Array = []; 12 | const pushQueue: Array = []; 13 | 14 | let listening = true; 15 | 16 | const pushValue = (value: any) => { 17 | if (pullQueue.length !== 0) { 18 | pullQueue.shift()({ value, done: false }); 19 | } else { 20 | pushQueue.push({ value }); 21 | } 22 | }; 23 | 24 | const pushError = (error: any) => { 25 | if (pullQueue.length !== 0) { 26 | pullQueue.shift()({ value: { errors: [error] }, done: false }); 27 | } else { 28 | pushQueue.push({ value: { errors: [error] } }); 29 | } 30 | }; 31 | 32 | const pullValue = () => 33 | new Promise((resolve) => { 34 | if (pushQueue.length !== 0) { 35 | const element = pushQueue.shift(); 36 | // either {value: {errors: [...]}} or {value: ...} 37 | resolve({ 38 | ...element, 39 | done: false, 40 | }); 41 | } else { 42 | pullQueue.push(resolve); 43 | } 44 | }); 45 | 46 | const subscription = observable.subscribe({ 47 | next(value: any) { 48 | pushValue(value); 49 | }, 50 | error(err: Error) { 51 | pushError(err); 52 | }, 53 | }); 54 | 55 | const emptyQueue = () => { 56 | if (listening) { 57 | listening = false; 58 | subscription.unsubscribe(); 59 | pullQueue.forEach((resolve) => resolve({ value: undefined, done: true })); 60 | pullQueue.length = 0; 61 | pushQueue.length = 0; 62 | } 63 | }; 64 | 65 | return { 66 | next() { 67 | return listening ? pullValue() : this.return(); 68 | }, 69 | return() { 70 | emptyQueue(); 71 | return Promise.resolve({ value: undefined, done: true }); 72 | }, 73 | throw(error) { 74 | emptyQueue(); 75 | return Promise.reject(error); 76 | }, 77 | [$$asyncIterator]() { 78 | return this; 79 | }, 80 | }; 81 | } 82 | -------------------------------------------------------------------------------- /src/stitch/proxiedResult.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError, GraphQLSchema, responsePathAsArray } from 'graphql'; 2 | 3 | import { SubschemaConfig, IGraphQLToolsResolveInfo } from '../Interfaces'; 4 | import { mergeDeep } from '../utils/index'; 5 | import { handleNull } from '../delegate/checkResultAndHandleErrors'; 6 | 7 | import { relocatedError } from './errors'; 8 | 9 | const hasSymbol = 10 | (typeof global !== 'undefined' && 'Symbol' in global) || 11 | // eslint-disable-next-line no-undef 12 | (typeof window !== 'undefined' && 'Symbol' in window); 13 | 14 | export const OBJECT_SUBSCHEMA_SYMBOL = hasSymbol 15 | ? Symbol('initialSubschema') 16 | : '@@__initialSubschema'; 17 | export const FIELD_SUBSCHEMA_MAP_SYMBOL = hasSymbol 18 | ? Symbol('subschemaMap') 19 | : '@@__subschemaMap'; 20 | export const ERROR_SYMBOL = hasSymbol 21 | ? Symbol('subschemaErrors') 22 | : '@@__subschemaErrors'; 23 | 24 | export function isProxiedResult(result: any) { 25 | return result != null ? result[ERROR_SYMBOL] : result; 26 | } 27 | 28 | export function getSubschema( 29 | result: any, 30 | responseKey: string, 31 | ): GraphQLSchema | SubschemaConfig { 32 | const subschema = 33 | result[FIELD_SUBSCHEMA_MAP_SYMBOL] && 34 | result[FIELD_SUBSCHEMA_MAP_SYMBOL][responseKey]; 35 | return subschema ? subschema : result[OBJECT_SUBSCHEMA_SYMBOL]; 36 | } 37 | 38 | export function setObjectSubschema( 39 | result: any, 40 | subschema: GraphQLSchema | SubschemaConfig, 41 | ) { 42 | result[OBJECT_SUBSCHEMA_SYMBOL] = subschema; 43 | } 44 | 45 | export function setErrors(result: any, errors: Array) { 46 | result[ERROR_SYMBOL] = errors; 47 | } 48 | 49 | export function getErrors( 50 | result: any, 51 | pathSegment: string, 52 | ): Array { 53 | const errors = result != null ? result[ERROR_SYMBOL] : result; 54 | 55 | if (!Array.isArray(errors)) { 56 | return null; 57 | } 58 | 59 | const fieldErrors = []; 60 | 61 | for (const error of errors) { 62 | if (!error.path || error.path[0] === pathSegment) { 63 | fieldErrors.push(error); 64 | } 65 | } 66 | 67 | return fieldErrors; 68 | } 69 | 70 | export function unwrapResult( 71 | parent: any, 72 | info: IGraphQLToolsResolveInfo, 73 | path: Array, 74 | ): any { 75 | let newParent: any = parent; 76 | const pathLength = path.length; 77 | for (let i = 0; i < pathLength; i++) { 78 | const responseKey = path[i]; 79 | const errors = getErrors(newParent, responseKey); 80 | const subschema = getSubschema(newParent, responseKey); 81 | 82 | const object = newParent[responseKey]; 83 | if (object == null) { 84 | return handleNull( 85 | info.fieldNodes, 86 | responsePathAsArray(info.path), 87 | errors, 88 | ); 89 | } 90 | 91 | setErrors( 92 | object, 93 | errors.map((error) => 94 | relocatedError( 95 | error, 96 | error.nodes, 97 | error.path != null ? error.path.slice(1) : undefined, 98 | ), 99 | ), 100 | ); 101 | setObjectSubschema(object, subschema); 102 | 103 | newParent = object; 104 | } 105 | 106 | return newParent; 107 | } 108 | 109 | export function dehoistResult( 110 | parent: any, 111 | delimeter: string = '__gqltf__', 112 | ): any { 113 | const result = Object.create(null); 114 | 115 | Object.keys(parent).forEach((alias) => { 116 | let obj = result; 117 | 118 | const fieldNames = alias.split(delimeter); 119 | const fieldName = fieldNames.pop(); 120 | fieldNames.forEach((key) => { 121 | obj = obj[key] = obj[key] || Object.create(null); 122 | }); 123 | obj[fieldName] = parent[alias]; 124 | }); 125 | 126 | result[ERROR_SYMBOL] = parent[ERROR_SYMBOL].map((error: GraphQLError) => { 127 | if (error.path != null) { 128 | const path = error.path.slice(); 129 | const pathSegment = path.shift(); 130 | const expandedPathSegment: Array< 131 | string | number 132 | > = (pathSegment as string).split(delimeter); 133 | return relocatedError( 134 | error, 135 | error.nodes, 136 | expandedPathSegment.concat(path), 137 | ); 138 | } 139 | 140 | return error; 141 | }); 142 | 143 | result[OBJECT_SUBSCHEMA_SYMBOL] = parent[OBJECT_SUBSCHEMA_SYMBOL]; 144 | 145 | return result; 146 | } 147 | 148 | export function mergeProxiedResults(target: any, ...sources: any): any { 149 | const errors = target[ERROR_SYMBOL].concat( 150 | sources.map((source: any) => source[ERROR_SYMBOL]), 151 | ); 152 | const fieldSubschemaMap = sources.reduce( 153 | (acc: Record, source: any) => { 154 | const subschema = source[OBJECT_SUBSCHEMA_SYMBOL]; 155 | Object.keys(source).forEach((key) => { 156 | acc[key] = subschema; 157 | }); 158 | return acc; 159 | }, 160 | {}, 161 | ); 162 | const result = mergeDeep(target, ...sources); 163 | result[ERROR_SYMBOL] = errors; 164 | result[FIELD_SUBSCHEMA_MAP_SYMBOL] = target[FIELD_SUBSCHEMA_MAP_SYMBOL] 165 | ? mergeDeep(target[FIELD_SUBSCHEMA_MAP_SYMBOL], fieldSubschemaMap) 166 | : fieldSubschemaMap; 167 | return result; 168 | } 169 | -------------------------------------------------------------------------------- /src/stitch/resolveFromParentTypename.ts: -------------------------------------------------------------------------------- 1 | export default function resolveFromParentTypename(parent: any) { 2 | const parentTypename: string = parent['__typename']; 3 | if (!parentTypename) { 4 | throw new Error( 5 | 'Did not fetch typename for object, unable to resolve interface.', 6 | ); 7 | } 8 | 9 | return parentTypename; 10 | } 11 | -------------------------------------------------------------------------------- /src/test/circularSchemaA.ts: -------------------------------------------------------------------------------- 1 | import TypeB from './circularSchemaB'; 2 | 3 | const TypeA = () => [ 4 | ` 5 | type TypeA { 6 | id: ID 7 | b: TypeB 8 | }`, 9 | TypeB, 10 | ]; 11 | 12 | export default TypeA; 13 | -------------------------------------------------------------------------------- /src/test/circularSchemaB.ts: -------------------------------------------------------------------------------- 1 | import TypeA from './circularSchemaA'; 2 | 3 | const TypeB = () => [ 4 | ` 5 | type TypeB { 6 | id: ID 7 | a: TypeA 8 | }`, 9 | TypeA, 10 | ]; 11 | 12 | export default TypeB; 13 | -------------------------------------------------------------------------------- /src/test/testDataloader.ts: -------------------------------------------------------------------------------- 1 | import DataLoader from 'dataloader'; 2 | import { graphql, GraphQLList } from 'graphql'; 3 | import { expect } from 'chai'; 4 | 5 | import { delegateToSchema } from '../delegate/index'; 6 | import { makeExecutableSchema } from '../generate/index'; 7 | import { mergeSchemas } from '../stitch/index'; 8 | import { IGraphQLToolsResolveInfo } from '../Interfaces'; 9 | 10 | describe('dataloader', () => { 11 | it('should work', async () => { 12 | const taskSchema = makeExecutableSchema({ 13 | typeDefs: ` 14 | type Task { 15 | id: ID! 16 | text: String 17 | userId: ID! 18 | } 19 | type Query { 20 | task(id: ID!): Task 21 | } 22 | `, 23 | resolvers: { 24 | Query: { 25 | task: (_root, { id }) => ({ 26 | id, 27 | text: `task ${id as string}`, 28 | userId: id, 29 | }), 30 | }, 31 | }, 32 | }); 33 | 34 | const userSchema = makeExecutableSchema({ 35 | typeDefs: ` 36 | type User { 37 | id: ID! 38 | email: String! 39 | } 40 | type Query { 41 | usersByIds(ids: [ID!]!): [User]! 42 | } 43 | `, 44 | resolvers: { 45 | Query: { 46 | usersByIds: (_root, { ids }) => 47 | ids.map((id: string) => ({ id, email: `${id}@tasks.com` })), 48 | }, 49 | }, 50 | }); 51 | 52 | const schema = mergeSchemas({ 53 | schemas: [taskSchema, userSchema], 54 | typeDefs: ` 55 | extend type Task { 56 | user: User! 57 | } 58 | `, 59 | resolvers: { 60 | Task: { 61 | user: { 62 | fragment: '... on Task { userId }', 63 | resolve(task, _args, context, info) { 64 | return context.usersLoader.load({ id: task.userId, info }); 65 | }, 66 | }, 67 | }, 68 | }, 69 | }); 70 | 71 | const usersLoader = new DataLoader( 72 | async (keys: Array<{ id: any; info: IGraphQLToolsResolveInfo }>) => { 73 | const users = await delegateToSchema({ 74 | schema: userSchema, 75 | operation: 'query', 76 | fieldName: 'usersByIds', 77 | args: { 78 | ids: keys.map((k: { id: any }) => k.id), 79 | }, 80 | context: null, 81 | info: keys[0].info, 82 | returnType: new GraphQLList(keys[0].info.returnType), 83 | }); 84 | 85 | expect(users).to.deep.equal([ 86 | { 87 | id: '1', 88 | email: '1@tasks.com', 89 | }, 90 | ]); 91 | 92 | return users; 93 | }, 94 | ); 95 | 96 | const query = `{ 97 | task(id: "1") { 98 | id 99 | text 100 | user { 101 | id 102 | email 103 | } 104 | } 105 | }`; 106 | 107 | const result = await graphql(schema, query, null, { usersLoader }); 108 | 109 | expect(result).to.deep.equal({ 110 | data: { 111 | task: { 112 | id: '1', 113 | text: 'task 1', 114 | user: { 115 | id: '1', 116 | email: '1@tasks.com', 117 | }, 118 | }, 119 | }, 120 | }); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /src/test/testDelegateToSchema.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema, graphql } from 'graphql'; 2 | import { expect } from 'chai'; 3 | 4 | import delegateToSchema from '../delegate/delegateToSchema'; 5 | import mergeSchemas from '../stitch/mergeSchemas'; 6 | import { IResolvers } from '../Interfaces'; 7 | import { makeExecutableSchema } from '../generate'; 8 | import { wrapSchema } from '../wrap'; 9 | 10 | import { 11 | propertySchema, 12 | bookingSchema, 13 | sampleData, 14 | Property, 15 | } from './testingSchemas'; 16 | 17 | function findPropertyByLocationName( 18 | properties: { [key: string]: Property }, 19 | name: string, 20 | ): Property | undefined { 21 | for (const key of Object.keys(properties)) { 22 | const property = properties[key]; 23 | if (property.location.name === name) { 24 | return property; 25 | } 26 | } 27 | } 28 | 29 | const COORDINATES_QUERY = ` 30 | query BookingCoordinates($bookingId: ID!) { 31 | bookingById (id: $bookingId) { 32 | property { 33 | location { 34 | coordinates 35 | } 36 | } 37 | } 38 | } 39 | `; 40 | 41 | function proxyResolvers(spec: string): IResolvers { 42 | return { 43 | Booking: { 44 | property: { 45 | fragment: '... on Booking { propertyId }', 46 | resolve(booking, _args, context, info) { 47 | const delegateFn = 48 | spec === 'standalone' 49 | ? delegateToSchema 50 | : info.mergeInfo.delegateToSchema; 51 | return delegateFn?.({ 52 | schema: propertySchema, 53 | operation: 'query', 54 | fieldName: 'propertyById', 55 | args: { id: booking.propertyId }, 56 | context, 57 | info, 58 | }); 59 | }, 60 | }, 61 | }, 62 | Location: { 63 | coordinates: { 64 | fragment: '... on Location { name }', 65 | resolve: (location) => { 66 | const name = location.name; 67 | return findPropertyByLocationName(sampleData.Property, name).location 68 | .coordinates; 69 | }, 70 | }, 71 | }, 72 | }; 73 | } 74 | 75 | const proxyTypeDefs = ` 76 | extend type Booking { 77 | property: Property! 78 | } 79 | extend type Location { 80 | coordinates: String! 81 | } 82 | `; 83 | 84 | describe('stitching', () => { 85 | describe('delegateToSchema', () => { 86 | ['standalone', 'info.mergeInfo'].forEach((spec) => { 87 | describe(spec, () => { 88 | let schema: GraphQLSchema; 89 | before(() => { 90 | schema = mergeSchemas({ 91 | schemas: [bookingSchema, propertySchema, proxyTypeDefs], 92 | resolvers: proxyResolvers(spec), 93 | }); 94 | }); 95 | it('should add fragments for deep types', async () => { 96 | const result = await graphql( 97 | schema, 98 | COORDINATES_QUERY, 99 | {}, 100 | {}, 101 | { bookingId: 'b1' }, 102 | ); 103 | 104 | expect(result).to.deep.equal({ 105 | data: { 106 | bookingById: { 107 | property: { 108 | location: { 109 | coordinates: sampleData.Property.p1.location.coordinates, 110 | }, 111 | }, 112 | }, 113 | }, 114 | }); 115 | }); 116 | }); 117 | }); 118 | }); 119 | }); 120 | 121 | describe('schema delegation', () => { 122 | it('should work even when there are default fields', async () => { 123 | const schema = makeExecutableSchema({ 124 | typeDefs: ` 125 | scalar JSON 126 | type Data { 127 | json(input: JSON = "test"): JSON 128 | } 129 | type Query { 130 | data: Data 131 | } 132 | `, 133 | resolvers: { 134 | Query: { 135 | data: () => ({}), 136 | }, 137 | Data: { 138 | json: (_root, args, context, info) => 139 | delegateToSchema({ 140 | schema: propertySchema, 141 | fieldName: 'jsonTest', 142 | args, 143 | context, 144 | info, 145 | }), 146 | }, 147 | }, 148 | }); 149 | 150 | const result = await graphql( 151 | schema, 152 | ` 153 | query { 154 | data { 155 | json 156 | } 157 | } 158 | `, 159 | ); 160 | 161 | expect(result).to.deep.equal({ 162 | data: { 163 | data: { 164 | json: 'test', 165 | }, 166 | }, 167 | }); 168 | }); 169 | 170 | it('should work even with variables', async () => { 171 | const innerSchema = makeExecutableSchema({ 172 | typeDefs: ` 173 | type User { 174 | id(show: Boolean): ID 175 | } 176 | type Query { 177 | user: User 178 | } 179 | `, 180 | resolvers: { 181 | Query: { 182 | user: () => ({}), 183 | }, 184 | User: { 185 | id: () => '123', 186 | }, 187 | }, 188 | }); 189 | const schema = wrapSchema(innerSchema); 190 | 191 | const result = await graphql( 192 | schema, 193 | ` 194 | query($show: Boolean) { 195 | user { 196 | id(show: $show) 197 | } 198 | } 199 | `, 200 | null, 201 | null, 202 | { show: true }, 203 | ); 204 | 205 | expect(result).to.deep.equal({ 206 | data: { 207 | user: { 208 | id: '123', 209 | }, 210 | }, 211 | }); 212 | }); 213 | }); 214 | -------------------------------------------------------------------------------- /src/test/testExtensionExtraction.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { parse } from 'graphql'; 3 | 4 | import { extractExtensionDefinitions } from '../generate/extensionDefinitions'; 5 | 6 | describe('Extension extraction', () => { 7 | it('extracts extended inputs', () => { 8 | const typeDefs = ` 9 | input Input { 10 | foo: String 11 | } 12 | 13 | extend input Input { 14 | bar: String 15 | } 16 | `; 17 | 18 | const astDocument = parse(typeDefs); 19 | const extensionAst = extractExtensionDefinitions(astDocument); 20 | 21 | expect(extensionAst.definitions).to.have.length(1); 22 | expect(extensionAst.definitions[0].kind).to.equal( 23 | 'InputObjectTypeExtension', 24 | ); 25 | }); 26 | 27 | it('extracts extended unions', () => { 28 | const typeDefs = ` 29 | type Person { 30 | name: String! 31 | } 32 | type Location { 33 | name: String! 34 | } 35 | union Searchable = Person | Location 36 | 37 | type Post { 38 | name: String! 39 | } 40 | extend union Searchable = Post 41 | `; 42 | 43 | const astDocument = parse(typeDefs); 44 | const extensionAst = extractExtensionDefinitions(astDocument); 45 | 46 | expect(extensionAst.definitions).to.have.length(1); 47 | expect(extensionAst.definitions[0].kind).to.equal('UnionTypeExtension'); 48 | }); 49 | 50 | it('extracts extended enums', () => { 51 | const typeDefs = ` 52 | enum Color { 53 | RED 54 | GREEN 55 | } 56 | 57 | extend enum Color { 58 | BLUE 59 | } 60 | `; 61 | 62 | const astDocument = parse(typeDefs); 63 | const extensionAst = extractExtensionDefinitions(astDocument); 64 | 65 | expect(extensionAst.definitions).to.have.length(1); 66 | expect(extensionAst.definitions[0].kind).to.equal('EnumTypeExtension'); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/test/testFragmentsAreNotDuplicated.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { ExecutionResult, graphql } from 'graphql'; 3 | 4 | import { addMocksToSchema, makeExecutableSchema, transformSchema } from '..'; 5 | 6 | describe('Merging schemas', () => { 7 | it('should not throw `There can be only one fragment named "FieldName"` errors', async () => { 8 | const originalSchema = makeExecutableSchema({ 9 | typeDefs: rawSchema, 10 | }); 11 | 12 | addMocksToSchema({ schema: originalSchema }); 13 | 14 | const originalResult = await graphql( 15 | originalSchema, 16 | query, 17 | undefined, 18 | undefined, 19 | variables, 20 | ); 21 | assertNoDuplicateFragmentErrors(originalResult); 22 | 23 | const transformedSchema = transformSchema(originalSchema, []); 24 | 25 | const transformedResult = await graphql( 26 | transformedSchema, 27 | query, 28 | undefined, 29 | undefined, 30 | variables, 31 | ); 32 | assertNoDuplicateFragmentErrors(transformedResult); 33 | }); 34 | }); 35 | 36 | const rawSchema = ` 37 | type Post { 38 | id: ID! 39 | title: String! 40 | owner: User! 41 | } 42 | 43 | type User { 44 | id: ID! 45 | email: String 46 | } 47 | 48 | type Query { 49 | post(id: ID!): Post 50 | } 51 | `; 52 | 53 | const query = ` 54 | query getPostById($id: ID!) { 55 | post(id: $id) { 56 | ...Post 57 | owner { 58 | ...PostOwner 59 | email 60 | } 61 | } 62 | } 63 | 64 | fragment Post on Post { 65 | id 66 | title 67 | owner { 68 | ...PostOwner 69 | } 70 | } 71 | 72 | fragment PostOwner on User { 73 | id 74 | } 75 | `; 76 | 77 | const variables = { 78 | id: 123, 79 | }; 80 | 81 | function assertNoDuplicateFragmentErrors(result: ExecutionResult) { 82 | // Run assertion against each array element for better test failure output. 83 | if (result.errors != null) { 84 | result.errors.forEach((error) => expect(error.message).to.equal('')); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/test/testGatsbyTransforms.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { 3 | GraphQLObjectType, 4 | GraphQLSchema, 5 | GraphQLFieldResolver, 6 | GraphQLNonNull, 7 | graphql, 8 | } from 'graphql'; 9 | 10 | import { VisitSchemaKind } from '../Interfaces'; 11 | import { transformSchema, RenameTypes } from '../wrap/index'; 12 | import { cloneType, healSchema, visitSchema } from '../utils/index'; 13 | import { makeExecutableSchema } from '../generate/index'; 14 | import { addMocksToSchema } from '../mock/index'; 15 | 16 | // see https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-source-graphql/src/transforms.js 17 | // and https://github.com/gatsbyjs/gatsby/issues/22128 18 | 19 | class NamespaceUnderFieldTransform { 20 | private readonly typeName: string; 21 | private readonly fieldName: string; 22 | private readonly resolver: GraphQLFieldResolver; 23 | 24 | constructor({ 25 | typeName, 26 | fieldName, 27 | resolver, 28 | }: { 29 | typeName: string; 30 | fieldName: string; 31 | resolver: GraphQLFieldResolver; 32 | }) { 33 | this.typeName = typeName; 34 | this.fieldName = fieldName; 35 | this.resolver = resolver; 36 | } 37 | 38 | transformSchema(schema: GraphQLSchema) { 39 | const query = schema.getQueryType(); 40 | 41 | const nestedType = cloneType(query); 42 | nestedType.name = this.typeName; 43 | 44 | const typeMap = schema.getTypeMap(); 45 | typeMap[this.typeName] = nestedType; 46 | 47 | const newQuery = new GraphQLObjectType({ 48 | name: query.name, 49 | fields: { 50 | [this.fieldName]: { 51 | type: new GraphQLNonNull(nestedType), 52 | resolve: (parent, args, context, info) => { 53 | if (this.resolver != null) { 54 | return this.resolver(parent, args, context, info); 55 | } 56 | 57 | return {}; 58 | }, 59 | }, 60 | }, 61 | }); 62 | typeMap[query.name] = newQuery; 63 | 64 | return healSchema(schema); 65 | } 66 | } 67 | 68 | class StripNonQueryTransform { 69 | transformSchema(schema: GraphQLSchema) { 70 | return visitSchema(schema, { 71 | [VisitSchemaKind.MUTATION]() { 72 | return null; 73 | }, 74 | [VisitSchemaKind.SUBSCRIPTION]() { 75 | return null; 76 | }, 77 | }); 78 | } 79 | } 80 | 81 | describe('Gatsby transforms', () => { 82 | it('work', async () => { 83 | const schema = makeExecutableSchema({ 84 | typeDefs: ` 85 | directive @cacheControl(maxAge: Int, scope: CacheControlScope) on FIELD_DEFINITION | OBJECT | INTERFACE 86 | 87 | enum CacheControlScope { 88 | PUBLIC 89 | PRIVATE 90 | } 91 | 92 | type Continent { 93 | code: String 94 | name: String 95 | countries: [Country] 96 | } 97 | 98 | type Country { 99 | code: String 100 | name: String 101 | native: String 102 | phone: String 103 | continent: Continent 104 | currency: String 105 | languages: [Language] 106 | emoji: String 107 | emojiU: String 108 | states: [State] 109 | } 110 | 111 | type Language { 112 | code: String 113 | name: String 114 | native: String 115 | rtl: Int 116 | } 117 | 118 | type Query { 119 | continents: [Continent] 120 | continent(code: String): Continent 121 | countries: [Country] 122 | country(code: String): Country 123 | languages: [Language] 124 | language(code: String): Language 125 | } 126 | 127 | type State { 128 | code: String 129 | name: String 130 | country: Country 131 | } 132 | 133 | scalar Upload 134 | `, 135 | }); 136 | 137 | addMocksToSchema({ schema }); 138 | 139 | const transformedSchema = transformSchema(schema, [ 140 | new StripNonQueryTransform(), 141 | new RenameTypes((name) => `CountriesQuery_${name}`), 142 | new NamespaceUnderFieldTransform({ 143 | typeName: 'CountriesQuery', 144 | fieldName: 'countries', 145 | resolver: () => ({}), 146 | }), 147 | ]); 148 | 149 | expect(transformedSchema).to.be.instanceOf(GraphQLSchema); 150 | 151 | const result = await graphql( 152 | transformedSchema, 153 | ` 154 | { 155 | countries { 156 | language(code: "en") { 157 | name 158 | } 159 | } 160 | } 161 | `, 162 | ); 163 | expect(result).to.deep.equal({ 164 | data: { 165 | countries: { 166 | language: { 167 | name: 'Hello World', 168 | }, 169 | }, 170 | }, 171 | }); 172 | }); 173 | }); 174 | -------------------------------------------------------------------------------- /src/test/testMakeRemoteExecutableSchema.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { forAwaitEach } from 'iterall'; 3 | import { 4 | GraphQLSchema, 5 | ExecutionResult, 6 | subscribe, 7 | parse, 8 | graphql, 9 | } from 'graphql'; 10 | 11 | import { makeRemoteExecutableSchema } from '../wrap/index'; 12 | import { 13 | propertySchema, 14 | subscriptionSchema, 15 | subscriptionPubSubTrigger, 16 | subscriptionPubSub, 17 | makeSchemaRemoteFromLink, 18 | } from '../test/testingSchemas'; 19 | 20 | describe('remote queries', () => { 21 | let schema: GraphQLSchema; 22 | before(async () => { 23 | const remoteSubschemaConfig = await makeSchemaRemoteFromLink( 24 | propertySchema, 25 | ); 26 | schema = makeRemoteExecutableSchema({ 27 | schema: remoteSubschemaConfig.schema, 28 | link: remoteSubschemaConfig.link, 29 | }); 30 | }); 31 | 32 | it('should work', async () => { 33 | const query = ` 34 | { 35 | interfaceTest(kind: ONE) { 36 | kind 37 | testString 38 | ...on TestImpl1 { 39 | foo 40 | } 41 | ...on TestImpl2 { 42 | bar 43 | } 44 | } 45 | } 46 | `; 47 | 48 | const expected = { 49 | data: { 50 | interfaceTest: { 51 | foo: 'foo', 52 | kind: 'ONE', 53 | testString: 'test', 54 | }, 55 | }, 56 | }; 57 | 58 | const result = await graphql(schema, query); 59 | expect(result).to.deep.equal(expected); 60 | }); 61 | }); 62 | 63 | describe('remote subscriptions', () => { 64 | let schema: GraphQLSchema; 65 | before(async () => { 66 | const remoteSubschemaConfig = await makeSchemaRemoteFromLink( 67 | subscriptionSchema, 68 | ); 69 | schema = makeRemoteExecutableSchema({ 70 | schema: remoteSubschemaConfig.schema, 71 | link: remoteSubschemaConfig.link, 72 | }); 73 | }); 74 | 75 | it('should work', (done) => { 76 | const mockNotification = { 77 | notifications: { 78 | text: 'Hello world', 79 | }, 80 | }; 81 | 82 | const subscription = parse(` 83 | subscription Subscription { 84 | notifications { 85 | text 86 | } 87 | } 88 | `); 89 | 90 | let notificationCnt = 0; 91 | subscribe(schema, subscription) 92 | .then((results) => { 93 | forAwaitEach( 94 | results as AsyncIterable, 95 | (result: ExecutionResult) => { 96 | expect(result).to.have.property('data'); 97 | expect(result.data).to.deep.equal(mockNotification); 98 | if (!notificationCnt++) { 99 | done(); 100 | } 101 | }, 102 | ).catch(done); 103 | }) 104 | .then(() => 105 | subscriptionPubSub.publish(subscriptionPubSubTrigger, mockNotification), 106 | ) 107 | .catch(done); 108 | }); 109 | 110 | it('should work without triggering multiple times per notification', (done) => { 111 | const mockNotification = { 112 | notifications: { 113 | text: 'Hello world', 114 | }, 115 | }; 116 | 117 | const subscription = parse(` 118 | subscription Subscription { 119 | notifications { 120 | text 121 | } 122 | } 123 | `); 124 | 125 | let notificationCnt = 0; 126 | const sub1 = subscribe(schema, subscription).then((results) => { 127 | forAwaitEach( 128 | results as AsyncIterable, 129 | (result: ExecutionResult) => { 130 | expect(result).to.have.property('data'); 131 | expect(result.data).to.deep.equal(mockNotification); 132 | notificationCnt++; 133 | }, 134 | ).catch(done); 135 | }); 136 | 137 | const sub2 = subscribe(schema, subscription).then((results) => { 138 | forAwaitEach( 139 | results as AsyncIterable, 140 | (result: ExecutionResult) => { 141 | expect(result).to.have.property('data'); 142 | expect(result.data).to.deep.equal(mockNotification); 143 | }, 144 | ).catch(done); 145 | }); 146 | 147 | Promise.all([sub1, sub2]) 148 | .then(() => { 149 | subscriptionPubSub 150 | .publish(subscriptionPubSubTrigger, mockNotification) 151 | .catch(done); 152 | subscriptionPubSub 153 | .publish(subscriptionPubSubTrigger, mockNotification) 154 | .catch(done); 155 | 156 | setTimeout(() => { 157 | expect(notificationCnt).to.eq(2); 158 | done(); 159 | }, 0); 160 | }) 161 | .catch(done); 162 | }); 163 | }); 164 | 165 | describe('respects buildSchema options', () => { 166 | const schema = ` 167 | type Query { 168 | # Field description 169 | custom: CustomScalar! 170 | } 171 | 172 | # Scalar description 173 | scalar CustomScalar 174 | `; 175 | 176 | it('without comment descriptions', () => { 177 | const remoteSchema = makeRemoteExecutableSchema({ schema }); 178 | 179 | const customScalar = remoteSchema.getType('CustomScalar'); 180 | expect(customScalar.description).to.eq(undefined); 181 | }); 182 | 183 | it('with comment descriptions', () => { 184 | const remoteSchema = makeRemoteExecutableSchema({ 185 | schema, 186 | buildSchemaOptions: { commentDescriptions: true }, 187 | }); 188 | 189 | const field = remoteSchema.getQueryType().getFields()['custom']; 190 | expect(field.description).to.eq('Field description'); 191 | const customScalar = remoteSchema.getType('CustomScalar'); 192 | expect(customScalar.description).to.eq('Scalar description'); 193 | }); 194 | }); 195 | -------------------------------------------------------------------------------- /src/test/testMapSchema.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { GraphQLObjectType, GraphQLSchema, graphqlSync } from 'graphql'; 3 | 4 | import { makeExecutableSchema, mapSchema } from '../index'; 5 | import { MapperKind } from '../Interfaces'; 6 | import { toConfig } from '../polyfills/index'; 7 | 8 | describe('mapSchema', () => { 9 | it('does not throw', () => { 10 | const schema = makeExecutableSchema({ 11 | typeDefs: ` 12 | type Query { 13 | version: String 14 | } 15 | `, 16 | }); 17 | 18 | const newSchema = mapSchema(schema, {}); 19 | expect(newSchema).to.be.instanceOf(GraphQLSchema); 20 | }); 21 | 22 | it('can add a resolver', () => { 23 | const schema = makeExecutableSchema({ 24 | typeDefs: ` 25 | type Query { 26 | version: Int 27 | } 28 | `, 29 | }); 30 | 31 | const newSchema = mapSchema(schema, { 32 | [MapperKind.QUERY]: (type) => { 33 | const queryConfig = toConfig(type); 34 | queryConfig.fields.version.resolve = () => 1; 35 | return new GraphQLObjectType(queryConfig); 36 | }, 37 | }); 38 | 39 | expect(newSchema).to.be.instanceOf(GraphQLSchema); 40 | 41 | const result = graphqlSync(newSchema, '{ version }'); 42 | expect(result.data.version).to.equal(1); 43 | }); 44 | 45 | it('can change the root query name', () => { 46 | const schema = makeExecutableSchema({ 47 | typeDefs: ` 48 | type Query { 49 | version: Int 50 | } 51 | `, 52 | }); 53 | 54 | const newSchema = mapSchema(schema, { 55 | [MapperKind.QUERY]: (type) => { 56 | const queryConfig = toConfig(type); 57 | queryConfig.name = 'RootQuery'; 58 | return new GraphQLObjectType(queryConfig); 59 | }, 60 | }); 61 | 62 | expect(newSchema).to.be.instanceOf(GraphQLSchema); 63 | expect(newSchema.getQueryType().name).to.equal('RootQuery'); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /src/test/testResolution.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import { parse, graphql, subscribe, ExecutionResult } from 'graphql'; 3 | import { PubSub } from 'graphql-subscriptions'; 4 | import { forAwaitEach } from 'iterall'; 5 | 6 | import { makeExecutableSchema, addSchemaLevelResolver } from '..'; 7 | 8 | describe('Resolve', () => { 9 | describe('addSchemaLevelResolver', () => { 10 | const pubsub = new PubSub(); 11 | const typeDefs = ` 12 | type RootQuery { 13 | printRoot: String! 14 | printRootAgain: String! 15 | } 16 | 17 | type RootMutation { 18 | printRoot: String! 19 | } 20 | 21 | type RootSubscription { 22 | printRoot: String! 23 | } 24 | 25 | schema { 26 | query: RootQuery 27 | mutation: RootMutation 28 | subscription: RootSubscription 29 | } 30 | `; 31 | const printRoot = (root: any) => root.toString(); 32 | const resolvers = { 33 | RootQuery: { 34 | printRoot, 35 | printRootAgain: printRoot, 36 | }, 37 | RootMutation: { 38 | printRoot, 39 | }, 40 | RootSubscription: { 41 | printRoot: { 42 | subscribe: () => pubsub.asyncIterator('printRootChannel'), 43 | }, 44 | }, 45 | }; 46 | const schema = makeExecutableSchema({ typeDefs, resolvers }); 47 | let schemaLevelResolverCalls = 0; 48 | addSchemaLevelResolver(schema, (root) => { 49 | schemaLevelResolverCalls += 1; 50 | return root; 51 | }); 52 | 53 | it('should run the schema level resolver once in a same query', () => { 54 | schemaLevelResolverCalls = 0; 55 | const root = 'queryRoot'; 56 | return graphql( 57 | schema, 58 | ` 59 | query TestOnce { 60 | printRoot 61 | printRootAgain 62 | } 63 | `, 64 | root, 65 | ).then(({ data }) => { 66 | assert.deepEqual(data, { 67 | printRoot: root, 68 | printRootAgain: root, 69 | }); 70 | assert.equal(schemaLevelResolverCalls, 1); 71 | }); 72 | }); 73 | 74 | it('should isolate roots from the different operation types', (done) => { 75 | schemaLevelResolverCalls = 0; 76 | const queryRoot = 'queryRoot'; 77 | const mutationRoot = 'mutationRoot'; 78 | const subscriptionRoot = 'subscriptionRoot'; 79 | const subscriptionRoot2 = 'subscriptionRoot2'; 80 | 81 | let subsCbkCalls = 0; 82 | const firstSubsTriggered = new Promise((resolveFirst) => { 83 | subscribe( 84 | schema, 85 | parse(` 86 | subscription TestSubscription { 87 | printRoot 88 | } 89 | `), 90 | ) 91 | .then((results) => { 92 | forAwaitEach( 93 | results as AsyncIterable, 94 | (result: ExecutionResult) => { 95 | if (result.errors != null) { 96 | return done( 97 | new Error( 98 | `Unexpected errors in GraphQL result: ${JSON.stringify( 99 | result.errors, 100 | )}`, 101 | ), 102 | ); 103 | } 104 | 105 | const subsData = result.data; 106 | subsCbkCalls++; 107 | try { 108 | if (subsCbkCalls === 1) { 109 | assert.equal(schemaLevelResolverCalls, 1); 110 | assert.deepEqual(subsData, { printRoot: subscriptionRoot }); 111 | return resolveFirst(); 112 | } else if (subsCbkCalls === 2) { 113 | assert.equal(schemaLevelResolverCalls, 4); 114 | assert.deepEqual(subsData, { 115 | printRoot: subscriptionRoot2, 116 | }); 117 | return done(); 118 | } 119 | } catch (e) { 120 | return done(e); 121 | } 122 | done(new Error('Too many subscription fired')); 123 | }, 124 | ).catch(done); 125 | }) 126 | .then(() => 127 | pubsub.publish('printRootChannel', { printRoot: subscriptionRoot }), 128 | ) 129 | .catch(done); 130 | }); 131 | 132 | firstSubsTriggered 133 | .then(() => 134 | graphql( 135 | schema, 136 | ` 137 | query TestQuery { 138 | printRoot 139 | } 140 | `, 141 | queryRoot, 142 | ), 143 | ) 144 | .then(({ data }) => { 145 | assert.equal(schemaLevelResolverCalls, 2); 146 | assert.deepEqual(data, { printRoot: queryRoot }); 147 | return graphql( 148 | schema, 149 | ` 150 | mutation TestMutation { 151 | printRoot 152 | } 153 | `, 154 | mutationRoot, 155 | ); 156 | }) 157 | .then(({ data: mutationData }) => { 158 | assert.equal(schemaLevelResolverCalls, 3); 159 | assert.deepEqual(mutationData, { printRoot: mutationRoot }); 160 | return pubsub.publish('printRootChannel', { 161 | printRoot: subscriptionRoot2, 162 | }); 163 | }) 164 | .catch(done); 165 | }); 166 | }); 167 | }); 168 | -------------------------------------------------------------------------------- /src/test/testStitchingFromSubschemas.ts: -------------------------------------------------------------------------------- 1 | // The below is meant to be an alternative canonical schema stitching example 2 | // which intermingles local (mocked) resolvers and stitched schemas and does 3 | // not require use of the fragment field, because it follows best practices of 4 | // always returning the necessary object fields: 5 | // https://medium.com/paypal-engineering/graphql-resolvers-best-practices-cd36fdbcef55 6 | 7 | // This is achieved at the considerable cost of moving all of the delegation 8 | // logic from the gateway to each subschema so that each subschema imports all 9 | // the required types and performs all delegation. 10 | 11 | // The fragment field is still necessary when working with a remote schema 12 | // where this is not possible. 13 | 14 | import { expect } from 'chai'; 15 | import { graphql } from 'graphql'; 16 | 17 | import { delegateToSchema, mergeSchemas, addMocksToSchema } from '../index'; 18 | 19 | const chirpTypeDefs = ` 20 | type Chirp { 21 | id: ID! 22 | text: String 23 | authorId: ID! 24 | author: User 25 | } 26 | `; 27 | 28 | const authorTypeDefs = ` 29 | type User { 30 | id: ID! 31 | email: String 32 | chirps: [Chirp] 33 | } 34 | `; 35 | 36 | const schemas = {}; 37 | const getSchema = (name: string) => schemas[name]; 38 | 39 | const chirpSchema = mergeSchemas({ 40 | schemas: [ 41 | chirpTypeDefs, 42 | authorTypeDefs, 43 | ` 44 | type Query { 45 | chirpById(id: ID!): Chirp 46 | chirpsByAuthorId(authorId: ID!): [Chirp] 47 | } 48 | `, 49 | ], 50 | resolvers: { 51 | Chirp: { 52 | author: (chirp, _args, context, info) => 53 | delegateToSchema({ 54 | schema: getSchema('authorSchema'), 55 | operation: 'query', 56 | fieldName: 'userById', 57 | args: { 58 | id: chirp.authorId, 59 | }, 60 | context, 61 | info, 62 | }), 63 | }, 64 | }, 65 | }); 66 | 67 | addMocksToSchema({ 68 | schema: chirpSchema, 69 | mocks: { 70 | Chirp: () => ({ 71 | authorId: '1', 72 | }), 73 | }, 74 | preserveResolvers: true, 75 | }); 76 | 77 | const authorSchema = mergeSchemas({ 78 | schemas: [ 79 | chirpTypeDefs, 80 | authorTypeDefs, 81 | ` 82 | type Query { 83 | userById(id: ID!): User 84 | } 85 | `, 86 | ], 87 | resolvers: { 88 | User: { 89 | chirps: (user, _args, context, info) => 90 | delegateToSchema({ 91 | schema: getSchema('chirpSchema'), 92 | operation: 'query', 93 | fieldName: 'chirpsByAuthorId', 94 | args: { 95 | authorId: user.id, 96 | }, 97 | context, 98 | info, 99 | }), 100 | }, 101 | }, 102 | }); 103 | 104 | addMocksToSchema({ 105 | schema: authorSchema, 106 | mocks: { 107 | User: () => ({ 108 | id: '1', 109 | }), 110 | }, 111 | preserveResolvers: true, 112 | }); 113 | 114 | schemas['chirpSchema'] = chirpSchema; 115 | schemas['authorSchema'] = authorSchema; 116 | 117 | const mergedSchema = mergeSchemas({ 118 | schemas: Object.keys(schemas).map((schemaName) => schemas[schemaName]), 119 | }); 120 | 121 | describe('merging without specifying fragments', () => { 122 | it('works', async () => { 123 | const query = ` 124 | query { 125 | userById(id: 5) { 126 | chirps { 127 | id 128 | textAlias: text 129 | author { 130 | email 131 | } 132 | } 133 | } 134 | } 135 | `; 136 | 137 | const result = await graphql(mergedSchema, query); 138 | 139 | expect(result.errors).to.equal(undefined); 140 | expect(result.data.userById.chirps[1].id).to.not.equal(null); 141 | expect(result.data.userById.chirps[1].text).to.not.equal(null); 142 | expect(result.data.userById.chirps[1].author.email).to.not.equal(null); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /src/test/testTypeMerging.ts: -------------------------------------------------------------------------------- 1 | // The below is meant to be an alternative canonical schema stitching example 2 | // which relies on type merging. 3 | 4 | import { expect } from 'chai'; 5 | import { graphql } from 'graphql'; 6 | 7 | import { mergeSchemas, addMocksToSchema, makeExecutableSchema } from '../index'; 8 | 9 | const chirpSchema = makeExecutableSchema({ 10 | typeDefs: ` 11 | type Chirp { 12 | id: ID! 13 | text: String 14 | author: User 15 | } 16 | 17 | type User { 18 | id: ID! 19 | chirps: [Chirp] 20 | } 21 | type Query { 22 | userById(id: ID!): User 23 | } 24 | `, 25 | }); 26 | 27 | addMocksToSchema({ schema: chirpSchema }); 28 | 29 | const authorSchema = makeExecutableSchema({ 30 | typeDefs: ` 31 | type User { 32 | id: ID! 33 | email: String 34 | } 35 | type Query { 36 | userById(id: ID!): User 37 | } 38 | `, 39 | }); 40 | 41 | addMocksToSchema({ schema: authorSchema }); 42 | 43 | const mergedSchema = mergeSchemas({ 44 | subschemas: [ 45 | { 46 | schema: chirpSchema, 47 | merge: { 48 | User: { 49 | fieldName: 'userById', 50 | args: (originalResult) => ({ id: originalResult.id }), 51 | selectionSet: '{ id }', 52 | }, 53 | }, 54 | }, 55 | { 56 | schema: authorSchema, 57 | merge: { 58 | User: { 59 | fieldName: 'userById', 60 | args: (originalResult) => ({ id: originalResult.id }), 61 | selectionSet: '{ id }', 62 | }, 63 | }, 64 | }, 65 | ], 66 | mergeTypes: true, 67 | }); 68 | 69 | describe('merging using type merging', () => { 70 | it('works', async () => { 71 | const query = ` 72 | query { 73 | userById(id: 5) { 74 | chirps { 75 | id 76 | textAlias: text 77 | author { 78 | email 79 | } 80 | } 81 | } 82 | } 83 | `; 84 | 85 | const result = await graphql(mergedSchema, query); 86 | 87 | expect(result.errors).to.equal(undefined); 88 | expect(result.data.userById.chirps[1].id).to.not.equal(null); 89 | expect(result.data.userById.chirps[1].text).to.not.equal(null); 90 | expect(result.data.userById.chirps[1].author.email).to.not.equal(null); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /src/test/testUpload.ts: -------------------------------------------------------------------------------- 1 | import { Server } from 'http'; 2 | import { AddressInfo } from 'net'; 3 | import { Readable } from 'stream'; 4 | 5 | import { expect } from 'chai'; 6 | import express, { Express } from 'express'; 7 | import graphqlHTTP from 'express-graphql'; 8 | import { GraphQLUpload, graphqlUploadExpress } from 'graphql-upload'; 9 | import FormData from 'form-data'; 10 | import fetch from 'node-fetch'; 11 | import { buildSchema } from 'graphql'; 12 | 13 | import { mergeSchemas } from '../stitch/index'; 14 | import { makeExecutableSchema } from '../generate/index'; 15 | import { createServerHttpLink } from '../links/index'; 16 | import { GraphQLUpload as ServerGraphQLUpload } from '../scalars/index'; 17 | import { SubschemaConfig } from '../Interfaces'; 18 | 19 | function streamToString(stream: Readable) { 20 | const chunks: Array = []; 21 | return new Promise((resolve, reject) => { 22 | stream.on('data', (chunk) => chunks.push(chunk)); 23 | stream.on('error', reject); 24 | stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))); 25 | }); 26 | } 27 | 28 | function startServer(e: Express): Promise { 29 | return new Promise((resolve, reject) => { 30 | e.listen(undefined, 'localhost', function (error) { 31 | if (error) { 32 | reject(error); 33 | } else { 34 | resolve(this); 35 | } 36 | }); 37 | }); 38 | } 39 | 40 | function testGraphqlMultipartRequest(query: string, port: number) { 41 | const body = new FormData(); 42 | 43 | body.append( 44 | 'operations', 45 | JSON.stringify({ 46 | query, 47 | variables: { 48 | file: null, 49 | }, 50 | }), 51 | ); 52 | body.append('map', '{ "1": ["variables.file"] }'); 53 | body.append('1', 'abc', { filename: __filename }); 54 | 55 | return fetch(`http://localhost:${port.toString()}`, { method: 'POST', body }); 56 | } 57 | 58 | describe('graphql upload', () => { 59 | it('should return a file after uploading one', async () => { 60 | const remoteSchema = makeExecutableSchema({ 61 | typeDefs: ` 62 | scalar Upload 63 | type Query { 64 | version: String 65 | } 66 | type Mutation { 67 | upload(file: Upload): String 68 | } 69 | `, 70 | resolvers: { 71 | Mutation: { 72 | upload: async (_root, { file }) => { 73 | const { createReadStream } = await file; 74 | const stream = createReadStream(); 75 | const s = await streamToString(stream); 76 | return s; 77 | }, 78 | }, 79 | Upload: GraphQLUpload, 80 | }, 81 | }); 82 | 83 | const remoteApp = express().use( 84 | graphqlUploadExpress(), 85 | graphqlHTTP({ schema: remoteSchema }), 86 | ); 87 | 88 | const remoteServer = await startServer(remoteApp); 89 | const remotePort = (remoteServer.address() as AddressInfo).port; 90 | 91 | const nonExecutableSchema = buildSchema(` 92 | scalar Upload 93 | type Query { 94 | version: String 95 | } 96 | type Mutation { 97 | upload(file: Upload): String 98 | } 99 | `); 100 | 101 | const subschema: SubschemaConfig = { 102 | schema: nonExecutableSchema, 103 | link: createServerHttpLink({ 104 | uri: `http://localhost:${remotePort.toString()}`, 105 | }), 106 | }; 107 | 108 | const gatewaySchema = mergeSchemas({ 109 | schemas: [subschema], 110 | resolvers: { 111 | Upload: ServerGraphQLUpload, 112 | }, 113 | }); 114 | 115 | const gatewayApp = express().use( 116 | graphqlUploadExpress(), 117 | graphqlHTTP({ schema: gatewaySchema }), 118 | ); 119 | 120 | const gatewayServer = await startServer(gatewayApp); 121 | const gatewayPort = (gatewayServer.address() as AddressInfo).port; 122 | const query = ` 123 | mutation upload($file: Upload!) { 124 | upload(file: $file) 125 | } 126 | `; 127 | const res = await testGraphqlMultipartRequest(query, gatewayPort); 128 | 129 | expect(await res.json()).to.deep.equal({ 130 | data: { 131 | upload: 'abc', 132 | }, 133 | }); 134 | 135 | remoteServer.close(); 136 | gatewayServer.close(); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /src/test/testUtils.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { GraphQLObjectType } from 'graphql'; 3 | 4 | import { healSchema } from '../utils/index'; 5 | import { toConfig } from '../polyfills/index'; 6 | import { makeExecutableSchema } from '../generate/index'; 7 | 8 | describe('heal', () => { 9 | it('should prune empty types', () => { 10 | const schema = makeExecutableSchema({ 11 | typeDefs: ` 12 | type WillBeEmptyObject { 13 | willBeRemoved: String 14 | } 15 | 16 | type Query { 17 | someQuery: WillBeEmptyObject 18 | } 19 | `, 20 | }); 21 | const originalTypeMap = schema.getTypeMap(); 22 | 23 | const config = toConfig(originalTypeMap['WillBeEmptyObject']); 24 | originalTypeMap['WillBeEmptyObject'] = new GraphQLObjectType({ 25 | ...config, 26 | fields: {}, 27 | }); 28 | 29 | healSchema(schema); 30 | 31 | const healedTypeMap = schema.getTypeMap(); 32 | expect(healedTypeMap).not.to.haveOwnProperty('WillBeEmptyObject'); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/utils/SchemaVisitor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLArgument, 3 | GraphQLEnumType, 4 | GraphQLEnumValue, 5 | GraphQLField, 6 | GraphQLInputField, 7 | GraphQLInputObjectType, 8 | GraphQLInterfaceType, 9 | GraphQLObjectType, 10 | GraphQLScalarType, 11 | GraphQLSchema, 12 | GraphQLUnionType, 13 | } from 'graphql'; 14 | 15 | // Abstract base class of any visitor implementation, defining the available 16 | // visitor methods along with their parameter types, and providing a static 17 | // helper function for determining whether a subclass implements a given 18 | // visitor method, as opposed to inheriting one of the stubs defined here. 19 | export abstract class SchemaVisitor { 20 | // All SchemaVisitor instances are created while visiting a specific 21 | // GraphQLSchema object, so this property holds a reference to that object, 22 | // in case a visitor method needs to refer to this.schema. 23 | public schema!: GraphQLSchema; 24 | 25 | // Determine if this SchemaVisitor (sub)class implements a particular 26 | // visitor method. 27 | public static implementsVisitorMethod(methodName: string) { 28 | if (!methodName.startsWith('visit')) { 29 | return false; 30 | } 31 | 32 | const method = this.prototype[methodName]; 33 | if (typeof method !== 'function') { 34 | return false; 35 | } 36 | 37 | if (this === SchemaVisitor) { 38 | // The SchemaVisitor class implements every visitor method. 39 | return true; 40 | } 41 | 42 | const stub = SchemaVisitor.prototype[methodName]; 43 | if (method === stub) { 44 | // If this.prototype[methodName] was just inherited from SchemaVisitor, 45 | // then this class does not really implement the method. 46 | return false; 47 | } 48 | 49 | return true; 50 | } 51 | 52 | // Concrete subclasses of SchemaVisitor should override one or more of these 53 | // visitor methods, in order to express their interest in handling certain 54 | // schema types/locations. Each method may return null to remove the given 55 | // type from the schema, a non-null value of the same type to update the 56 | // type in the schema, or nothing to leave the type as it was. 57 | 58 | // eslint-disable-next-line @typescript-eslint/no-empty-function 59 | public visitSchema(_schema: GraphQLSchema): void {} 60 | 61 | public visitScalar( 62 | _scalar: GraphQLScalarType, 63 | // eslint-disable-next-line @typescript-eslint/no-empty-function 64 | ): GraphQLScalarType | void | null {} 65 | 66 | public visitObject( 67 | _object: GraphQLObjectType, 68 | // eslint-disable-next-line @typescript-eslint/no-empty-function 69 | ): GraphQLObjectType | void | null {} 70 | 71 | public visitFieldDefinition( 72 | _field: GraphQLField, 73 | _details: { 74 | objectType: GraphQLObjectType | GraphQLInterfaceType; 75 | }, 76 | // eslint-disable-next-line @typescript-eslint/no-empty-function 77 | ): GraphQLField | void | null {} 78 | 79 | public visitArgumentDefinition( 80 | _argument: GraphQLArgument, 81 | _details: { 82 | field: GraphQLField; 83 | objectType: GraphQLObjectType | GraphQLInterfaceType; 84 | }, 85 | // eslint-disable-next-line @typescript-eslint/no-empty-function 86 | ): GraphQLArgument | void | null {} 87 | 88 | public visitInterface( 89 | _iface: GraphQLInterfaceType, 90 | // eslint-disable-next-line @typescript-eslint/no-empty-function 91 | ): GraphQLInterfaceType | void | null {} 92 | 93 | // eslint-disable-next-line @typescript-eslint/no-empty-function 94 | public visitUnion(_union: GraphQLUnionType): GraphQLUnionType | void | null {} 95 | 96 | // eslint-disable-next-line @typescript-eslint/no-empty-function 97 | public visitEnum(_type: GraphQLEnumType): GraphQLEnumType | void | null {} 98 | 99 | public visitEnumValue( 100 | _value: GraphQLEnumValue, 101 | _details: { 102 | enumType: GraphQLEnumType; 103 | }, 104 | // eslint-disable-next-line @typescript-eslint/no-empty-function 105 | ): GraphQLEnumValue | void | null {} 106 | 107 | public visitInputObject( 108 | _object: GraphQLInputObjectType, 109 | // eslint-disable-next-line @typescript-eslint/no-empty-function 110 | ): GraphQLInputObjectType | void | null {} 111 | 112 | public visitInputFieldDefinition( 113 | _field: GraphQLInputField, 114 | _details: { 115 | objectType: GraphQLInputObjectType; 116 | }, 117 | // eslint-disable-next-line @typescript-eslint/no-empty-function 118 | ): GraphQLInputField | void | null {} 119 | } 120 | -------------------------------------------------------------------------------- /src/utils/astFromType.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isNonNullType, 3 | Kind, 4 | GraphQLType, 5 | TypeNode, 6 | isListType, 7 | } from 'graphql'; 8 | 9 | export function astFromType(type: GraphQLType): TypeNode { 10 | if (isNonNullType(type)) { 11 | const innerType = astFromType(type.ofType); 12 | if (innerType.kind === Kind.NON_NULL_TYPE) { 13 | throw new Error( 14 | `Invalid type node ${JSON.stringify( 15 | type, 16 | )}. Inner type of non-null type cannot be a non-null type.`, 17 | ); 18 | } 19 | return { 20 | kind: Kind.NON_NULL_TYPE, 21 | type: innerType, 22 | }; 23 | } else if (isListType(type)) { 24 | return { 25 | kind: Kind.LIST_TYPE, 26 | type: astFromType(type.ofType), 27 | }; 28 | } 29 | 30 | return { 31 | kind: Kind.NAMED_TYPE, 32 | name: { 33 | kind: Kind.NAME, 34 | value: type.name, 35 | }, 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/utils/clone.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLDirective, 3 | GraphQLEnumType, 4 | GraphQLInputObjectType, 5 | GraphQLInterfaceType, 6 | GraphQLObjectType, 7 | GraphQLObjectTypeConfig, 8 | GraphQLNamedType, 9 | GraphQLScalarType, 10 | GraphQLSchema, 11 | GraphQLUnionType, 12 | isObjectType, 13 | isInterfaceType, 14 | isUnionType, 15 | isInputObjectType, 16 | isEnumType, 17 | isScalarType, 18 | } from 'graphql'; 19 | 20 | import { isSpecifiedScalarType, toConfig } from '../polyfills/index'; 21 | 22 | import { graphqlVersion } from './graphqlVersion'; 23 | import { mapSchema } from './map'; 24 | 25 | /** 26 | * @category Schema Utility 27 | */ 28 | export function cloneDirective(directive: GraphQLDirective): GraphQLDirective { 29 | return new GraphQLDirective(toConfig(directive)); 30 | } 31 | 32 | /** 33 | * @category Type Utility 34 | */ 35 | export function cloneType(type: GraphQLNamedType): GraphQLNamedType { 36 | if (isObjectType(type)) { 37 | const config = toConfig(type); 38 | return new GraphQLObjectType({ 39 | ...config, 40 | interfaces: 41 | typeof config.interfaces === 'function' 42 | ? config.interfaces 43 | : config.interfaces.slice(), 44 | }); 45 | } else if (isInterfaceType(type)) { 46 | const config = toConfig(type); 47 | const newConfig = { 48 | ...config, 49 | interfaces: 50 | graphqlVersion() >= 15 51 | ? typeof ((config as unknown) as GraphQLObjectTypeConfig) 52 | .interfaces === 'function' 53 | ? ((config as unknown) as GraphQLObjectTypeConfig) 54 | .interfaces 55 | : ((config as unknown) as { 56 | interfaces: Array; 57 | }).interfaces.slice() 58 | : undefined, 59 | }; 60 | return new GraphQLInterfaceType(newConfig); 61 | } else if (isUnionType(type)) { 62 | const config = toConfig(type); 63 | return new GraphQLUnionType({ 64 | ...config, 65 | types: config.types.slice(), 66 | }); 67 | } else if (isInputObjectType(type)) { 68 | return new GraphQLInputObjectType(toConfig(type)); 69 | } else if (isEnumType(type)) { 70 | return new GraphQLEnumType(toConfig(type)); 71 | } else if (isScalarType(type)) { 72 | return isSpecifiedScalarType(type) 73 | ? type 74 | : new GraphQLScalarType(toConfig(type)); 75 | } 76 | 77 | throw new Error(`Invalid type ${type as string}`); 78 | } 79 | 80 | /** 81 | * @category Schema Utility 82 | */ 83 | export function cloneSchema(schema: GraphQLSchema): GraphQLSchema { 84 | return mapSchema(schema); 85 | } 86 | -------------------------------------------------------------------------------- /src/utils/each.ts: -------------------------------------------------------------------------------- 1 | import { IndexedObject } from '../Interfaces'; 2 | 3 | export default function each( 4 | arrayOrObject: IndexedObject, 5 | callback: (value: V, key: string) => void, 6 | ) { 7 | Object.keys(arrayOrObject).forEach((key) => { 8 | callback(arrayOrObject[key], key); 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/fieldNodes.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FieldNode, 3 | Kind, 4 | FragmentDefinitionNode, 5 | SelectionSetNode, 6 | } from 'graphql'; 7 | 8 | /** 9 | * @category Field Node Utility 10 | */ 11 | export function renameFieldNode(fieldNode: FieldNode, name: string): FieldNode { 12 | return { 13 | ...fieldNode, 14 | alias: { 15 | kind: Kind.NAME, 16 | value: 17 | fieldNode.alias != null ? fieldNode.alias.value : fieldNode.name.value, 18 | }, 19 | name: { 20 | kind: Kind.NAME, 21 | value: name, 22 | }, 23 | }; 24 | } 25 | 26 | /** 27 | * @category Field Node Utility 28 | */ 29 | export function preAliasFieldNode( 30 | fieldNode: FieldNode, 31 | str: string, 32 | ): FieldNode { 33 | return { 34 | ...fieldNode, 35 | alias: { 36 | kind: Kind.NAME, 37 | value: `${str}${ 38 | fieldNode.alias != null ? fieldNode.alias.value : fieldNode.name.value 39 | }`, 40 | }, 41 | }; 42 | } 43 | 44 | /** 45 | * @category Field Node Utility 46 | */ 47 | export function wrapFieldNode( 48 | fieldNode: FieldNode, 49 | path: Array, 50 | ): FieldNode { 51 | let newFieldNode = fieldNode; 52 | path.forEach((fieldName) => { 53 | newFieldNode = { 54 | kind: Kind.FIELD, 55 | name: { 56 | kind: Kind.NAME, 57 | value: fieldName, 58 | }, 59 | selectionSet: { 60 | kind: Kind.SELECTION_SET, 61 | selections: [fieldNode], 62 | }, 63 | }; 64 | }); 65 | 66 | return newFieldNode; 67 | } 68 | 69 | /** 70 | * @category Field Node Utility 71 | */ 72 | export function collectFields( 73 | selectionSet: SelectionSetNode | undefined, 74 | fragments: Record, 75 | fields: Array = [], 76 | visitedFragmentNames = {}, 77 | ): Array { 78 | if (selectionSet != null) { 79 | selectionSet.selections.forEach((selection) => { 80 | switch (selection.kind) { 81 | case Kind.FIELD: 82 | fields.push(selection); 83 | break; 84 | case Kind.INLINE_FRAGMENT: 85 | collectFields( 86 | selection.selectionSet, 87 | fragments, 88 | fields, 89 | visitedFragmentNames, 90 | ); 91 | break; 92 | case Kind.FRAGMENT_SPREAD: { 93 | const fragmentName = selection.name.value; 94 | if (!visitedFragmentNames[fragmentName]) { 95 | visitedFragmentNames[fragmentName] = true; 96 | collectFields( 97 | fragments[fragmentName].selectionSet, 98 | fragments, 99 | fields, 100 | visitedFragmentNames, 101 | ); 102 | } 103 | break; 104 | } 105 | default: 106 | // unreachable 107 | break; 108 | } 109 | }); 110 | } 111 | 112 | return fields; 113 | } 114 | 115 | /** 116 | * @category Field Node Utility 117 | */ 118 | export function hoistFieldNodes({ 119 | fieldNode, 120 | fieldNames, 121 | path = [], 122 | delimeter = '__gqltf__', 123 | fragments, 124 | }: { 125 | fieldNode: FieldNode; 126 | fieldNames?: Array; 127 | path?: Array; 128 | delimeter?: string; 129 | fragments: Record; 130 | }): Array { 131 | const alias = 132 | fieldNode.alias != null ? fieldNode.alias.value : fieldNode.name.value; 133 | 134 | let newFieldNodes: Array = []; 135 | 136 | if (path.length) { 137 | const remainingPathSegments = path.slice(); 138 | const initialPathSegment = remainingPathSegments.shift(); 139 | 140 | collectFields(fieldNode.selectionSet, fragments).forEach( 141 | (possibleFieldNode: FieldNode) => { 142 | if (possibleFieldNode.name.value === initialPathSegment) { 143 | newFieldNodes = newFieldNodes.concat( 144 | hoistFieldNodes({ 145 | fieldNode: preAliasFieldNode( 146 | possibleFieldNode, 147 | `${alias}${delimeter}`, 148 | ), 149 | fieldNames, 150 | path: remainingPathSegments, 151 | delimeter, 152 | fragments, 153 | }), 154 | ); 155 | } 156 | }, 157 | ); 158 | } else { 159 | collectFields(fieldNode.selectionSet, fragments).forEach( 160 | (possibleFieldNode: FieldNode) => { 161 | if (!fieldNames || fieldNames.includes(possibleFieldNode.name.value)) { 162 | newFieldNodes.push( 163 | preAliasFieldNode(possibleFieldNode, `${alias}${delimeter}`), 164 | ); 165 | } 166 | }, 167 | ); 168 | } 169 | 170 | return newFieldNodes; 171 | } 172 | -------------------------------------------------------------------------------- /src/utils/fields.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLFieldConfigMap, 3 | GraphQLObjectType, 4 | GraphQLFieldConfig, 5 | } from 'graphql'; 6 | import { TypeMap } from 'graphql/type/schema'; 7 | 8 | import { toConfig } from '../polyfills/index'; 9 | 10 | /** 11 | * @category Schema Utility 12 | */ 13 | export function appendFields( 14 | typeMap: TypeMap, 15 | typeName: string, 16 | fields: GraphQLFieldConfigMap, 17 | ): void { 18 | let type = typeMap[typeName]; 19 | if (type != null) { 20 | const typeConfig = toConfig(type); 21 | const originalFields = typeConfig.fields; 22 | const newFields = {}; 23 | Object.keys(originalFields).forEach((fieldName) => { 24 | newFields[fieldName] = originalFields[fieldName]; 25 | }); 26 | Object.keys(fields).forEach((fieldName) => { 27 | newFields[fieldName] = fields[fieldName]; 28 | }); 29 | type = new GraphQLObjectType({ 30 | ...typeConfig, 31 | fields: newFields, 32 | }); 33 | } else { 34 | type = new GraphQLObjectType({ 35 | name: typeName, 36 | fields, 37 | }); 38 | } 39 | typeMap[typeName] = type; 40 | } 41 | 42 | /** 43 | * @category Schema Utility 44 | */ 45 | export function removeFields( 46 | typeMap: TypeMap, 47 | typeName: string, 48 | testFn: (fieldName: string, field: GraphQLFieldConfig) => boolean, 49 | ): GraphQLFieldConfigMap { 50 | let type = typeMap[typeName]; 51 | const typeConfig = toConfig(type); 52 | const originalFields = typeConfig.fields; 53 | const newFields = {}; 54 | const removedFields = {}; 55 | Object.keys(originalFields).forEach((fieldName) => { 56 | if (testFn(fieldName, originalFields[fieldName])) { 57 | removedFields[fieldName] = originalFields[fieldName]; 58 | } else { 59 | newFields[fieldName] = originalFields[fieldName]; 60 | } 61 | }); 62 | type = new GraphQLObjectType({ 63 | ...typeConfig, 64 | fields: newFields, 65 | }); 66 | typeMap[typeName] = type; 67 | 68 | return removedFields; 69 | } 70 | -------------------------------------------------------------------------------- /src/utils/filterSchema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLEnumType, 3 | GraphQLInputObjectType, 4 | GraphQLInterfaceType, 5 | GraphQLObjectType, 6 | GraphQLScalarType, 7 | GraphQLUnionType, 8 | GraphQLType, 9 | } from 'graphql'; 10 | 11 | import { 12 | GraphQLSchemaWithTransforms, 13 | MapperKind, 14 | FieldFilter, 15 | RootFieldFilter, 16 | } from '../Interfaces'; 17 | import { toConfig } from '../polyfills/index'; 18 | 19 | import { mapSchema } from './map'; 20 | 21 | /** 22 | * @category Schema Utility 23 | */ 24 | export default function filterSchema({ 25 | schema, 26 | rootFieldFilter = () => true, 27 | typeFilter = () => true, 28 | fieldFilter = () => true, 29 | }: { 30 | schema: GraphQLSchemaWithTransforms; 31 | rootFieldFilter?: RootFieldFilter; 32 | typeFilter?: (typeName: string, type: GraphQLType) => boolean; 33 | fieldFilter?: (typeName: string, fieldName: string) => boolean; 34 | }): GraphQLSchemaWithTransforms { 35 | const filteredSchema: GraphQLSchemaWithTransforms = mapSchema(schema, { 36 | [MapperKind.QUERY]: (type: GraphQLObjectType) => 37 | filterRootFields(type, 'Query', rootFieldFilter), 38 | [MapperKind.MUTATION]: (type: GraphQLObjectType) => 39 | filterRootFields(type, 'Mutation', rootFieldFilter), 40 | [MapperKind.SUBSCRIPTION]: (type: GraphQLObjectType) => 41 | filterRootFields(type, 'Subscription', rootFieldFilter), 42 | [MapperKind.OBJECT_TYPE]: (type: GraphQLObjectType) => 43 | typeFilter(type.name, type) 44 | ? filterObjectFields(type, fieldFilter) 45 | : null, 46 | [MapperKind.INTERFACE_TYPE]: (type: GraphQLInterfaceType) => 47 | typeFilter(type.name, type) ? undefined : null, 48 | [MapperKind.UNION_TYPE]: (type: GraphQLUnionType) => 49 | typeFilter(type.name, type) ? undefined : null, 50 | [MapperKind.INPUT_OBJECT_TYPE]: (type: GraphQLInputObjectType) => 51 | typeFilter(type.name, type) ? undefined : null, 52 | [MapperKind.ENUM_TYPE]: (type: GraphQLEnumType) => 53 | typeFilter(type.name, type) ? undefined : null, 54 | [MapperKind.SCALAR_TYPE]: (type: GraphQLScalarType) => 55 | typeFilter(type.name, type) ? undefined : null, 56 | }); 57 | 58 | filteredSchema.transforms = schema.transforms; 59 | 60 | return filteredSchema; 61 | } 62 | 63 | function filterRootFields( 64 | type: GraphQLObjectType, 65 | operation: 'Query' | 'Mutation' | 'Subscription', 66 | rootFieldFilter: RootFieldFilter, 67 | ): GraphQLObjectType { 68 | const config = toConfig(type); 69 | Object.keys(config.fields).forEach((fieldName) => { 70 | if (!rootFieldFilter(operation, fieldName, config.fields[fieldName])) { 71 | delete config.fields[fieldName]; 72 | } 73 | }); 74 | return new GraphQLObjectType(config); 75 | } 76 | 77 | function filterObjectFields( 78 | type: GraphQLObjectType, 79 | fieldFilter: FieldFilter, 80 | ): GraphQLObjectType { 81 | const config = toConfig(type); 82 | Object.keys(config.fields).forEach((fieldName) => { 83 | if (!fieldFilter(type.name, fieldName, config.fields[fieldName])) { 84 | delete config.fields[fieldName]; 85 | } 86 | }); 87 | return new GraphQLObjectType(config); 88 | } 89 | -------------------------------------------------------------------------------- /src/utils/forEachDefaultValue.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getNamedType, 3 | GraphQLSchema, 4 | isObjectType, 5 | isInputObjectType, 6 | } from 'graphql'; 7 | 8 | import { IDefaultValueIteratorFn } from '../Interfaces'; 9 | 10 | /** 11 | * @category Schema Utility 12 | */ 13 | export function forEachDefaultValue( 14 | schema: GraphQLSchema, 15 | fn: IDefaultValueIteratorFn, 16 | ): void { 17 | const typeMap = schema.getTypeMap(); 18 | Object.keys(typeMap).forEach((typeName) => { 19 | const type = typeMap[typeName]; 20 | 21 | if (!getNamedType(type).name.startsWith('__')) { 22 | if (isObjectType(type)) { 23 | const fields = type.getFields(); 24 | Object.keys(fields).forEach((fieldName) => { 25 | const field = fields[fieldName]; 26 | 27 | field.args.forEach((arg) => { 28 | arg.defaultValue = fn(arg.type, arg.defaultValue); 29 | }); 30 | }); 31 | } else if (isInputObjectType(type)) { 32 | const fields = type.getFields(); 33 | Object.keys(fields).forEach((fieldName) => { 34 | const field = fields[fieldName]; 35 | field.defaultValue = fn(field.type, field.defaultValue); 36 | }); 37 | } 38 | } 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /src/utils/forEachField.ts: -------------------------------------------------------------------------------- 1 | import { getNamedType, GraphQLSchema, isObjectType } from 'graphql'; 2 | 3 | import { IFieldIteratorFn } from '../Interfaces'; 4 | 5 | /** 6 | * @category Schema Utility 7 | */ 8 | export function forEachField( 9 | schema: GraphQLSchema, 10 | fn: IFieldIteratorFn, 11 | ): void { 12 | const typeMap = schema.getTypeMap(); 13 | Object.keys(typeMap).forEach((typeName) => { 14 | const type = typeMap[typeName]; 15 | 16 | // TODO: maybe have an option to include these? 17 | if (!getNamedType(type).name.startsWith('__') && isObjectType(type)) { 18 | const fields = type.getFields(); 19 | Object.keys(fields).forEach((fieldName) => { 20 | const field = fields[fieldName]; 21 | fn(field, typeName, fieldName); 22 | }); 23 | } 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/fragments.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InlineFragmentNode, 3 | SelectionNode, 4 | Kind, 5 | parse, 6 | OperationDefinitionNode, 7 | } from 'graphql'; 8 | 9 | /** 10 | * @category Fragment Utility 11 | */ 12 | export function concatInlineFragments( 13 | type: string, 14 | fragments: Array, 15 | ): InlineFragmentNode { 16 | const fragmentSelections: Array = fragments.reduce( 17 | (selections, fragment) => 18 | selections.concat(fragment.selectionSet.selections), 19 | [], 20 | ); 21 | 22 | const deduplicatedFragmentSelection: Array = deduplicateSelection( 23 | fragmentSelections, 24 | ); 25 | 26 | return { 27 | kind: Kind.INLINE_FRAGMENT, 28 | typeCondition: { 29 | kind: Kind.NAMED_TYPE, 30 | name: { 31 | kind: Kind.NAME, 32 | value: type, 33 | }, 34 | }, 35 | selectionSet: { 36 | kind: Kind.SELECTION_SET, 37 | selections: deduplicatedFragmentSelection, 38 | }, 39 | }; 40 | } 41 | 42 | const hasOwn = Object.prototype.hasOwnProperty; 43 | 44 | function deduplicateSelection( 45 | nodes: Array, 46 | ): Array { 47 | const selectionMap = nodes.reduce<{ [key: string]: SelectionNode }>( 48 | (map, node) => { 49 | switch (node.kind) { 50 | case 'Field': { 51 | if (node.alias != null) { 52 | if (hasOwn.call(map, node.alias.value)) { 53 | return map; 54 | } 55 | 56 | return { 57 | ...map, 58 | [node.alias.value]: node, 59 | }; 60 | } 61 | 62 | if (hasOwn.call(map, node.name.value)) { 63 | return map; 64 | } 65 | 66 | return { 67 | ...map, 68 | [node.name.value]: node, 69 | }; 70 | } 71 | case 'FragmentSpread': { 72 | if (hasOwn.call(map, node.name.value)) { 73 | return map; 74 | } 75 | 76 | return { 77 | ...map, 78 | [node.name.value]: node, 79 | }; 80 | } 81 | case 'InlineFragment': { 82 | if (map.__fragment != null) { 83 | const fragment = map.__fragment as InlineFragmentNode; 84 | 85 | return { 86 | ...map, 87 | __fragment: concatInlineFragments( 88 | fragment.typeCondition.name.value, 89 | [fragment, node], 90 | ), 91 | }; 92 | } 93 | 94 | return { 95 | ...map, 96 | __fragment: node, 97 | }; 98 | } 99 | default: { 100 | return map; 101 | } 102 | } 103 | }, 104 | {}, 105 | ); 106 | 107 | const selection = Object.keys(selectionMap).reduce( 108 | (selectionList, node) => selectionList.concat(selectionMap[node]), 109 | [], 110 | ); 111 | 112 | return selection; 113 | } 114 | 115 | /** 116 | * @category Fragment Utility 117 | */ 118 | export function parseFragmentToInlineFragment( 119 | definitions: string, 120 | ): InlineFragmentNode { 121 | if (definitions.trim().startsWith('fragment')) { 122 | const document = parse(definitions); 123 | for (const definition of document.definitions) { 124 | if (definition.kind === Kind.FRAGMENT_DEFINITION) { 125 | return { 126 | kind: Kind.INLINE_FRAGMENT, 127 | typeCondition: definition.typeCondition, 128 | selectionSet: definition.selectionSet, 129 | }; 130 | } 131 | } 132 | } 133 | 134 | const query = parse(`{${definitions}}`) 135 | .definitions[0] as OperationDefinitionNode; 136 | for (const selection of query.selectionSet.selections) { 137 | if (selection.kind === Kind.INLINE_FRAGMENT) { 138 | return selection; 139 | } 140 | } 141 | 142 | throw new Error('Could not parse fragment'); 143 | } 144 | -------------------------------------------------------------------------------- /src/utils/getResolversFromSchema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLSchema, 3 | isScalarType, 4 | isEnumType, 5 | isInterfaceType, 6 | isUnionType, 7 | isObjectType, 8 | } from 'graphql'; 9 | 10 | import { IResolvers } from '../Interfaces'; 11 | import { isSpecifiedScalarType } from '../polyfills/index'; 12 | 13 | import { cloneType } from './clone'; 14 | 15 | /** 16 | * @category Schema Utility 17 | */ 18 | export function getResolversFromSchema(schema: GraphQLSchema): IResolvers { 19 | const resolvers = Object.create({}); 20 | 21 | const typeMap = schema.getTypeMap(); 22 | 23 | Object.keys(typeMap).forEach((typeName) => { 24 | const type = typeMap[typeName]; 25 | 26 | if (isScalarType(type)) { 27 | if (!isSpecifiedScalarType(type)) { 28 | resolvers[typeName] = cloneType(type); 29 | } 30 | } else if (isEnumType(type)) { 31 | resolvers[typeName] = {}; 32 | 33 | const values = type.getValues(); 34 | values.forEach((value) => { 35 | resolvers[typeName][value.name] = value.value; 36 | }); 37 | } else if (isInterfaceType(type)) { 38 | if (type.resolveType != null) { 39 | resolvers[typeName] = { 40 | __resolveType: type.resolveType, 41 | }; 42 | } 43 | } else if (isUnionType(type)) { 44 | if (type.resolveType != null) { 45 | resolvers[typeName] = { 46 | __resolveType: type.resolveType, 47 | }; 48 | } 49 | } else if (isObjectType(type)) { 50 | resolvers[typeName] = {}; 51 | 52 | if (type.isTypeOf != null) { 53 | resolvers[typeName].__isTypeOf = type.isTypeOf; 54 | } 55 | 56 | const fields = type.getFields(); 57 | Object.keys(fields).forEach((fieldName) => { 58 | const field = fields[fieldName]; 59 | 60 | resolvers[typeName][fieldName] = { 61 | resolve: field.resolve, 62 | subscribe: field.subscribe, 63 | }; 64 | }); 65 | } 66 | }); 67 | 68 | return resolvers; 69 | } 70 | -------------------------------------------------------------------------------- /src/utils/graphqlVersion.ts: -------------------------------------------------------------------------------- 1 | import { 2 | versionInfo, 3 | getOperationRootType, 4 | lexicographicSortSchema, 5 | printError, 6 | } from 'graphql'; 7 | 8 | let version: number; 9 | 10 | if (versionInfo != null && versionInfo.major >= 15) { 11 | version = 15; 12 | } else if (getOperationRootType != null) { 13 | version = 14; 14 | } else if (lexicographicSortSchema != null) { 15 | version = 13; 16 | } else if (printError != null) { 17 | version = 12; 18 | } else { 19 | version = 11; 20 | } 21 | 22 | export function graphqlVersion() { 23 | return version; 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/implementsAbstractType.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLType, 3 | GraphQLSchema, 4 | doTypesOverlap, 5 | isCompositeType, 6 | } from 'graphql'; 7 | 8 | export default function implementsAbstractType( 9 | schema: GraphQLSchema, 10 | typeA: GraphQLType, 11 | typeB: GraphQLType, 12 | ) { 13 | if (typeA === typeB) { 14 | return true; 15 | } else if (isCompositeType(typeA) && isCompositeType(typeB)) { 16 | return doTypesOverlap(schema, typeA, typeB); 17 | } 18 | 19 | return false; 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { default as filterSchema } from './filterSchema'; 2 | export { cloneSchema, cloneDirective, cloneType } from './clone'; 3 | export { healSchema, healTypes } from './heal'; 4 | export { SchemaVisitor } from './SchemaVisitor'; 5 | export { SchemaDirectiveVisitor } from './SchemaDirectiveVisitor'; 6 | export { visitSchema } from './visitSchema'; 7 | export { getResolversFromSchema } from './getResolversFromSchema'; 8 | export { forEachField } from './forEachField'; 9 | export { forEachDefaultValue } from './forEachDefaultValue'; 10 | export { 11 | transformInputValue, 12 | parseInputValue, 13 | parseInputValueLiteral, 14 | serializeInputValue, 15 | } from './transformInputValue'; 16 | export { 17 | concatInlineFragments, 18 | parseFragmentToInlineFragment, 19 | } from './fragments'; 20 | export { parseSelectionSet, typeContainsSelectionSet } from './selectionSets'; 21 | export { mergeDeep } from './mergeDeep'; 22 | export { 23 | collectFields, 24 | wrapFieldNode, 25 | renameFieldNode, 26 | hoistFieldNodes, 27 | } from './fieldNodes'; 28 | export { appendFields, removeFields } from './fields'; 29 | export { createNamedStub } from './stub'; 30 | export { graphqlVersion } from './graphqlVersion'; 31 | export { mapSchema } from './map'; 32 | -------------------------------------------------------------------------------- /src/utils/isEmptyObject.ts: -------------------------------------------------------------------------------- 1 | export default function isEmptyObject(obj: Record): boolean { 2 | if (obj == null) { 3 | return true; 4 | } 5 | 6 | for (const key in obj) { 7 | if (Object.hasOwnProperty.call(obj, key)) { 8 | return false; 9 | } 10 | } 11 | return true; 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/mergeDeep.ts: -------------------------------------------------------------------------------- 1 | export function mergeDeep(target: any, ...sources: any): any { 2 | const output = { 3 | ...target, 4 | }; 5 | sources.forEach((source: any) => { 6 | if (isObject(target) && isObject(source)) { 7 | Object.keys(source).forEach((key) => { 8 | if (isObject(source[key])) { 9 | if (!(key in target)) { 10 | Object.assign(output, { [key]: source[key] }); 11 | } else { 12 | output[key] = mergeDeep(target[key], source[key]); 13 | } 14 | } else { 15 | Object.assign(output, { [key]: source[key] }); 16 | } 17 | }); 18 | } 19 | }); 20 | return output; 21 | } 22 | 23 | function isObject(item: any): boolean { 24 | return item && typeof item === 'object' && !Array.isArray(item); 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/selectionSets.ts: -------------------------------------------------------------------------------- 1 | import { 2 | OperationDefinitionNode, 3 | SelectionSetNode, 4 | parse, 5 | Kind, 6 | GraphQLObjectType, 7 | getNamedType, 8 | } from 'graphql'; 9 | 10 | /** 11 | * @category Selection Set Utility 12 | */ 13 | export function parseSelectionSet(selectionSet: string): SelectionSetNode { 14 | const query = parse(selectionSet).definitions[0] as OperationDefinitionNode; 15 | return query.selectionSet; 16 | } 17 | 18 | /** 19 | * @category Selection Set Utility 20 | */ 21 | export function typeContainsSelectionSet( 22 | type: GraphQLObjectType, 23 | selectionSet: SelectionSetNode, 24 | ): boolean { 25 | const fields = type.getFields(); 26 | 27 | for (const selection of selectionSet.selections) { 28 | if (selection.kind === Kind.FIELD) { 29 | const field = fields[selection.name.value]; 30 | 31 | if (field == null) { 32 | return false; 33 | } 34 | 35 | if (selection.selectionSet != null) { 36 | return typeContainsSelectionSet( 37 | getNamedType(field.type) as GraphQLObjectType, 38 | selection.selectionSet, 39 | ); 40 | } 41 | } else if (selection.kind === Kind.INLINE_FRAGMENT) { 42 | const containsSelectionSet = typeContainsSelectionSet( 43 | type, 44 | selection.selectionSet, 45 | ); 46 | if (!containsSelectionSet) { 47 | return false; 48 | } 49 | } 50 | } 51 | 52 | return true; 53 | } 54 | -------------------------------------------------------------------------------- /src/utils/stub.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLInterfaceType, 4 | GraphQLInputObjectType, 5 | GraphQLString, 6 | GraphQLNamedType, 7 | GraphQLInt, 8 | GraphQLFloat, 9 | GraphQLBoolean, 10 | GraphQLID, 11 | isObjectType, 12 | isInterfaceType, 13 | isInputObjectType, 14 | } from 'graphql'; 15 | 16 | /** 17 | * @category Type Utility 18 | */ 19 | export function createNamedStub( 20 | name: string, 21 | type: 'object' | 'interface' | 'input', 22 | ): GraphQLObjectType | GraphQLInputObjectType | GraphQLInterfaceType { 23 | let constructor: any; 24 | if (type === 'object') { 25 | constructor = GraphQLObjectType; 26 | } else if (type === 'interface') { 27 | constructor = GraphQLInterfaceType; 28 | } else { 29 | constructor = GraphQLInputObjectType; 30 | } 31 | 32 | return new constructor({ 33 | name, 34 | fields: { 35 | __fake: { 36 | type: GraphQLString, 37 | }, 38 | }, 39 | }); 40 | } 41 | 42 | export function isStub(type: GraphQLNamedType): boolean { 43 | if (isObjectType(type) || isInterfaceType(type) || isInputObjectType(type)) { 44 | const fields = type.getFields(); 45 | const fieldNames = Object.keys(fields); 46 | return fieldNames.length === 1 && fields[fieldNames[0]].name === '__fake'; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | export function getBuiltInForStub(type: GraphQLNamedType): GraphQLNamedType { 53 | switch (type.name) { 54 | case GraphQLInt.name: 55 | return GraphQLInt; 56 | case GraphQLFloat.name: 57 | return GraphQLFloat; 58 | case GraphQLString.name: 59 | return GraphQLString; 60 | case GraphQLBoolean.name: 61 | return GraphQLBoolean; 62 | case GraphQLID.name: 63 | return GraphQLID; 64 | default: 65 | return type; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/utils/transformInputValue.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLEnumType, 3 | GraphQLInputType, 4 | GraphQLScalarType, 5 | getNullableType, 6 | isLeafType, 7 | isListType, 8 | isInputObjectType, 9 | } from 'graphql'; 10 | 11 | type InputValueTransformer = ( 12 | type: GraphQLEnumType | GraphQLScalarType, 13 | originalValue: any, 14 | ) => any; 15 | 16 | /** 17 | * @category Input Value Utility 18 | */ 19 | export function transformInputValue( 20 | type: GraphQLInputType, 21 | value: any, 22 | transformer: InputValueTransformer, 23 | ) { 24 | if (value == null) { 25 | return value; 26 | } 27 | 28 | const nullableType = getNullableType(type); 29 | 30 | if (isLeafType(nullableType)) { 31 | return transformer(nullableType, value); 32 | } else if (isListType(nullableType)) { 33 | return value.map((listMember: any) => 34 | transformInputValue(nullableType.ofType, listMember, transformer), 35 | ); 36 | } else if (isInputObjectType(nullableType)) { 37 | const fields = nullableType.getFields(); 38 | const newValue = {}; 39 | Object.keys(value).forEach((key) => { 40 | newValue[key] = transformInputValue( 41 | fields[key].type, 42 | value[key], 43 | transformer, 44 | ); 45 | }); 46 | return newValue; 47 | } 48 | 49 | // unreachable, no other possible return value 50 | } 51 | 52 | /** 53 | * @category Input Value Utility 54 | */ 55 | export function serializeInputValue(type: GraphQLInputType, value: any) { 56 | return transformInputValue(type, value, (t, v) => t.serialize(v)); 57 | } 58 | 59 | /** 60 | * @category Input Value Utility 61 | */ 62 | export function parseInputValue(type: GraphQLInputType, value: any) { 63 | return transformInputValue(type, value, (t, v) => t.parseValue(v)); 64 | } 65 | 66 | /** 67 | * @category Input Value Utility 68 | */ 69 | export function parseInputValueLiteral(type: GraphQLInputType, value: any) { 70 | return transformInputValue(type, value, (t, v) => t.parseLiteral(v, {})); 71 | } 72 | -------------------------------------------------------------------------------- /src/utils/updateArgument.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLInputType, 3 | ArgumentNode, 4 | VariableDefinitionNode, 5 | Kind, 6 | } from 'graphql'; 7 | 8 | import { astFromType } from './astFromType'; 9 | 10 | export function updateArgument( 11 | argName: string, 12 | argType: GraphQLInputType, 13 | argumentNodes: Record, 14 | variableDefinitionsMap: Record, 15 | variableValues: Record, 16 | newArg: any, 17 | ): void { 18 | let varName; 19 | let numGeneratedVariables = 0; 20 | do { 21 | varName = `_v${(numGeneratedVariables++).toString()}_${argName}`; 22 | } while (variableDefinitionsMap[varName] != null); 23 | 24 | argumentNodes[argName] = { 25 | kind: Kind.ARGUMENT, 26 | name: { 27 | kind: Kind.NAME, 28 | value: argName, 29 | }, 30 | value: { 31 | kind: Kind.VARIABLE, 32 | name: { 33 | kind: Kind.NAME, 34 | value: varName, 35 | }, 36 | }, 37 | }; 38 | variableDefinitionsMap[varName] = { 39 | kind: Kind.VARIABLE_DEFINITION, 40 | variable: { 41 | kind: Kind.VARIABLE, 42 | name: { 43 | kind: Kind.NAME, 44 | value: varName, 45 | }, 46 | }, 47 | type: astFromType(argType), 48 | }; 49 | 50 | variableValues[varName] = newArg; 51 | } 52 | -------------------------------------------------------------------------------- /src/utils/updateEachKey.ts: -------------------------------------------------------------------------------- 1 | import { IndexedObject } from '../Interfaces'; 2 | 3 | /** 4 | * A more powerful version of each that has the ability to replace or remove 5 | * array or object keys. 6 | */ 7 | export default function updateEachKey( 8 | arrayOrObject: IndexedObject, 9 | // The callback can return nothing to leave the key untouched, null to remove 10 | // the key from the array or object, or a non-null V to replace the value. 11 | updater: (value: V, key: string) => void | null | V, 12 | ) { 13 | let deletedCount = 0; 14 | 15 | Object.keys(arrayOrObject).forEach((key) => { 16 | const result = updater(arrayOrObject[key], key); 17 | 18 | if (typeof result === 'undefined') { 19 | return; 20 | } 21 | 22 | if (result === null) { 23 | delete arrayOrObject[key]; 24 | deletedCount++; 25 | return; 26 | } 27 | 28 | arrayOrObject[key] = result; 29 | }); 30 | 31 | if (deletedCount > 0 && Array.isArray(arrayOrObject)) { 32 | // Remove any holes from the array due to deleted elements. 33 | arrayOrObject.splice(0).forEach((elem) => { 34 | arrayOrObject.push(elem); 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/utils/valueFromASTUntyped.ts: -------------------------------------------------------------------------------- 1 | import { ValueNode, Kind } from 'graphql'; 2 | 3 | // Similar to the graphql-js function of the same name, slightly simplified: 4 | // https://github.com/graphql/graphql-js/blob/master/src/utilities/valueFromASTUntyped.js 5 | export default function valueFromASTUntyped(valueNode: ValueNode): any { 6 | switch (valueNode.kind) { 7 | case Kind.NULL: 8 | return null; 9 | case Kind.INT: 10 | return parseInt(valueNode.value, 10); 11 | case Kind.FLOAT: 12 | return parseFloat(valueNode.value); 13 | case Kind.STRING: 14 | case Kind.ENUM: 15 | case Kind.BOOLEAN: 16 | return valueNode.value; 17 | case Kind.LIST: 18 | return valueNode.values.map(valueFromASTUntyped); 19 | case Kind.OBJECT: { 20 | const obj = Object.create(null); 21 | valueNode.fields.forEach((field) => { 22 | obj[field.name.value] = valueFromASTUntyped(field.value); 23 | }); 24 | return obj; 25 | } 26 | /* istanbul ignore next */ 27 | default: 28 | throw new Error('Unexpected value kind: ' + valueNode.kind); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/wrap/resolvers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLSchema, 3 | GraphQLFieldResolver, 4 | GraphQLObjectType, 5 | } from 'graphql'; 6 | 7 | import { 8 | Transform, 9 | IResolvers, 10 | Operation, 11 | SubschemaConfig, 12 | } from '../Interfaces'; 13 | import delegateToSchema from '../delegate/delegateToSchema'; 14 | import { handleResult } from '../delegate/checkResultAndHandleErrors'; 15 | 16 | import { makeMergedType } from '../stitch/makeMergedType'; 17 | import { getResponseKeyFromInfo } from '../stitch/getResponseKeyFromInfo'; 18 | import { getErrors, getSubschema } from '../stitch/proxiedResult'; 19 | 20 | export type Mapping = { 21 | [typeName: string]: { 22 | [fieldName: string]: { 23 | name: string; 24 | operation: Operation; 25 | }; 26 | }; 27 | }; 28 | 29 | export function generateProxyingResolvers({ 30 | subschemaConfig, 31 | transforms, 32 | createProxyingResolver = defaultCreateProxyingResolver, 33 | }: { 34 | subschemaConfig: SubschemaConfig; 35 | transforms?: Array; 36 | createProxyingResolver?: ({ 37 | schema, 38 | transforms, 39 | operation, 40 | fieldName, 41 | }: { 42 | schema?: GraphQLSchema | SubschemaConfig; 43 | transforms?: Array; 44 | operation?: Operation; 45 | fieldName?: string; 46 | }) => GraphQLFieldResolver; 47 | }): IResolvers { 48 | const targetSchema = subschemaConfig.schema; 49 | 50 | const mapping = generateSimpleMapping(targetSchema); 51 | 52 | const result = {}; 53 | Object.keys(mapping).forEach((name) => { 54 | result[name] = {}; 55 | const innerMapping = mapping[name]; 56 | Object.keys(innerMapping).forEach((from) => { 57 | const to = innerMapping[from]; 58 | const resolverType = 59 | to.operation === 'subscription' ? 'subscribe' : 'resolve'; 60 | result[name][from] = { 61 | [resolverType]: createProxyingResolver({ 62 | schema: subschemaConfig, 63 | transforms, 64 | operation: to.operation, 65 | fieldName: to.name, 66 | }), 67 | }; 68 | }); 69 | }); 70 | return result; 71 | } 72 | 73 | export function generateSimpleMapping(targetSchema: GraphQLSchema): Mapping { 74 | const query = targetSchema.getQueryType(); 75 | const mutation = targetSchema.getMutationType(); 76 | const subscription = targetSchema.getSubscriptionType(); 77 | 78 | const result: Mapping = {}; 79 | if (query != null) { 80 | result[query.name] = generateMappingFromObjectType(query, 'query'); 81 | } 82 | if (mutation != null) { 83 | result[mutation.name] = generateMappingFromObjectType(mutation, 'mutation'); 84 | } 85 | if (subscription != null) { 86 | result[subscription.name] = generateMappingFromObjectType( 87 | subscription, 88 | 'subscription', 89 | ); 90 | } 91 | 92 | return result; 93 | } 94 | 95 | export function generateMappingFromObjectType( 96 | type: GraphQLObjectType, 97 | operation: Operation, 98 | ): { 99 | [fieldName: string]: { 100 | name: string; 101 | operation: Operation; 102 | }; 103 | } { 104 | const result = {}; 105 | const fields = type.getFields(); 106 | Object.keys(fields).forEach((fieldName) => { 107 | result[fieldName] = { 108 | name: fieldName, 109 | operation, 110 | }; 111 | }); 112 | return result; 113 | } 114 | 115 | function defaultCreateProxyingResolver({ 116 | schema, 117 | transforms, 118 | }: { 119 | schema: SubschemaConfig; 120 | transforms: Array; 121 | }): GraphQLFieldResolver { 122 | return (parent, _args, context, info) => { 123 | if (parent != null) { 124 | const responseKey = getResponseKeyFromInfo(info); 125 | const errors = getErrors(parent, responseKey); 126 | 127 | if (errors != null) { 128 | const subschema = getSubschema(parent, responseKey); 129 | 130 | // if parent contains a proxied result from this subschema, can return that result 131 | if (schema === subschema) { 132 | const result = parent[responseKey]; 133 | return handleResult(result, errors, subschema, context, info); 134 | } 135 | } 136 | } 137 | 138 | return delegateToSchema({ 139 | schema, 140 | context, 141 | info, 142 | transforms, 143 | }); 144 | }; 145 | } 146 | 147 | export function stripResolvers(schema: GraphQLSchema): void { 148 | const typeMap = schema.getTypeMap(); 149 | Object.keys(typeMap).forEach((typeName) => { 150 | if (!typeName.startsWith('__')) { 151 | makeMergedType(typeMap[typeName]); 152 | } 153 | }); 154 | } 155 | -------------------------------------------------------------------------------- /src/wrap/transformSchema.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema } from 'graphql'; 2 | 3 | import { 4 | Transform, 5 | SubschemaConfig, 6 | GraphQLSchemaWithTransforms, 7 | } from '../Interfaces'; 8 | 9 | import { wrapSchema } from './wrapSchema'; 10 | 11 | // This function is deprecated in favor of wrapSchema as the name is misleading. 12 | // transformSchema does not just "transform" a schema, it wraps a schema with transforms 13 | // using a round of delegation. 14 | // The applySchemaTransforms function actually "transforms" the schema and is used during wrapping. 15 | export function transformSchema( 16 | subschemaOrSubschemaConfig: GraphQLSchema | SubschemaConfig, 17 | transforms: Array, 18 | ): GraphQLSchemaWithTransforms { 19 | const schema: GraphQLSchemaWithTransforms = wrapSchema( 20 | subschemaOrSubschemaConfig, 21 | transforms, 22 | ); 23 | 24 | schema.transforms = transforms.slice().reverse(); 25 | return schema; 26 | } 27 | -------------------------------------------------------------------------------- /src/wrap/transforms.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema } from 'graphql'; 2 | 3 | import { Request, Transform } from '../Interfaces'; 4 | import { cloneSchema } from '../utils/index'; 5 | 6 | export function applySchemaTransforms( 7 | originalSchema: GraphQLSchema, 8 | transforms: Array, 9 | ): GraphQLSchema { 10 | // Schemas are cloned prior to passing to each transform as 11 | // transforms cannot be trusted and may modify the passed in schema. 12 | return transforms.reduce( 13 | (schema: GraphQLSchema, transform: Transform) => 14 | transform.transformSchema != null 15 | ? transform.transformSchema(cloneSchema(schema)) 16 | : schema, 17 | originalSchema, 18 | ); 19 | } 20 | 21 | export function applyRequestTransforms( 22 | originalRequest: Request, 23 | transforms: Array, 24 | ): Request { 25 | return transforms.reduce( 26 | (request: Request, transform: Transform) => 27 | transform.transformRequest != null 28 | ? transform.transformRequest(request) 29 | : request, 30 | 31 | originalRequest, 32 | ); 33 | } 34 | 35 | export function applyResultTransforms( 36 | originalResult: any, 37 | transforms: Array, 38 | ): any { 39 | return transforms.reduceRight( 40 | (result: any, transform: Transform) => 41 | transform.transformResult != null 42 | ? transform.transformResult(result) 43 | : result, 44 | originalResult, 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/wrap/transforms/AddArgumentsAsVariables.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArgumentNode, 3 | DocumentNode, 4 | FragmentDefinitionNode, 5 | GraphQLArgument, 6 | GraphQLField, 7 | GraphQLObjectType, 8 | GraphQLSchema, 9 | Kind, 10 | OperationDefinitionNode, 11 | SelectionNode, 12 | VariableDefinitionNode, 13 | } from 'graphql'; 14 | 15 | import { Transform, Request } from '../../Interfaces'; 16 | import { serializeInputValue } from '../../utils/index'; 17 | import { updateArgument } from '../../utils/updateArgument'; 18 | 19 | export default class AddArgumentsAsVariables implements Transform { 20 | private readonly targetSchema: GraphQLSchema; 21 | private readonly args: { [key: string]: any }; 22 | 23 | constructor(targetSchema: GraphQLSchema, args: { [key: string]: any }) { 24 | this.targetSchema = targetSchema; 25 | this.args = args; 26 | } 27 | 28 | public transformRequest(originalRequest: Request): Request { 29 | const { document, newVariables } = addVariablesToRootField( 30 | this.targetSchema, 31 | originalRequest, 32 | this.args, 33 | ); 34 | 35 | return { 36 | document, 37 | variables: newVariables, 38 | }; 39 | } 40 | } 41 | 42 | function addVariablesToRootField( 43 | targetSchema: GraphQLSchema, 44 | originalRequest: Request, 45 | args: { [key: string]: any }, 46 | ): { 47 | document: DocumentNode; 48 | newVariables: { [key: string]: any }; 49 | } { 50 | const document = originalRequest.document; 51 | const variableValues = originalRequest.variables; 52 | 53 | const operations: Array = document.definitions.filter( 54 | (def) => def.kind === Kind.OPERATION_DEFINITION, 55 | ) as Array; 56 | const fragments: Array = document.definitions.filter( 57 | (def) => def.kind === Kind.FRAGMENT_DEFINITION, 58 | ) as Array; 59 | 60 | const newOperations = operations.map((operation: OperationDefinitionNode) => { 61 | const variableDefinitionMap = {}; 62 | operation.variableDefinitions.forEach((def) => { 63 | const varName = def.variable.name.value; 64 | variableDefinitionMap[varName] = def; 65 | }); 66 | 67 | let type: GraphQLObjectType | null | undefined; 68 | if (operation.operation === 'subscription') { 69 | type = targetSchema.getSubscriptionType(); 70 | } else if (operation.operation === 'mutation') { 71 | type = targetSchema.getMutationType(); 72 | } else { 73 | type = targetSchema.getQueryType(); 74 | } 75 | const newSelectionSet: Array = []; 76 | 77 | operation.selectionSet.selections.forEach((selection: SelectionNode) => { 78 | if (selection.kind === Kind.FIELD) { 79 | const argumentNodes = selection.arguments; 80 | const argumentNodeMap: Record = {}; 81 | argumentNodes.forEach((argument: ArgumentNode) => { 82 | argumentNodeMap[argument.name.value] = argument; 83 | }); 84 | 85 | const targetField = type.getFields()[selection.name.value]; 86 | 87 | // excludes __typename 88 | if (targetField != null) { 89 | updateArguments( 90 | targetField, 91 | argumentNodeMap, 92 | variableDefinitionMap, 93 | variableValues, 94 | args, 95 | ); 96 | } 97 | 98 | newSelectionSet.push({ 99 | ...selection, 100 | arguments: Object.keys(argumentNodeMap).map( 101 | (argName) => argumentNodeMap[argName], 102 | ), 103 | }); 104 | } else { 105 | newSelectionSet.push(selection); 106 | } 107 | }); 108 | 109 | return { 110 | ...operation, 111 | variableDefinitions: Object.keys(variableDefinitionMap).map( 112 | (varName) => variableDefinitionMap[varName], 113 | ), 114 | selectionSet: { 115 | kind: Kind.SELECTION_SET, 116 | selections: newSelectionSet, 117 | }, 118 | }; 119 | }); 120 | 121 | return { 122 | document: { 123 | ...document, 124 | definitions: [...newOperations, ...fragments], 125 | }, 126 | newVariables: variableValues, 127 | }; 128 | } 129 | 130 | const hasOwn = Object.prototype.hasOwnProperty; 131 | 132 | function updateArguments( 133 | targetField: GraphQLField, 134 | argumentNodeMap: Record, 135 | variableDefinitionMap: Record, 136 | variableValues: Record, 137 | newArgs: Record, 138 | ): void { 139 | targetField.args.forEach((argument: GraphQLArgument) => { 140 | const argName = argument.name; 141 | const argType = argument.type; 142 | 143 | if (hasOwn.call(newArgs, argName)) { 144 | updateArgument( 145 | argName, 146 | argType, 147 | argumentNodeMap, 148 | variableDefinitionMap, 149 | variableValues, 150 | serializeInputValue(argType, newArgs[argName]), 151 | ); 152 | } 153 | }); 154 | } 155 | -------------------------------------------------------------------------------- /src/wrap/transforms/AddMergedTypeSelectionSets.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DocumentNode, 3 | GraphQLSchema, 4 | GraphQLType, 5 | Kind, 6 | SelectionSetNode, 7 | TypeInfo, 8 | visit, 9 | visitWithTypeInfo, 10 | } from 'graphql'; 11 | 12 | import { Transform, Request, MergedTypeInfo } from '../../Interfaces'; 13 | 14 | export default class AddMergedTypeFragments implements Transform { 15 | private readonly targetSchema: GraphQLSchema; 16 | private readonly mapping: Record; 17 | 18 | constructor( 19 | targetSchema: GraphQLSchema, 20 | mapping: Record, 21 | ) { 22 | this.targetSchema = targetSchema; 23 | this.mapping = mapping; 24 | } 25 | 26 | public transformRequest(originalRequest: Request): Request { 27 | const document = addMergedTypeSelectionSets( 28 | this.targetSchema, 29 | originalRequest.document, 30 | this.mapping, 31 | ); 32 | return { 33 | ...originalRequest, 34 | document, 35 | }; 36 | } 37 | } 38 | 39 | function addMergedTypeSelectionSets( 40 | targetSchema: GraphQLSchema, 41 | document: DocumentNode, 42 | mapping: Record, 43 | ): DocumentNode { 44 | const typeInfo = new TypeInfo(targetSchema); 45 | return visit( 46 | document, 47 | visitWithTypeInfo(typeInfo, { 48 | [Kind.SELECTION_SET]( 49 | node: SelectionSetNode, 50 | ): SelectionSetNode | null | undefined { 51 | const parentType: 52 | | GraphQLType 53 | | null 54 | | undefined = typeInfo.getParentType(); 55 | if (parentType != null) { 56 | const parentTypeName = parentType.name; 57 | let selections = node.selections; 58 | 59 | if (mapping[parentTypeName] != null) { 60 | const selectionSet = mapping[parentTypeName].selectionSet; 61 | if (selectionSet != null) { 62 | selections = selections.concat(selectionSet.selections); 63 | } 64 | } 65 | 66 | if (selections !== node.selections) { 67 | return { 68 | ...node, 69 | selections, 70 | }; 71 | } 72 | } 73 | }, 74 | }), 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /src/wrap/transforms/AddReplacementFragments.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DocumentNode, 3 | GraphQLSchema, 4 | GraphQLType, 5 | Kind, 6 | SelectionSetNode, 7 | TypeInfo, 8 | visit, 9 | visitWithTypeInfo, 10 | } from 'graphql'; 11 | 12 | import { 13 | Transform, 14 | Request, 15 | ReplacementFragmentMapping, 16 | } from '../../Interfaces'; 17 | 18 | export default class AddReplacementFragments implements Transform { 19 | private readonly targetSchema: GraphQLSchema; 20 | private readonly mapping: ReplacementFragmentMapping; 21 | 22 | constructor( 23 | targetSchema: GraphQLSchema, 24 | mapping: ReplacementFragmentMapping, 25 | ) { 26 | this.targetSchema = targetSchema; 27 | this.mapping = mapping; 28 | } 29 | 30 | public transformRequest(originalRequest: Request): Request { 31 | const document = replaceFieldsWithFragments( 32 | this.targetSchema, 33 | originalRequest.document, 34 | this.mapping, 35 | ); 36 | return { 37 | ...originalRequest, 38 | document, 39 | }; 40 | } 41 | } 42 | 43 | function replaceFieldsWithFragments( 44 | targetSchema: GraphQLSchema, 45 | document: DocumentNode, 46 | mapping: ReplacementFragmentMapping, 47 | ): DocumentNode { 48 | const typeInfo = new TypeInfo(targetSchema); 49 | return visit( 50 | document, 51 | visitWithTypeInfo(typeInfo, { 52 | [Kind.SELECTION_SET]( 53 | node: SelectionSetNode, 54 | ): SelectionSetNode | null | undefined { 55 | const parentType: 56 | | GraphQLType 57 | | null 58 | | undefined = typeInfo.getParentType(); 59 | if (parentType != null) { 60 | const parentTypeName = parentType.name; 61 | let selections = node.selections; 62 | 63 | if (mapping[parentTypeName] != null) { 64 | node.selections.forEach((selection) => { 65 | if (selection.kind === Kind.FIELD) { 66 | const name = selection.name.value; 67 | const fragment = mapping[parentTypeName][name]; 68 | if (fragment != null) { 69 | selections = selections.concat(fragment); 70 | } 71 | } 72 | }); 73 | } 74 | 75 | if (selections !== node.selections) { 76 | return { 77 | ...node, 78 | selections, 79 | }; 80 | } 81 | } 82 | }, 83 | }), 84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /src/wrap/transforms/AddReplacementSelectionSets.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DocumentNode, 3 | GraphQLSchema, 4 | GraphQLType, 5 | Kind, 6 | SelectionSetNode, 7 | TypeInfo, 8 | visit, 9 | visitWithTypeInfo, 10 | } from 'graphql'; 11 | 12 | import { 13 | Transform, 14 | Request, 15 | ReplacementSelectionSetMapping, 16 | } from '../../Interfaces'; 17 | 18 | export default class AddReplacementSelectionSets implements Transform { 19 | private readonly schema: GraphQLSchema; 20 | private readonly mapping: ReplacementSelectionSetMapping; 21 | 22 | constructor(schema: GraphQLSchema, mapping: ReplacementSelectionSetMapping) { 23 | this.schema = schema; 24 | this.mapping = mapping; 25 | } 26 | 27 | public transformRequest(originalRequest: Request): Request { 28 | const document = replaceFieldsWithSelectionSet( 29 | this.schema, 30 | originalRequest.document, 31 | this.mapping, 32 | ); 33 | return { 34 | ...originalRequest, 35 | document, 36 | }; 37 | } 38 | } 39 | 40 | function replaceFieldsWithSelectionSet( 41 | schema: GraphQLSchema, 42 | document: DocumentNode, 43 | mapping: ReplacementSelectionSetMapping, 44 | ): DocumentNode { 45 | const typeInfo = new TypeInfo(schema); 46 | return visit( 47 | document, 48 | visitWithTypeInfo(typeInfo, { 49 | [Kind.SELECTION_SET]( 50 | node: SelectionSetNode, 51 | ): SelectionSetNode | null | undefined { 52 | const parentType: 53 | | GraphQLType 54 | | null 55 | | undefined = typeInfo.getParentType(); 56 | if (parentType != null) { 57 | const parentTypeName = parentType.name; 58 | let selections = node.selections; 59 | 60 | if (mapping[parentTypeName] != null) { 61 | node.selections.forEach((selection) => { 62 | if (selection.kind === Kind.FIELD) { 63 | const name = selection.name.value; 64 | const selectionSet = mapping[parentTypeName][name]; 65 | if (selectionSet != null) { 66 | selections = selections.concat(selectionSet.selections); 67 | } 68 | } 69 | }); 70 | } 71 | 72 | if (selections !== node.selections) { 73 | return { 74 | ...node, 75 | selections, 76 | }; 77 | } 78 | } 79 | }, 80 | }), 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /src/wrap/transforms/AddTypenameToAbstract.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema } from 'graphql'; 2 | 3 | import { Transform, Request } from '../../Interfaces'; 4 | import { addTypenameToAbstract } from '../../delegate/addTypenameToAbstract'; 5 | 6 | export default class AddTypenameToAbstract implements Transform { 7 | private readonly targetSchema: GraphQLSchema; 8 | 9 | constructor(targetSchema: GraphQLSchema) { 10 | this.targetSchema = targetSchema; 11 | } 12 | 13 | public transformRequest(originalRequest: Request): Request { 14 | const document = addTypenameToAbstract( 15 | this.targetSchema, 16 | originalRequest.document, 17 | ); 18 | return { 19 | ...originalRequest, 20 | document, 21 | }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/wrap/transforms/CheckResultAndHandleErrors.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema, GraphQLOutputType } from 'graphql'; 2 | 3 | import { checkResultAndHandleErrors } from '../../delegate/checkResultAndHandleErrors'; 4 | import { 5 | Transform, 6 | SubschemaConfig, 7 | IGraphQLToolsResolveInfo, 8 | } from '../../Interfaces'; 9 | 10 | export default class CheckResultAndHandleErrors implements Transform { 11 | private readonly context?: Record; 12 | private readonly info: IGraphQLToolsResolveInfo; 13 | private readonly fieldName?: string; 14 | private readonly subschema?: GraphQLSchema | SubschemaConfig; 15 | private readonly returnType?: GraphQLOutputType; 16 | private readonly typeMerge?: boolean; 17 | 18 | constructor( 19 | info: IGraphQLToolsResolveInfo, 20 | fieldName?: string, 21 | subschema?: GraphQLSchema | SubschemaConfig, 22 | context?: Record, 23 | returnType: GraphQLOutputType = info.returnType, 24 | typeMerge?: boolean, 25 | ) { 26 | this.context = context; 27 | this.info = info; 28 | this.fieldName = fieldName; 29 | this.subschema = subschema; 30 | this.returnType = returnType; 31 | this.typeMerge = typeMerge; 32 | } 33 | 34 | public transformResult(result: any): any { 35 | return checkResultAndHandleErrors( 36 | result, 37 | this.context != null ? this.context : {}, 38 | this.info, 39 | this.fieldName, 40 | this.subschema, 41 | this.returnType, 42 | this.typeMerge, 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/wrap/transforms/ExtendSchema.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema, extendSchema, parse } from 'graphql'; 2 | 3 | import { 4 | Transform, 5 | IFieldResolver, 6 | IResolvers, 7 | Request, 8 | } from '../../Interfaces'; 9 | import { addResolversToSchema } from '../../generate/index'; 10 | import { defaultMergedResolver } from '../../stitch/index'; 11 | 12 | import MapFields, { FieldNodeTransformerMap } from './MapFields'; 13 | 14 | export default class ExtendSchema implements Transform { 15 | private readonly typeDefs: string | undefined; 16 | private readonly resolvers: IResolvers | undefined; 17 | private readonly defaultFieldResolver: IFieldResolver | undefined; 18 | private readonly transformer: MapFields; 19 | 20 | constructor({ 21 | typeDefs, 22 | resolvers = {}, 23 | defaultFieldResolver, 24 | fieldNodeTransformerMap, 25 | }: { 26 | typeDefs?: string; 27 | resolvers?: IResolvers; 28 | defaultFieldResolver?: IFieldResolver; 29 | fieldNodeTransformerMap?: FieldNodeTransformerMap; 30 | }) { 31 | this.typeDefs = typeDefs; 32 | this.resolvers = resolvers; 33 | this.defaultFieldResolver = 34 | defaultFieldResolver != null 35 | ? defaultFieldResolver 36 | : defaultMergedResolver; 37 | this.transformer = new MapFields( 38 | fieldNodeTransformerMap != null ? fieldNodeTransformerMap : {}, 39 | ); 40 | } 41 | 42 | public transformSchema(schema: GraphQLSchema): GraphQLSchema { 43 | this.transformer.transformSchema(schema); 44 | 45 | return addResolversToSchema({ 46 | schema: this.typeDefs 47 | ? extendSchema(schema, parse(this.typeDefs)) 48 | : schema, 49 | resolvers: this.resolvers != null ? this.resolvers : {}, 50 | defaultFieldResolver: this.defaultFieldResolver, 51 | }); 52 | } 53 | 54 | public transformRequest(originalRequest: Request): Request { 55 | return this.transformer.transformRequest(originalRequest); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/wrap/transforms/ExtractField.ts: -------------------------------------------------------------------------------- 1 | import { visit, Kind, SelectionSetNode, BREAK, FieldNode } from 'graphql'; 2 | 3 | import { Transform, Request } from '../../Interfaces'; 4 | 5 | export default class ExtractField implements Transform { 6 | private readonly from: Array; 7 | private readonly to: Array; 8 | 9 | constructor({ from, to }: { from: Array; to: Array }) { 10 | this.from = from; 11 | this.to = to; 12 | } 13 | 14 | public transformRequest(originalRequest: Request): Request { 15 | let fromSelection: SelectionSetNode | undefined; 16 | const ourPathFrom = JSON.stringify(this.from); 17 | const ourPathTo = JSON.stringify(this.to); 18 | let fieldPath: Array = []; 19 | visit(originalRequest.document, { 20 | [Kind.FIELD]: { 21 | enter: (node: FieldNode) => { 22 | fieldPath.push(node.name.value); 23 | if (ourPathFrom === JSON.stringify(fieldPath)) { 24 | fromSelection = node.selectionSet; 25 | return BREAK; 26 | } 27 | }, 28 | leave: () => { 29 | fieldPath.pop(); 30 | }, 31 | }, 32 | }); 33 | 34 | fieldPath = []; 35 | const newDocument = visit(originalRequest.document, { 36 | [Kind.FIELD]: { 37 | enter: (node: FieldNode) => { 38 | fieldPath.push(node.name.value); 39 | if ( 40 | ourPathTo === JSON.stringify(fieldPath) && 41 | fromSelection != null 42 | ) { 43 | return { 44 | ...node, 45 | selectionSet: fromSelection, 46 | }; 47 | } 48 | }, 49 | leave: () => { 50 | fieldPath.pop(); 51 | }, 52 | }, 53 | }); 54 | return { 55 | ...originalRequest, 56 | document: newDocument, 57 | }; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/wrap/transforms/FilterInterfaceFields.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLField, GraphQLSchema } from 'graphql'; 2 | 3 | import { Transform, FieldFilter } from '../../Interfaces'; 4 | 5 | import TransformInterfaceFields from './TransformInterfaceFields'; 6 | 7 | export default class FilterInterfaceFields implements Transform { 8 | private readonly transformer: TransformInterfaceFields; 9 | 10 | constructor(filter: FieldFilter) { 11 | this.transformer = new TransformInterfaceFields( 12 | (typeName: string, fieldName: string, field: GraphQLField) => 13 | filter(typeName, fieldName, field) ? undefined : null, 14 | ); 15 | } 16 | 17 | public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { 18 | return this.transformer.transformSchema(originalSchema); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/wrap/transforms/FilterObjectFields.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLField, GraphQLSchema } from 'graphql'; 2 | 3 | import { Transform, FieldFilter } from '../../Interfaces'; 4 | 5 | import TransformObjectFields from './TransformObjectFields'; 6 | 7 | export default class FilterObjectFields implements Transform { 8 | private readonly transformer: TransformObjectFields; 9 | 10 | constructor(filter: FieldFilter) { 11 | this.transformer = new TransformObjectFields( 12 | (typeName: string, fieldName: string, field: GraphQLField) => 13 | filter(typeName, fieldName, field) ? undefined : null, 14 | ); 15 | } 16 | 17 | public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { 18 | return this.transformer.transformSchema(originalSchema); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/wrap/transforms/FilterRootFields.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLField, GraphQLSchema } from 'graphql'; 2 | 3 | import { Transform } from '../../Interfaces'; 4 | 5 | import TransformRootFields from './TransformRootFields'; 6 | 7 | export type RootFilter = ( 8 | operation: 'Query' | 'Mutation' | 'Subscription', 9 | fieldName: string, 10 | field: GraphQLField, 11 | ) => boolean; 12 | 13 | export default class FilterRootFields implements Transform { 14 | private readonly transformer: TransformRootFields; 15 | 16 | constructor(filter: RootFilter) { 17 | this.transformer = new TransformRootFields( 18 | ( 19 | operation: 'Query' | 'Mutation' | 'Subscription', 20 | fieldName: string, 21 | field: GraphQLField, 22 | ) => { 23 | if (filter(operation, fieldName, field)) { 24 | return undefined; 25 | } 26 | 27 | return null; 28 | }, 29 | ); 30 | } 31 | 32 | public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { 33 | return this.transformer.transformSchema(originalSchema); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/wrap/transforms/FilterTypes.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema, GraphQLNamedType } from 'graphql'; 2 | 3 | import { mapSchema } from '../../utils/index'; 4 | import { Transform, MapperKind } from '../../Interfaces'; 5 | 6 | export default class FilterTypes implements Transform { 7 | private readonly filter: (type: GraphQLNamedType) => boolean; 8 | 9 | constructor(filter: (type: GraphQLNamedType) => boolean) { 10 | this.filter = filter; 11 | } 12 | 13 | public transformSchema(schema: GraphQLSchema): GraphQLSchema { 14 | return mapSchema(schema, { 15 | [MapperKind.TYPE]: (type: GraphQLNamedType) => { 16 | if (this.filter(type)) { 17 | return undefined; 18 | } 19 | 20 | return null; 21 | }, 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/wrap/transforms/HoistField.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema, GraphQLObjectType, getNullableType } from 'graphql'; 2 | 3 | import { healSchema, wrapFieldNode, renameFieldNode } from '../../utils/index'; 4 | import { createMergedResolver } from '../../stitch/index'; 5 | import { appendFields, removeFields } from '../../utils/fields'; 6 | import { Transform, Request } from '../../Interfaces'; 7 | 8 | import MapFields from './MapFields'; 9 | 10 | export default class HoistField implements Transform { 11 | private readonly typeName: string; 12 | private readonly path: Array; 13 | private readonly newFieldName: string; 14 | private readonly pathToField: Array; 15 | private readonly oldFieldName: string; 16 | private readonly transformer: Transform; 17 | 18 | constructor(typeName: string, path: Array, newFieldName: string) { 19 | this.typeName = typeName; 20 | this.path = path; 21 | this.newFieldName = newFieldName; 22 | 23 | this.pathToField = this.path.slice(); 24 | this.oldFieldName = this.pathToField.pop(); 25 | this.transformer = new MapFields({ 26 | [typeName]: { 27 | [newFieldName]: (fieldNode) => 28 | wrapFieldNode( 29 | renameFieldNode(fieldNode, this.oldFieldName), 30 | this.pathToField, 31 | ), 32 | }, 33 | }); 34 | } 35 | 36 | public transformSchema(schema: GraphQLSchema): GraphQLSchema { 37 | const typeMap = schema.getTypeMap(); 38 | 39 | const innerType: GraphQLObjectType = this.pathToField.reduce( 40 | (acc, pathSegment) => 41 | getNullableType(acc.getFields()[pathSegment].type) as GraphQLObjectType, 42 | typeMap[this.typeName] as GraphQLObjectType, 43 | ); 44 | 45 | const targetField = removeFields( 46 | typeMap, 47 | innerType.name, 48 | (fieldName) => fieldName === this.oldFieldName, 49 | )[this.oldFieldName]; 50 | 51 | const targetType = targetField.type as GraphQLObjectType; 52 | 53 | appendFields(typeMap, this.typeName, { 54 | [this.newFieldName]: { 55 | type: targetType, 56 | resolve: createMergedResolver({ fromPath: this.pathToField }), 57 | }, 58 | }); 59 | 60 | healSchema(schema); 61 | 62 | return this.transformer.transformSchema(schema); 63 | } 64 | 65 | public transformRequest(originalRequest: Request): Request { 66 | return this.transformer.transformRequest(originalRequest); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/wrap/transforms/MapFields.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLSchema, 3 | FieldNode, 4 | SelectionNode, 5 | FragmentDefinitionNode, 6 | } from 'graphql'; 7 | 8 | import { Transform, Request } from '../../Interfaces'; 9 | import { toConfig } from '../../polyfills/index'; 10 | 11 | import TransformObjectFields from './TransformObjectFields'; 12 | 13 | export type FieldNodeTransformer = ( 14 | fieldNode: FieldNode, 15 | fragments: Record, 16 | ) => SelectionNode | Array; 17 | 18 | export type FieldNodeTransformerMap = { 19 | [typeName: string]: { 20 | [fieldName: string]: FieldNodeTransformer; 21 | }; 22 | }; 23 | 24 | export default class MapFields implements Transform { 25 | private readonly transformer: TransformObjectFields; 26 | 27 | constructor(fieldNodeTransformerMap: FieldNodeTransformerMap) { 28 | this.transformer = new TransformObjectFields( 29 | (_typeName, _fieldName, field) => toConfig(field), 30 | (typeName, fieldName, fieldNode, fragments) => { 31 | const typeTransformers = fieldNodeTransformerMap[typeName]; 32 | if (typeTransformers == null) { 33 | return fieldNode; 34 | } 35 | 36 | const fieldNodeTransformer = typeTransformers[fieldName]; 37 | if (fieldNodeTransformer == null) { 38 | return fieldNode; 39 | } 40 | 41 | return fieldNodeTransformer(fieldNode, fragments); 42 | }, 43 | ); 44 | } 45 | 46 | public transformSchema(schema: GraphQLSchema): GraphQLSchema { 47 | return this.transformer.transformSchema(schema); 48 | } 49 | 50 | public transformRequest(request: Request): Request { 51 | return this.transformer.transformRequest(request); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/wrap/transforms/RenameInterfaceFields.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLField, GraphQLSchema } from 'graphql'; 2 | 3 | import { Transform, Request } from '../../Interfaces'; 4 | 5 | import TransformInterfaceFields from './TransformInterfaceFields'; 6 | 7 | export default class RenameInterfaceFields implements Transform { 8 | private readonly transformer: TransformInterfaceFields; 9 | 10 | constructor( 11 | renamer: ( 12 | typeName: string, 13 | fieldName: string, 14 | field: GraphQLField, 15 | ) => string, 16 | ) { 17 | this.transformer = new TransformInterfaceFields( 18 | (typeName: string, fieldName: string, field: GraphQLField) => ({ 19 | name: renamer(typeName, fieldName, field), 20 | }), 21 | ); 22 | } 23 | 24 | public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { 25 | return this.transformer.transformSchema(originalSchema); 26 | } 27 | 28 | public transformRequest(originalRequest: Request): Request { 29 | return this.transformer.transformRequest(originalRequest); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/wrap/transforms/RenameObjectFields.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLField, GraphQLSchema } from 'graphql'; 2 | 3 | import { Transform, Request } from '../../Interfaces'; 4 | 5 | import TransformObjectFields from './TransformObjectFields'; 6 | 7 | export default class RenameObjectFields implements Transform { 8 | private readonly transformer: TransformObjectFields; 9 | 10 | constructor( 11 | renamer: ( 12 | typeName: string, 13 | fieldName: string, 14 | field: GraphQLField, 15 | ) => string, 16 | ) { 17 | this.transformer = new TransformObjectFields( 18 | (typeName: string, fieldName: string, field: GraphQLField) => ({ 19 | name: renamer(typeName, fieldName, field), 20 | }), 21 | ); 22 | } 23 | 24 | public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { 25 | return this.transformer.transformSchema(originalSchema); 26 | } 27 | 28 | public transformRequest(originalRequest: Request): Request { 29 | return this.transformer.transformRequest(originalRequest); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/wrap/transforms/RenameRootFields.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLField, GraphQLSchema } from 'graphql'; 2 | 3 | import { Transform, Request } from '../../Interfaces'; 4 | 5 | import TransformRootFields from './TransformRootFields'; 6 | 7 | export default class RenameRootFields implements Transform { 8 | private readonly transformer: TransformRootFields; 9 | 10 | constructor( 11 | renamer: ( 12 | operation: 'Query' | 'Mutation' | 'Subscription', 13 | name: string, 14 | field: GraphQLField, 15 | ) => string, 16 | ) { 17 | this.transformer = new TransformRootFields( 18 | ( 19 | operation: 'Query' | 'Mutation' | 'Subscription', 20 | fieldName: string, 21 | field: GraphQLField, 22 | ) => ({ 23 | name: renamer(operation, fieldName, field), 24 | }), 25 | ); 26 | } 27 | 28 | public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { 29 | return this.transformer.transformSchema(originalSchema); 30 | } 31 | 32 | public transformRequest(originalRequest: Request): Request { 33 | return this.transformer.transformRequest(originalRequest); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/wrap/transforms/RenameRootTypes.ts: -------------------------------------------------------------------------------- 1 | import { 2 | visit, 3 | GraphQLSchema, 4 | NamedTypeNode, 5 | Kind, 6 | GraphQLObjectType, 7 | } from 'graphql'; 8 | 9 | import { Request, Result, MapperKind, Transform } from '../../Interfaces'; 10 | import { mapSchema } from '../../utils/index'; 11 | import { toConfig } from '../../polyfills/index'; 12 | 13 | export default class RenameRootTypes implements Transform { 14 | private readonly renamer: (name: string) => string | undefined; 15 | private map: { [key: string]: string }; 16 | private reverseMap: { [key: string]: string }; 17 | 18 | constructor(renamer: (name: string) => string | undefined) { 19 | this.renamer = renamer; 20 | this.map = {}; 21 | this.reverseMap = {}; 22 | } 23 | 24 | public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { 25 | return mapSchema(originalSchema, { 26 | [MapperKind.ROOT_OBJECT]: (type) => { 27 | const oldName = type.name; 28 | const newName = this.renamer(oldName); 29 | if (newName && newName !== oldName) { 30 | this.map[oldName] = type.name; 31 | this.reverseMap[newName] = oldName; 32 | return new GraphQLObjectType({ 33 | ...toConfig(type), 34 | name: newName, 35 | }); 36 | } 37 | }, 38 | }); 39 | } 40 | 41 | public transformRequest(originalRequest: Request): Request { 42 | const newDocument = visit(originalRequest.document, { 43 | [Kind.NAMED_TYPE]: (node: NamedTypeNode) => { 44 | const name = node.name.value; 45 | if (name in this.reverseMap) { 46 | return { 47 | ...node, 48 | name: { 49 | kind: Kind.NAME, 50 | value: this.reverseMap[name], 51 | }, 52 | }; 53 | } 54 | }, 55 | }); 56 | return { 57 | document: newDocument, 58 | variables: originalRequest.variables, 59 | }; 60 | } 61 | 62 | public transformResult(result: Result): Result { 63 | return { 64 | ...result, 65 | data: this.renameTypes(result.data), 66 | }; 67 | } 68 | 69 | private renameTypes(value: any): any { 70 | if (value == null) { 71 | return value; 72 | } else if (Array.isArray(value)) { 73 | value.forEach((v, index) => { 74 | value[index] = this.renameTypes(v); 75 | }); 76 | return value; 77 | } else if (typeof value === 'object') { 78 | Object.keys(value).forEach((key) => { 79 | value[key] = 80 | key === '__typename' 81 | ? this.renamer(value[key]) 82 | : this.renameTypes(value[key]); 83 | }); 84 | return value; 85 | } 86 | 87 | return value; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/wrap/transforms/RenameTypes.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLEnumType, 3 | GraphQLInputObjectType, 4 | GraphQLInterfaceType, 5 | GraphQLNamedType, 6 | GraphQLObjectType, 7 | GraphQLSchema, 8 | GraphQLScalarType, 9 | GraphQLUnionType, 10 | Kind, 11 | NamedTypeNode, 12 | isEnumType, 13 | isInputObjectType, 14 | isInterfaceType, 15 | isObjectType, 16 | isScalarType, 17 | isUnionType, 18 | visit, 19 | } from 'graphql'; 20 | 21 | import { isSpecifiedScalarType, toConfig } from '../../polyfills/index'; 22 | import { Transform, Request, Result, MapperKind } from '../../Interfaces'; 23 | import { mapSchema } from '../../utils/index'; 24 | 25 | export type RenameOptions = { 26 | renameBuiltins: boolean; 27 | renameScalars: boolean; 28 | }; 29 | 30 | export default class RenameTypes implements Transform { 31 | private readonly renamer: (name: string) => string | undefined; 32 | private map: { [key: string]: string }; 33 | private reverseMap: { [key: string]: string }; 34 | private readonly renameBuiltins: boolean; 35 | private readonly renameScalars: boolean; 36 | 37 | constructor( 38 | renamer: (name: string) => string | undefined, 39 | options?: RenameOptions, 40 | ) { 41 | this.renamer = renamer; 42 | this.map = {}; 43 | this.reverseMap = {}; 44 | const { renameBuiltins = false, renameScalars = true } = 45 | options != null ? options : {}; 46 | this.renameBuiltins = renameBuiltins; 47 | this.renameScalars = renameScalars; 48 | } 49 | 50 | public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { 51 | return mapSchema(originalSchema, { 52 | [MapperKind.TYPE]: (type: GraphQLNamedType) => { 53 | if (isSpecifiedScalarType(type) && !this.renameBuiltins) { 54 | return undefined; 55 | } 56 | if (isScalarType(type) && !this.renameScalars) { 57 | return undefined; 58 | } 59 | const oldName = type.name; 60 | const newName = this.renamer(oldName); 61 | if (newName !== undefined && newName !== oldName) { 62 | this.map[oldName] = newName; 63 | this.reverseMap[newName] = oldName; 64 | 65 | const newConfig = { 66 | ...toConfig(type), 67 | name: newName, 68 | }; 69 | 70 | if (isObjectType(type)) { 71 | return new GraphQLObjectType(newConfig); 72 | } else if (isInterfaceType(type)) { 73 | return new GraphQLInterfaceType(newConfig); 74 | } else if (isUnionType(type)) { 75 | return new GraphQLUnionType(newConfig); 76 | } else if (isInputObjectType(type)) { 77 | return new GraphQLInputObjectType(newConfig); 78 | } else if (isEnumType(type)) { 79 | return new GraphQLEnumType(newConfig); 80 | } else if (isScalarType(type)) { 81 | return new GraphQLScalarType(newConfig); 82 | } 83 | 84 | throw new Error(`Unknown type ${type as string}.`); 85 | } 86 | }, 87 | 88 | [MapperKind.ROOT_OBJECT]() { 89 | return undefined; 90 | }, 91 | }); 92 | } 93 | 94 | public transformRequest(originalRequest: Request): Request { 95 | const newDocument = visit(originalRequest.document, { 96 | [Kind.NAMED_TYPE]: (node: NamedTypeNode) => { 97 | const name = node.name.value; 98 | if (name in this.reverseMap) { 99 | return { 100 | ...node, 101 | name: { 102 | kind: Kind.NAME, 103 | value: this.reverseMap[name], 104 | }, 105 | }; 106 | } 107 | }, 108 | }); 109 | return { 110 | document: newDocument, 111 | variables: originalRequest.variables, 112 | }; 113 | } 114 | 115 | public transformResult(result: Result): Result { 116 | return { 117 | ...result, 118 | data: this.transformData(result.data), 119 | }; 120 | } 121 | 122 | private transformData(data: any): any { 123 | if (data == null) { 124 | return data; 125 | } else if (Array.isArray(data)) { 126 | return data.map((value) => this.transformData(value)); 127 | } else if (typeof data === 'object') { 128 | return this.transformObject(data); 129 | } 130 | 131 | return data; 132 | } 133 | 134 | private transformObject(object: Record): Record { 135 | Object.keys(object).forEach((key) => { 136 | const value = object[key]; 137 | if (key === '__typename') { 138 | const newName = this.map[value]; 139 | if (newName !== undefined) { 140 | object[key] = newName; 141 | } 142 | } else { 143 | object[key] = this.transformData(value); 144 | } 145 | }); 146 | 147 | return object; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/wrap/transforms/ReplaceFieldWithFragment.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DocumentNode, 3 | GraphQLSchema, 4 | GraphQLType, 5 | InlineFragmentNode, 6 | Kind, 7 | SelectionSetNode, 8 | TypeInfo, 9 | OperationDefinitionNode, 10 | parse, 11 | visit, 12 | visitWithTypeInfo, 13 | } from 'graphql'; 14 | 15 | import { concatInlineFragments } from '../../utils/index'; 16 | import { Transform, Request } from '../../Interfaces'; 17 | 18 | export default class ReplaceFieldWithFragment implements Transform { 19 | private readonly targetSchema: GraphQLSchema; 20 | private readonly mapping: FieldToFragmentMapping; 21 | 22 | constructor( 23 | targetSchema: GraphQLSchema, 24 | fragments: Array<{ 25 | field: string; 26 | fragment: string; 27 | }>, 28 | ) { 29 | this.targetSchema = targetSchema; 30 | this.mapping = {}; 31 | for (const { field, fragment } of fragments) { 32 | const parsedFragment = parseFragmentToInlineFragment(fragment); 33 | const actualTypeName = parsedFragment.typeCondition.name.value; 34 | if (this.mapping[actualTypeName] == null) { 35 | this.mapping[actualTypeName] = {}; 36 | } 37 | 38 | if (this.mapping[actualTypeName][field] == null) { 39 | this.mapping[actualTypeName][field] = [parsedFragment]; 40 | } else { 41 | this.mapping[actualTypeName][field].push(parsedFragment); 42 | } 43 | } 44 | } 45 | 46 | public transformRequest(originalRequest: Request): Request { 47 | const document = replaceFieldsWithFragments( 48 | this.targetSchema, 49 | originalRequest.document, 50 | this.mapping, 51 | ); 52 | return { 53 | ...originalRequest, 54 | document, 55 | }; 56 | } 57 | } 58 | 59 | type FieldToFragmentMapping = { 60 | [typeName: string]: { [fieldName: string]: Array }; 61 | }; 62 | 63 | function replaceFieldsWithFragments( 64 | targetSchema: GraphQLSchema, 65 | document: DocumentNode, 66 | mapping: FieldToFragmentMapping, 67 | ): DocumentNode { 68 | const typeInfo = new TypeInfo(targetSchema); 69 | return visit( 70 | document, 71 | visitWithTypeInfo(typeInfo, { 72 | [Kind.SELECTION_SET]( 73 | node: SelectionSetNode, 74 | ): SelectionSetNode | null | undefined { 75 | const parentType: GraphQLType = typeInfo.getParentType(); 76 | if (parentType != null) { 77 | const parentTypeName = parentType.name; 78 | let selections = node.selections; 79 | 80 | if (mapping[parentTypeName] != null) { 81 | node.selections.forEach((selection) => { 82 | if (selection.kind === Kind.FIELD) { 83 | const name = selection.name.value; 84 | const fragments = mapping[parentTypeName][name]; 85 | if (fragments != null && fragments.length > 0) { 86 | const fragment = concatInlineFragments( 87 | parentTypeName, 88 | fragments, 89 | ); 90 | selections = selections.concat(fragment); 91 | } 92 | } 93 | }); 94 | } 95 | 96 | if (selections !== node.selections) { 97 | return { 98 | ...node, 99 | selections, 100 | }; 101 | } 102 | } 103 | }, 104 | }), 105 | ); 106 | } 107 | 108 | function parseFragmentToInlineFragment( 109 | definitions: string, 110 | ): InlineFragmentNode { 111 | if (definitions.trim().startsWith('fragment')) { 112 | const document = parse(definitions); 113 | for (const definition of document.definitions) { 114 | if (definition.kind === Kind.FRAGMENT_DEFINITION) { 115 | return { 116 | kind: Kind.INLINE_FRAGMENT, 117 | typeCondition: definition.typeCondition, 118 | selectionSet: definition.selectionSet, 119 | }; 120 | } 121 | } 122 | } 123 | 124 | const query = parse(`{${definitions}}`) 125 | .definitions[0] as OperationDefinitionNode; 126 | for (const selection of query.selectionSet.selections) { 127 | if (selection.kind === Kind.INLINE_FRAGMENT) { 128 | return selection; 129 | } 130 | } 131 | 132 | throw new Error('Could not parse fragment'); 133 | } 134 | -------------------------------------------------------------------------------- /src/wrap/transforms/TransformInterfaceFields.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema, GraphQLField, isInterfaceType } from 'graphql'; 2 | 3 | import { 4 | Transform, 5 | Request, 6 | FieldTransformer, 7 | FieldNodeTransformer, 8 | } from '../../Interfaces'; 9 | 10 | import TransformCompositeFields from './TransformCompositeFields'; 11 | 12 | export default class TransformInterfaceFields implements Transform { 13 | private readonly interfaceFieldTransformer: FieldTransformer; 14 | private readonly fieldNodeTransformer: FieldNodeTransformer; 15 | private transformer: TransformCompositeFields; 16 | 17 | constructor( 18 | interfaceFieldTransformer: FieldTransformer, 19 | fieldNodeTransformer?: FieldNodeTransformer, 20 | ) { 21 | this.interfaceFieldTransformer = interfaceFieldTransformer; 22 | this.fieldNodeTransformer = fieldNodeTransformer; 23 | } 24 | 25 | public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { 26 | const compositeToObjectFieldTransformer = ( 27 | typeName: string, 28 | fieldName: string, 29 | field: GraphQLField, 30 | ) => { 31 | if (isInterfaceType(originalSchema.getType(typeName))) { 32 | return this.interfaceFieldTransformer(typeName, fieldName, field); 33 | } 34 | 35 | return undefined; 36 | }; 37 | 38 | this.transformer = new TransformCompositeFields( 39 | compositeToObjectFieldTransformer, 40 | this.fieldNodeTransformer, 41 | ); 42 | 43 | return this.transformer.transformSchema(originalSchema); 44 | } 45 | 46 | public transformRequest(originalRequest: Request): Request { 47 | return this.transformer.transformRequest(originalRequest); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/wrap/transforms/TransformObjectFields.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema, GraphQLField, isObjectType } from 'graphql'; 2 | 3 | import { 4 | Transform, 5 | Request, 6 | FieldTransformer, 7 | FieldNodeTransformer, 8 | } from '../../Interfaces'; 9 | 10 | import TransformCompositeFields from './TransformCompositeFields'; 11 | 12 | export default class TransformObjectFields implements Transform { 13 | private readonly objectFieldTransformer: FieldTransformer; 14 | private readonly fieldNodeTransformer: FieldNodeTransformer; 15 | private transformer: TransformCompositeFields; 16 | 17 | constructor( 18 | objectFieldTransformer: FieldTransformer, 19 | fieldNodeTransformer?: FieldNodeTransformer, 20 | ) { 21 | this.objectFieldTransformer = objectFieldTransformer; 22 | this.fieldNodeTransformer = fieldNodeTransformer; 23 | } 24 | 25 | public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { 26 | const compositeToObjectFieldTransformer = ( 27 | typeName: string, 28 | fieldName: string, 29 | field: GraphQLField, 30 | ) => { 31 | if (isObjectType(originalSchema.getType(typeName))) { 32 | return this.objectFieldTransformer(typeName, fieldName, field); 33 | } 34 | 35 | return undefined; 36 | }; 37 | 38 | this.transformer = new TransformCompositeFields( 39 | compositeToObjectFieldTransformer, 40 | this.fieldNodeTransformer, 41 | ); 42 | 43 | return this.transformer.transformSchema(originalSchema); 44 | } 45 | 46 | public transformRequest(originalRequest: Request): Request { 47 | return this.transformer.transformRequest(originalRequest); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/wrap/transforms/TransformQuery.ts: -------------------------------------------------------------------------------- 1 | import { 2 | visit, 3 | Kind, 4 | SelectionSetNode, 5 | FragmentDefinitionNode, 6 | GraphQLError, 7 | } from 'graphql'; 8 | 9 | import { Transform, Request, Result } from '../../Interfaces'; 10 | 11 | export type QueryTransformer = ( 12 | selectionSet: SelectionSetNode, 13 | fragments: Record, 14 | ) => SelectionSetNode; 15 | 16 | export type ResultTransformer = (result: any) => any; 17 | 18 | export type ErrorPathTransformer = ( 19 | path: ReadonlyArray, 20 | ) => Array; 21 | 22 | export default class TransformQuery implements Transform { 23 | private readonly path: Array; 24 | private readonly queryTransformer: QueryTransformer; 25 | private readonly resultTransformer: ResultTransformer; 26 | private readonly errorPathTransformer: ErrorPathTransformer; 27 | private readonly fragments: Record; 28 | 29 | constructor({ 30 | path, 31 | queryTransformer, 32 | resultTransformer = (result) => result, 33 | errorPathTransformer = (errorPath) => [].concat(errorPath), 34 | fragments = {}, 35 | }: { 36 | path: Array; 37 | queryTransformer: QueryTransformer; 38 | resultTransformer?: ResultTransformer; 39 | errorPathTransformer?: ErrorPathTransformer; 40 | fragments?: Record; 41 | }) { 42 | this.path = path; 43 | this.queryTransformer = queryTransformer; 44 | this.resultTransformer = resultTransformer; 45 | this.errorPathTransformer = errorPathTransformer; 46 | this.fragments = fragments; 47 | } 48 | 49 | public transformRequest(originalRequest: Request): Request { 50 | const document = originalRequest.document; 51 | 52 | const pathLength = this.path.length; 53 | let index = 0; 54 | const newDocument = visit(document, { 55 | [Kind.FIELD]: { 56 | enter: (node) => { 57 | if (index === pathLength || node.name.value !== this.path[index]) { 58 | return false; 59 | } 60 | 61 | index++; 62 | 63 | if (index === pathLength) { 64 | const selectionSet = this.queryTransformer( 65 | node.selectionSet, 66 | this.fragments, 67 | ); 68 | 69 | return { 70 | ...node, 71 | selectionSet, 72 | }; 73 | } 74 | }, 75 | leave: () => { 76 | index--; 77 | }, 78 | }, 79 | }); 80 | return { 81 | ...originalRequest, 82 | document: newDocument, 83 | }; 84 | } 85 | 86 | public transformResult(originalResult: Result): Result { 87 | const data = this.transformData(originalResult.data); 88 | const errors = originalResult.errors; 89 | return { 90 | data, 91 | errors: errors != null ? this.transformErrors(errors) : undefined, 92 | }; 93 | } 94 | 95 | private transformData(data: any): any { 96 | const leafIndex = this.path.length - 1; 97 | let index = 0; 98 | let newData = data; 99 | if (newData) { 100 | let next = this.path[index]; 101 | while (index < leafIndex) { 102 | if (data[next]) { 103 | newData = newData[next]; 104 | } else { 105 | break; 106 | } 107 | index++; 108 | next = this.path[index]; 109 | } 110 | newData[next] = this.resultTransformer(newData[next]); 111 | } 112 | return newData; 113 | } 114 | 115 | private transformErrors( 116 | errors: ReadonlyArray, 117 | ): ReadonlyArray { 118 | return errors.map((error) => { 119 | const path: ReadonlyArray = error.path; 120 | 121 | let match = true; 122 | let index = 0; 123 | while (index < this.path.length) { 124 | if (path[index] !== this.path[index]) { 125 | match = false; 126 | break; 127 | } 128 | index++; 129 | } 130 | 131 | const newPath = match 132 | ? path 133 | .slice(0, index) 134 | .concat(this.errorPathTransformer(path.slice(index))) 135 | : path; 136 | 137 | return new GraphQLError( 138 | error.message, 139 | error.nodes, 140 | error.source, 141 | error.positions, 142 | newPath, 143 | error.originalError, 144 | error.extensions, 145 | ); 146 | }); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/wrap/transforms/TransformRootFields.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema, GraphQLField, GraphQLFieldConfig } from 'graphql'; 2 | 3 | import { Transform, Request, FieldNodeTransformer } from '../../Interfaces'; 4 | 5 | import TransformObjectFields from './TransformObjectFields'; 6 | 7 | export type RootTransformer = ( 8 | operation: 'Query' | 'Mutation' | 'Subscription', 9 | fieldName: string, 10 | field: GraphQLField, 11 | ) => GraphQLFieldConfig | RenamedField | null | undefined; 12 | 13 | type RenamedField = { name: string; field?: GraphQLFieldConfig }; 14 | 15 | export default class TransformRootFields implements Transform { 16 | private readonly transformer: TransformObjectFields; 17 | 18 | constructor( 19 | rootFieldTransformer: RootTransformer, 20 | fieldNodeTransformer?: FieldNodeTransformer, 21 | ) { 22 | const rootToObjectFieldTransformer = ( 23 | typeName: string, 24 | fieldName: string, 25 | field: GraphQLField, 26 | ) => { 27 | if ( 28 | typeName === 'Query' || 29 | typeName === 'Mutation' || 30 | typeName === 'Subscription' 31 | ) { 32 | return rootFieldTransformer(typeName, fieldName, field); 33 | } 34 | 35 | return undefined; 36 | }; 37 | this.transformer = new TransformObjectFields( 38 | rootToObjectFieldTransformer, 39 | fieldNodeTransformer, 40 | ); 41 | } 42 | 43 | public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { 44 | return this.transformer.transformSchema(originalSchema); 45 | } 46 | 47 | public transformRequest(originalRequest: Request): Request { 48 | return this.transformer.transformRequest(originalRequest); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/wrap/transforms/WrapFields.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema, GraphQLObjectType } from 'graphql'; 2 | 3 | import { Transform, Request } from '../../Interfaces'; 4 | import { 5 | hoistFieldNodes, 6 | healSchema, 7 | appendFields, 8 | removeFields, 9 | } from '../../utils/index'; 10 | import { 11 | defaultMergedResolver, 12 | createMergedResolver, 13 | } from '../../stitch/index'; 14 | 15 | import MapFields from './MapFields'; 16 | 17 | export default class WrapFields implements Transform { 18 | private readonly outerTypeName: string; 19 | private readonly wrappingFieldNames: Array; 20 | private readonly wrappingTypeNames: Array; 21 | private readonly numWraps: number; 22 | private readonly fieldNames: Array; 23 | private readonly transformer: Transform; 24 | 25 | constructor( 26 | outerTypeName: string, 27 | wrappingFieldNames: Array, 28 | wrappingTypeNames: Array, 29 | fieldNames?: Array, 30 | ) { 31 | this.outerTypeName = outerTypeName; 32 | this.wrappingFieldNames = wrappingFieldNames; 33 | this.wrappingTypeNames = wrappingTypeNames; 34 | this.numWraps = wrappingFieldNames.length; 35 | this.fieldNames = fieldNames; 36 | 37 | const remainingWrappingFieldNames = this.wrappingFieldNames.slice(); 38 | const outerMostWrappingFieldName = remainingWrappingFieldNames.shift(); 39 | this.transformer = new MapFields({ 40 | [outerTypeName]: { 41 | [outerMostWrappingFieldName]: (fieldNode, fragments) => 42 | hoistFieldNodes({ 43 | fieldNode, 44 | path: remainingWrappingFieldNames, 45 | fieldNames: this.fieldNames, 46 | fragments, 47 | }), 48 | }, 49 | }); 50 | } 51 | 52 | public transformSchema(schema: GraphQLSchema): GraphQLSchema { 53 | const typeMap = schema.getTypeMap(); 54 | 55 | const targetFields = removeFields( 56 | typeMap, 57 | this.outerTypeName, 58 | !this.fieldNames 59 | ? () => true 60 | : (fieldName) => this.fieldNames.includes(fieldName), 61 | ); 62 | 63 | let wrapIndex = this.numWraps - 1; 64 | 65 | const innerMostWrappingTypeName = this.wrappingTypeNames[wrapIndex]; 66 | appendFields(typeMap, innerMostWrappingTypeName, targetFields); 67 | 68 | for (wrapIndex--; wrapIndex > -1; wrapIndex--) { 69 | appendFields(typeMap, this.wrappingTypeNames[wrapIndex], { 70 | [this.wrappingFieldNames[wrapIndex + 1]]: { 71 | type: typeMap[ 72 | this.wrappingTypeNames[wrapIndex + 1] 73 | ] as GraphQLObjectType, 74 | resolve: defaultMergedResolver, 75 | }, 76 | }); 77 | } 78 | 79 | appendFields(typeMap, this.outerTypeName, { 80 | [this.wrappingFieldNames[0]]: { 81 | type: typeMap[this.wrappingTypeNames[0]] as GraphQLObjectType, 82 | resolve: createMergedResolver({ dehoist: true }), 83 | }, 84 | }); 85 | 86 | healSchema(schema); 87 | 88 | return this.transformer.transformSchema(schema); 89 | } 90 | 91 | public transformRequest(originalRequest: Request): Request { 92 | return this.transformer.transformRequest(originalRequest); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/wrap/transforms/WrapQuery.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FieldNode, 3 | visit, 4 | Kind, 5 | SelectionNode, 6 | SelectionSetNode, 7 | } from 'graphql'; 8 | 9 | import { Transform, Request, Result } from '../../Interfaces'; 10 | 11 | export type QueryWrapper = ( 12 | subtree: SelectionSetNode, 13 | ) => SelectionNode | SelectionSetNode; 14 | 15 | export default class WrapQuery implements Transform { 16 | private readonly wrapper: QueryWrapper; 17 | private readonly extractor: (result: any) => any; 18 | private readonly path: Array; 19 | 20 | constructor( 21 | path: Array, 22 | wrapper: QueryWrapper, 23 | extractor: (result: any) => any, 24 | ) { 25 | this.path = path; 26 | this.wrapper = wrapper; 27 | this.extractor = extractor; 28 | } 29 | 30 | public transformRequest(originalRequest: Request): Request { 31 | const document = originalRequest.document; 32 | const fieldPath: Array = []; 33 | const ourPath = JSON.stringify(this.path); 34 | const newDocument = visit(document, { 35 | [Kind.FIELD]: { 36 | enter: (node: FieldNode) => { 37 | fieldPath.push(node.name.value); 38 | if (ourPath === JSON.stringify(fieldPath)) { 39 | const wrapResult = this.wrapper(node.selectionSet); 40 | 41 | // Selection can be either a single selection or a selection set. If it's just one selection, 42 | // let's wrap it in a selection set. Otherwise, keep it as is. 43 | const selectionSet = 44 | wrapResult != null && wrapResult.kind === Kind.SELECTION_SET 45 | ? wrapResult 46 | : { 47 | kind: Kind.SELECTION_SET, 48 | selections: [wrapResult], 49 | }; 50 | 51 | return { 52 | ...node, 53 | selectionSet, 54 | }; 55 | } 56 | }, 57 | leave: () => { 58 | fieldPath.pop(); 59 | }, 60 | }, 61 | }); 62 | return { 63 | ...originalRequest, 64 | document: newDocument, 65 | }; 66 | } 67 | 68 | public transformResult(originalResult: Result): Result { 69 | const rootData = originalResult.data; 70 | if (rootData != null) { 71 | let data = rootData; 72 | const path = [...this.path]; 73 | while (path.length > 1) { 74 | const next = path.shift(); 75 | if (data[next]) { 76 | data = data[next]; 77 | } 78 | } 79 | data[path[0]] = this.extractor(data[path[0]]); 80 | } 81 | 82 | return { 83 | data: rootData, 84 | errors: originalResult.errors, 85 | }; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/wrap/transforms/WrapType.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema } from 'graphql'; 2 | 3 | import { Transform, Request } from '../../Interfaces'; 4 | 5 | import WrapFields from './WrapFields'; 6 | 7 | export default class WrapType implements Transform { 8 | private readonly transformer: Transform; 9 | 10 | constructor(outerTypeName: string, innerTypeName: string, fieldName: string) { 11 | this.transformer = new WrapFields( 12 | outerTypeName, 13 | [fieldName], 14 | [innerTypeName], 15 | undefined, 16 | ); 17 | } 18 | 19 | public transformSchema(schema: GraphQLSchema): GraphQLSchema { 20 | return this.transformer.transformSchema(schema); 21 | } 22 | 23 | public transformRequest(originalRequest: Request): Request { 24 | return this.transformer.transformRequest(originalRequest); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/wrap/transforms/index.ts: -------------------------------------------------------------------------------- 1 | export { default as CheckResultAndHandleErrors } from './CheckResultAndHandleErrors'; 2 | export { default as ExpandAbstractTypes } from './ExpandAbstractTypes'; 3 | export { default as AddReplacementSelectionSets } from './AddReplacementSelectionSets'; 4 | export { default as AddMergedTypeSelectionSets } from './AddMergedTypeSelectionSets'; 5 | export { default as AddArgumentsAsVariables } from './AddArgumentsAsVariables'; 6 | export { default as FilterToSchema } from './FilterToSchema'; 7 | export { default as AddTypenameToAbstract } from './AddTypenameToAbstract'; 8 | 9 | export { default as RenameTypes } from './RenameTypes'; 10 | export { default as FilterTypes } from './FilterTypes'; 11 | export { default as RenameRootTypes } from './RenameRootTypes'; 12 | export { default as TransformCompositeFields } from './TransformCompositeFields'; 13 | export { default as TransformRootFields } from './TransformRootFields'; 14 | export { default as RenameRootFields } from './RenameRootFields'; 15 | export { default as FilterRootFields } from './FilterRootFields'; 16 | export { default as TransformObjectFields } from './TransformObjectFields'; 17 | export { default as RenameObjectFields } from './RenameObjectFields'; 18 | export { default as FilterObjectFields } from './FilterObjectFields'; 19 | export { default as TransformInterfaceFields } from './TransformInterfaceFields'; 20 | export { default as RenameInterfaceFields } from './RenameInterfaceFields'; 21 | export { default as FilterInterfaceFields } from './FilterInterfaceFields'; 22 | export { default as TransformQuery } from './TransformQuery'; 23 | 24 | export { default as ExtendSchema } from './ExtendSchema'; 25 | export { default as WrapType } from './WrapType'; 26 | export { default as WrapFields } from './WrapFields'; 27 | export { default as HoistField } from './HoistField'; 28 | export { default as MapFields } from './MapFields'; 29 | 30 | // superseded by AddReplacementFragments 31 | export { default as ReplaceFieldWithFragment } from './ReplaceFieldWithFragment'; 32 | // superseded by AddReplacementSelectionSets 33 | export { default as AddReplacementFragments } from './AddReplacementFragments'; 34 | // superseded by TransformQuery 35 | export { default as WrapQuery } from './WrapQuery'; 36 | export { default as ExtractField } from './ExtractField'; 37 | -------------------------------------------------------------------------------- /src/wrap/wrapSchema.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema } from 'graphql'; 2 | 3 | import { addResolversToSchema } from '../generate/index'; 4 | import { Transform, SubschemaConfig, isSubschemaConfig } from '../Interfaces'; 5 | import { cloneSchema } from '../utils/index'; 6 | 7 | import { generateProxyingResolvers, stripResolvers } from './resolvers'; 8 | import { applySchemaTransforms } from './transforms'; 9 | 10 | export function wrapSchema( 11 | subschemaOrSubschemaConfig: GraphQLSchema | SubschemaConfig, 12 | transforms?: Array, 13 | ): GraphQLSchema { 14 | const subschemaConfig: SubschemaConfig = isSubschemaConfig( 15 | subschemaOrSubschemaConfig, 16 | ) 17 | ? subschemaOrSubschemaConfig 18 | : { schema: subschemaOrSubschemaConfig }; 19 | 20 | const schema = cloneSchema(subschemaConfig.schema); 21 | stripResolvers(schema); 22 | 23 | addResolversToSchema({ 24 | schema, 25 | resolvers: generateProxyingResolvers({ subschemaConfig, transforms }), 26 | resolverValidationOptions: { 27 | allowResolversNotInSchema: true, 28 | }, 29 | }); 30 | 31 | let schemaTransforms: Array = []; 32 | if (subschemaConfig.transforms != null) { 33 | schemaTransforms = schemaTransforms.concat(subschemaConfig.transforms); 34 | } 35 | if (transforms != null) { 36 | schemaTransforms = schemaTransforms.concat(transforms); 37 | } 38 | 39 | return applySchemaTransforms(schema, schemaTransforms); 40 | } 41 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "./src", 4 | "outDir": "./dist", 5 | 6 | "lib": ["es7", "esnext.asynciterable", "dom"], 7 | 8 | "target": "es5", 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "esModuleInterop": true, 12 | "importHelpers": true, 13 | 14 | "noImplicitAny": true, 15 | "noImplicitUseStrict": true, 16 | "suppressImplicitAnyIndexErrors": true, 17 | "noUnusedLocals": true, 18 | 19 | "experimentalDecorators": true, 20 | "emitDecoratorMetadata": true, 21 | 22 | "sourceMap": true, 23 | "declaration": true, 24 | "removeComments": false 25 | }, 26 | "exclude": ["node_modules", "dist"] 27 | } 28 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "library", 3 | "out": "typedoc", 4 | "excludeNotExported": true, 5 | "excludePrivate": true, 6 | "excludeExternals": true, 7 | "includeVersion": true, 8 | "hideGenerator": true, 9 | "listInvalidSymbolLinks": true, 10 | "categoryOrder": [ 11 | "Schema Generation", 12 | "Schema Delegation", 13 | "Schema Wrapping", 14 | "Schema Stitching", 15 | "Schema Utility", 16 | "Type Utility", 17 | "Field Node Utility", 18 | "Fragment Utility", 19 | "Selection Set Utility", 20 | "Input Value Utility", 21 | "toConfig Polyfill", 22 | "*" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'apollo-upload-client' 2 | --------------------------------------------------------------------------------