From 46ceaaa31b99e66b43382791b2df67445e9ce901 Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Mon, 30 Mar 2020 13:25:13 +0200 Subject: [PATCH 1/9] Make it independent from graphql-tools --- .github/workflows/main.yml | 6 +- package.json | 7 +- packages/common/package.json | 9 +- .../src/extract-resolvers-from-schema.ts | 2 +- packages/common/src/index.ts | 10 +- packages/common/src/iterate.ts | 87 + packages/common/src/merge.ts | 25 + packages/common/src/resolvers-composition.ts | 5 +- packages/common/src/schema-error.ts | 11 + packages/common/src/stub.ts | 64 + packages/common/src/transport-input.ts | 46 + .../extract-resolvers-from-schema.spec.ts | 2 +- packages/common/tests/get-directives.spec.ts | 2 +- .../print-schema-with-directives.spec.ts | 3 +- .../tests/resolvers-composition.spec.ts | 4 +- packages/core/package.json | 2 + packages/core/src/index.ts | 8 +- packages/core/src/{ => load}/documents.ts | 4 +- packages/core/src/load/index.ts | 3 + .../load-typedefs/collect-sources.ts | 12 +- .../src/{ => load}/load-typedefs/load-file.ts | 2 +- .../src/{ => load}/load-typedefs/options.ts | 2 +- .../src/{ => load}/load-typedefs/parse.ts | 4 +- packages/core/src/{ => load}/schema.ts | 8 +- .../{load-typedefs.ts => load/typedefs.ts} | 12 +- .../src/{ => utils}/filter-document-kind.ts | 2 +- .../{ => utils}/import-parser/definition.ts | 26 +- .../src/{ => utils}/import-parser/index.ts | 42 +- packages/core/src/utils/index.ts | 2 + packages/core/src/utils/pointers.ts | 2 +- .../extensions/export-schema-with-def.js | 2 +- .../schema-dir/extensions/export-schema.js | 2 +- packages/core/tsconfig.json | 2 +- packages/graphql-tag-pluck/package.json | 4 +- .../tests/test-files/loaders/with-extend.js | 2 +- packages/loaders/url/package.json | 2 +- packages/loaders/url/src/index.ts | 7 +- packages/loaders/url/tests/url-loader.spec.ts | 2 +- packages/schema-merging/package.json | 3 +- .../schema-merging/src/merge-resolvers.ts | 4 +- packages/schema-merging/src/merge-schemas.ts | 13 +- .../src/typedefs-mergers/arguments.ts | 6 +- .../src/typedefs-mergers/comments.ts | 17 +- .../src/typedefs-mergers/enum-values.ts | 2 +- .../src/typedefs-mergers/fields.ts | 8 +- .../src/typedefs-mergers/input-type.ts | 4 +- .../src/typedefs-mergers/interface.ts | 2 +- .../merge-named-type-array.ts | 6 +- .../src/typedefs-mergers/merge-nodes.ts | 2 +- .../src/typedefs-mergers/merge-typedefs.ts | 36 +- .../src/typedefs-mergers/type.ts | 2 +- .../tests/merge-schemas.spec.ts | 2 +- .../tests/merge-typedefs.spec.ts | 3 +- packages/schema/.gitignore | 72 + packages/schema/.npmignore | 13 + packages/schema/package.json | 42 + .../schema/src/add-resolvers-to-schema.ts | 218 ++ .../schema/src/add-schema-level-resolvers.ts | 62 + .../schema/src/assert-resolvers-present.ts | 52 + .../schema/src/attach-directive-resolvers.ts | 44 + .../src/build-schema-from-type-definitions.ts | 46 + .../src/check-for-resolve-type-resolver.ts | 22 + packages/schema/src/concatenate-type-defs.ts | 31 + packages/schema/src/decorate-with-logger.ts | 49 + .../src/extend-resolvers-from-interfaces.ts | 24 + packages/schema/src/extension-definitions.ts | 38 + ...chema-directive-from-directive-resolver.ts | 3 +- packages/schema/src/heal.ts | 137 + packages/schema/src/index.ts | 19 + packages/schema/src/logger.ts | 29 + packages/schema/src/make-executable-schema.ts | 103 + .../src/make-remote-executable-schema.ts | 179 ++ packages/schema/src/types.ts | 218 ++ .../src/visitor/schema-directive-visitor.ts | 285 ++ packages/schema/src/visitor/schema-visitor.ts | 100 + .../src/visitor/value-from-ast-untyped.ts | 30 + packages/schema/src/visitor/visit-schema.ts | 267 ++ .../schema/tests/fixtures/circularSchemaA.ts | 12 + .../schema/tests/fixtures/circularSchemaB.ts | 12 + .../schema/tests/schema-generation.spec.ts | 2856 +++++++++++++++++ packages/schema/tsconfig.json | 23 + packages/types/.gitignore | 72 + packages/types/.npmignore | 13 + packages/types/package.json | 33 + packages/types/src/index.ts | 54 + packages/types/tsconfig.json | 24 + packages/version/.gitignore | 72 + packages/version/.npmignore | 13 + packages/version/package.json | 33 + packages/version/src/index.ts | 17 + packages/version/tsconfig.json | 24 + yarn.lock | 181 +- 92 files changed, 5829 insertions(+), 240 deletions(-) create mode 100644 packages/common/src/iterate.ts create mode 100644 packages/common/src/merge.ts create mode 100644 packages/common/src/schema-error.ts create mode 100644 packages/common/src/stub.ts create mode 100644 packages/common/src/transport-input.ts rename packages/core/src/{ => load}/documents.ts (89%) create mode 100644 packages/core/src/load/index.ts rename packages/core/src/{ => load}/load-typedefs/collect-sources.ts (95%) rename packages/core/src/{ => load}/load-typedefs/load-file.ts (96%) rename packages/core/src/{ => load}/load-typedefs/options.ts (94%) rename packages/core/src/{ => load}/load-typedefs/parse.ts (97%) rename packages/core/src/{ => load}/schema.ts (93%) rename packages/core/src/{load-typedefs.ts => load/typedefs.ts} (93%) rename packages/core/src/{ => utils}/filter-document-kind.ts (95%) rename packages/core/src/{ => utils}/import-parser/definition.ts (87%) rename packages/core/src/{ => utils}/import-parser/index.ts (89%) create mode 100644 packages/core/src/utils/index.ts create mode 100644 packages/schema/.gitignore create mode 100644 packages/schema/.npmignore create mode 100644 packages/schema/package.json create mode 100644 packages/schema/src/add-resolvers-to-schema.ts create mode 100644 packages/schema/src/add-schema-level-resolvers.ts create mode 100644 packages/schema/src/assert-resolvers-present.ts create mode 100644 packages/schema/src/attach-directive-resolvers.ts create mode 100644 packages/schema/src/build-schema-from-type-definitions.ts create mode 100644 packages/schema/src/check-for-resolve-type-resolver.ts create mode 100644 packages/schema/src/concatenate-type-defs.ts create mode 100644 packages/schema/src/decorate-with-logger.ts create mode 100644 packages/schema/src/extend-resolvers-from-interfaces.ts create mode 100644 packages/schema/src/extension-definitions.ts rename packages/{common => schema}/src/get-schema-directive-from-directive-resolver.ts (84%) create mode 100644 packages/schema/src/heal.ts create mode 100644 packages/schema/src/index.ts create mode 100644 packages/schema/src/logger.ts create mode 100644 packages/schema/src/make-executable-schema.ts create mode 100644 packages/schema/src/make-remote-executable-schema.ts create mode 100644 packages/schema/src/types.ts create mode 100644 packages/schema/src/visitor/schema-directive-visitor.ts create mode 100644 packages/schema/src/visitor/schema-visitor.ts create mode 100644 packages/schema/src/visitor/value-from-ast-untyped.ts create mode 100644 packages/schema/src/visitor/visit-schema.ts create mode 100644 packages/schema/tests/fixtures/circularSchemaA.ts create mode 100644 packages/schema/tests/fixtures/circularSchemaB.ts create mode 100644 packages/schema/tests/schema-generation.spec.ts create mode 100644 packages/schema/tsconfig.json create mode 100644 packages/types/.gitignore create mode 100644 packages/types/.npmignore create mode 100644 packages/types/package.json create mode 100644 packages/types/src/index.ts create mode 100644 packages/types/tsconfig.json create mode 100644 packages/version/.gitignore create mode 100644 packages/version/.npmignore create mode 100644 packages/version/package.json create mode 100644 packages/version/src/index.ts create mode 100644 packages/version/tsconfig.json diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a9c818d8..a37877d3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,6 +25,7 @@ jobs: id: yarn-cache run: echo "::set-output name=dir::$(yarn cache dir)" - uses: actions/cache@v1 + name: Yarn Cache with: path: ${{ steps.yarn-cache.outputs.dir }} key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} @@ -72,7 +73,10 @@ jobs: - name: Get yarn cache id: yarn-cache run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v1 + if: matrix.os == 'ubuntu-latest' + - name: Yarn Cache + uses: actions/cache@v1 + if: matrix.os == 'ubuntu-latest' with: path: ${{ steps.yarn-cache.outputs.dir }} key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} diff --git a/package.json b/package.json index fe990899..fd9bc34d 100644 --- a/package.json +++ b/package.json @@ -32,20 +32,19 @@ ] }, "devDependencies": { - "@ardatan/bob": "0.2.7", "@types/jest": "25.2.1", "@types/lodash": "4.14.149", + "bob-the-bundler": "0.3.4", "graphql": "15.0.0", - "graphql-tools-fork": "9.0.1", "husky": "4.2.3", "jest": "25.2.7", "lerna": "3.20.2", "lint-staged": "10.1.2", "prettier": "2.0.2", "rimraf": "3.0.2", + "tslint": "6.1.1", "ts-jest": "25.3.1", - "typescript": "3.8.3", - "tslint": "6.1.1" + "typescript": "3.8.3" }, "workspaces": [ "packages/*", diff --git a/packages/common/package.json b/packages/common/package.json index e4d3db81..6a856f62 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -27,13 +27,16 @@ "input": "./src/index.ts" }, "devDependencies": { - "@types/aggregate-error": "1.0.1" + "@types/aggregate-error": "1.0.1", + "@types/lodash.get": "4.4.6", + "@types/lodash.set": "4.3.6" }, "dependencies": { + "@graphql-toolkit/types": "0.10.2", "aggregate-error": "3.0.1", "camel-case": "4.1.1", - "graphql-tools-fork": "9.0.1", - "lodash": "4.17.15" + "lodash.get": "4.4.2", + "lodash.set": "4.3.2" }, "publishConfig": { "access": "public", diff --git a/packages/common/src/extract-resolvers-from-schema.ts b/packages/common/src/extract-resolvers-from-schema.ts index 06a32a57..6ee733bf 100644 --- a/packages/common/src/extract-resolvers-from-schema.ts +++ b/packages/common/src/extract-resolvers-from-schema.ts @@ -9,8 +9,8 @@ import { isEnumType, isUnionType, } from 'graphql'; -import { IResolvers } from 'graphql-tools-fork'; import { extractFieldResolversFromObjectType } from './extract-field-resolvers-from-object-type'; +import { IResolvers } from '@graphql-toolkit/types'; export interface ExtractResolversFromSchemaOptions { selectedTypeDefs?: DocumentNode; diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index ed7781e6..4f37552c 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -1,14 +1,11 @@ export * from './loaders'; export * from './helpers'; export * from './debug-log'; -export * from './extract-field-resolvers-from-object-type'; -export * from './extract-resolvers-from-schema'; export * from './fix-windows-path'; export * from './flatten-array'; export * from './get-directives'; export * from './get-fields-with-directives'; export * from './get-implementing-types'; -export * from './get-schema-directive-from-directive-resolver'; export * from './print-schema-with-directives'; export * from './get-fields-with-directives'; export * from './validate-documents'; @@ -19,4 +16,11 @@ export * from './parse-graphql-sdl'; export * from './get-user-types-from-schema'; export * from './create-schema-definition'; export * from './build-operation-for-field'; +export * from './schema-error'; +export * from './iterate'; +export * from './merge'; +export * from './stub'; +export * from './transport-input'; +export * from './extract-field-resolvers-from-object-type'; +export * from './extract-resolvers-from-schema'; export * from './types'; diff --git a/packages/common/src/iterate.ts b/packages/common/src/iterate.ts new file mode 100644 index 00000000..0e67b610 --- /dev/null +++ b/packages/common/src/iterate.ts @@ -0,0 +1,87 @@ +import { getNamedType, GraphQLSchema, isObjectType, isInputObjectType, GraphQLField, GraphQLInputType } from 'graphql'; + +export type IndexedObject = { [key: string]: V } | ReadonlyArray; +export type IFieldIteratorFn = (fieldDef: GraphQLField, typeName: string, fieldName: string) => void; +export type IDefaultValueIteratorFn = (type: GraphQLInputType, value: any) => void; + +export function forEachField(schema: GraphQLSchema, fn: IFieldIteratorFn): void { + const typeMap = schema.getTypeMap(); + Object.keys(typeMap).forEach((typeName) => { + const type = typeMap[typeName]; + + // TODO: maybe have an option to include these? + if (!getNamedType(type).name.startsWith('__') && isObjectType(type)) { + const fields = type.getFields(); + Object.keys(fields).forEach((fieldName) => { + const field = fields[fieldName]; + fn(field, typeName, fieldName); + }); + } + }); +} + +export function forEachDefaultValue(schema: GraphQLSchema, fn: IDefaultValueIteratorFn): void { + const typeMap = schema.getTypeMap(); + Object.keys(typeMap).forEach((typeName) => { + const type = typeMap[typeName]; + + if (!getNamedType(type).name.startsWith('__')) { + if (isObjectType(type)) { + const fields = type.getFields(); + Object.keys(fields).forEach((fieldName) => { + const field = fields[fieldName]; + + field.args.forEach((arg) => { + arg.defaultValue = fn(arg.type, arg.defaultValue); + }); + }); + } else if (isInputObjectType(type)) { + const fields = type.getFields(); + Object.keys(fields).forEach((fieldName) => { + const field = fields[fieldName]; + field.defaultValue = fn(field.type, field.defaultValue); + }); + } + } + }); +} + +// A more powerful version of each that has the ability to replace or remove +// array or object keys. +export function updateEachKey( + arrayOrObject: IndexedObject, + // The callback can return nothing to leave the key untouched, null to remove + // the key from the array or object, or a non-null V to replace the value. + updater: (value: V, key: string) => void | null | V +) { + let deletedCount = 0; + + Object.keys(arrayOrObject).forEach((key) => { + const result = updater(arrayOrObject[key], key); + + if (typeof result === 'undefined') { + return; + } + + if (result === null) { + delete arrayOrObject[key]; + deletedCount++; + return; + } + + arrayOrObject[key] = result; + }); + + if (deletedCount > 0 && Array.isArray(arrayOrObject)) { + // Remove any holes from the array due to deleted elements. + arrayOrObject.splice(0).forEach((elem) => { + arrayOrObject.push(elem); + }); + } +} + +export function each(arrayOrObject: IndexedObject, callback: (value: V, key: string) => void) { + Object.keys(arrayOrObject).forEach((key) => { + callback(arrayOrObject[key], key); + }); +} diff --git a/packages/common/src/merge.ts b/packages/common/src/merge.ts new file mode 100644 index 00000000..0b5cd0ab --- /dev/null +++ b/packages/common/src/merge.ts @@ -0,0 +1,25 @@ +export function mergeDeep(target: any, ...sources: any): any { + const output = { + ...target, + }; + sources.forEach((source: any) => { + if (isObject(target) && isObject(source)) { + Object.keys(source).forEach((key) => { + if (isObject(source[key])) { + if (!(key in target)) { + Object.assign(output, { [key]: source[key] }); + } else { + output[key] = mergeDeep(target[key], source[key]); + } + } else { + Object.assign(output, { [key]: source[key] }); + } + }); + } + }); + return output; +} + +function isObject(item: any): boolean { + return item && typeof item === 'object' && !Array.isArray(item); +} diff --git a/packages/common/src/resolvers-composition.ts b/packages/common/src/resolvers-composition.ts index a8ed5754..22d91d09 100644 --- a/packages/common/src/resolvers-composition.ts +++ b/packages/common/src/resolvers-composition.ts @@ -1,7 +1,8 @@ -import { IResolvers, IFieldResolver } from 'graphql-tools-fork'; +import { IResolvers, IFieldResolver } from '@graphql-toolkit/types'; import { chainFunctions, asArray } from './helpers'; import { flattenArray } from './flatten-array'; -import { get, set } from 'lodash'; +import get from 'lodash.get'; +import set from 'lodash.set'; import { isScalarType } from 'graphql'; export type ResolversComposition = IFieldResolver> = ( diff --git a/packages/common/src/schema-error.ts b/packages/common/src/schema-error.ts new file mode 100644 index 00000000..3b94c9aa --- /dev/null +++ b/packages/common/src/schema-error.ts @@ -0,0 +1,11 @@ +// @schemaDefinition: A GraphQL type schema in shorthand +// @resolvers: Definitions for resolvers to be merged with schema +export class SchemaError extends Error { + public message: string; + + constructor(message: string) { + super(message); + this.message = message; + Error.captureStackTrace(this, this.constructor); + } +} diff --git a/packages/common/src/stub.ts b/packages/common/src/stub.ts new file mode 100644 index 00000000..15d85273 --- /dev/null +++ b/packages/common/src/stub.ts @@ -0,0 +1,64 @@ +import { + GraphQLObjectType, + GraphQLInterfaceType, + GraphQLInputObjectType, + GraphQLString, + GraphQLNamedType, + GraphQLInt, + GraphQLFloat, + GraphQLBoolean, + GraphQLID, + isObjectType, + isInterfaceType, + isInputObjectType, +} from 'graphql'; + +export function createNamedStub( + name: string, + type: 'object' | 'interface' | 'input' +): GraphQLObjectType | GraphQLInputObjectType | GraphQLInterfaceType { + let constructor: any; + if (type === 'object') { + constructor = GraphQLObjectType; + } else if (type === 'interface') { + constructor = GraphQLInterfaceType; + } else { + constructor = GraphQLInputObjectType; + } + + return new constructor({ + name, + fields: { + __fake: { + type: GraphQLString, + }, + }, + }); +} + +export function isStub(type: GraphQLNamedType): boolean { + if (isObjectType(type) || isInterfaceType(type) || isInputObjectType(type)) { + const fields = type.getFields(); + const fieldNames = Object.keys(fields); + return fieldNames.length === 1 && fields[fieldNames[0]].name === '__fake'; + } + + return false; +} + +export function getBuiltInForStub(type: GraphQLNamedType): GraphQLNamedType { + switch (type.name) { + case GraphQLInt.name: + return GraphQLInt; + case GraphQLFloat.name: + return GraphQLFloat; + case GraphQLString.name: + return GraphQLString; + case GraphQLBoolean.name: + return GraphQLBoolean; + case GraphQLID.name: + return GraphQLID; + default: + return type; + } +} diff --git a/packages/common/src/transport-input.ts b/packages/common/src/transport-input.ts new file mode 100644 index 00000000..464da237 --- /dev/null +++ b/packages/common/src/transport-input.ts @@ -0,0 +1,46 @@ +import { + GraphQLEnumType, + GraphQLInputType, + GraphQLScalarType, + getNullableType, + isLeafType, + isListType, + isInputObjectType, +} from 'graphql'; + +type InputValueTransformer = (type: GraphQLEnumType | GraphQLScalarType, originalValue: any) => any; + +export function transformInputValue(type: GraphQLInputType, value: any, transformer: InputValueTransformer) { + if (value == null) { + return value; + } + + const nullableType = getNullableType(type); + + if (isLeafType(nullableType)) { + return transformer(nullableType, value); + } else if (isListType(nullableType)) { + return value.map((listMember: any) => transformInputValue(nullableType.ofType, listMember, transformer)); + } else if (isInputObjectType(nullableType)) { + const fields = nullableType.getFields(); + const newValue = {}; + Object.keys(value).forEach((key) => { + newValue[key] = transformInputValue(fields[key].type, value[key], transformer); + }); + return newValue; + } + + // unreachable, no other possible return value +} + +export function serializeInputValue(type: GraphQLInputType, value: any) { + return transformInputValue(type, value, (t, v) => t.serialize(v)); +} + +export function parseInputValue(type: GraphQLInputType, value: any) { + return transformInputValue(type, value, (t, v) => t.parseValue(v)); +} + +export function parseInputValueLiteral(type: GraphQLInputType, value: any) { + return transformInputValue(type, value, (t, v) => t.parseLiteral(v, {})); +} diff --git a/packages/common/tests/extract-resolvers-from-schema.spec.ts b/packages/common/tests/extract-resolvers-from-schema.spec.ts index 34d07ec1..4475c1e8 100644 --- a/packages/common/tests/extract-resolvers-from-schema.spec.ts +++ b/packages/common/tests/extract-resolvers-from-schema.spec.ts @@ -1,5 +1,5 @@ import { extractResolversFromSchema } from '../src'; -import { makeExecutableSchema } from 'graphql-tools-fork'; +import { makeExecutableSchema } from '@graphql-toolkit/schema'; import gql from 'graphql-tag'; describe('extractResolversFromSchema', () => { diff --git a/packages/common/tests/get-directives.spec.ts b/packages/common/tests/get-directives.spec.ts index a55824fd..9d922f59 100644 --- a/packages/common/tests/get-directives.spec.ts +++ b/packages/common/tests/get-directives.spec.ts @@ -1,5 +1,5 @@ import { GraphQLSchema } from 'graphql'; -import { makeExecutableSchema } from 'graphql-tools-fork'; +import { makeExecutableSchema } from '@graphql-toolkit/schema'; import { getDirectives } from '../src'; describe('getDirectives', () => { diff --git a/packages/common/tests/print-schema-with-directives.spec.ts b/packages/common/tests/print-schema-with-directives.spec.ts index 189e12cb..68e7865f 100644 --- a/packages/common/tests/print-schema-with-directives.spec.ts +++ b/packages/common/tests/print-schema-with-directives.spec.ts @@ -1,4 +1,5 @@ -import { makeExecutableSchema, RenameTypes, transformSchema } from 'graphql-tools-fork'; +import { RenameTypes, transformSchema } from 'graphql-tools'; +import { makeExecutableSchema } from '@graphql-toolkit/schema'; import { buildSchema, printSchema } from 'graphql'; import { printSchemaWithDirectives } from '../src'; import GraphQLJSON from 'graphql-type-json'; diff --git a/packages/common/tests/resolvers-composition.spec.ts b/packages/common/tests/resolvers-composition.spec.ts index 7bf590e2..673e9898 100644 --- a/packages/common/tests/resolvers-composition.spec.ts +++ b/packages/common/tests/resolvers-composition.spec.ts @@ -1,6 +1,6 @@ import gql from 'graphql-tag'; -import { composeResolvers, ResolversComposerMapping, extractResolversFromSchema } from '../src'; -import { makeExecutableSchema } from 'graphql-tools-fork'; +import { composeResolvers, ResolversComposerMapping } from '../src'; +import { makeExecutableSchema } from '@graphql-toolkit/schema'; import { execute, GraphQLScalarType, Kind } from 'graphql'; import { $$asyncIterator, createAsyncIterator } from 'iterall'; diff --git a/packages/core/package.json b/packages/core/package.json index 3cf5ca05..24b34c3c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -34,6 +34,8 @@ }, "dependencies": { "@graphql-toolkit/common": "0.10.2", + "@graphql-toolkit/types": "0.10.2", + "@graphql-toolkit/version": "0.10.2", "@graphql-toolkit/schema-merging": "0.10.2", "aggregate-error": "3.0.1", "globby": "11.0.0", diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a9610727..7744f1c2 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,5 +1,3 @@ -export * from './load-typedefs'; -export * from './schema'; -export * from './documents'; -export * from './filter-document-kind'; -export * from './import-parser'; +export * from '@graphql-toolkit/types'; +export * from './load'; +export * from './utils'; diff --git a/packages/core/src/documents.ts b/packages/core/src/load/documents.ts similarity index 89% rename from packages/core/src/documents.ts rename to packages/core/src/load/documents.ts index 08fe988b..0d1cbfe1 100644 --- a/packages/core/src/documents.ts +++ b/packages/core/src/load/documents.ts @@ -1,11 +1,11 @@ import { Source } from '@graphql-toolkit/common'; import { Kind } from 'graphql'; -import { LoadTypedefsOptions, loadTypedefs, loadTypedefsSync, UnnormalizedTypeDefPointer } from './load-typedefs'; +import { LoadTypedefsOptions, loadTypedefs, loadTypedefsSync, UnnormalizedTypeDefPointer } from './typedefs'; export const OPERATION_KINDS = [Kind.OPERATION_DEFINITION, Kind.FRAGMENT_DEFINITION]; export const NON_OPERATION_KINDS = Object.keys(Kind) .reduce((prev, v) => [...prev, Kind[v]], []) - .filter(v => !OPERATION_KINDS.includes(v)); + .filter((v) => !OPERATION_KINDS.includes(v)); export function loadDocuments( documentDef: UnnormalizedTypeDefPointer | UnnormalizedTypeDefPointer[], diff --git a/packages/core/src/load/index.ts b/packages/core/src/load/index.ts new file mode 100644 index 00000000..6aedf7ca --- /dev/null +++ b/packages/core/src/load/index.ts @@ -0,0 +1,3 @@ +export * from './documents'; +export * from './typedefs'; +export * from './schema'; diff --git a/packages/core/src/load-typedefs/collect-sources.ts b/packages/core/src/load/load-typedefs/collect-sources.ts similarity index 95% rename from packages/core/src/load-typedefs/collect-sources.ts rename to packages/core/src/load/load-typedefs/collect-sources.ts index c142f1f5..c811065f 100644 --- a/packages/core/src/load-typedefs/collect-sources.ts +++ b/packages/core/src/load/load-typedefs/collect-sources.ts @@ -1,11 +1,11 @@ import { Source, isDocumentString, parseGraphQLSDL, asArray, printSchemaWithDirectives } from '@graphql-toolkit/common'; import { isSchema, Kind, parse } from 'graphql'; import isGlob from 'is-glob'; -import { LoadTypedefsOptions } from '../load-typedefs'; +import { LoadTypedefsOptions } from '../typedefs'; import { loadFile, loadFileSync } from './load-file'; -import { stringToHash, useStack, StackNext, StackFn } from '../utils/helpers'; -import { useCustomLoader, useCustomLoaderSync } from '../utils/custom-loader'; -import { useQueue, useSyncQueue } from '../utils/queue'; +import { stringToHash, useStack, StackNext, StackFn } from '../../utils/helpers'; +import { useCustomLoader, useCustomLoaderSync } from '../../utils/custom-loader'; +import { useQueue, useSyncQueue } from '../../utils/queue'; type AddSource = (data: { pointer: string; source: Source; noCache?: boolean }) => void; type AddGlob = (data: { pointer: string; pointerOptions: any }) => void; @@ -26,7 +26,7 @@ export async function collectSources({ const globs: string[] = []; const globOptions: any = {}; const queue = useQueue({ concurrency: CONCURRENCY_LIMIT }); - const unixify = await import('unixify').then(m => m.default || m); + const unixify = await import('unixify').then((m) => m.default || m); const { addSource, addGlob, collect } = createHelpers({ sources, @@ -197,7 +197,7 @@ function includeIgnored< >({ options, unixify, globs }: { options: T; unixify: any; globs: string[] }) { if (options.ignore) { const ignoreList = asArray(options.ignore) - .map(g => `!(${g})`) + .map((g) => `!(${g})`) .map(unixify); if (ignoreList.length > 0) { diff --git a/packages/core/src/load-typedefs/load-file.ts b/packages/core/src/load/load-typedefs/load-file.ts similarity index 96% rename from packages/core/src/load-typedefs/load-file.ts rename to packages/core/src/load/load-typedefs/load-file.ts index 35b5cf5b..f3b6b1ed 100644 --- a/packages/core/src/load-typedefs/load-file.ts +++ b/packages/core/src/load/load-typedefs/load-file.ts @@ -1,5 +1,5 @@ import { Source, debugLog } from '@graphql-toolkit/common'; -import { LoadTypedefsOptions } from '../load-typedefs'; +import { LoadTypedefsOptions } from '../typedefs'; export async function loadFile(pointer: string, options: LoadTypedefsOptions): Promise { const cached = useCache({ pointer, options }); diff --git a/packages/core/src/load-typedefs/options.ts b/packages/core/src/load/load-typedefs/options.ts similarity index 94% rename from packages/core/src/load-typedefs/options.ts rename to packages/core/src/load/load-typedefs/options.ts index 7694ec30..f6854033 100644 --- a/packages/core/src/load-typedefs/options.ts +++ b/packages/core/src/load/load-typedefs/options.ts @@ -1,5 +1,5 @@ import { resolveBuiltinModule, resolveBuiltinModuleSync } from '@graphql-toolkit/common'; -import { LoadTypedefsOptions } from './../load-typedefs'; +import { LoadTypedefsOptions } from '../typedefs'; function applyDefaultOptions(options: LoadTypedefsOptions>) { options.cache = options.cache || {}; diff --git a/packages/core/src/load-typedefs/parse.ts b/packages/core/src/load/load-typedefs/parse.ts similarity index 97% rename from packages/core/src/load-typedefs/parse.ts rename to packages/core/src/load/load-typedefs/parse.ts index b8f55a35..2069389b 100644 --- a/packages/core/src/load-typedefs/parse.ts +++ b/packages/core/src/load/load-typedefs/parse.ts @@ -1,8 +1,8 @@ import { Source, printSchemaWithDirectives, fixSchemaAst } from '@graphql-toolkit/common'; import { printWithComments, resetComments } from '@graphql-toolkit/schema-merging'; import { Kind, parse, Source as GraphQLSource, DefinitionNode } from 'graphql'; -import { isEmptySDL, processImportSyntax, processImportSyntaxSync } from '../import-parser'; -import { filterKind } from '../filter-document-kind'; +import { isEmptySDL, processImportSyntax, processImportSyntaxSync } from '../../utils/import-parser'; +import { filterKind } from '../../utils/filter-document-kind'; type Options = any; type Input = { diff --git a/packages/core/src/schema.ts b/packages/core/src/load/schema.ts similarity index 93% rename from packages/core/src/schema.ts rename to packages/core/src/load/schema.ts index 511a3ae6..bbb32834 100644 --- a/packages/core/src/schema.ts +++ b/packages/core/src/load/schema.ts @@ -1,7 +1,7 @@ -import { loadTypedefs, LoadTypedefsOptions, UnnormalizedTypeDefPointer, loadTypedefsSync } from './load-typedefs'; import { GraphQLSchema, BuildSchemaOptions, DocumentNode } from 'graphql'; -import { OPERATION_KINDS } from './documents'; import { mergeSchemasAsync, mergeSchemas, MergeSchemasConfig } from '@graphql-toolkit/schema-merging'; +import { loadTypedefs, LoadTypedefsOptions, UnnormalizedTypeDefPointer, loadTypedefsSync } from './typedefs'; +import { OPERATION_KINDS } from './documents'; export type LoadSchemaOptions = BuildSchemaOptions & LoadTypedefsOptions & Partial; @@ -17,7 +17,7 @@ export async function loadSchema( const schemas: GraphQLSchema[] = []; const typeDefs: DocumentNode[] = []; - sources.forEach(source => { + sources.forEach((source) => { if (source.schema) { schemas.push(source.schema); } else { @@ -46,7 +46,7 @@ export function loadSchemaSync( const schemas: GraphQLSchema[] = []; const typeDefs: DocumentNode[] = []; - sources.forEach(source => { + sources.forEach((source) => { if (source.schema) { schemas.push(source.schema); } else { diff --git a/packages/core/src/load-typedefs.ts b/packages/core/src/load/typedefs.ts similarity index 93% rename from packages/core/src/load-typedefs.ts rename to packages/core/src/load/typedefs.ts index a63a3c65..631fdc8b 100644 --- a/packages/core/src/load-typedefs.ts +++ b/packages/core/src/load/typedefs.ts @@ -1,11 +1,11 @@ import { DefinitionNode } from 'graphql'; import { Source, SingleFileOptions, Loader, compareStrings } from '@graphql-toolkit/common'; -import { normalizePointers } from './utils/pointers'; -import { RawModule } from './import-parser'; +import { RawModule } from '../utils/import-parser'; import { prepareOptions, prepareOptionsSync } from './load-typedefs/options'; import { collectSources, collectSourcesSync } from './load-typedefs/collect-sources'; import { parseSource, parseSourceSync } from './load-typedefs/parse'; -import { useLimit } from './utils/helpers'; +import { normalizePointers } from '../utils/pointers'; +import { useLimit } from '../utils/helpers'; const CONCURRENCY_LIMIT = 100; @@ -47,7 +47,7 @@ export async function loadTypedefs( const limit = useLimit(CONCURRENCY_LIMIT); await Promise.all( - sources.map(partialSource => + sources.map((partialSource) => limit(() => parseSource({ partialSource, @@ -83,7 +83,7 @@ export function loadTypedefsSync( const validSources: Source[] = []; const definitionsCacheForImport: DefinitionNode[][] = []; - sources.forEach(partialSource => { + sources.forEach((partialSource) => { parseSourceSync({ partialSource, options, @@ -116,7 +116,7 @@ function prepareResult({ throw new Error(` Unable to find any GraphQL type definitions for the following pointers: ${pointerList.map( - p => ` + (p) => ` - ${p} ` )}`); diff --git a/packages/core/src/filter-document-kind.ts b/packages/core/src/utils/filter-document-kind.ts similarity index 95% rename from packages/core/src/filter-document-kind.ts rename to packages/core/src/utils/filter-document-kind.ts index a2c84105..fdce5fc0 100644 --- a/packages/core/src/filter-document-kind.ts +++ b/packages/core/src/utils/filter-document-kind.ts @@ -14,7 +14,7 @@ export const filterKind = (content: DocumentNode, filterKinds: null | string[]) } if (invalidDefinitions.length > 0) { - invalidDefinitions.forEach(d => { + invalidDefinitions.forEach((d) => { debugLog(`Filtered document of kind ${d.kind} due to filter policy (${filterKinds.join(', ')})`); }); } diff --git a/packages/core/src/import-parser/definition.ts b/packages/core/src/utils/import-parser/definition.ts similarity index 87% rename from packages/core/src/import-parser/definition.ts rename to packages/core/src/utils/import-parser/definition.ts index 2344c28f..c655b791 100644 --- a/packages/core/src/import-parser/definition.ts +++ b/packages/core/src/utils/import-parser/definition.ts @@ -47,7 +47,7 @@ export function completeDefinitionPool( const visitedDefinitions: { [name: string]: boolean } = {}; while (newTypeDefinitions.length > 0) { - const schemaMap: DefinitionMap = keyBy(reverse(allDefinitions), d => ('name' in d ? d.name.value : 'schema')); + const schemaMap: DefinitionMap = keyBy(reverse(allDefinitions), (d) => ('name' in d ? d.name.value : 'schema')); const newDefinition = newTypeDefinitions.shift(); if (visitedDefinitions['name' in newDefinition ? newDefinition.name.value : 'schema']) { @@ -96,7 +96,7 @@ function collectNewTypeDefinitions( } if (newDefinition.kind === 'EnumTypeDefinition') { - newDefinition.values.forEach(value => value.directives.forEach(collectDirective)); + newDefinition.values.forEach((value) => value.directives.forEach(collectDirective)); } if (newDefinition.kind === 'InputObjectTypeDefinition') { @@ -109,14 +109,14 @@ function collectNewTypeDefinitions( newDefinition.fields.forEach(collectNode); const interfaceImplementations = allDefinitions.filter( - d => d.kind === 'ObjectTypeDefinition' && d.interfaces.some(i => i.name.value === interfaceName) + (d) => d.kind === 'ObjectTypeDefinition' && d.interfaces.some((i) => i.name.value === interfaceName) ); newTypeDefinitions.push(...interfaceImplementations); } if (newDefinition.kind === 'UnionTypeDefinition') { - newDefinition.types.forEach(type => { - if (!definitionPool.some(d => 'name' in d && d.name.value === type.name.value)) { + newDefinition.types.forEach((type) => { + if (!definitionPool.some((d) => 'name' in d && d.name.value === type.name.value)) { const typeName = type.name.value; const typeMatch = schemaMap[typeName]; @@ -131,8 +131,8 @@ function collectNewTypeDefinitions( if (newDefinition.kind === 'ObjectTypeDefinition') { // collect missing interfaces - newDefinition.interfaces.forEach(int => { - if (!definitionPool.some(d => 'name' in d && d.name.value === int.name.value)) { + newDefinition.interfaces.forEach((int) => { + if (!definitionPool.some((d) => 'name' in d && d.name.value === int.name.value)) { const interfaceName = int.name.value; const interfaceMatch = schemaMap[interfaceName]; @@ -145,7 +145,7 @@ function collectNewTypeDefinitions( }); // iterate over all fields - newDefinition.fields.forEach(field => { + newDefinition.fields.forEach((field) => { collectNode(field); // collect missing argument input types field.arguments.forEach(collectNode); @@ -153,8 +153,8 @@ function collectNewTypeDefinitions( } if (newDefinition.kind === 'SchemaDefinition') { - newDefinition.operationTypes.forEach(operationType => { - if (!definitionPool.some(d => 'name' in d && d.name.value === operationType.type.name.value)) { + newDefinition.operationTypes.forEach((operationType) => { + if (!definitionPool.some((d) => 'name' in d && d.name.value === operationType.type.name.value)) { const typeName = operationType.type.name.value; const typeMatch = schemaMap[typeName]; @@ -181,7 +181,7 @@ function collectNewTypeDefinitions( if (node.kind === Kind.FRAGMENT_SPREAD) { const fragmentName = node.name.value; - if (!definitionPool.some(d => 'name' in d && d.name.value === fragmentName)) { + if (!definitionPool.some((d) => 'name' in d && d.name.value === fragmentName)) { const fragmentMatch = schemaMap[fragmentName]; if (!fragmentMatch) { @@ -206,7 +206,7 @@ function collectNewTypeDefinitions( // collect missing argument input types if ( - !definitionPool.some(d => 'name' in d && d.name.value === nodeTypeName) && + !definitionPool.some((d) => 'name' in d && d.name.value === nodeTypeName) && !includes(builtinTypes, nodeTypeName) ) { const argTypeMatch = schemaMap[nodeTypeName]; @@ -224,7 +224,7 @@ function collectNewTypeDefinitions( function collectDirective(directive: DirectiveNode) { const directiveName = directive.name.value; if ( - !definitionPool.some(d => 'name' in d && d.name.value === directiveName) && + !definitionPool.some((d) => 'name' in d && d.name.value === directiveName) && !includes(builtinDirectives, directiveName) ) { const directive = schemaMap[directiveName] as DirectiveDefinitionNode; diff --git a/packages/core/src/import-parser/index.ts b/packages/core/src/utils/import-parser/index.ts similarity index 89% rename from packages/core/src/import-parser/index.ts rename to packages/core/src/utils/import-parser/index.ts index 717454e8..9785dea5 100644 --- a/packages/core/src/import-parser/index.ts +++ b/packages/core/src/utils/import-parser/index.ts @@ -1,7 +1,7 @@ import { DefinitionNode, parse, ObjectTypeDefinitionNode, DocumentNode, Kind } from 'graphql'; import { groupBy, keyBy, isEqual, uniqBy, flatten } from 'lodash'; -import { LoadTypedefsOptions } from '../load-typedefs'; -import { loadFile, loadFileSync } from '../load-typedefs/load-file'; +import { LoadTypedefsOptions } from '../../load/typedefs'; +import { loadFile, loadFileSync } from '../../load/load-typedefs/load-file'; import { completeDefinitionPool } from './definition'; import { Source, compareNodes } from '@graphql-toolkit/common'; @@ -41,7 +41,7 @@ export function parseImportLine(importLine: string): RawModule { const [, wildcard, importsString, , from] = matches; // Extract imported types - const imports = wildcard === '*' ? ['*'] : importsString.split(',').map(d => d.trim()); + const imports = wildcard === '*' ? ['*'] : importsString.split(',').map((d) => d.trim()); // Return information about the import line return { imports, from }; @@ -68,9 +68,9 @@ export function parseImportLine(importLine: string): RawModule { export function parseSDL(sdl: string): RawModule[] { return sdl .split('\n') - .map(l => l.trim()) - .filter(l => l.startsWith('# import ') || l.startsWith('#import ')) - .map(l => l.replace('#', '').trim()) + .map((l) => l.trim()) + .filter((l) => l.startsWith('# import ') || l.startsWith('#import ')) + .map((l) => l.replace('#', '').trim()) .map(parseImportLine); } @@ -149,7 +149,7 @@ function process({ processedTypeNames.push(type.name.value); mergedFirstTypes.push(type); } else { - const existingType = mergedFirstTypes.find(t => t.name.value === type.name.value); + const existingType = mergedFirstTypes.find((t) => t.name.value === type.name.value); if ('fields' in existingType) { (existingType as any).fields = uniqBy( @@ -196,8 +196,8 @@ export function isEmptySDL(sdl: string): boolean { return ( sdl .split('\n') - .map(l => l.trim()) - .filter(l => !(l.length === 0 || l.startsWith('#'))).length === 0 + .map((l) => l.trim()) + .filter((l) => !(l.length === 0 || l.startsWith('#'))).length === 0 ); } @@ -257,7 +257,7 @@ export async function collectDefinitions( // Process each file (recursively) await Promise.all( - rawModules.map(async module => { + rawModules.map(async (module) => { // If it was not yet processed (in case of circular dependencies) const filepath = resolveModuleFilePath(source.location, module.from, options); if ( @@ -297,7 +297,7 @@ export function collectDefinitionsSync( const rawModules = preapreRawModules({ allDefinitions, source, imports, options, typeDefinitions }); // Process each file (recursively) - rawModules.forEach(module => { + rawModules.forEach((module) => { // If it was not yet processed (in case of circular dependencies) const filepath = resolveModuleFilePath(source.location, module.from, options); if ( @@ -356,7 +356,7 @@ function canProcess({ module: RawModule; }) { const processedFile = options.processedFiles.get(filepath); - if (!processedFile || !processedFile.find(rModule => isEqual(rModule, module))) { + if (!processedFile || !processedFile.find((rModule) => isEqual(rModule, module))) { // Mark this specific import line as processed for this file (for cicular dependency cases) options.processedFiles.set(filepath, processedFile ? processedFile.concat(module) : [module]); @@ -388,24 +388,24 @@ function filterImportedDefinitions( if (imports.length === 1 && imports[0] === '*' && allDefinitions.length > 1) { const previousTypeDefinitions: { [key: string]: DefinitionNode } = keyBy( flatten(allDefinitions.slice(0, allDefinitions.length - 1)).filter( - def => 'name' in def && !rootFields.includes(def.name.value) + (def) => 'name' in def && !rootFields.includes(def.name.value) ), - def => 'name' in def && def.name.value + (def) => 'name' in def && def.name.value ); return typeDefinitions.filter( - typeDef => typeDef.kind === 'ObjectTypeDefinition' && previousTypeDefinitions[typeDef.name.value] + (typeDef) => typeDef.kind === 'ObjectTypeDefinition' && previousTypeDefinitions[typeDef.name.value] ) as ObjectTypeDefinitionNode[]; } return filteredDefinitions; } else { - const importedTypes = imports.map(i => i.split('.')[0]); - const result = filteredDefinitions.filter(d => 'name' in d && importedTypes.includes(d.name.value)); - const fieldImports = imports.filter(i => i.split('.').length > 1); - const groupedFieldImports = groupBy(fieldImports, x => x.split('.')[0]); + const importedTypes = imports.map((i) => i.split('.')[0]); + const result = filteredDefinitions.filter((d) => 'name' in d && importedTypes.includes(d.name.value)); + const fieldImports = imports.filter((i) => i.split('.').length > 1); + const groupedFieldImports = groupBy(fieldImports, (x) => x.split('.')[0]); for (const rootType in groupedFieldImports) { - const fields = groupedFieldImports[rootType].map(x => x.split('.')[1]); - const objectTypeDefinition: any = filteredDefinitions.find(def => 'name' in def && def.name.value === rootType); + const fields = groupedFieldImports[rootType].map((x) => x.split('.')[1]); + const objectTypeDefinition: any = filteredDefinitions.find((def) => 'name' in def && def.name.value === rootType); if (objectTypeDefinition && 'fields' in objectTypeDefinition && !fields.includes('*')) { objectTypeDefinition.fields = objectTypeDefinition.fields.filter( diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts new file mode 100644 index 00000000..1b05b499 --- /dev/null +++ b/packages/core/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from './import-parser'; +export * from './filter-document-kind'; diff --git a/packages/core/src/utils/pointers.ts b/packages/core/src/utils/pointers.ts index 33128037..8f9b7e2e 100644 --- a/packages/core/src/utils/pointers.ts +++ b/packages/core/src/utils/pointers.ts @@ -1,5 +1,5 @@ import { asArray } from '@graphql-toolkit/common'; -import { UnnormalizedTypeDefPointer } from './../load-typedefs'; +import { UnnormalizedTypeDefPointer } from '../load/typedefs'; export function normalizePointers( unnormalizedPointerOrPointers: UnnormalizedTypeDefPointer | UnnormalizedTypeDefPointer[] diff --git a/packages/core/tests/loaders/schema/test-files/schema-dir/extensions/export-schema-with-def.js b/packages/core/tests/loaders/schema/test-files/schema-dir/extensions/export-schema-with-def.js index 5fd96977..22f8caa5 100644 --- a/packages/core/tests/loaders/schema/test-files/schema-dir/extensions/export-schema-with-def.js +++ b/packages/core/tests/loaders/schema/test-files/schema-dir/extensions/export-schema-with-def.js @@ -1,4 +1,4 @@ -const { makeExecutableSchema } = require('graphql-tools-fork'); +const { makeExecutableSchema } = require('@graphql-toolkit/schema'); const { doc } = require('./type-defs'); const schema = makeExecutableSchema({ diff --git a/packages/core/tests/loaders/schema/test-files/schema-dir/extensions/export-schema.js b/packages/core/tests/loaders/schema/test-files/schema-dir/extensions/export-schema.js index 6de38cf2..0dc481c1 100644 --- a/packages/core/tests/loaders/schema/test-files/schema-dir/extensions/export-schema.js +++ b/packages/core/tests/loaders/schema/test-files/schema-dir/extensions/export-schema.js @@ -1,4 +1,4 @@ -const { makeExecutableSchema } = require('graphql-tools-fork'); +const { makeExecutableSchema } = require('@graphql-toolkit/schema'); const { doc } = require('./type-defs'); const schema = makeExecutableSchema({ diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index fbf856f2..e0f4ce68 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -22,6 +22,6 @@ "importHelpers": true, "baseUrl": "." }, - "include": ["src/index.ts", "src/typings.d.ts"], + "include": ["src"], "exclude": ["node_modules"] } diff --git a/packages/graphql-tag-pluck/package.json b/packages/graphql-tag-pluck/package.json index b668a366..baf714f6 100644 --- a/packages/graphql-tag-pluck/package.json +++ b/packages/graphql-tag-pluck/package.json @@ -26,10 +26,10 @@ "input": "./src/index.ts" }, "dependencies": { + "@graphql-toolkit/common": "0.10.2", "@babel/parser": "7.9.4", "@babel/traverse": "7.9.0", - "@babel/types": "7.9.0", - "@graphql-toolkit/common": "0.10.2" + "@babel/types": "7.9.0" }, "optionalDependencies": { "vue-template-compiler": "^2.6.11" diff --git a/packages/loaders/code-file/tests/test-files/loaders/with-extend.js b/packages/loaders/code-file/tests/test-files/loaders/with-extend.js index 02e99dd2..c9c102d5 100644 --- a/packages/loaders/code-file/tests/test-files/loaders/with-extend.js +++ b/packages/loaders/code-file/tests/test-files/loaders/with-extend.js @@ -1,4 +1,4 @@ -const { makeExecutableSchema } = require('graphql-tools-fork'); +const { makeExecutableSchema } = require('@graphql-toolkit/schema'); const schema = makeExecutableSchema({ typeDefs: /* GraphQL */ ` diff --git a/packages/loaders/url/package.json b/packages/loaders/url/package.json index c2f89dbf..532ae6c1 100644 --- a/packages/loaders/url/package.json +++ b/packages/loaders/url/package.json @@ -35,8 +35,8 @@ }, "dependencies": { "@graphql-toolkit/common": "0.10.2", + "@graphql-toolkit/schema": "0.10.2", "cross-fetch": "3.0.4", - "graphql-tools-fork": "9.0.1", "tslib": "1.11.1", "valid-url": "1.0.9" }, diff --git a/packages/loaders/url/src/index.ts b/packages/loaders/url/src/index.ts index 2c595d9a..ea8218e1 100644 --- a/packages/loaders/url/src/index.ts +++ b/packages/loaders/url/src/index.ts @@ -5,19 +5,17 @@ import { print, getIntrospectionQuery, IntrospectionOptions, - introspectionFromSchema, } from 'graphql'; import { SchemaPointerSingle, Source, DocumentLoader, SingleFileOptions, - parseGraphQLJSON, printSchemaWithDirectives, } from '@graphql-toolkit/common'; import { isWebUri } from 'valid-url'; import { fetch as crossFetch } from 'cross-fetch'; -import { makeRemoteExecutableSchema, Fetcher } from 'graphql-tools-fork'; +import { makeRemoteExecutableSchema, Fetcher } from '@graphql-toolkit/schema'; export type FetchFn = typeof import('cross-fetch').fetch; @@ -57,7 +55,7 @@ export class UrlLoader implements DocumentLoader { if (options.customFetch) { if (typeof options.customFetch === 'string') { const [moduleName, fetchFnName] = options.customFetch.split('#'); - fetch = await import(moduleName).then(module => (fetchFnName ? module[fetchFnName] : module)); + fetch = await import(moduleName).then((module) => (fetchFnName ? module[fetchFnName] : module)); } } @@ -89,7 +87,6 @@ export class UrlLoader implements DocumentLoader { query: parse(getIntrospectionQuery({ descriptions: true, ...options })), variables: {}, operationName: 'IntrospectionQuery', - context: {}, }); let errorMessage; diff --git a/packages/loaders/url/tests/url-loader.spec.ts b/packages/loaders/url/tests/url-loader.spec.ts index bb35224a..a75f4005 100644 --- a/packages/loaders/url/tests/url-loader.spec.ts +++ b/packages/loaders/url/tests/url-loader.spec.ts @@ -1,6 +1,6 @@ jest.mock('cross-fetch'); -import { makeExecutableSchema } from 'graphql-tools-fork'; import { UrlLoader } from '../src'; +import { makeExecutableSchema } from '@graphql-toolkit/schema'; import { printSchemaWithDirectives } from '@graphql-toolkit/common'; import { ApolloServer } from 'apollo-server-express'; import * as express from 'express'; diff --git a/packages/schema-merging/package.json b/packages/schema-merging/package.json index 30527e72..bf493bbf 100644 --- a/packages/schema-merging/package.json +++ b/packages/schema-merging/package.json @@ -31,8 +31,9 @@ }, "dependencies": { "@graphql-toolkit/common": "0.10.2", + "@graphql-toolkit/schema": "0.10.2", + "@graphql-toolkit/types": "0.10.2", "deepmerge": "4.2.2", - "graphql-tools-fork": "9.0.1", "tslib": "1.11.1" }, "publishConfig": { diff --git a/packages/schema-merging/src/merge-resolvers.ts b/packages/schema-merging/src/merge-resolvers.ts index c3962983..2c1aea67 100644 --- a/packages/schema-merging/src/merge-resolvers.ts +++ b/packages/schema-merging/src/merge-resolvers.ts @@ -1,4 +1,4 @@ -import { IResolvers } from 'graphql-tools-fork'; +import { IResolvers } from '@graphql-toolkit/types'; import * as deepMerge from 'deepmerge'; import { isScalarType } from 'graphql'; @@ -60,7 +60,7 @@ export function mergeResolvers let result: T = {} as T; if (resolversFactories.length) { result = ((...args: any[]) => { - const resultsOfFactories = resolversFactories.map(factory => factory(...args)); + const resultsOfFactories = resolversFactories.map((factory) => factory(...args)); return deepMerge.all([...resolvers, ...resultsOfFactories], { isMergeableObject }) as any; }) as any; } else { diff --git a/packages/schema-merging/src/merge-schemas.ts b/packages/schema-merging/src/merge-schemas.ts index 8da3e54e..378f85ce 100644 --- a/packages/schema-merging/src/merge-schemas.ts +++ b/packages/schema-merging/src/merge-schemas.ts @@ -1,20 +1,19 @@ import { GraphQLSchema, DocumentNode, buildASTSchema, BuildSchemaOptions, buildSchema } from 'graphql'; import { - IResolvers, SchemaDirectiveVisitor, + addResolversToSchema, IResolverValidationOptions, - ILogger, - addResolveFunctionsToSchema, addErrorLoggingToSchema, -} from 'graphql-tools-fork'; -import { mergeTypeDefs, Config } from './typedefs-mergers/merge-typedefs'; -import { mergeResolvers } from './merge-resolvers'; +} from '@graphql-toolkit/schema'; import { extractResolversFromSchema, ResolversComposerMapping, composeResolvers, asArray, } from '@graphql-toolkit/common'; +import { IResolvers, ILogger } from '@graphql-toolkit/types'; +import { mergeTypeDefs, Config } from './typedefs-mergers/merge-typedefs'; +import { mergeResolvers } from './merge-resolvers'; import { mergeExtensions, extractExtensionsFromSchema, applyExtensions, SchemaExtensions } from './extensions'; export interface MergeSchemasConfig extends Config, BuildSchemaOptions { @@ -88,7 +87,7 @@ function makeSchema( // add resolvers if (resolvers) { - addResolveFunctionsToSchema({ + addResolversToSchema({ schema, resolvers, resolverValidationOptions: { diff --git a/packages/schema-merging/src/typedefs-mergers/arguments.ts b/packages/schema-merging/src/typedefs-mergers/arguments.ts index 7e6e90ff..8a25ffd3 100644 --- a/packages/schema-merging/src/typedefs-mergers/arguments.ts +++ b/packages/schema-merging/src/typedefs-mergers/arguments.ts @@ -1,13 +1,13 @@ import { InputValueDefinitionNode } from 'graphql'; -import { Config } from '.'; import { compareNodes } from '@graphql-toolkit/common'; +import { Config } from '.'; export function mergeArguments( args1: InputValueDefinitionNode[], args2: InputValueDefinitionNode[], config: Config ): InputValueDefinitionNode[] { - const result = deduplicateArguments([].concat(args2, args1).filter(a => a)); + const result = deduplicateArguments([].concat(args2, args1).filter((a) => a)); if (config && config.sort) { result.sort(compareNodes); } @@ -16,7 +16,7 @@ export function mergeArguments( function deduplicateArguments(args: ReadonlyArray): InputValueDefinitionNode[] { return args.reduce((acc, current) => { - const dup = acc.find(arg => arg.name.value === current.name.value); + const dup = acc.find((arg) => arg.name.value === current.name.value); if (!dup) { return acc.concat([current]); diff --git a/packages/schema-merging/src/typedefs-mergers/comments.ts b/packages/schema-merging/src/typedefs-mergers/comments.ts index cfe6dee1..8846914d 100644 --- a/packages/schema-merging/src/typedefs-mergers/comments.ts +++ b/packages/schema-merging/src/typedefs-mergers/comments.ts @@ -4,7 +4,6 @@ import { TypeDefinitionNode, FieldDefinitionNode, InputValueDefinitionNode, - DocumentNode, ASTNode, NameNode, TypeNode, @@ -26,7 +25,7 @@ export function collectComment(node: TypeDefinitionNode): void { switch (node.kind) { case 'EnumTypeDefinition': - node.values.forEach(value => { + node.values.forEach((value) => { pushComment(value, entityName, value.name.value); }); break; @@ -39,7 +38,7 @@ export function collectComment(node: TypeDefinitionNode): void { pushComment(field, entityName, field.name.value); if (isFieldDefinitionNode(field) && field.arguments) { - field.arguments.forEach(arg => { + field.arguments.forEach((arg) => { pushComment(arg, entityName, field.name.value, arg.name.value); }); } @@ -103,7 +102,7 @@ export function printComment(comment: string): string { * print all items together separated by separator if provided */ function join(maybeArray?: readonly any[], separator?: string) { - return maybeArray ? maybeArray.filter(x => x).join(separator || '') : ''; + return maybeArray ? maybeArray.filter((x) => x).join(separator || '') : ''; } function addDescription(cb: VisitFn): VisitFn { @@ -173,18 +172,18 @@ function printBlockString(value: string, isDescription: boolean) { export function printWithComments(ast: ASTNode) { return visit(ast, { leave: { - Name: node => node.value, - Variable: node => `$${node.name}`, + Name: (node) => node.value, + Variable: (node) => `$${node.name}`, // Document - Document: node => + Document: (node) => `${node.definitions - .map(defNode => `${defNode}\n${defNode[0] === '#' ? '' : '\n'}`) + .map((defNode) => `${defNode}\n${defNode[0] === '#' ? '' : '\n'}`) .join('') .trim()}\n`, - OperationTypeDefinition: node => `${node.operation}: ${node.type}`, + OperationTypeDefinition: (node) => `${node.operation}: ${node.type}`, VariableDefinition: ({ variable, type, defaultValue }) => `${variable}: ${type}${wrap(' = ', defaultValue)}`, diff --git a/packages/schema-merging/src/typedefs-mergers/enum-values.ts b/packages/schema-merging/src/typedefs-mergers/enum-values.ts index 25cf00f1..72d20dab 100644 --- a/packages/schema-merging/src/typedefs-mergers/enum-values.ts +++ b/packages/schema-merging/src/typedefs-mergers/enum-values.ts @@ -1,7 +1,7 @@ import { EnumValueDefinitionNode } from 'graphql/language/ast'; +import { compareNodes } from '@graphql-toolkit/common'; import { mergeDirectives } from './directives'; import { Config } from './merge-typedefs'; -import { compareNodes } from '@graphql-toolkit/common'; export function mergeEnumValues( first: ReadonlyArray, diff --git a/packages/schema-merging/src/typedefs-mergers/fields.ts b/packages/schema-merging/src/typedefs-mergers/fields.ts index cf5fe50a..7b7f690f 100644 --- a/packages/schema-merging/src/typedefs-mergers/fields.ts +++ b/packages/schema-merging/src/typedefs-mergers/fields.ts @@ -1,12 +1,12 @@ -import { Config } from './merge-typedefs'; import { FieldDefinitionNode, InputValueDefinitionNode, TypeNode, NameNode } from 'graphql'; +import { isNotEqual, compareNodes } from '@graphql-toolkit/common'; import { extractType, isWrappingTypeNode, isListTypeNode, isNonNullTypeNode, printTypeNode } from './utils'; +import { Config } from './merge-typedefs'; import { mergeDirectives } from './directives'; -import { isNotEqual, compareNodes } from '@graphql-toolkit/common'; import { mergeArguments } from './arguments'; function fieldAlreadyExists(fieldsArr: ReadonlyArray, otherField: any): boolean { - const result: FieldDefinitionNode | null = fieldsArr.find(field => field.name.value === otherField.name.value); + const result: FieldDefinitionNode | null = fieldsArr.find((field) => field.name.value === otherField.name.value); if (result) { const t1 = extractType(result.type); @@ -55,7 +55,7 @@ export function mergeFields !config.exclusions.includes(`${type.name.value}.${field.name.value}`)); + return result.filter((field) => !config.exclusions.includes(`${type.name.value}.${field.name.value}`)); } return result; } diff --git a/packages/schema-merging/src/typedefs-mergers/input-type.ts b/packages/schema-merging/src/typedefs-mergers/input-type.ts index 5e57d379..7fce1246 100644 --- a/packages/schema-merging/src/typedefs-mergers/input-type.ts +++ b/packages/schema-merging/src/typedefs-mergers/input-type.ts @@ -1,8 +1,8 @@ -import { Config } from './merge-typedefs'; import { InputObjectTypeDefinitionNode } from 'graphql'; +import { InputValueDefinitionNode, InputObjectTypeExtensionNode } from 'graphql/language/ast'; +import { Config } from './merge-typedefs'; import { mergeFields } from './fields'; import { mergeDirectives } from './directives'; -import { InputValueDefinitionNode, InputObjectTypeExtensionNode } from 'graphql/language/ast'; export function mergeInputType( node: InputObjectTypeDefinitionNode | InputObjectTypeExtensionNode, diff --git a/packages/schema-merging/src/typedefs-mergers/interface.ts b/packages/schema-merging/src/typedefs-mergers/interface.ts index fb5a1d2d..78f84e3d 100644 --- a/packages/schema-merging/src/typedefs-mergers/interface.ts +++ b/packages/schema-merging/src/typedefs-mergers/interface.ts @@ -1,5 +1,5 @@ -import { Config } from './merge-typedefs'; import { InterfaceTypeDefinitionNode, InterfaceTypeExtensionNode } from 'graphql'; +import { Config } from './merge-typedefs'; import { mergeFields } from './fields'; import { mergeDirectives } from './directives'; diff --git a/packages/schema-merging/src/typedefs-mergers/merge-named-type-array.ts b/packages/schema-merging/src/typedefs-mergers/merge-named-type-array.ts index 7bd60c5b..4e2f07a5 100644 --- a/packages/schema-merging/src/typedefs-mergers/merge-named-type-array.ts +++ b/packages/schema-merging/src/typedefs-mergers/merge-named-type-array.ts @@ -1,9 +1,9 @@ import { NamedTypeNode } from 'graphql/language/ast'; -import { Config } from '.'; import { compareNodes } from '@graphql-toolkit/common'; +import { Config } from '.'; function alreadyExists(arr: ReadonlyArray, other: NamedTypeNode): boolean { - return !!arr.find(i => i.name.value === other.name.value); + return !!arr.find((i) => i.name.value === other.name.value); } export function mergeNamedTypeArray( @@ -11,7 +11,7 @@ export function mergeNamedTypeArray( second: ReadonlyArray, config: Config ): NamedTypeNode[] { - const result = [...second, ...first.filter(d => !alreadyExists(second, d))]; + const result = [...second, ...first.filter((d) => !alreadyExists(second, d))]; if (config && config.sort) { result.sort(compareNodes); } diff --git a/packages/schema-merging/src/typedefs-mergers/merge-nodes.ts b/packages/schema-merging/src/typedefs-mergers/merge-nodes.ts index 5beea314..2a25a81c 100644 --- a/packages/schema-merging/src/typedefs-mergers/merge-nodes.ts +++ b/packages/schema-merging/src/typedefs-mergers/merge-nodes.ts @@ -1,4 +1,3 @@ -import { Config } from './merge-typedefs'; import { DefinitionNode } from 'graphql'; import { isGraphQLEnum, @@ -15,6 +14,7 @@ import { isGraphQLScalarExtension, isGraphQLInterfaceExtension, } from './utils'; +import { Config } from './merge-typedefs'; import { mergeType } from './type'; import { mergeEnum } from './enum'; import { mergeUnion } from './union'; diff --git a/packages/schema-merging/src/typedefs-mergers/merge-typedefs.ts b/packages/schema-merging/src/typedefs-mergers/merge-typedefs.ts index 09207dfd..b375e3e1 100644 --- a/packages/schema-merging/src/typedefs-mergers/merge-typedefs.ts +++ b/packages/schema-merging/src/typedefs-mergers/merge-typedefs.ts @@ -1,24 +1,8 @@ -import { - DefinitionNode, - DocumentNode, - GraphQLSchema, - parse, - print, - Source, - GraphQLObjectType, - isSpecifiedScalarType, - isIntrospectionType, - printType, - ObjectTypeExtensionNode, - GraphQLNamedType, - Kind, - isScalarType, - isSchema, -} from 'graphql'; +import { DefinitionNode, DocumentNode, GraphQLSchema, parse, Source, Kind, isSchema } from 'graphql'; +import { createSchemaDefinition, printSchemaWithDirectives } from '@graphql-toolkit/common'; import { isSourceTypes, isStringTypes, isSchemaDefinition } from './utils'; import { MergedResultMap, mergeGraphQLNodes } from './merge-nodes'; import { resetComments, printWithComments } from './comments'; -import { createSchemaDefinition, printSchemaWithDirectives } from '@graphql-toolkit/common'; type Omit = Pick>; type CompareFn = (a: T, b: T) => number; @@ -135,7 +119,7 @@ export function mergeGraphQLTypes( resetComments(); const allNodes: ReadonlyArray = types - .map(type => { + .map((type) => { if (isSchema(type)) { return parse(printSchemaWithDirectives(type)); } else if (isStringTypes(type) || isSourceTypes(type)) { @@ -144,7 +128,7 @@ export function mergeGraphQLTypes( return type; }) - .map(ast => ast.definitions) + .map((ast) => ast.definitions) .reduce((defs, newDef = []) => [...defs, ...newDef], []); // XXX: right now we don't handle multiple schema definitions @@ -155,8 +139,8 @@ export function mergeGraphQLTypes( } = allNodes.filter(isSchemaDefinition).reduce( (def, node) => { node.operationTypes - .filter(op => op.type.name.value) - .forEach(op => { + .filter((op) => op.type.name.value) + .forEach((op) => { def[op.operation] = op.type.name.value; }); @@ -177,9 +161,11 @@ export function mergeGraphQLTypes( } if (config && config.useSchemaDefinition) { - const queryType = schemaDef.query ? schemaDef.query : allTypes.find(t => t === 'Query'); - const mutationType = schemaDef.mutation ? schemaDef.mutation : allTypes.find(t => t === 'Mutation'); - const subscriptionType = schemaDef.subscription ? schemaDef.subscription : allTypes.find(t => t === 'Subscription'); + const queryType = schemaDef.query ? schemaDef.query : allTypes.find((t) => t === 'Query'); + const mutationType = schemaDef.mutation ? schemaDef.mutation : allTypes.find((t) => t === 'Mutation'); + const subscriptionType = schemaDef.subscription + ? schemaDef.subscription + : allTypes.find((t) => t === 'Subscription'); schemaDef = { query: queryType, mutation: mutationType, diff --git a/packages/schema-merging/src/typedefs-mergers/type.ts b/packages/schema-merging/src/typedefs-mergers/type.ts index 7cd41e2b..2fb1c2f8 100644 --- a/packages/schema-merging/src/typedefs-mergers/type.ts +++ b/packages/schema-merging/src/typedefs-mergers/type.ts @@ -1,5 +1,5 @@ import { Config } from './merge-typedefs'; -import { ObjectTypeDefinitionNode, ObjectTypeExtensionNode, Kind } from 'graphql'; +import { ObjectTypeDefinitionNode, ObjectTypeExtensionNode } from 'graphql'; import { mergeFields } from './fields'; import { mergeDirectives } from './directives'; import { mergeNamedTypeArray } from './merge-named-type-array'; diff --git a/packages/schema-merging/tests/merge-schemas.spec.ts b/packages/schema-merging/tests/merge-schemas.spec.ts index 05ff4a08..7197599f 100644 --- a/packages/schema-merging/tests/merge-schemas.spec.ts +++ b/packages/schema-merging/tests/merge-schemas.spec.ts @@ -1,4 +1,4 @@ -import { makeExecutableSchema } from 'graphql-tools-fork'; +import { makeExecutableSchema } from '@graphql-toolkit/schema'; import { graphql, buildSchema, GraphQLScalarType, Kind, buildASTSchema, GraphQLSchema, ListValueNode } from 'graphql'; import { mergeSchemas, mergeSchemasAsync } from '../src/merge-schemas'; diff --git a/packages/schema-merging/tests/merge-typedefs.spec.ts b/packages/schema-merging/tests/merge-typedefs.spec.ts index 47864c57..44fc43e8 100644 --- a/packages/schema-merging/tests/merge-typedefs.spec.ts +++ b/packages/schema-merging/tests/merge-typedefs.spec.ts @@ -1,5 +1,6 @@ import { mergeTypeDefs, mergeGraphQLTypes } from '../src'; -import { makeExecutableSchema, mergeSchemas } from 'graphql-tools-fork'; +import { makeExecutableSchema } from '@graphql-toolkit/schema'; +import { mergeSchemas } from 'graphql-tools'; import { buildSchema, buildClientSchema, print, parse } from 'graphql'; import { stripWhitespaces } from './utils'; import gql from 'graphql-tag'; diff --git a/packages/schema/.gitignore b/packages/schema/.gitignore new file mode 100644 index 00000000..b2931f95 --- /dev/null +++ b/packages/schema/.gitignore @@ -0,0 +1,72 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# next.js build output +.next + + +dist +build +temp +.idea + +test-results/ +junit.xml + +*.tgz \ No newline at end of file diff --git a/packages/schema/.npmignore b/packages/schema/.npmignore new file mode 100644 index 00000000..4ff88439 --- /dev/null +++ b/packages/schema/.npmignore @@ -0,0 +1,13 @@ +src +node_modules +tests +!dist +.circleci +.prettierrc +bump.js +jest.config.js +tsconfig.json +yarn.lock +yarn-error.log +bundle-test +*.tgz \ No newline at end of file diff --git a/packages/schema/package.json b/packages/schema/package.json new file mode 100644 index 00000000..e8fade85 --- /dev/null +++ b/packages/schema/package.json @@ -0,0 +1,42 @@ +{ + "name": "@graphql-toolkit/schema", + "version": "0.10.2", + "description": "A set of utils for faster development of GraphQL tools", + "repository": "git@github.com:ardatan/graphql-toolkit.git", + "author": "Kamil Kisiela ", + "license": "MIT", + "scripts": { + "lint": "tslint src/**/*.ts", + "clean": "rimraf dist", + "prebuild": "yarn clean", + "build": "bob", + "prepack": "bob-update-version", + "test": "jest --passWithNoTests --no-watchman --config ../../jest.config.js" + }, + "sideEffects": false, + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "typings": "dist/index.d.ts", + "typescript": { + "definition": "dist/index.d.ts" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0" + }, + "buildOptions": { + "input": "./src/index.ts" + }, + "devDependencies": { + "@types/deepmerge": "2.2.0" + }, + "dependencies": { + "@graphql-toolkit/common": "0.10.2", + "@graphql-toolkit/types": "0.10.2", + "@graphql-toolkit/version": "0.10.2", + "tslib": "1.11.1" + }, + "publishConfig": { + "access": "public", + "directory": "dist" + } +} diff --git a/packages/schema/src/add-resolvers-to-schema.ts b/packages/schema/src/add-resolvers-to-schema.ts new file mode 100644 index 00000000..59747dca --- /dev/null +++ b/packages/schema/src/add-resolvers-to-schema.ts @@ -0,0 +1,218 @@ +import { + GraphQLField, + GraphQLEnumType, + GraphQLSchema, + isSchema, + isScalarType, + isEnumType, + isUnionType, + isInterfaceType, + isObjectType, + GraphQLEnumTypeConfig, +} from 'graphql'; +import { IResolvers } from '@graphql-toolkit/types'; +import { + SchemaError, + forEachField, + forEachDefaultValue, + parseInputValue, + serializeInputValue, +} from '@graphql-toolkit/common'; + +import { IResolverValidationOptions, IAddResolversToSchemaOptions } from './types'; +import { healSchema } from './heal'; +import { checkForResolveTypeResolver } from './check-for-resolve-type-resolver'; +import { extendResolversFromInterfaces } from './extend-resolvers-from-interfaces'; + +export function addResolversToSchema( + schemaOrOptions: GraphQLSchema | IAddResolversToSchemaOptions, + legacyInputResolvers?: IResolvers, + legacyInputValidationOptions?: IResolverValidationOptions +): GraphQLSchema { + const options: IAddResolversToSchemaOptions = isSchema(schemaOrOptions) + ? { + schema: schemaOrOptions, + resolvers: legacyInputResolvers, + resolverValidationOptions: legacyInputValidationOptions, + } + : schemaOrOptions; + + const { + schema, + resolvers: inputResolvers, + defaultFieldResolver, + resolverValidationOptions = {}, + inheritResolversFromInterfaces = false, + } = options; + + const { allowResolversNotInSchema = false, requireResolversForResolveType } = resolverValidationOptions; + + const resolvers = inheritResolversFromInterfaces + ? extendResolversFromInterfaces(schema, inputResolvers) + : inputResolvers; + + const typeMap = schema.getTypeMap(); + + Object.keys(resolvers).forEach((typeName) => { + const resolverValue = resolvers[typeName]; + const resolverType = typeof resolverValue; + + if (resolverType !== 'object' && resolverType !== 'function') { + throw new SchemaError( + `"${typeName}" defined in resolvers, but has invalid value "${ + resolverValue as string + }". A resolver's value must be of type object or function.` + ); + } + + const type = schema.getType(typeName); + + if (!type && typeName !== '__schema') { + if (allowResolversNotInSchema) { + return; + } + + throw new SchemaError(`"${typeName}" defined in resolvers, but not in schema`); + } + + if (isScalarType(type)) { + // Support -- without recommending -- overriding default scalar types + Object.keys(resolverValue).forEach((fieldName) => { + if (fieldName.startsWith('__')) { + type[fieldName.substring(2)] = resolverValue[fieldName]; + } else { + type[fieldName] = resolverValue[fieldName]; + } + }); + } else if (isEnumType(type)) { + // We've encountered an enum resolver that is being used to provide an + // internal enum value. + // Reference: https://www.apollographql.com/docs/graphql-tools/scalars.html#internal-values + Object.keys(resolverValue).forEach((fieldName) => { + if (!type.getValue(fieldName)) { + if (allowResolversNotInSchema) { + return; + } + throw new SchemaError(`${typeName}.${fieldName} was defined in resolvers, but enum is not in schema`); + } + }); + + const config = enumTypeToConfig(type); + + const values = type.getValues(); + const newValues = {}; + values.forEach((value) => { + const newValue = Object.keys(resolverValue).includes(value.name) ? resolverValue[value.name] : value.name; + newValues[value.name] = { + value: newValue, + deprecationReason: value.deprecationReason, + description: value.description, + astNode: value.astNode, + }; + }); + + // healSchema called later to update all fields to new type + typeMap[typeName] = new GraphQLEnumType({ + ...config, + values: newValues, + }); + } else if (isUnionType(type)) { + Object.keys(resolverValue).forEach((fieldName) => { + if (fieldName.startsWith('__')) { + // this is for isTypeOf and resolveType and all the other stuff. + type[fieldName.substring(2)] = resolverValue[fieldName]; + return; + } + if (allowResolversNotInSchema) { + return; + } + + throw new SchemaError(`${typeName} was defined in resolvers, but it's not an object`); + }); + } else if (isObjectType(type) || isInterfaceType(type)) { + Object.keys(resolverValue).forEach((fieldName) => { + if (fieldName.startsWith('__')) { + // this is for isTypeOf and resolveType and all the other stuff. + type[fieldName.substring(2)] = resolverValue[fieldName]; + return; + } + + const fields = type.getFields(); + const field = fields[fieldName]; + + if (field == null) { + if (allowResolversNotInSchema) { + return; + } + + throw new SchemaError(`${typeName}.${fieldName} defined in resolvers, but not in schema`); + } + + const fieldResolve = resolverValue[fieldName]; + if (typeof fieldResolve === 'function') { + // for convenience. Allows shorter syntax in resolver definition file + field.resolve = fieldResolve; + } else { + if (typeof fieldResolve !== 'object') { + throw new SchemaError(`Resolver ${typeName}.${fieldName} must be object or function`); + } + setFieldProperties(field, fieldResolve); + } + }); + } + }); + + checkForResolveTypeResolver(schema, requireResolversForResolveType); + + // serialize all default values prior to healing fields with new scalar/enum types. + forEachDefaultValue(schema, serializeInputValue); + // schema may have new scalar/enum types that require healing + healSchema(schema); + // reparse all default values with new parsing functions. + forEachDefaultValue(schema, parseInputValue); + + if (defaultFieldResolver != null) { + forEachField(schema, (field) => { + if (!field.resolve) { + field.resolve = defaultFieldResolver; + } + }); + } + + return schema; +} + +function setFieldProperties(field: GraphQLField, propertiesObj: Record) { + Object.keys(propertiesObj).forEach((propertyName) => { + field[propertyName] = propertiesObj[propertyName]; + }); +} + +function enumTypeToConfig(type: GraphQLEnumType): GraphQLEnumTypeConfig { + if (type.toConfig != null) { + return type.toConfig(); + } + + const newValues = {}; + + type.getValues().forEach((value) => { + newValues[value.name] = { + description: value.description, + value: value.value, + deprecationReason: value.deprecationReason, + extensions: value.extensions, + astNode: value.astNode, + }; + }); + + const typeConfig = { + name: type.name, + description: type.description, + values: newValues, + extensions: type.extensions, + astNode: type.astNode, + extensionASTNodes: type.extensionASTNodes != null ? type.extensionASTNodes : [], + }; + + return typeConfig; +} diff --git a/packages/schema/src/add-schema-level-resolvers.ts b/packages/schema/src/add-schema-level-resolvers.ts new file mode 100644 index 00000000..27b49fab --- /dev/null +++ b/packages/schema/src/add-schema-level-resolvers.ts @@ -0,0 +1,62 @@ +import { defaultFieldResolver, GraphQLSchema, GraphQLFieldResolver } from 'graphql'; + +// wraps all resolvers of query, mutation or subscription fields +// with the provided function to simulate a root schema level resolver +export function addSchemaLevelResolver(schema: GraphQLSchema, fn: GraphQLFieldResolver): void { + // TODO test that schema is a schema, fn is a function + const rootTypes = [schema.getQueryType(), schema.getMutationType(), schema.getSubscriptionType()].filter((x) => + Boolean(x) + ); + rootTypes.forEach((type) => { + if (type != null) { + // XXX this should run at most once per request to simulate a true root resolver + // for graphql-js this is an approximation that works with queries but not mutations + const rootResolveFn = runAtMostOncePerRequest(fn); + const fields = type.getFields(); + Object.keys(fields).forEach((fieldName) => { + // XXX if the type is a subscription, a same query AST will be ran multiple times so we + // deactivate here the runOnce if it's a subscription. This may not be optimal though... + if (type === schema.getSubscriptionType()) { + fields[fieldName].resolve = wrapResolver(fields[fieldName].resolve, fn); + } else { + fields[fieldName].resolve = wrapResolver(fields[fieldName].resolve, rootResolveFn); + } + }); + } + }); +} + +// XXX badly named function. this doesn't really wrap, it just chains resolvers... +function wrapResolver( + innerResolver: GraphQLFieldResolver | undefined, + outerResolver: GraphQLFieldResolver +): GraphQLFieldResolver { + return (obj, args, ctx, info) => + Promise.resolve(outerResolver(obj, args, ctx, info)).then((root) => { + if (innerResolver != null) { + return innerResolver(root, args, ctx, info); + } + return defaultFieldResolver(root, args, ctx, info); + }); +} + +// XXX this function only works for resolvers +// XXX very hacky way to remember if the function +// already ran for this request. This will only work +// if people don't actually cache the operation. +// if they do cache the operation, they will have to +// manually remove the __runAtMostOnce before every request. +function runAtMostOncePerRequest(fn: GraphQLFieldResolver): GraphQLFieldResolver { + let value: any; + const randomNumber = Math.random(); + return (root, args, ctx, info) => { + if (!info.operation['__runAtMostOnce']) { + info.operation['__runAtMostOnce'] = {}; + } + if (!info.operation['__runAtMostOnce'][randomNumber]) { + info.operation['__runAtMostOnce'][randomNumber] = true; + value = fn(root, args, ctx, info); + } + return value; + }; +} diff --git a/packages/schema/src/assert-resolvers-present.ts b/packages/schema/src/assert-resolvers-present.ts new file mode 100644 index 00000000..aa772e83 --- /dev/null +++ b/packages/schema/src/assert-resolvers-present.ts @@ -0,0 +1,52 @@ +import { SchemaError, forEachField } from '@graphql-toolkit/common'; +import { GraphQLSchema, GraphQLField, getNamedType, isScalarType } from 'graphql'; +import { IResolverValidationOptions } from './types'; + +export function assertResolversPresent( + schema: GraphQLSchema, + resolverValidationOptions: IResolverValidationOptions = {} +): void { + const { + requireResolversForArgs = false, + requireResolversForNonScalar = false, + requireResolversForAllFields = false, + } = resolverValidationOptions; + + if (requireResolversForAllFields && (requireResolversForArgs || requireResolversForNonScalar)) { + throw new TypeError( + 'requireResolversForAllFields takes precedence over the more specific assertions. ' + + 'Please configure either requireResolversForAllFields or requireResolversForArgs / ' + + 'requireResolversForNonScalar, but not a combination of them.' + ); + } + + forEachField(schema, (field, typeName, fieldName) => { + // requires a resolver for *every* field. + if (requireResolversForAllFields) { + expectResolver(field, typeName, fieldName); + } + + // requires a resolver on every field that has arguments + if (requireResolversForArgs && field.args.length > 0) { + expectResolver(field, typeName, fieldName); + } + + // requires a resolver on every field that returns a non-scalar type + if (requireResolversForNonScalar && !isScalarType(getNamedType(field.type))) { + expectResolver(field, typeName, fieldName); + } + }); +} + +function expectResolver(field: GraphQLField, typeName: string, fieldName: string) { + if (!field.resolve) { + // tslint:disable-next-line:no-console + console.warn( + `Resolver missing for "${typeName}.${fieldName}". To disable this warning check https://github.com/apollographql/graphql-tools/issues/131` + ); + return; + } + if (typeof field.resolve !== 'function') { + throw new SchemaError(`Resolver "${typeName}.${fieldName}" must be a function`); + } +} diff --git a/packages/schema/src/attach-directive-resolvers.ts b/packages/schema/src/attach-directive-resolvers.ts new file mode 100644 index 00000000..867711e7 --- /dev/null +++ b/packages/schema/src/attach-directive-resolvers.ts @@ -0,0 +1,44 @@ +import { GraphQLSchema, GraphQLField, defaultFieldResolver } from 'graphql'; +import { IDirectiveResolvers } from '@graphql-toolkit/types'; +import { SchemaDirectiveVisitor } from './visitor/schema-directive-visitor'; + +export function attachDirectiveResolvers(schema: GraphQLSchema, directiveResolvers: IDirectiveResolvers) { + if (typeof directiveResolvers !== 'object') { + throw new Error(`Expected directiveResolvers to be of type object, got ${typeof directiveResolvers}`); + } + + if (Array.isArray(directiveResolvers)) { + throw new Error('Expected directiveResolvers to be of type object, got Array'); + } + + const schemaDirectives = Object.create(null); + + Object.keys(directiveResolvers).forEach((directiveName) => { + schemaDirectives[directiveName] = class extends SchemaDirectiveVisitor { + public visitFieldDefinition(field: GraphQLField) { + const resolver = directiveResolvers[directiveName]; + const originalResolver = field.resolve != null ? field.resolve : defaultFieldResolver; + const directiveArgs = this.args; + field.resolve = (...args) => { + const [source /* original args */, , context, info] = args; + return resolver( + () => + new Promise((resolve, reject) => { + const result = originalResolver.apply(field, args); + if (result instanceof Error) { + reject(result); + } + resolve(result); + }), + source, + directiveArgs, + context, + info + ); + }; + } + }; + }); + + SchemaDirectiveVisitor.visitSchemaDirectives(schema, schemaDirectives); +} diff --git a/packages/schema/src/build-schema-from-type-definitions.ts b/packages/schema/src/build-schema-from-type-definitions.ts new file mode 100644 index 00000000..da1e845a --- /dev/null +++ b/packages/schema/src/build-schema-from-type-definitions.ts @@ -0,0 +1,46 @@ +import { parse, extendSchema, buildASTSchema, GraphQLSchema, DocumentNode, ASTNode } from 'graphql'; +import { SchemaError } from '@graphql-toolkit/common'; + +import { ITypeDefinitions, GraphQLParseOptions } from './types'; + +import { extractExtensionDefinitions, filterExtensionDefinitions } from './extension-definitions'; +import { concatenateTypeDefs } from './concatenate-type-defs'; + +export function buildSchemaFromTypeDefinitions( + typeDefinitions: ITypeDefinitions, + parseOptions?: GraphQLParseOptions +): GraphQLSchema { + // TODO: accept only array here, otherwise interfaces get confusing. + let myDefinitions = typeDefinitions; + let astDocument: DocumentNode; + + if (isDocumentNode(typeDefinitions)) { + astDocument = typeDefinitions; + } else if (typeof myDefinitions !== 'string') { + if (!Array.isArray(myDefinitions)) { + const type = typeof myDefinitions; + throw new SchemaError(`typeDefs must be a string, array or schema AST, got ${type}`); + } + myDefinitions = concatenateTypeDefs(myDefinitions); + } + + if (typeof myDefinitions === 'string') { + astDocument = parse(myDefinitions, parseOptions); + } + + const typesAst = filterExtensionDefinitions(astDocument); + + const backcompatOptions = { commentDescriptions: true }; + let schema: GraphQLSchema = buildASTSchema(typesAst, backcompatOptions); + + const extensionsAst = extractExtensionDefinitions(astDocument); + if (extensionsAst.definitions.length > 0) { + schema = extendSchema(schema, extensionsAst, backcompatOptions); + } + + return schema; +} + +function isDocumentNode(typeDefinitions: ITypeDefinitions): typeDefinitions is DocumentNode { + return (typeDefinitions as ASTNode).kind !== undefined; +} diff --git a/packages/schema/src/check-for-resolve-type-resolver.ts b/packages/schema/src/check-for-resolve-type-resolver.ts new file mode 100644 index 00000000..a67313c5 --- /dev/null +++ b/packages/schema/src/check-for-resolve-type-resolver.ts @@ -0,0 +1,22 @@ +import { GraphQLInterfaceType, GraphQLUnionType, GraphQLSchema, isAbstractType } from 'graphql'; +import { SchemaError } from '@graphql-toolkit/common'; + +// If we have any union or interface types throw if no there is no resolveType or isTypeOf resolvers +export function checkForResolveTypeResolver(schema: GraphQLSchema, requireResolversForResolveType?: boolean) { + Object.keys(schema.getTypeMap()) + .map((typeName) => schema.getType(typeName)) + .forEach((type: GraphQLUnionType | GraphQLInterfaceType) => { + if (!isAbstractType(type)) { + return; + } + if (!type.resolveType) { + if (!requireResolversForResolveType) { + return; + } + throw new SchemaError( + `Type "${type.name}" is missing a "__resolveType" resolver. Pass false into ` + + '"resolverValidationOptions.requireResolversForResolveType" to disable this error.' + ); + } + }); +} diff --git a/packages/schema/src/concatenate-type-defs.ts b/packages/schema/src/concatenate-type-defs.ts new file mode 100644 index 00000000..46023785 --- /dev/null +++ b/packages/schema/src/concatenate-type-defs.ts @@ -0,0 +1,31 @@ +import { SchemaError } from '@graphql-toolkit/common'; +import { print, ASTNode } from 'graphql'; +import { ITypedef } from './types'; + +export function concatenateTypeDefs(typeDefinitionsAry: Array, calledFunctionRefs = [] as any): string { + let resolvedTypeDefinitions: Array = []; + typeDefinitionsAry.forEach((typeDef: ITypedef) => { + if (typeof typeDef === 'function') { + if (calledFunctionRefs.indexOf(typeDef) === -1) { + calledFunctionRefs.push(typeDef); + resolvedTypeDefinitions = resolvedTypeDefinitions.concat(concatenateTypeDefs(typeDef(), calledFunctionRefs)); + } + } else if (typeof typeDef === 'string') { + resolvedTypeDefinitions.push(typeDef.trim()); + } else if ((typeDef as ASTNode).kind !== undefined) { + resolvedTypeDefinitions.push(print(typeDef).trim()); + } else { + const type = typeof typeDef; + throw new SchemaError(`typeDef array must contain only strings and functions, got ${type}`); + } + }); + return uniq(resolvedTypeDefinitions.map((x) => x.trim())).join('\n'); +} + +function uniq(array: Array): Array { + return array.reduce( + (accumulator, currentValue) => + accumulator.indexOf(currentValue) === -1 ? [...accumulator, currentValue] : accumulator, + [] + ); +} diff --git a/packages/schema/src/decorate-with-logger.ts b/packages/schema/src/decorate-with-logger.ts new file mode 100644 index 00000000..8e0a30f1 --- /dev/null +++ b/packages/schema/src/decorate-with-logger.ts @@ -0,0 +1,49 @@ +import { defaultFieldResolver, GraphQLFieldResolver } from 'graphql'; +import { ILogger } from '@graphql-toolkit/types'; + +/* + * fn: The function to decorate with the logger + * logger: an object instance of type Logger + * hint: an optional hint to add to the error's message + */ +export function decorateWithLogger( + fn: GraphQLFieldResolver, + logger: ILogger, + hint: string +): GraphQLFieldResolver { + const resolver = fn != null ? fn : defaultFieldResolver; + + const logError = (e: Error) => { + // TODO: clone the error properly + const newE = new Error(); + newE.stack = e.stack; + /* istanbul ignore else: always get the hint from addErrorLoggingToSchema */ + if (hint) { + newE['originalMessage'] = e.message; + newE['message'] = `Error in resolver ${hint}\n${e.message}`; + } + logger.log(newE); + }; + + return (root, args, ctx, info) => { + try { + const result = resolver(root, args, ctx, info); + // If the resolver returns a Promise log any Promise rejects. + if (result && typeof result.then === 'function' && typeof result.catch === 'function') { + result.catch((reason: Error | string) => { + // make sure that it's an error we're logging. + const error = reason instanceof Error ? reason : new Error(reason); + logError(error); + + // We don't want to leave an unhandled exception so pass on error. + return reason; + }); + } + return result; + } catch (e) { + logError(e); + // we want to pass on the error, just in case. + throw e; + } + }; +} diff --git a/packages/schema/src/extend-resolvers-from-interfaces.ts b/packages/schema/src/extend-resolvers-from-interfaces.ts new file mode 100644 index 00000000..80e32f9a --- /dev/null +++ b/packages/schema/src/extend-resolvers-from-interfaces.ts @@ -0,0 +1,24 @@ +import { GraphQLObjectType, GraphQLSchema, isObjectType, isInterfaceType } from 'graphql'; +import { version } from '@graphql-toolkit/version'; +import { IResolvers } from '@graphql-toolkit/types'; + +export function extendResolversFromInterfaces(schema: GraphQLSchema, resolvers: IResolvers) { + const typeNames = Object.keys({ + ...schema.getTypeMap(), + ...resolvers, + }); + + const extendedResolvers: IResolvers = {}; + typeNames.forEach((typeName) => { + const typeResolvers = resolvers[typeName]; + const type = schema.getType(typeName); + if (isObjectType(type) || (version >= 15 && isInterfaceType(type))) { + const interfaceResolvers = (type as GraphQLObjectType).getInterfaces().map((iFace) => resolvers[iFace.name]); + extendedResolvers[typeName] = Object.assign({}, ...interfaceResolvers, typeResolvers); + } else if (typeResolvers != null) { + extendedResolvers[typeName] = typeResolvers; + } + }); + + return extendedResolvers; +} diff --git a/packages/schema/src/extension-definitions.ts b/packages/schema/src/extension-definitions.ts new file mode 100644 index 00000000..e2a364c5 --- /dev/null +++ b/packages/schema/src/extension-definitions.ts @@ -0,0 +1,38 @@ +import { DocumentNode, DefinitionNode, Kind } from 'graphql'; +import { version } from '@graphql-toolkit/version'; + +export function extractExtensionDefinitions(ast: DocumentNode) { + const extensionDefs = ast.definitions.filter( + (def: DefinitionNode) => + def.kind === Kind.OBJECT_TYPE_EXTENSION || + (version >= 13 && def.kind === Kind.INTERFACE_TYPE_EXTENSION) || + def.kind === Kind.INPUT_OBJECT_TYPE_EXTENSION || + def.kind === Kind.UNION_TYPE_EXTENSION || + def.kind === Kind.ENUM_TYPE_EXTENSION || + def.kind === Kind.SCALAR_TYPE_EXTENSION || + def.kind === Kind.SCHEMA_EXTENSION + ); + + return { + ...ast, + definitions: extensionDefs, + }; +} + +export function filterExtensionDefinitions(ast: DocumentNode) { + const extensionDefs = ast.definitions.filter( + (def: DefinitionNode) => + def.kind !== Kind.OBJECT_TYPE_EXTENSION && + def.kind !== Kind.INTERFACE_TYPE_EXTENSION && + def.kind !== Kind.INPUT_OBJECT_TYPE_EXTENSION && + def.kind !== Kind.UNION_TYPE_EXTENSION && + def.kind !== Kind.ENUM_TYPE_EXTENSION && + def.kind !== Kind.SCALAR_TYPE_EXTENSION && + def.kind !== Kind.SCHEMA_EXTENSION + ); + + return { + ...ast, + definitions: extensionDefs, + }; +} diff --git a/packages/common/src/get-schema-directive-from-directive-resolver.ts b/packages/schema/src/get-schema-directive-from-directive-resolver.ts similarity index 84% rename from packages/common/src/get-schema-directive-from-directive-resolver.ts rename to packages/schema/src/get-schema-directive-from-directive-resolver.ts index 975eb399..748cfe15 100644 --- a/packages/common/src/get-schema-directive-from-directive-resolver.ts +++ b/packages/schema/src/get-schema-directive-from-directive-resolver.ts @@ -1,5 +1,6 @@ -import { DirectiveResolverFn, SchemaDirectiveVisitor } from 'graphql-tools-fork'; +import { DirectiveResolverFn } from '@graphql-toolkit/types'; import { GraphQLField, defaultFieldResolver } from 'graphql'; +import { SchemaDirectiveVisitor } from './visitor/schema-directive-visitor'; export function getSchemaDirectiveFromDirectiveResolver( directiveResolver: DirectiveResolverFn diff --git a/packages/schema/src/heal.ts b/packages/schema/src/heal.ts new file mode 100644 index 00000000..ce5cc0be --- /dev/null +++ b/packages/schema/src/heal.ts @@ -0,0 +1,137 @@ +import { + GraphQLSchema, + GraphQLNamedType, + GraphQLDirective, + GraphQLObjectType, + GraphQLInterfaceType, + GraphQLInputObjectType, + GraphQLScalarType, + GraphQLUnionType, + GraphQLEnumType, + GraphQLType, + GraphQLList, + GraphQLNonNull, + isNamedType, +} from 'graphql'; +import { VisitableSchemaType } from './types'; +import { each, updateEachKey } from '@graphql-toolkit/common'; + +type NamedTypeMap = { + [key: string]: GraphQLNamedType; +}; + +const hasOwn = Object.prototype.hasOwnProperty; + +export function healSchema(schema: GraphQLSchema) { + heal(schema); + return schema; + + function heal(type: VisitableSchemaType) { + if (type instanceof GraphQLSchema) { + const originalTypeMap: NamedTypeMap = type.getTypeMap(); + const actualNamedTypeMap: NamedTypeMap = Object.create(null); + + // If any of the .name properties of the GraphQLNamedType objects in + // schema.getTypeMap() have changed, the keys of the type map need to + // be updated accordingly. + + each(originalTypeMap, (namedType, typeName) => { + if (typeName.startsWith('__')) { + return; + } + + const actualName = namedType.name; + if (actualName.startsWith('__')) { + return; + } + + if (hasOwn.call(actualNamedTypeMap, actualName)) { + throw new Error(`Duplicate schema type name ${actualName}`); + } + + actualNamedTypeMap[actualName] = namedType; + + // Note: we are deliberately leaving namedType in the schema by its + // original name (which might be different from actualName), so that + // references by that name can be healed. + }); + + // Now add back every named type by its actual name. + each(actualNamedTypeMap, (namedType, typeName) => { + originalTypeMap[typeName] = namedType; + }); + + // Directive declaration argument types can refer to named types. + each(type.getDirectives(), (decl: GraphQLDirective) => { + if (decl.args) { + each(decl.args, (arg) => { + arg.type = healType(arg.type); + }); + } + }); + + each(originalTypeMap, (namedType, typeName) => { + if (!typeName.startsWith('__')) { + heal(namedType); + } + }); + + updateEachKey(originalTypeMap, (namedType, typeName) => { + // Dangling references to renamed types should remain in the schema + // during healing, but must be removed now, so that the following + // invariant holds for all names: schema.getType(name).name === name + if (!typeName.startsWith('__') && !hasOwn.call(actualNamedTypeMap, typeName)) { + return null; + } + }); + } else if (type instanceof GraphQLObjectType) { + healFields(type); + each(type.getInterfaces(), (iface) => heal(iface)); + } else if (type instanceof GraphQLInterfaceType) { + healFields(type); + } else if (type instanceof GraphQLInputObjectType) { + each(type.getFields(), (field) => { + field.type = healType(field.type); + }); + } else if (type instanceof GraphQLScalarType) { + // Nothing to do. + } else if (type instanceof GraphQLUnionType) { + updateEachKey(type.getTypes(), (t) => healType(t)); + } else if (type instanceof GraphQLEnumType) { + // Nothing to do. + } else { + throw new Error(`Unexpected schema type: ${type}`); + } + } + + function healFields(type: GraphQLObjectType | GraphQLInterfaceType) { + each(type.getFields(), (field) => { + field.type = healType(field.type); + if (field.args) { + each(field.args, (arg) => { + arg.type = healType(arg.type); + }); + } + }); + } + + function healType(type: T): T { + // Unwrap the two known wrapper types + if (type instanceof GraphQLList) { + type = new GraphQLList(healType(type.ofType)) as T; + } else if (type instanceof GraphQLNonNull) { + type = new GraphQLNonNull(healType(type.ofType)) as T; + } else if (isNamedType(type)) { + // If a type annotation on a field or an argument or a union member is + // any `GraphQLNamedType` with a `name`, then it must end up identical + // to `schema.getType(name)`, since `schema.getTypeMap()` is the source + // of truth for all named schema types. + const namedType = type as GraphQLNamedType; + const officialType = schema.getType(namedType.name); + if (officialType && namedType !== officialType) { + return officialType as T; + } + } + return type; + } +} diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts new file mode 100644 index 00000000..a9df3bc6 --- /dev/null +++ b/packages/schema/src/index.ts @@ -0,0 +1,19 @@ +export { addResolversToSchema } from './add-resolvers-to-schema'; +export { buildSchemaFromTypeDefinitions } from './build-schema-from-type-definitions'; +export { checkForResolveTypeResolver } from './check-for-resolve-type-resolver'; +export { concatenateTypeDefs } from './concatenate-type-defs'; +export { extendResolversFromInterfaces } from './extend-resolvers-from-interfaces'; +export { extractExtensionDefinitions, filterExtensionDefinitions } from './extension-definitions'; +export { attachDirectiveResolvers } from './attach-directive-resolvers'; +export { getSchemaDirectiveFromDirectiveResolver } from './get-schema-directive-from-directive-resolver'; +export { addSchemaLevelResolver } from './add-schema-level-resolvers'; +export { assertResolversPresent } from './assert-resolvers-present'; +export { Logger } from './logger'; +export { decorateWithLogger } from './decorate-with-logger'; +export * from './heal'; +export * from './make-executable-schema'; +export * from './make-remote-executable-schema'; +export * from './types'; +export { SchemaDirectiveVisitor } from './visitor/schema-directive-visitor'; +export { SchemaVisitor } from './visitor/schema-visitor'; +export { visitSchema } from './visitor/visit-schema'; diff --git a/packages/schema/src/logger.ts b/packages/schema/src/logger.ts new file mode 100644 index 00000000..f7deaffb --- /dev/null +++ b/packages/schema/src/logger.ts @@ -0,0 +1,29 @@ +import { ILogger } from '@graphql-toolkit/types'; + +export class Logger implements ILogger { + public errors: Array; + public name: string | undefined; + private readonly callback: Function | undefined; + + constructor(name?: string, callback?: Function) { + this.name = name; + this.errors = []; + this.callback = callback; + // TODO: should assert that callback is a function + } + + public log(err: Error) { + this.errors.push(err); + if (typeof this.callback === 'function') { + this.callback(err); + } + } + + public printOneError(e: Error): string { + return e.stack ? e.stack : ''; + } + + public printAllErrors() { + return this.errors.reduce((agg: string, e: Error) => `${agg}\n${this.printOneError(e)}`, ''); + } +} diff --git a/packages/schema/src/make-executable-schema.ts b/packages/schema/src/make-executable-schema.ts new file mode 100644 index 00000000..3380f2cb --- /dev/null +++ b/packages/schema/src/make-executable-schema.ts @@ -0,0 +1,103 @@ +import { SchemaError, mergeDeep, forEachField } from '@graphql-toolkit/common'; +import { IExecutableSchemaDefinition } from './types'; +import { buildSchemaFromTypeDefinitions } from './build-schema-from-type-definitions'; +import { addResolversToSchema } from './add-resolvers-to-schema'; +import { attachDirectiveResolvers } from './attach-directive-resolvers'; +import { SchemaDirectiveVisitor } from './visitor/schema-directive-visitor'; +import { assertResolversPresent } from './assert-resolvers-present'; +import { GraphQLSchema, GraphQLFieldResolver, defaultFieldResolver } from 'graphql'; +import { ILogger } from '@graphql-toolkit/types'; +import { decorateWithLogger } from './decorate-with-logger'; +import { addSchemaLevelResolver } from './add-schema-level-resolvers'; + +export function makeExecutableSchema({ + typeDefs, + resolvers = {}, + parseOptions = {}, + resolverValidationOptions = {}, + inheritResolversFromInterfaces = false, + directiveResolvers, + schemaDirectives, + allowUndefinedInResolve = true, + logger, +}: IExecutableSchemaDefinition) { + if (typeof resolverValidationOptions !== 'object') { + throw new SchemaError('Expected `resolverValidationOptions` to be an object'); + } + + if (!typeDefs) { + throw new SchemaError('Must provide typeDefs'); + } + + // We allow passing in an array of resolver maps, in which case we merge them + const resolverMap = Array.isArray(resolvers) + ? resolvers.filter((resolverObj) => typeof resolverObj === 'object').reduce(mergeDeep, {}) + : resolvers; + + // Arguments are now validated and cleaned up + + const schema = buildSchemaFromTypeDefinitions(typeDefs, parseOptions); + + addResolversToSchema({ + schema, + resolvers: resolverMap, + resolverValidationOptions, + inheritResolversFromInterfaces, + }); + + assertResolversPresent(schema, resolverValidationOptions); + + if (!allowUndefinedInResolve) { + addCatchUndefinedToSchema(schema); + } + + if (logger != null) { + addErrorLoggingToSchema(schema, logger); + } + + if (typeof resolvers['__schema'] === 'function') { + addSchemaLevelResolver(schema, resolvers['__schema'] as GraphQLFieldResolver); + } + + if (directiveResolvers != null) { + attachDirectiveResolvers(schema, directiveResolvers); + } + + if (schemaDirectives != null) { + SchemaDirectiveVisitor.visitSchemaDirectives(schema, schemaDirectives); + } + + return schema; +} + +function decorateToCatchUndefined(fn: GraphQLFieldResolver, hint: string): GraphQLFieldResolver { + const resolve = fn == null ? defaultFieldResolver : fn; + + return (root, args, ctx, info) => { + const result = resolve(root, args, ctx, info); + if (typeof result === 'undefined') { + throw new Error(`Resolver for "${hint}" returned undefined`); + } + return result; + }; +} + +export function addCatchUndefinedToSchema(schema: GraphQLSchema): void { + forEachField(schema, (field, typeName, fieldName) => { + const errorHint = `${typeName}.${fieldName}`; + field.resolve = decorateToCatchUndefined(field.resolve, errorHint); + }); +} + +export function addErrorLoggingToSchema(schema: GraphQLSchema, logger?: ILogger): void { + if (!logger) { + throw new Error('Must provide a logger'); + } + if (typeof logger.log !== 'function') { + throw new Error('Logger.log must be a function'); + } + forEachField(schema, (field, typeName, fieldName) => { + const errorHint = `${typeName}.${fieldName}`; + field.resolve = decorateWithLogger(field.resolve, logger, errorHint); + }); +} diff --git a/packages/schema/src/make-remote-executable-schema.ts b/packages/schema/src/make-remote-executable-schema.ts new file mode 100644 index 00000000..30bd402d --- /dev/null +++ b/packages/schema/src/make-remote-executable-schema.ts @@ -0,0 +1,179 @@ +import { + ExecutionResult, + GraphQLSchema, + DocumentNode, + buildSchema, + printSchema, + GraphQLFieldResolver, + Kind, + GraphQLInterfaceType, + GraphQLUnionType, + GraphQLScalarType, + GraphQLID, + GraphQLString, + GraphQLFloat, + GraphQLBoolean, + GraphQLInt, + GraphQLObjectType, + ValueNode, +} from 'graphql'; +import { IResolverObject, IResolvers } from '@graphql-toolkit/types'; +import { makeExecutableSchema } from './make-executable-schema'; + +export type FetcherOperation = { + query: DocumentNode; + operationName?: string; + variables?: { [key: string]: any }; +}; +export type Fetcher = (operation: FetcherOperation) => Promise; + +export function makeRemoteExecutableSchema({ schema, fetcher }: { schema: GraphQLSchema | string; fetcher?: Fetcher }) { + let typeDefs: string; + + if (typeof schema === 'string') { + typeDefs = schema; + schema = buildSchema(typeDefs); + } else { + typeDefs = printSchema(schema); + } + + // prepare query resolvers + const queryResolvers: IResolverObject = {}; + const queryType = schema.getQueryType(); + const queries = queryType.getFields(); + Object.keys(queries).forEach((key) => { + queryResolvers[key] = createResolver(fetcher); + }); + + // prepare mutation resolvers + const mutationResolvers: IResolverObject = {}; + const mutationType = schema.getMutationType(); + if (mutationType) { + const mutations = mutationType.getFields(); + Object.keys(mutations).forEach((key) => { + mutationResolvers[key] = createResolver(fetcher); + }); + } + + // prepare subscription resolvers + const subscriptionType = schema.getSubscriptionType(); + if (subscriptionType) { + throw new Error('Subscriptions are not yet implemented'); + } + + // merge resolvers into resolver map + const resolvers: IResolvers = { [queryType.name]: queryResolvers }; + + if (!isEmptyObject(mutationResolvers)) { + resolvers[mutationType.name] = mutationResolvers; + } + + // add missing abstract resolvers (scalar, unions, interfaces) + const typeMap = schema.getTypeMap(); + const types = Object.keys(typeMap).map((name) => typeMap[name]); + + for (const type of types) { + if (type instanceof GraphQLInterfaceType || type instanceof GraphQLUnionType) { + resolvers[type.name] = { + __resolveType(parent: any, _context: any, info: any) { + const parentTypename: string = parent['__typename']; + if (!parentTypename) { + throw new Error('Did not fetch typename for object, unable to resolve interface.'); + } + + const resolvedType = info.schema.getType(parentTypename); + + if (!(resolvedType instanceof GraphQLObjectType)) { + throw new Error('__typename did not match an object type: ' + parentTypename); + } + + return resolvedType; + }, + }; + } else if (type instanceof GraphQLScalarType) { + if ( + !( + type === GraphQLID || + type === GraphQLString || + type === GraphQLFloat || + type === GraphQLBoolean || + type === GraphQLInt + ) + ) { + resolvers[type.name] = new GraphQLScalarType({ + name: type.name, + description: type.description, + astNode: type.astNode, + serialize(value: any) { + return value; + }, + parseValue(value: any) { + return value; + }, + parseLiteral(ast: ValueNode) { + return parseLiteral(ast); + }, + }); + } + } + } + + return makeExecutableSchema({ + typeDefs, + resolvers, + }); +} + +export function createResolver(fetcher: Fetcher): GraphQLFieldResolver { + return async (_root, _args, _context, info) => { + const fragments = Object.keys(info.fragments).map((fragment) => info.fragments[fragment]); + const document = { + kind: Kind.DOCUMENT, + definitions: [info.operation, ...fragments], + }; + + return fetcher({ + query: document, + variables: info.variableValues, + }); + }; +} + +function isEmptyObject(obj: Object): boolean { + if (!obj) { + return true; + } + + for (const key in obj) { + if (Object.hasOwnProperty.call(obj, key)) { + return false; + } + } + return true; +} + +function parseLiteral(ast: ValueNode): any { + switch (ast.kind) { + case Kind.STRING: + case Kind.BOOLEAN: { + return ast.value; + } + case Kind.INT: + case Kind.FLOAT: { + return parseFloat(ast.value); + } + case Kind.OBJECT: { + const value = Object.create(null); + ast.fields.forEach((field) => { + value[field.name.value] = parseLiteral(field.value); + }); + + return value; + } + case Kind.LIST: { + return ast.values.map(parseLiteral); + } + default: + return null; + } +} diff --git a/packages/schema/src/types.ts b/packages/schema/src/types.ts new file mode 100644 index 00000000..128d3d2e --- /dev/null +++ b/packages/schema/src/types.ts @@ -0,0 +1,218 @@ +import { IResolvers, IFieldResolver, IDirectiveResolvers, ILogger } from '@graphql-toolkit/types'; +import { + GraphQLSchema, + GraphQLField, + GraphQLNamedType, + GraphQLScalarType, + DocumentNode, + GraphQLEnumValue, + GraphQLEnumType, + GraphQLUnionType, + GraphQLArgument, + GraphQLInputField, + GraphQLInputObjectType, + GraphQLInterfaceType, + GraphQLObjectType, + GraphQLDirective, +} from 'graphql'; +import { SchemaVisitor } from './visitor/schema-visitor'; +import { SchemaDirectiveVisitor } from './visitor/schema-directive-visitor'; + +export interface IResolverValidationOptions { + requireResolversForArgs?: boolean; + requireResolversForNonScalar?: boolean; + requireResolversForAllFields?: boolean; + requireResolversForResolveType?: boolean; + allowResolversNotInSchema?: boolean; +} + +export interface IAddResolversToSchemaOptions { + schema: GraphQLSchema; + resolvers: IResolvers; + defaultFieldResolver?: IFieldResolver; + resolverValidationOptions?: IResolverValidationOptions; + inheritResolversFromInterfaces?: boolean; +} + +export type ITypedef = (() => Array) | string | DocumentNode; + +export type ITypeDefinitions = ITypedef | Array; + +export interface IExecutableSchemaDefinition { + typeDefs: ITypeDefinitions; + resolvers?: IResolvers | Array>; + logger?: ILogger; + allowUndefinedInResolve?: boolean; + inheritResolversFromInterfaces?: boolean; + directiveResolvers?: IDirectiveResolvers; + schemaDirectives?: { [name: string]: typeof SchemaDirectiveVisitor }; + parseOptions?: GraphQLParseOptions; + resolverValidationOptions?: IResolverValidationOptions; +} + +export interface GraphQLParseOptions { + noLocation?: boolean; + allowLegacySDLEmptyFields?: boolean; + allowLegacySDLImplementsInterfaces?: boolean; + experimentalFragmentVariables?: boolean; +} + +export type IndexedObject = { [key: string]: V } | ReadonlyArray; + +export type IFieldIteratorFn = (fieldDef: GraphQLField, typeName: string, fieldName: string) => void; + +export type VisitableSchemaType = + | GraphQLSchema + | GraphQLObjectType + | GraphQLInterfaceType + | GraphQLInputObjectType + | GraphQLNamedType + | GraphQLScalarType + | GraphQLField + | GraphQLInputField + | GraphQLArgument + | GraphQLUnionType + | GraphQLEnumType + | GraphQLEnumValue; + +export type VisitorSelector = ( + type: VisitableSchemaType, + methodName: string +) => Array; + +export enum VisitSchemaKind { + TYPE = 'VisitSchemaKind.TYPE', + SCALAR_TYPE = 'VisitSchemaKind.SCALAR_TYPE', + ENUM_TYPE = 'VisitSchemaKind.ENUM_TYPE', + COMPOSITE_TYPE = 'VisitSchemaKind.COMPOSITE_TYPE', + OBJECT_TYPE = 'VisitSchemaKind.OBJECT_TYPE', + INPUT_OBJECT_TYPE = 'VisitSchemaKind.INPUT_OBJECT_TYPE', + ABSTRACT_TYPE = 'VisitSchemaKind.ABSTRACT_TYPE', + UNION_TYPE = 'VisitSchemaKind.UNION_TYPE', + INTERFACE_TYPE = 'VisitSchemaKind.INTERFACE_TYPE', + ROOT_OBJECT = 'VisitSchemaKind.ROOT_OBJECT', + QUERY = 'VisitSchemaKind.QUERY', + MUTATION = 'VisitSchemaKind.MUTATION', + SUBSCRIPTION = 'VisitSchemaKind.SUBSCRIPTION', +} + +export interface SchemaVisitorMap { + [VisitSchemaKind.TYPE]?: NamedTypeVisitor; + [VisitSchemaKind.SCALAR_TYPE]?: ScalarTypeVisitor; + [VisitSchemaKind.ENUM_TYPE]?: EnumTypeVisitor; + [VisitSchemaKind.COMPOSITE_TYPE]?: CompositeTypeVisitor; + [VisitSchemaKind.OBJECT_TYPE]?: ObjectTypeVisitor; + [VisitSchemaKind.INPUT_OBJECT_TYPE]?: InputObjectTypeVisitor; + [VisitSchemaKind.ABSTRACT_TYPE]?: AbstractTypeVisitor; + [VisitSchemaKind.UNION_TYPE]?: UnionTypeVisitor; + [VisitSchemaKind.INTERFACE_TYPE]?: InterfaceTypeVisitor; + [VisitSchemaKind.ROOT_OBJECT]?: ObjectTypeVisitor; + [VisitSchemaKind.QUERY]?: ObjectTypeVisitor; + [VisitSchemaKind.MUTATION]?: ObjectTypeVisitor; + [VisitSchemaKind.SUBSCRIPTION]?: ObjectTypeVisitor; +} + +export type NamedTypeVisitor = (type: GraphQLNamedType, schema: GraphQLSchema) => GraphQLNamedType | null | undefined; + +export type ScalarTypeVisitor = ( + type: GraphQLScalarType, + schema: GraphQLSchema +) => GraphQLScalarType | null | undefined; + +export type EnumTypeVisitor = (type: GraphQLEnumType, schema: GraphQLSchema) => GraphQLEnumType | null | undefined; + +export type CompositeTypeVisitor = ( + type: GraphQLObjectType | GraphQLInterfaceType | GraphQLUnionType, + schema: GraphQLSchema +) => GraphQLObjectType | GraphQLInterfaceType | GraphQLUnionType | null | undefined; + +export type ObjectTypeVisitor = ( + type: GraphQLObjectType, + schema: GraphQLSchema +) => GraphQLObjectType | null | undefined; + +export type InputObjectTypeVisitor = ( + type: GraphQLInputObjectType, + schema: GraphQLSchema +) => GraphQLInputObjectType | null | undefined; + +export type AbstractTypeVisitor = ( + type: GraphQLInterfaceType | GraphQLUnionType, + schema: GraphQLSchema +) => GraphQLInterfaceType | GraphQLUnionType | null | undefined; + +export type UnionTypeVisitor = (type: GraphQLUnionType, schema: GraphQLSchema) => GraphQLUnionType | null | undefined; + +export type InterfaceTypeVisitor = ( + type: GraphQLInterfaceType, + schema: GraphQLSchema +) => GraphQLInterfaceType | null | undefined; + +export enum MapperKind { + TYPE = 'MapperKind.TYPE', + SCALAR_TYPE = 'MapperKind.SCALAR_TYPE', + ENUM_TYPE = 'MapperKind.ENUM_TYPE', + COMPOSITE_TYPE = 'MapperKind.COMPOSITE_TYPE', + OBJECT_TYPE = 'MapperKind.OBJECT_TYPE', + INPUT_OBJECT_TYPE = 'MapperKind.INPUT_OBJECT_TYPE', + ABSTRACT_TYPE = 'MapperKind.ABSTRACT_TYPE', + UNION_TYPE = 'MapperKind.UNION_TYPE', + INTERFACE_TYPE = 'MapperKind.INTERFACE_TYPE', + ROOT_OBJECT = 'MapperKind.ROOT_OBJECT', + QUERY = 'MapperKind.QUERY', + MUTATION = 'MapperKind.MUTATION', + SUBSCRIPTION = 'MapperKind.SUBSCRIPTION', + DIRECTIVE = 'MapperKind.DIRECTIVE', +} + +export interface SchemaMapper { + [MapperKind.TYPE]?: NamedTypeMapper; + [MapperKind.SCALAR_TYPE]?: ScalarTypeMapper; + [MapperKind.ENUM_TYPE]?: EnumTypeMapper; + [MapperKind.COMPOSITE_TYPE]?: CompositeTypeMapper; + [MapperKind.OBJECT_TYPE]?: ObjectTypeMapper; + [MapperKind.INPUT_OBJECT_TYPE]?: InputObjectTypeMapper; + [MapperKind.ABSTRACT_TYPE]?: AbstractTypeMapper; + [MapperKind.UNION_TYPE]?: UnionTypeMapper; + [MapperKind.INTERFACE_TYPE]?: InterfaceTypeMapper; + [MapperKind.ROOT_OBJECT]?: ObjectTypeMapper; + [MapperKind.QUERY]?: ObjectTypeMapper; + [MapperKind.MUTATION]?: ObjectTypeMapper; + [MapperKind.SUBSCRIPTION]?: ObjectTypeMapper; + [MapperKind.DIRECTIVE]?: DirectiveMapper; +} + +export type NamedTypeMapper = (type: GraphQLNamedType, schema: GraphQLSchema) => GraphQLNamedType | null | undefined; + +export type ScalarTypeMapper = (type: GraphQLScalarType, schema: GraphQLSchema) => GraphQLScalarType | null | undefined; + +export type EnumTypeMapper = (type: GraphQLEnumType, schema: GraphQLSchema) => GraphQLEnumType | null | undefined; + +export type CompositeTypeMapper = ( + type: GraphQLObjectType | GraphQLInterfaceType | GraphQLUnionType, + schema: GraphQLSchema +) => GraphQLObjectType | GraphQLInterfaceType | GraphQLUnionType | null | undefined; + +export type ObjectTypeMapper = (type: GraphQLObjectType, schema: GraphQLSchema) => GraphQLObjectType | null | undefined; + +export type InputObjectTypeMapper = ( + type: GraphQLInputObjectType, + schema: GraphQLSchema +) => GraphQLInputObjectType | null | undefined; + +export type AbstractTypeMapper = ( + type: GraphQLInterfaceType | GraphQLUnionType, + schema: GraphQLSchema +) => GraphQLInterfaceType | GraphQLUnionType | null | undefined; + +export type UnionTypeMapper = (type: GraphQLUnionType, schema: GraphQLSchema) => GraphQLUnionType | null | undefined; + +export type InterfaceTypeMapper = ( + type: GraphQLInterfaceType, + schema: GraphQLSchema +) => GraphQLInterfaceType | null | undefined; + +export type DirectiveMapper = ( + directive: GraphQLDirective, + schema: GraphQLSchema +) => GraphQLDirective | null | undefined; diff --git a/packages/schema/src/visitor/schema-directive-visitor.ts b/packages/schema/src/visitor/schema-directive-visitor.ts new file mode 100644 index 00000000..221e5936 --- /dev/null +++ b/packages/schema/src/visitor/schema-directive-visitor.ts @@ -0,0 +1,285 @@ +import { GraphQLDirective, GraphQLSchema, DirectiveLocationEnum, TypeSystemExtensionNode } from 'graphql'; +import { getArgumentValues } from 'graphql/execution/values'; +import { each } from '@graphql-toolkit/common'; + +import { VisitableSchemaType } from '../types'; + +import { valueFromASTUntyped } from './value-from-ast-untyped'; +import { SchemaVisitor } from './schema-visitor'; +import { visitSchema } from './visit-schema'; + +const hasOwn = Object.prototype.hasOwnProperty; + +// This class represents a reusable implementation of a @directive that may +// appear in a GraphQL schema written in Schema Definition Language. +// +// By overriding one or more visit{Object,Union,...} methods, a subclass +// registers interest in certain schema types, such as GraphQLObjectType, +// GraphQLUnionType, etc. When SchemaDirectiveVisitor.visitSchemaDirectives is +// called with a GraphQLSchema object and a map of visitor subclasses, the +// overidden methods of those subclasses allow the visitors to obtain +// references to any type objects that have @directives attached to them, +// enabling visitors to inspect or modify the schema as appropriate. +// +// For example, if a directive called @rest(url: "...") appears after a field +// definition, a SchemaDirectiveVisitor subclass could provide meaning to that +// directive by overriding the visitFieldDefinition method (which receives a +// GraphQLField parameter), and then the body of that visitor method could +// manipulate the field's resolver function to fetch data from a REST endpoint +// described by the url argument passed to the @rest directive: +// +// const typeDefs = ` +// type Query { +// people: [Person] @rest(url: "/api/v1/people") +// }`; +// +// const schema = makeExecutableSchema({ typeDefs }); +// +// SchemaDirectiveVisitor.visitSchemaDirectives(schema, { +// rest: class extends SchemaDirectiveVisitor { +// public visitFieldDefinition(field: GraphQLField) { +// const { url } = this.args; +// field.resolve = () => fetch(url); +// } +// } +// }); +// +// The subclass in this example is defined as an anonymous class expression, +// for brevity. A truly reusable SchemaDirectiveVisitor would most likely be +// defined in a library using a named class declaration, and then exported for +// consumption by other modules and packages. +// +// See below for a complete list of overridable visitor methods, their +// parameter types, and more details about the properties exposed by instances +// of the SchemaDirectiveVisitor class. + +export class SchemaDirectiveVisitor extends SchemaVisitor { + // The name of the directive this visitor is allowed to visit (that is, the + // identifier that appears after the @ character in the schema). Note that + // this property is per-instance rather than static because subclasses of + // SchemaDirectiveVisitor can be instantiated multiple times to visit + // directives of different names. In other words, SchemaDirectiveVisitor + // implementations are effectively anonymous, and it's up to the caller of + // SchemaDirectiveVisitor.visitSchemaDirectives to assign names to them. + public name: string; + + // A map from parameter names to argument values, as obtained from a + // specific occurrence of a @directive(arg1: value1, arg2: value2, ...) in + // the schema. Visitor methods may refer to this object via this.args. + public args: { [name: string]: any }; + + // A reference to the type object that this visitor was created to visit. + public visitedType: VisitableSchemaType; + + // A shared object that will be available to all visitor instances via + // this.context. Callers of visitSchemaDirectives can provide their own + // object, or just use the default empty object. + public context: { [key: string]: any }; + + // Override this method to return a custom GraphQLDirective (or modify one + // already present in the schema) to enforce argument types, provide default + // argument values, or specify schema locations where this @directive may + // appear. By default, any declaration found in the schema will be returned. + public static getDirectiveDeclaration( + directiveName: string, + schema: GraphQLSchema + ): GraphQLDirective | null | undefined { + return schema.getDirective(directiveName); + } + + // Call SchemaDirectiveVisitor.visitSchemaDirectives to visit every + // @directive in the schema and create an appropriate SchemaDirectiveVisitor + // instance to visit the object decorated by the @directive. + public static visitSchemaDirectives( + schema: GraphQLSchema, + directiveVisitors: { + // The keys of this object correspond to directive names as they appear + // in the schema, and the values should be subclasses (not instances!) + // of the SchemaDirectiveVisitor class. This distinction is important + // because a new SchemaDirectiveVisitor instance will be created each + // time a matching directive is found in the schema AST, with arguments + // and other metadata specific to that occurrence. To help prevent the + // mistake of passing instances, the SchemaDirectiveVisitor constructor + // method is marked as protected. + [directiveName: string]: typeof SchemaDirectiveVisitor; + }, + // Optional context object that will be available to all visitor instances + // via this.context. Defaults to an empty null-prototype object. + context: { + [key: string]: any; + } = Object.create(null) + ): { + // The visitSchemaDirectives method returns a map from directive names to + // lists of SchemaDirectiveVisitors created while visiting the schema. + [directiveName: string]: Array; + } { + // If the schema declares any directives for public consumption, record + // them here so that we can properly coerce arguments when/if we encounter + // an occurrence of the directive while walking the schema below. + const declaredDirectives = this.getDeclaredDirectives(schema, directiveVisitors); + + // Map from directive names to lists of SchemaDirectiveVisitor instances + // created while visiting the schema. + const createdVisitors: { + [directiveName: string]: Array; + } = Object.create(null); + Object.keys(directiveVisitors).forEach((directiveName) => { + createdVisitors[directiveName] = []; + }); + + function visitorSelector(type: VisitableSchemaType, methodName: string): Array { + let directiveNodes = type.astNode != null ? type.astNode.directives : []; + + const extensionASTNodes: ReadonlyArray = (type as { + extensionASTNodes?: Array; + }).extensionASTNodes; + + if (extensionASTNodes != null) { + extensionASTNodes.forEach((extensionASTNode) => { + directiveNodes = directiveNodes.concat(extensionASTNode.directives); + }); + } + + const visitors: Array = []; + directiveNodes.forEach((directiveNode) => { + const directiveName = directiveNode.name.value; + if (!hasOwn.call(directiveVisitors, directiveName)) { + return; + } + + const visitorClass = directiveVisitors[directiveName]; + + // Avoid creating visitor objects if visitorClass does not override + // the visitor method named by methodName. + if (!visitorClass.implementsVisitorMethod(methodName)) { + return; + } + + const decl = declaredDirectives[directiveName]; + let args: { [key: string]: any }; + + if (decl != null) { + // If this directive was explicitly declared, use the declared + // argument types (and any default values) to check, coerce, and/or + // supply default values for the given arguments. + args = getArgumentValues(decl, directiveNode); + } else { + // If this directive was not explicitly declared, just convert the + // argument nodes to their corresponding JavaScript values. + args = Object.create(null); + if (directiveNode.arguments != null) { + directiveNode.arguments.forEach((arg) => { + args[arg.name.value] = valueFromASTUntyped(arg.value); + }); + } + } + + // As foretold in comments near the top of the visitSchemaDirectives + // method, this is where instances of the SchemaDirectiveVisitor class + // get created and assigned names. While subclasses could override the + // constructor method, the constructor is marked as protected, so + // these are the only arguments that will ever be passed. + visitors.push( + new visitorClass({ + name: directiveName, + args, + visitedType: type, + schema, + context, + }) + ); + }); + + if (visitors.length > 0) { + visitors.forEach((visitor) => { + createdVisitors[visitor.name].push(visitor); + }); + } + + return visitors; + } + + visitSchema(schema, visitorSelector); + + return createdVisitors; + } + + protected static getDeclaredDirectives( + schema: GraphQLSchema, + directiveVisitors: { + [directiveName: string]: typeof SchemaDirectiveVisitor; + } + ) { + const declaredDirectives: { + [directiveName: string]: GraphQLDirective; + } = Object.create(null); + + each(schema.getDirectives(), (decl: GraphQLDirective) => { + declaredDirectives[decl.name] = decl; + }); + + // If the visitor subclass overrides getDirectiveDeclaration, and it + // returns a non-null GraphQLDirective, use that instead of any directive + // declared in the schema itself. Reasoning: if a SchemaDirectiveVisitor + // goes to the trouble of implementing getDirectiveDeclaration, it should + // be able to rely on that implementation. + each(directiveVisitors, (visitorClass, directiveName) => { + const decl = visitorClass.getDirectiveDeclaration(directiveName, schema); + if (decl != null) { + declaredDirectives[directiveName] = decl; + } + }); + + each(declaredDirectives, (decl, name) => { + if (!hasOwn.call(directiveVisitors, name)) { + // SchemaDirectiveVisitors.visitSchemaDirectives might be called + // multiple times with partial directiveVisitors maps, so it's not + // necessarily an error for directiveVisitors to be missing an + // implementation of a directive that was declared in the schema. + return; + } + const visitorClass = directiveVisitors[name]; + + each(decl.locations, (loc) => { + const visitorMethodName = directiveLocationToVisitorMethodName(loc); + if ( + SchemaVisitor.implementsVisitorMethod(visitorMethodName) && + !visitorClass.implementsVisitorMethod(visitorMethodName) + ) { + // While visitor subclasses may implement extra visitor methods, + // it's definitely a mistake if the GraphQLDirective declares itself + // applicable to certain schema locations, and the visitor subclass + // does not implement all the corresponding methods. + throw new Error(`SchemaDirectiveVisitor for @${name} must implement ${visitorMethodName} method`); + } + }); + }); + + return declaredDirectives; + } + + // Mark the constructor protected to enforce passing SchemaDirectiveVisitor + // subclasses (not instances) to visitSchemaDirectives. + protected constructor(config: { + name: string; + args: { [name: string]: any }; + visitedType: VisitableSchemaType; + schema: GraphQLSchema; + context: { [key: string]: any }; + }) { + super(); + this.name = config.name; + this.args = config.args; + this.visitedType = config.visitedType; + this.schema = config.schema; + this.context = config.context; + } +} + +// Convert a string like "FIELD_DEFINITION" to "visitFieldDefinition". +function directiveLocationToVisitorMethodName(loc: DirectiveLocationEnum) { + return ( + 'visit' + + loc.replace(/([^_]*)_?/g, (_wholeMatch, part: string) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()) + ); +} diff --git a/packages/schema/src/visitor/schema-visitor.ts b/packages/schema/src/visitor/schema-visitor.ts new file mode 100644 index 00000000..49dd338f --- /dev/null +++ b/packages/schema/src/visitor/schema-visitor.ts @@ -0,0 +1,100 @@ +import { + GraphQLArgument, + GraphQLEnumType, + GraphQLEnumValue, + GraphQLField, + GraphQLInputField, + GraphQLInputObjectType, + GraphQLInterfaceType, + GraphQLObjectType, + GraphQLScalarType, + GraphQLSchema, + GraphQLUnionType, +} from 'graphql'; + +// Abstract base class of any visitor implementation, defining the available +// visitor methods along with their parameter types, and providing a static +// helper function for determining whether a subclass implements a given +// visitor method, as opposed to inheriting one of the stubs defined here. +export abstract class SchemaVisitor { + // All SchemaVisitor instances are created while visiting a specific + // GraphQLSchema object, so this property holds a reference to that object, + // in case a visitor method needs to refer to this.schema. + public schema!: GraphQLSchema; + + // Determine if this SchemaVisitor (sub)class implements a particular + // visitor method. + public static implementsVisitorMethod(methodName: string) { + if (!methodName.startsWith('visit')) { + return false; + } + + const method = this.prototype[methodName]; + if (typeof method !== 'function') { + return false; + } + + if (this === SchemaVisitor) { + // The SchemaVisitor class implements every visitor method. + return true; + } + + const stub = SchemaVisitor.prototype[methodName]; + if (method === stub) { + // If this.prototype[methodName] was just inherited from SchemaVisitor, + // then this class does not really implement the method. + return false; + } + + return true; + } + + // Concrete subclasses of SchemaVisitor should override one or more of these + // visitor methods, in order to express their interest in handling certain + // schema types/locations. Each method may return null to remove the given + // type from the schema, a non-null value of the same type to update the + // type in the schema, or nothing to leave the type as it was. + + public visitSchema(_schema: GraphQLSchema): void {} + + public visitScalar(_scalar: GraphQLScalarType): GraphQLScalarType | void | null {} + + public visitObject(_object: GraphQLObjectType): GraphQLObjectType | void | null {} + + public visitFieldDefinition( + _field: GraphQLField, + _details: { + objectType: GraphQLObjectType | GraphQLInterfaceType; + } + ): GraphQLField | void | null {} + + public visitArgumentDefinition( + _argument: GraphQLArgument, + _details: { + field: GraphQLField; + objectType: GraphQLObjectType | GraphQLInterfaceType; + } + ): GraphQLArgument | void | null {} + + public visitInterface(_iface: GraphQLInterfaceType): GraphQLInterfaceType | void | null {} + + public visitUnion(_union: GraphQLUnionType): GraphQLUnionType | void | null {} + + public visitEnum(_type: GraphQLEnumType): GraphQLEnumType | void | null {} + + public visitEnumValue( + _value: GraphQLEnumValue, + _details: { + enumType: GraphQLEnumType; + } + ): GraphQLEnumValue | void | null {} + + public visitInputObject(_object: GraphQLInputObjectType): GraphQLInputObjectType | void | null {} + + public visitInputFieldDefinition( + _field: GraphQLInputField, + _details: { + objectType: GraphQLInputObjectType; + } + ): GraphQLInputField | void | null {} +} diff --git a/packages/schema/src/visitor/value-from-ast-untyped.ts b/packages/schema/src/visitor/value-from-ast-untyped.ts new file mode 100644 index 00000000..eb66b291 --- /dev/null +++ b/packages/schema/src/visitor/value-from-ast-untyped.ts @@ -0,0 +1,30 @@ +import { ValueNode, Kind } from 'graphql'; + +// Similar to the graphql-js function of the same name, slightly simplified: +// https://github.com/graphql/graphql-js/blob/master/src/utilities/valueFromASTUntyped.js +export function valueFromASTUntyped(valueNode: ValueNode): any { + switch (valueNode.kind) { + case Kind.NULL: + return null; + case Kind.INT: + return parseInt(valueNode.value, 10); + case Kind.FLOAT: + return parseFloat(valueNode.value); + case Kind.STRING: + case Kind.ENUM: + case Kind.BOOLEAN: + return valueNode.value; + case Kind.LIST: + return valueNode.values.map(valueFromASTUntyped); + case Kind.OBJECT: { + const obj = Object.create(null); + valueNode.fields.forEach((field) => { + obj[field.name.value] = valueFromASTUntyped(field.value); + }); + return obj; + } + /* istanbul ignore next */ + default: + throw new Error('Unexpected value kind: ' + valueNode.kind); + } +} diff --git a/packages/schema/src/visitor/visit-schema.ts b/packages/schema/src/visitor/visit-schema.ts new file mode 100644 index 00000000..3b006485 --- /dev/null +++ b/packages/schema/src/visitor/visit-schema.ts @@ -0,0 +1,267 @@ +import { + GraphQLInterfaceType, + GraphQLObjectType, + GraphQLSchema, + isNamedType, + GraphQLType, + GraphQLNamedType, + GraphQLInputField, + isSchema, + isObjectType, + isInterfaceType, + isInputObjectType, + isScalarType, + isUnionType, + isEnumType, + isInputType, +} from 'graphql'; +import { each, updateEachKey } from '@graphql-toolkit/common'; +import { healSchema } from '../heal'; + +import { VisitableSchemaType, VisitorSelector, VisitSchemaKind, NamedTypeVisitor, SchemaVisitorMap } from '../types'; + +import { SchemaVisitor } from './schema-visitor'; + +// Generic function for visiting GraphQLSchema objects. +export function visitSchema( + schema: GraphQLSchema, + // To accommodate as many different visitor patterns as possible, the + // visitSchema function does not simply accept a single instance of the + // SchemaVisitor class, but instead accepts a function that takes the + // current VisitableSchemaType object and the name of a visitor method and + // returns an array of SchemaVisitor instances that implement the visitor + // method and have an interest in handling the given VisitableSchemaType + // object. In the simplest case, this function can always return an array + // containing a single visitor object, without even looking at the type or + // methodName parameters. In other cases, this function might sometimes + // return an empty array to indicate there are no visitors that should be + // applied to the given VisitableSchemaType object. For an example of a + // visitor pattern that benefits from this abstraction, see the + // SchemaDirectiveVisitor class below. + visitorOrVisitorSelector: VisitorSelector | Array | SchemaVisitor | SchemaVisitorMap +): GraphQLSchema { + const visitorSelector = + typeof visitorOrVisitorSelector === 'function' ? visitorOrVisitorSelector : () => visitorOrVisitorSelector; + + // Helper function that calls visitorSelector and applies the resulting + // visitors to the given type, with arguments [type, ...args]. + function callMethod(methodName: string, type: T, ...args: Array): T | null { + let visitors = visitorSelector(type, methodName); + visitors = Array.isArray(visitors) ? visitors : [visitors]; + + let finalType: T | null = type; + visitors.every((visitorOrVisitorDef) => { + let newType; + if (visitorOrVisitorDef instanceof SchemaVisitor) { + newType = visitorOrVisitorDef[methodName](finalType, ...args); + } else if ( + isNamedType(finalType) && + (methodName === 'visitScalar' || + methodName === 'visitEnum' || + methodName === 'visitObject' || + methodName === 'visitInputObject' || + methodName === 'visitUnion' || + methodName === 'visitInterface') + ) { + const specifiers = getTypeSpecifiers(finalType, schema); + const typeVisitor = getVisitor(visitorOrVisitorDef, specifiers); + newType = typeVisitor != null ? typeVisitor(finalType, schema) : undefined; + } + + if (typeof newType === 'undefined') { + // Keep going without modifying type. + return true; + } + + if (methodName === 'visitSchema' || isSchema(finalType)) { + throw new Error(`Method ${methodName} cannot replace schema with ${newType as string}`); + } + + if (newType === null) { + // Stop the loop and return null form callMethod, which will cause + // the type to be removed from the schema. + finalType = null; + return false; + } + + // Update type to the new type returned by the visitor method, so that + // later directives will see the new type, and callMethod will return + // the final type. + finalType = newType; + return true; + }); + + // If there were no directives for this type object, or if all visitor + // methods returned nothing, type will be returned unmodified. + return finalType; + } + + // Recursive helper function that calls any appropriate visitor methods for + // each object in the schema, then traverses the object's children (if any). + function visit(type: T): T | null { + if (isSchema(type)) { + // Unlike the other types, the root GraphQLSchema object cannot be + // replaced by visitor methods, because that would make life very hard + // for SchemaVisitor subclasses that rely on the original schema object. + callMethod('visitSchema', type); + + const typeMap: Record = type.getTypeMap(); + each(typeMap, (namedType, typeName) => { + if (!typeName.startsWith('__') && namedType != null) { + // Call visit recursively to let it determine which concrete + // subclass of GraphQLNamedType we found in the type map. + // We do not use updateEachKey because we want to preserve + // deleted types in the typeMap so that other types that reference + // the deleted types can be healed. + typeMap[typeName] = visit(namedType); + } + }); + + return type; + } + + if (isObjectType(type)) { + // Note that callMethod('visitObject', type) may not actually call any + // methods, if there are no @directive annotations associated with this + // type, or if this SchemaDirectiveVisitor subclass does not override + // the visitObject method. + const newObject = callMethod('visitObject', type); + if (newObject != null) { + visitFields(newObject); + } + return newObject; + } + + if (isInterfaceType(type)) { + const newInterface = callMethod('visitInterface', type); + if (newInterface != null) { + visitFields(newInterface); + } + return newInterface; + } + + if (isInputObjectType(type)) { + const newInputObject = callMethod('visitInputObject', type); + + if (newInputObject != null) { + const fieldMap = newInputObject.getFields() as Record; + updateEachKey(fieldMap, (field) => + callMethod('visitInputFieldDefinition', field, { + // Since we call a different method for input object fields, we + // can't reuse the visitFields function here. + objectType: newInputObject, + }) + ); + } + + return newInputObject; + } + + if (isScalarType(type)) { + return callMethod('visitScalar', type); + } + + if (isUnionType(type)) { + return callMethod('visitUnion', type); + } + + if (isEnumType(type)) { + const newEnum = callMethod('visitEnum', type); + + if (newEnum != null) { + updateEachKey(newEnum.getValues(), (value) => + callMethod('visitEnumValue', value, { + enumType: newEnum, + }) + ); + } + + return newEnum; + } + + throw new Error(`Unexpected schema type: ${(type as unknown) as string}`); + } + + function visitFields(type: GraphQLObjectType | GraphQLInterfaceType) { + updateEachKey(type.getFields(), (field) => { + // It would be nice if we could call visit(field) recursively here, but + // GraphQLField is merely a type, not a value that can be detected using + // an instanceof check, so we have to visit the fields in this lexical + // context, so that TypeScript can validate the call to + // visitFieldDefinition. + const newField = callMethod('visitFieldDefinition', field, { + // While any field visitor needs a reference to the field object, some + // field visitors may also need to know the enclosing (parent) type, + // perhaps to determine if the parent is a GraphQLObjectType or a + // GraphQLInterfaceType. To obtain a reference to the parent, a + // visitor method can have a second parameter, which will be an object + // with an .objectType property referring to the parent. + objectType: type, + }); + + if (newField.args != null) { + updateEachKey(newField.args, (arg) => + callMethod('visitArgumentDefinition', arg, { + // Like visitFieldDefinition, visitArgumentDefinition takes a + // second parameter that provides additional context, namely the + // parent .field and grandparent .objectType. Remember that the + // current GraphQLSchema is always available via this.schema. + field: newField, + objectType: type, + }) + ); + } + + return newField; + }); + } + + visit(schema); + + // Automatically update any references to named schema types replaced + // during the traversal, so implementors don't have to worry about that. + healSchema(schema); + + // Return schema for convenience, even though schema parameter has all updated types. + return schema; +} + +function getTypeSpecifiers(type: GraphQLType, schema: GraphQLSchema): Array { + const specifiers = [VisitSchemaKind.TYPE]; + if (isObjectType(type)) { + specifiers.push(VisitSchemaKind.COMPOSITE_TYPE, VisitSchemaKind.OBJECT_TYPE); + const query = schema.getQueryType(); + const mutation = schema.getMutationType(); + const subscription = schema.getSubscriptionType(); + if (type === query) { + specifiers.push(VisitSchemaKind.ROOT_OBJECT, VisitSchemaKind.QUERY); + } else if (type === mutation) { + specifiers.push(VisitSchemaKind.ROOT_OBJECT, VisitSchemaKind.MUTATION); + } else if (type === subscription) { + specifiers.push(VisitSchemaKind.ROOT_OBJECT, VisitSchemaKind.SUBSCRIPTION); + } + } else if (isInputType(type)) { + specifiers.push(VisitSchemaKind.INPUT_OBJECT_TYPE); + } else if (isInterfaceType(type)) { + specifiers.push(VisitSchemaKind.COMPOSITE_TYPE, VisitSchemaKind.ABSTRACT_TYPE, VisitSchemaKind.INTERFACE_TYPE); + } else if (isUnionType(type)) { + specifiers.push(VisitSchemaKind.COMPOSITE_TYPE, VisitSchemaKind.ABSTRACT_TYPE, VisitSchemaKind.UNION_TYPE); + } else if (isEnumType(type)) { + specifiers.push(VisitSchemaKind.ENUM_TYPE); + } else if (isScalarType(type)) { + specifiers.push(VisitSchemaKind.SCALAR_TYPE); + } + + return specifiers; +} + +function getVisitor(visitorDef: SchemaVisitorMap, specifiers: Array): NamedTypeVisitor | null { + let typeVisitor: NamedTypeVisitor | undefined; + const stack = [...specifiers]; + while (!typeVisitor && stack.length > 0) { + const next = stack.pop(); + typeVisitor = visitorDef[next] as NamedTypeVisitor; + } + + return typeVisitor != null ? typeVisitor : null; +} diff --git a/packages/schema/tests/fixtures/circularSchemaA.ts b/packages/schema/tests/fixtures/circularSchemaA.ts new file mode 100644 index 00000000..12c1bb40 --- /dev/null +++ b/packages/schema/tests/fixtures/circularSchemaA.ts @@ -0,0 +1,12 @@ +import TypeB from './circularSchemaB'; + +const TypeA = () => [ + ` +type TypeA { + id: ID + b: TypeB +}`, + TypeB, +]; + +export default TypeA; diff --git a/packages/schema/tests/fixtures/circularSchemaB.ts b/packages/schema/tests/fixtures/circularSchemaB.ts new file mode 100644 index 00000000..e417fecc --- /dev/null +++ b/packages/schema/tests/fixtures/circularSchemaB.ts @@ -0,0 +1,12 @@ +import TypeA from './circularSchemaA'; + +const TypeB = () => [ + ` +type TypeB { + id: ID + a: TypeA +}`, + TypeA, +]; + +export default TypeB; diff --git a/packages/schema/tests/schema-generation.spec.ts b/packages/schema/tests/schema-generation.spec.ts new file mode 100644 index 00000000..b95b8a4f --- /dev/null +++ b/packages/schema/tests/schema-generation.spec.ts @@ -0,0 +1,2856 @@ +import { Logger } from './../src/logger'; +// TODO: reduce code repetition in this file. +// see https://github.com/apollostack/graphql-tools/issues/26 + +import { GraphQLJSON } from 'graphql-type-json'; +import { + graphql, + GraphQLResolveInfo, + GraphQLScalarType, + Kind, + IntValueNode, + parse, + ExecutionResult, + GraphQLError, + GraphQLEnumType, + execute, + VariableDefinitionNode, + DocumentNode, + GraphQLBoolean, + graphqlSync, + GraphQLSchema, +} from 'graphql'; +import { + IResolvers, + IDirectiveResolvers, + NextResolverFn, + ILogger, +} from '@graphql-toolkit/types'; + +import { + visitSchema, + concatenateTypeDefs, + makeExecutableSchema, + addResolversToSchema, + attachDirectiveResolvers, + IResolverValidationOptions, + VisitSchemaKind, + ITypeDefinitions, + addErrorLoggingToSchema, +} from '../src'; +import { version } from '@graphql-toolkit/version'; + +import TypeA from './fixtures/circularSchemaA'; +import { addSchemaLevelResolver } from '../src/add-schema-level-resolvers'; + +interface Bird { + name: string; + wingspan?: number; +} + +function expectWarning(fn: () => void, warnMatcher?: string) { + const originalWarn = console.warn; + let warning: string = null; + + try { + console.warn = function warn(message: string) { + warning = message; + }; + + fn(); + + if (undefined === warnMatcher) { + expect(warning).toBe(null); + } else { + expect(warning).toMatch(warnMatcher); + } + } finally { + console.warn = originalWarn; + } +} + +const testSchema = ` + type RootQuery { + usecontext: String + useTestConnector: String + useContextConnector: String + species(name: String): String + stuff: String + } + schema { + query: RootQuery + } + `; +const testResolvers = { + __schema: () => ({ stuff: 'stuff', species: 'ROOT' }), + RootQuery: { + usecontext: (_r: any, _a: { [key: string]: any }, ctx: any) => + ctx.usecontext, + useTestConnector: (_r: any, _a: { [key: string]: any }, ctx: any) => + ctx.connectors.TestConnector.get(), + useContextConnector: (_r: any, _a: { [key: string]: any }, ctx: any) => + ctx.connectors.ContextConnector.get(), + species: (root: any, { name }: { name: string }) => + (root.species as string) + name, + }, +}; + +describe('generating schema from shorthand', () => { + test('throws an error if no schema is provided', () => { + expect(() => makeExecutableSchema(undefined)).toThrowError('undefined'); + }); + + test('throws an error if typeDefinitionNodes are not provided', () => { + expect(() => + makeExecutableSchema({ typeDefs: undefined, resolvers: {} }), + ).toThrowError('Must provide typeDefs'); + }); + + test('throws an error if no resolveFunctions are provided', () => { + expect(() => + makeExecutableSchema({ typeDefs: 'blah', resolvers: {} }), + ).toThrowError(GraphQLError); + }); + + test('throws an error if typeDefinitionNodes is neither string nor array nor schema AST', () => { + expect(() => + makeExecutableSchema({ + typeDefs: ({} as unknown) as ITypeDefinitions, + resolvers: {}, + }), + ).toThrowError( + 'typeDefs must be a string, array or schema AST, got object', + ); + }); + + test('throws an error if typeDefinitionNode array contains not only functions and strings', () => { + expect(() => + makeExecutableSchema({ + typeDefs: ([17] as unknown) as ITypeDefinitions, + resolvers: {}, + }), + ).toThrowError( + 'typeDef array must contain only strings and functions, got number', + ); + }); + + test('throws an error if resolverValidationOptions is not an object', () => { + const options = { + typeDefs: 'blah', + resolvers: {}, + resolverValidationOptions: ('string' as unknown) as IResolverValidationOptions, + }; + expect(() => makeExecutableSchema(options)).toThrowError( + 'Expected `resolverValidationOptions` to be an object', + ); + }); + + test('can generate a schema', () => { + const shorthand = ` + """ + A bird species + """ + type BirdSpecies { + name: String!, + wingspan: Int + } + type RootQuery { + species(name: String!): [BirdSpecies] + } + + schema { + query: RootQuery + } + `; + + const introspectionQuery = `{ + species: __type(name: "BirdSpecies"){ + name, + description, + fields{ + name + type{ + name + kind + ofType{ + name + } + } + } + } + query: __type(name: "RootQuery"){ + name, + description, + fields{ + name + type { + name + kind + ofType { + name + } + } + args { + name + type { + name + kind + ofType { + name + } + } + } + } + } + }`; + + const solution = { + data: { + species: { + name: 'BirdSpecies', + description: 'A bird species', + fields: [ + { + name: 'name', + type: { + kind: 'NON_NULL', + name: null as string, + ofType: { + name: 'String', + }, + }, + }, + { + name: 'wingspan', + type: { + kind: 'SCALAR', + name: 'Int', + ofType: null, + }, + }, + ], + }, + query: { + name: 'RootQuery', + description: version >= 15 ? (null as string) : '', + fields: [ + { + name: 'species', + type: { + kind: 'LIST', + name: null as string, + ofType: { + name: 'BirdSpecies', + }, + }, + args: [ + { + name: 'name', + type: { + name: null as string, + kind: 'NON_NULL', + ofType: { + name: 'String', + }, + }, + }, + ], + }, + ], + }, + }, + }; + + const jsSchema = makeExecutableSchema({ + typeDefs: shorthand, + resolvers: {}, + }); + const resultPromise = graphql(jsSchema, introspectionQuery); + return resultPromise.then((result) => + expect(result).toEqual(solution as ExecutionResult), + ); + }); + + test('can generate a schema from an array of types', () => { + const typeDefAry = [ + ` + type Query { + foo: String + } + `, + ` + schema { + query: Query + } + `, + ]; + + const jsSchema = makeExecutableSchema({ + typeDefs: typeDefAry, + resolvers: {}, + }); + expect(jsSchema.getQueryType().name).toBe('Query'); + }); + + test('can generate a schema from a parsed type definition', () => { + const typeDefSchema = parse(` + type Query { + foo: String + } + schema { + query: Query + } + `); + + const jsSchema = makeExecutableSchema({ + typeDefs: typeDefSchema, + resolvers: {}, + }); + expect(jsSchema.getQueryType().name).toBe('Query'); + }); + + test('can generate a schema from an array of parsed and none parsed type definitions', () => { + const typeDefSchema = [ + parse(` + type Query { + foo: String + }`), + ` + schema { + query: Query + } + `, + ]; + + const jsSchema = makeExecutableSchema({ + typeDefs: typeDefSchema, + resolvers: {}, + }); + expect(jsSchema.getQueryType().name).toBe('Query'); + }); + + test('can generate a schema from an array of types with extensions', () => { + const typeDefAry = [ + ` + type Query { + foo: String + } + `, + ` + schema { + query: Query + } + `, + ` + extend type Query { + bar: String + } + `, + ]; + + const jsSchema = makeExecutableSchema({ + typeDefs: typeDefAry, + resolvers: {}, + }); + expect(jsSchema.getQueryType().name).toBe('Query'); + expect(jsSchema.getQueryType().getFields().foo).toBeDefined(); + expect(jsSchema.getQueryType().getFields().bar).toBeDefined(); + }); + + test('can concatenateTypeDefs created by a function inside a closure', () => { + const typeA = { typeDefs: () => ['type TypeA { foo: String }'] }; + const typeB = { typeDefs: () => ['type TypeB { bar: String }'] }; + const typeC = { typeDefs: () => ['type TypeC { foo: String }'] }; + const typeD = { typeDefs: () => ['type TypeD { bar: String }'] }; + + function combineTypeDefs(...args: Array): any { + return { typeDefs: () => args.map((o) => o.typeDefs) }; + } + + const combinedAandB = combineTypeDefs(typeA, typeB); + const combinedCandD = combineTypeDefs(typeC, typeD); + + const result = concatenateTypeDefs([ + combinedAandB.typeDefs, + combinedCandD.typeDefs, + ]); + + expect(result).toMatch('type TypeA'); + expect(result).toMatch('type TypeB'); + expect(result).toMatch('type TypeC'); + expect(result).toMatch('type TypeD'); + }); + + test('properly deduplicates the array of type DefinitionNodes', () => { + const typeDefAry = [ + ` + type Query { + foo: String + } + `, + ` + schema { + query: Query + } + `, + ` + schema { + query: Query + } + `, + ]; + + const jsSchema = makeExecutableSchema({ + typeDefs: typeDefAry, + resolvers: {}, + }); + expect(jsSchema.getQueryType().name).toBe('Query'); + }); + + test('works with imports, even circular ones', () => { + const typeDefAry = [ + ` + type Query { + foo: TypeA + } + `, + ` + schema { + query: Query + } + `, + TypeA, + ]; + + const jsSchema = makeExecutableSchema({ + typeDefs: typeDefAry, + resolvers: { + Query: { foo: () => null }, + TypeA: { b: () => null }, + TypeB: { a: () => null }, + }, + }); + expect(jsSchema.getQueryType().name).toBe('Query'); + }); + + test('can generate a schema with resolvers', () => { + const shorthand = ` + type BirdSpecies { + name: String!, + wingspan: Int + } + type RootQuery { + species(name: String!): [BirdSpecies] + } + schema { + query: RootQuery + } + `; + + const resolveFunctions = { + RootQuery: { + species: (_root: any, { name }: { name: string }) => [ + { + name: `Hello ${name}!`, + wingspan: 200, + }, + ], + }, + }; + + const testQuery = `{ + species(name: "BigBird"){ + name + wingspan + } + }`; + + const solution = { + data: { + species: [ + { + name: 'Hello BigBird!', + wingspan: 200, + }, + ], + }, + }; + const jsSchema = makeExecutableSchema({ + typeDefs: shorthand, + resolvers: resolveFunctions, + }); + const resultPromise = graphql(jsSchema, testQuery); + return resultPromise.then((result) => + expect(result).toEqual(solution as ExecutionResult), + ); + }); + + test('can generate a schema with extensions that can use resolvers', () => { + const shorthand = ` + type BirdSpecies { + name: String!, + wingspan: Int + } + type RootQuery { + species(name: String!): [BirdSpecies] + } + schema { + query: RootQuery + } + extend type BirdSpecies { + height: Float + } + `; + + const resolveFunctions = { + RootQuery: { + species: (_root: any, { name }: { name: string }) => [ + { + name: `Hello ${name}!`, + wingspan: 200, + height: 30.2, + }, + ], + }, + BirdSpecies: { + name: (bird: Bird) => bird.name, + wingspan: (bird: Bird) => bird.wingspan, + height: (bird: Bird & { height: number }) => bird.height, + }, + }; + + const testQuery = `{ + species(name: "BigBird"){ + name + wingspan + height + } + }`; + + const solution = { + data: { + species: [ + { + name: 'Hello BigBird!', + wingspan: 200, + height: 30.2, + }, + ], + }, + }; + const jsSchema = makeExecutableSchema({ + typeDefs: shorthand, + resolvers: resolveFunctions, + }); + const resultPromise = graphql(jsSchema, testQuery); + return resultPromise.then((result) => + expect(result).toEqual(solution as ExecutionResult), + ); + }); + + test('supports resolveType for unions', () => { + const shorthand = ` + union Searchable = Person | Location + type Person { + name: String + age: Int + } + type Location { + name: String + coordinates: String + } + type RootQuery { + search(name: String): [Searchable] + } + schema { + query: RootQuery + } + `; + + const resolveFunctions = { + RootQuery: { + search: { + resolve(_root: any, { name }: { name: string }) { + return [ + { + name: `Tom ${name}`, + age: 100, + }, + { + name: 'North Pole', + coordinates: '90, 0', + }, + ]; + }, + }, + }, + Searchable: { + __resolveType(data: any, _context: any, info: GraphQLResolveInfo) { + if (data.age) { + return info.schema.getType('Person'); + } + if (data.coordinates) { + return info.schema.getType('Location'); + } + return null; + }, + }, + }; + + const testQuery = `{ + search(name: "a"){ + ... on Person { + name + age + } + ... on Location { + name + coordinates + } + } + }`; + + const solution = { + data: { + search: [ + { + name: 'Tom a', + age: 100, + }, + { + name: 'North Pole', + coordinates: '90, 0', + }, + ], + }, + }; + + const jsSchema = makeExecutableSchema({ + typeDefs: shorthand, + resolvers: resolveFunctions, + }); + const resultPromise = graphql(jsSchema, testQuery); + return resultPromise.then((result) => + expect(result).toEqual(solution as ExecutionResult), + ); + }); + + test('can generate a schema with an array of resolvers', () => { + const shorthand = ` + type BirdSpecies { + name: String!, + wingspan: Int + } + type RootQuery { + numberOfSpecies: Int + species(name: String!): [BirdSpecies] + } + schema { + query: RootQuery + } + extend type BirdSpecies { + height: Float + } + `; + + const resolvers = { + RootQuery: { + species: (_root: any, { name }: { name: string }) => [ + { + name: `Hello ${name}!`, + wingspan: 200, + height: 30.2, + }, + ], + }, + }; + + const otherResolvers = { + BirdSpecies: { + name: (bird: Bird) => bird.name, + wingspan: (bird: Bird) => bird.wingspan, + height: (bird: Bird & { height: number }) => bird.height, + }, + RootQuery: { + numberOfSpecies() { + return 1; + }, + }, + }; + + const testQuery = `{ + numberOfSpecies + species(name: "BigBird"){ + name + wingspan + height + } + }`; + + const solution = { + data: { + numberOfSpecies: 1, + species: [ + { + name: 'Hello BigBird!', + wingspan: 200, + height: 30.2, + }, + ], + }, + }; + const jsSchema = makeExecutableSchema({ + typeDefs: shorthand, + resolvers: [resolvers, otherResolvers], + }); + const resultPromise = graphql(jsSchema, testQuery); + return resultPromise.then((result) => + expect(result).toEqual(solution as ExecutionResult), + ); + }); + + describe('scalar types', () => { + test('supports passing a GraphQLScalarType in resolveFunctions', () => { + // Here GraphQLJSON is used as an example of non-default GraphQLScalarType + const shorthand = ` + scalar JSON + + type Foo { + aField: JSON + } + + type Query { + foo: Foo + } + `; + const resolveFunctions = { + JSON: GraphQLJSON, + }; + const jsSchema = makeExecutableSchema({ + typeDefs: shorthand, + resolvers: resolveFunctions, + }); + expect(jsSchema.getQueryType().name).toBe('Query'); + expect(jsSchema.getType('JSON')).toBeInstanceOf(GraphQLScalarType); + expect(jsSchema.getType('JSON')).toHaveProperty('description'); + expect(typeof jsSchema.getType('JSON').description).toBe('string'); + expect(jsSchema.getType('JSON')['description'].length).toBeGreaterThan(0); + }); + + test('supports passing a default scalar type', () => { + const shorthand = ` + type Foo { + aField: Boolean + } + + type Query { + foo: Foo + } + `; + const resolveFunctions = { + Boolean: GraphQLBoolean, + }; + const jsSchema = makeExecutableSchema({ + typeDefs: shorthand, + resolvers: resolveFunctions, + }); + expect(jsSchema.getQueryType().name).toBe('Query'); + expect(jsSchema.getType('Boolean')).toBe(GraphQLBoolean); + }); + + test('allow overriding default scalar type fields', () => { + const originalSerialize = GraphQLBoolean.serialize; + const shorthand = ` + type Foo { + aField: Boolean + } + + type Query { + foo: Foo + } + `; + const resolveFunctions = { + Boolean: new GraphQLScalarType({ + name: 'Boolean', + serialize: () => false, + }), + Query: { + foo: () => ({ aField: true }), + }, + }; + const jsSchema = makeExecutableSchema({ + typeDefs: shorthand, + resolvers: resolveFunctions, + }); + const testQuery = ` + { + foo { + aField + } + } + `; + const result = graphqlSync(jsSchema, testQuery); + expect(result.data.foo.aField).toBe(false); + addResolversToSchema({ + schema: jsSchema, + resolvers: { + Boolean: { + serialize: originalSerialize, + }, + }, + }); + }); + + test('retains scalars after walking/recreating the schema', () => { + const shorthand = ` + scalar Test + + type Foo { + testField: Test + } + + type Query { + test: Test + testIn(input: Test): Test + } + `; + const resolveFunctions = { + Test: new GraphQLScalarType({ + name: 'Test', + description: 'Test resolver', + serialize(value) { + if (typeof value !== 'string' || value.indexOf('scalar:') !== 0) { + return `scalar:${value as string}`; + } + return value; + }, + parseValue(value) { + return `scalar:${value as string}`; + }, + parseLiteral(ast: any) { + switch (ast.kind) { + case Kind.STRING: + case Kind.INT: + return `scalar:${ast.value as string}`; + default: + return null; + } + }, + }), + Query: { + testIn(_: any, { input }: any) { + expect(input).toMatch('scalar:'); + return input; + }, + test() { + return 42; + }, + }, + }; + const walkedSchema = visitSchema( + makeExecutableSchema({ + typeDefs: shorthand, + resolvers: resolveFunctions, + }), + { + [VisitSchemaKind.ENUM_TYPE](type: GraphQLEnumType) { + return type; + }, + }, + ); + expect(walkedSchema.getType('Test')).toBeInstanceOf(GraphQLScalarType); + expect(walkedSchema.getType('Test')).toHaveProperty('description'); + expect(walkedSchema.getType('Test').description).toBe('Test resolver'); + const testQuery = ` + { + test + testIn(input: 1) + }`; + const resultPromise = graphql(walkedSchema, testQuery); + return resultPromise.then((result) => + expect(result.data).toEqual({ + test: 'scalar:42', + testIn: 'scalar:1', + }), + ); + }); + + test('should support custom scalar usage on client-side query execution', () => { + const shorthand = ` + scalar CustomScalar + + type TestType { + testField: String + } + + type RootQuery { + myQuery(t: CustomScalar): TestType + } + + schema { + query: RootQuery + } + `; + + const resolveFunctions = { + CustomScalar: new GraphQLScalarType({ + name: 'CustomScalar', + serialize(value) { + return value; + }, + parseValue(value) { + return value; + }, + parseLiteral(ast: any) { + switch (ast.kind) { + case Kind.STRING: + return ast.value; + default: + return null; + } + }, + }), + }; + + const testQuery = ` + query myQuery($t: CustomScalar) { + myQuery(t: $t) { + testField + } + }`; + + const jsSchema = makeExecutableSchema({ + typeDefs: shorthand, + resolvers: resolveFunctions, + }); + const resultPromise = graphql(jsSchema, testQuery); + return resultPromise.then((result) => expect(result.errors).toBeFalsy()); + }); + + test('should work with an Odd custom scalar type', () => { + const oddValue = (value: number) => (value % 2 === 1 ? value : null); + + const OddType = new GraphQLScalarType({ + name: 'Odd', + description: 'Odd custom scalar type', + parseValue: oddValue, + serialize: oddValue, + parseLiteral(ast) { + if (ast.kind === Kind.INT) { + const intValue: IntValueNode = ast; + return oddValue(parseInt(intValue.value, 10)); + } + return null; + }, + }); + + const typeDefs = ` + scalar Odd + + type Post { + id: Int! + title: String + something: Odd + } + + type Query { + post: Post + } + + schema { + query: Query + } + `; + + const testValue = 3; + const resolvers = { + Odd: OddType, + Query: { + post() { + return { + id: 1, + title: 'My first post', + something: testValue, + }; + }, + }, + }; + + const jsSchema = makeExecutableSchema({ + typeDefs, + resolvers, + }); + const testQuery = ` + { + post { + something + } + } + `; + const resultPromise = graphql(jsSchema, testQuery); + return resultPromise.then((result) => { + expect(result.data['post'].something).toEqual(testValue); + expect(result.errors).toEqual(undefined); + }); + }); + + test('should work with a Date custom scalar type', () => { + const DateType = new GraphQLScalarType({ + name: 'Date', + description: 'Date custom scalar type', + parseValue(value) { + return new Date(value); + }, + serialize(value) { + return value.getTime(); + }, + parseLiteral(ast) { + if (ast.kind === Kind.INT) { + const intValue: IntValueNode = ast; + return parseInt(intValue.value, 10); + } + return null; + }, + }); + + const typeDefs = ` + scalar Date + + type Post { + id: Int! + title: String + something: Date + } + + type Query { + post: Post + } + + schema { + query: Query + } + `; + + const testDate = new Date(2016, 0, 1); + + const resolvers = { + Date: DateType, + Query: { + post() { + return { + id: 1, + title: 'My first post', + something: testDate, + }; + }, + }, + }; + + const jsSchema = makeExecutableSchema({ + typeDefs, + resolvers, + }); + const testQuery = ` + { + post { + something + } + } + `; + const resultPromise = graphql(jsSchema, testQuery); + return resultPromise.then((result) => { + expect(result.data['post'].something).toEqual(testDate.getTime()); + expect(result.errors).toEqual(undefined); + }); + }); + }); + + describe('enum support', () => { + test('supports passing a GraphQLEnumType in resolveFunctions', () => { + const shorthand = ` + enum Color { + RED + } + + enum NumericEnum { + TEST + } + + schema { + query: Query + } + + type Query { + color: Color + numericEnum: NumericEnum + } + `; + + const resolveFunctions = { + Color: { + RED: '#EA3232', + }, + NumericEnum: { + TEST: 1, + }, + }; + + const jsSchema = makeExecutableSchema({ + typeDefs: shorthand, + resolvers: resolveFunctions, + }); + + expect(jsSchema.getQueryType().name).toBe('Query'); + expect(jsSchema.getType('Color')).toBeInstanceOf(GraphQLEnumType); + expect(jsSchema.getType('NumericEnum')).toBeInstanceOf(GraphQLEnumType); + }); + + test('supports passing the value for a GraphQLEnumType in resolveFunctions', () => { + const shorthand = ` + enum Color { + RED + BLUE + } + + enum NumericEnum { + TEST + } + + schema { + query: Query + } + + type Query { + redColor: Color + blueColor: Color + numericEnum: NumericEnum + } + `; + + const testQuery = `{ + redColor + blueColor + numericEnum + }`; + + const resolveFunctions = { + Color: { + RED: '#EA3232', + BLUE: '#0000FF', + }, + NumericEnum: { + TEST: 1, + }, + Query: { + redColor() { + return '#EA3232'; + }, + blueColor() { + return '#0000FF'; + }, + numericEnum() { + return 1; + }, + }, + }; + + const jsSchema = makeExecutableSchema({ + typeDefs: shorthand, + resolvers: resolveFunctions, + }); + + const resultPromise = graphql(jsSchema, testQuery); + return resultPromise.then((result) => { + expect(result.data['redColor']).toEqual('RED'); + expect(result.data['blueColor']).toEqual('BLUE'); + expect(result.data['numericEnum']).toEqual('TEST'); + expect(result.errors).toEqual(undefined); + }); + }); + + test('supports resolving the value for a GraphQLEnumType in input types', () => { + const shorthand = ` + enum Color { + RED + BLUE + } + + enum NumericEnum { + TEST + } + + schema { + query: Query + } + + type Query { + colorTest(color: Color): String + numericTest(num: NumericEnum): Int + } + `; + + const testQuery = `{ + red: colorTest(color: RED) + blue: colorTest(color: BLUE) + num: numericTest(num: TEST) + }`; + + const resolveFunctions = { + Color: { + RED: '#EA3232', + BLUE: '#0000FF', + }, + NumericEnum: { + TEST: 1, + }, + Query: { + colorTest(_root: any, args: { color: string }) { + return args.color; + }, + numericTest(_root: any, args: { num: number }) { + return args.num; + }, + }, + }; + + const jsSchema = makeExecutableSchema({ + typeDefs: shorthand, + resolvers: resolveFunctions, + }); + + const resultPromise = graphql(jsSchema, testQuery); + return resultPromise.then((result) => { + expect(result.data['red']).toEqual(resolveFunctions.Color.RED); + expect(result.data['blue']).toEqual(resolveFunctions.Color.BLUE); + expect(result.data['num']).toEqual(resolveFunctions.NumericEnum.TEST); + expect(result.errors).toEqual(undefined); + }); + }); + }); + + describe('default value support', () => { + test('supports default field values', () => { + const shorthand = ` + enum Color { + RED + } + + schema { + query: Query + } + + type Query { + colorTest(color: Color = RED): String + } + `; + + const testQuery = `{ + red: colorTest + }`; + + const resolveFunctions = { + Color: { + RED: '#EA3232', + }, + Query: { + colorTest(_root: any, args: { color: string }) { + return args.color; + }, + }, + }; + + const jsSchema = makeExecutableSchema({ + typeDefs: shorthand, + resolvers: resolveFunctions, + }); + + const resultPromise = graphql(jsSchema, testQuery); + return resultPromise.then((result) => { + expect(result.data['red']).toEqual(resolveFunctions.Color.RED); + expect(result.errors).toEqual(undefined); + }); + }); + + test('supports changing default field values', () => { + const shorthand = ` + enum Color { + RED + } + + schema { + query: Query + } + + type Query { + colorTest(color: Color = RED): String + } + `; + + const testQuery = `{ + red: colorTest + }`; + + const resolveFunctions = { + Color: { + RED: '#EA3232', + }, + Query: { + colorTest(_root: any, args: { color: string }) { + return args.color; + }, + }, + }; + + const jsSchema = makeExecutableSchema({ + typeDefs: shorthand, + resolvers: resolveFunctions, + }); + + addResolversToSchema({ + schema: jsSchema, + resolvers: { + Color: { + RED: 'override', + }, + }, + }); + + const resultPromise = graphql(jsSchema, testQuery); + return resultPromise.then((result) => { + expect(result.data['red']).toEqual('override'); + expect(result.errors).toEqual(undefined); + }); + }); + }); + + test('can set description and deprecation reason', () => { + const shorthand = ` + type BirdSpecies { + name: String!, + wingspan: Int + } + type RootQuery { + species(name: String!): [BirdSpecies] + } + schema { + query: RootQuery + } + `; + + const resolveFunctions = { + RootQuery: { + species: { + description: 'A species', + deprecationReason: 'Just because', + resolve: (_root: any, { name }: { name: string }) => [ + { + name: `Hello ${name}!`, + wingspan: 200, + }, + ], + }, + }, + }; + + const testQuery = `{ + __type(name: "RootQuery"){ + name + fields(includeDeprecated: true){ + name + description + deprecationReason + } + } + }`; + + const solution = { + data: { + __type: { + name: 'RootQuery', + fields: [ + { + name: 'species', + description: 'A species', + deprecationReason: 'Just because', + }, + ], + }, + }, + }; + + const jsSchema = makeExecutableSchema({ + typeDefs: shorthand, + resolvers: resolveFunctions, + }); + const resultPromise = graphql(jsSchema, testQuery); + return resultPromise.then((result) => + expect(result).toEqual(solution as ExecutionResult), + ); + }); + + test('shows a warning if a field has arguments but no resolver', () => { + const short = ` + type Query{ + bird(id: ID): String + } + schema { + query: Query + }`; + + const rf = { Query: {} }; + + expectWarning(() => { + makeExecutableSchema({ + typeDefs: short, + resolvers: rf, + resolverValidationOptions: { + requireResolversForArgs: true, + }, + }); + }, 'Resolver missing for "Query.bird"'); + }); + + test('does not throw an error if `resolverValidationOptions.requireResolversForArgs` is false', () => { + const short = ` + type Query{ + bird(id: ID): String + } + schema { + query: Query + }`; + + const rf = { Query: {} }; + + expect( + makeExecutableSchema.bind(null, { typeDefs: short, resolvers: rf }), + ).not.toThrow(); + }); + + test('throws an error if a resolver is not a function', () => { + const short = ` + type Query{ + bird(id: ID): String + } + schema { + query: Query + }`; + + const rf = { Query: { bird: 'NOT A FUNCTION' } }; + + expect(() => + makeExecutableSchema({ + typeDefs: short, + resolvers: rf, + }), + ).toThrowError('Resolver Query.bird must be object or function'); + }); + + test('shows a warning if a field is not scalar, but has no resolver', () => { + const short = ` + type Bird{ + id: ID + } + type Query{ + bird: Bird + } + schema { + query: Query + }`; + + const rf = {}; + + const resolverValidationOptions: IResolverValidationOptions = { + requireResolversForNonScalar: true, + }; + + expectWarning(() => { + makeExecutableSchema({ + typeDefs: short, + resolvers: rf, + resolverValidationOptions, + }); + }, 'Resolver missing for "Query.bird"'); + }); + + test('allows non-scalar field to use default resolver if `resolverValidationOptions.requireResolversForNonScalar` = false', () => { + const short = ` + type Bird{ + id: ID + } + type Query{ + bird: Bird + } + schema { + query: Query + }`; + + const rf = {}; + + expect( + makeExecutableSchema.bind(null, { + typeDefs: short, + resolvers: rf, + resolverValidationOptions: { requireResolversForNonScalar: false }, + }), + ).not.toThrow(); + }); + + test('throws if resolver defined for non-object/interface type', () => { + const short = ` + union Searchable = Person | Location + type Person { + name: String + age: Int + } + type Location { + name: String + coordinates: String + } + type RootQuery { + search(name: String): [Searchable] + } + schema { + query: RootQuery + } + `; + + const rf = { + Searchable: { + name: () => 'Something', + }, + }; + + expect(() => + makeExecutableSchema({ typeDefs: short, resolvers: rf }), + ).toThrowError( + "Searchable was defined in resolvers, but it's not an object", + ); + + expect(() => + makeExecutableSchema({ + typeDefs: short, + resolvers: rf, + resolverValidationOptions: { + allowResolversNotInSchema: true, + }, + }), + ).not.toThrowError(); + }); + + test('throws if resolver defined for non existent type', () => { + const short = ` + type Person { + name: String + age: Int + } + type RootQuery { + search(name: String): [Person] + } + schema { + query: RootQuery + } + `; + + const rf = { + Searchable: { + name: () => 'Something', + }, + }; + + expect(() => + makeExecutableSchema({ typeDefs: short, resolvers: rf }), + ).toThrowError('"Searchable" defined in resolvers, but not in schema'); + + expect(() => + makeExecutableSchema({ + typeDefs: short, + resolvers: rf, + resolverValidationOptions: { + allowResolversNotInSchema: true, + }, + }), + ).not.toThrowError(); + }); + + test('throws if resolver value is invalid', () => { + const short = ` + type Person { + name: String + age: Int + } + type RootQuery { + search(name: String): [Person] + } + schema { + query: RootQuery + } + `; + + const rf = { + Searchable: undefined, + } as any; + + expect(() => + makeExecutableSchema({ typeDefs: short, resolvers: rf }), + ).toThrowError( + '"Searchable" defined in resolvers, but has invalid value "undefined". A resolver\'s value ' + + 'must be of type object or function.', + ); + }); + + test('doesnt let you define resolver field not present in schema', () => { + const short = ` + type Person { + name: String + age: Int + } + type RootQuery { + search(name: String): [Person] + } + schema { + query: RootQuery + } + `; + + const rf = { + RootQuery: { + name: () => 'Something', + }, + }; + + expect(() => + makeExecutableSchema({ typeDefs: short, resolvers: rf }), + ).toThrowError('RootQuery.name defined in resolvers, but not in schema'); + + expect(() => + makeExecutableSchema({ + typeDefs: short, + resolvers: rf, + resolverValidationOptions: { + allowResolversNotInSchema: true, + }, + }), + ).not.toThrowError(); + }); + + test('does not let you define resolver field for enum values not present in schema', () => { + const short = ` + enum Color { + RED + } + + enum NumericEnum { + TEST + } + + schema { + query: Query + } + + type Query { + color: Color + numericEnum: NumericEnum + } + `; + + const rf = { + Color: { + RED: '#EA3232', + NO_RESOLVER: '#EA3232', + }, + NumericEnum: { + TEST: 1, + }, + }; + + expect(() => + makeExecutableSchema({ typeDefs: short, resolvers: rf }), + ).toThrowError( + 'Color.NO_RESOLVER was defined in resolvers, but enum is not in schema', + ); + + expect(() => + makeExecutableSchema({ + typeDefs: short, + resolvers: rf, + resolverValidationOptions: { + allowResolversNotInSchema: true, + }, + }), + ).not.toThrowError(); + }); + + test('throws if conflicting validation options are passed', () => { + const typeDefs = ` + type Bird { + id: ID + } + type Query { + bird: Bird + } + schema { + query: Query + }`; + const resolvers = {}; + + function assertOptionsError( + resolverValidationOptions: IResolverValidationOptions, + ) { + expect(() => + makeExecutableSchema({ + typeDefs, + resolvers, + resolverValidationOptions, + }), + ).toThrow(); + } + + assertOptionsError({ + requireResolversForAllFields: true, + requireResolversForNonScalar: true, + requireResolversForArgs: true, + }); + assertOptionsError({ + requireResolversForAllFields: true, + requireResolversForNonScalar: true, + }); + assertOptionsError({ + requireResolversForAllFields: true, + requireResolversForArgs: true, + }); + }); + + test('throws for any missing field if `resolverValidationOptions.requireResolversForAllFields` = true', () => { + const typeDefs = ` + type Bird { + id: ID + } + type Query { + bird: Bird + } + schema { + query: Query + }`; + + function assertFieldError(errorMatcher: string, resolvers: IResolvers) { + expectWarning(() => { + makeExecutableSchema({ + typeDefs, + resolvers, + resolverValidationOptions: { + requireResolversForAllFields: true, + }, + }); + }, errorMatcher); + } + + assertFieldError(version >= 15 ? 'Query.bird' : 'Bird.id', {}); + assertFieldError('Query.bird', { + Bird: { + id: (bird: { id: string }) => bird.id, + }, + }); + assertFieldError('Bird.id', { + Query: { + bird: () => ({ id: '123' }), + }, + }); + }); + + test('does not throw if all fields are satisfied when `resolverValidationOptions.requireResolversForAllFields` = true', () => { + const typeDefs = ` + type Bird { + id: ID + } + type Query { + bird: Bird + } + schema { + query: Query + }`; + + const resolvers = { + Bird: { + id: (bird: { id: string }) => bird.id, + }, + Query: { + bird: () => ({ id: '123' }), + }, + }; + + expect(() => + makeExecutableSchema({ + typeDefs, + resolvers, + resolverValidationOptions: { requireResolversForAllFields: true }, + }), + ).not.toThrow(); + }); + + test('throws an error if a resolve field cannot be used', (done) => { + const shorthand = ` + type BirdSpecies { + name: String!, + wingspan: Int + } + type RootQuery { + species(name: String!): [BirdSpecies] + } + schema { + query: RootQuery + } + `; + + const resolveFunctions = { + RootQuery: { + speciez: (_root: any, { name }: { name: string }) => [ + { + name: `Hello ${name}!`, + wingspan: 200, + }, + ], + }, + }; + expect(() => + makeExecutableSchema({ + typeDefs: shorthand, + resolvers: resolveFunctions, + }), + ).toThrowError('RootQuery.speciez defined in resolvers, but not in schema'); + done(); + }); + test('throws an error if a resolve type is not in schema', (done) => { + const shorthand = ` + type BirdSpecies { + name: String!, + wingspan: Int + } + type RootQuery { + species(name: String!): [BirdSpecies] + } + schema { + query: RootQuery + } + `; + const resolveFunctions = { + BootQuery: { + species: (_root: any, { name }: { name: string }) => [ + { + name: `Hello ${name}!`, + wingspan: 200, + }, + ], + }, + }; + expect(() => + makeExecutableSchema({ + typeDefs: shorthand, + resolvers: resolveFunctions, + }), + ).toThrowError('"BootQuery" defined in resolvers, but not in schema'); + done(); + }); +}); + +describe('providing useful errors from resolvers', () => { + test('logs an error if a resolver fails', () => { + const shorthand = ` + type RootQuery { + species(name: String): String + } + schema { + query: RootQuery + } + `; + const resolve = { + RootQuery: { + species: (): string => { + throw new Error('oops!'); + }, + }, + }; + + // TODO: Should use a spy here instead of logger class + // to make sure we don't duplicate tests from Logger. + const logger = new Logger(); + const jsSchema = makeExecutableSchema({ + typeDefs: shorthand, + resolvers: resolve, + logger, + }); + const testQuery = '{ species }'; + const expected = 'Error in resolver RootQuery.species\noops!'; + return graphql(jsSchema, testQuery).then((_res) => { + expect(logger.errors.length).toEqual(1); + expect(logger.errors[0].message).toEqual(expected); + }); + }); + + test('will throw errors on undefined if you tell it to', () => { + const shorthand = ` + type RootQuery { + species(name: String): String + stuff: String + } + schema { + query: RootQuery + } + `; + const resolve = { + RootQuery: { + species: (): string => undefined, + stuff: () => 'stuff', + }, + }; + + const logger = new Logger(); + const jsSchema = makeExecutableSchema({ + typeDefs: shorthand, + resolvers: resolve, + logger, + allowUndefinedInResolve: false, + }); + const testQuery = '{ species, stuff }'; + const expectedErr = /Resolver for "RootQuery.species" returned undefined/; + const expectedResData = { species: null as string, stuff: 'stuff' }; + return graphql(jsSchema, testQuery).then((res) => { + expect(logger.errors.length).toEqual(1); + expect(logger.errors[0].message).toMatch(expectedErr); + expect(res.data).toEqual(expectedResData); + }); + }); + + test('decorateToCatchUndefined preserves default resolvers', () => { + const shorthand = ` + type Thread { + name: String + } + type RootQuery { + thread(name: String): Thread + } + schema { + query: RootQuery + } + `; + const resolve = { + RootQuery: { + thread(_root: any, args: { [key: string]: any }) { + return args; + }, + }, + }; + + const jsSchema = makeExecutableSchema({ + typeDefs: shorthand, + resolvers: resolve, + allowUndefinedInResolve: false, + }); + const testQuery = `{ + thread(name: "SomeThread") { + name + } + }`; + const expectedResData = { + thread: { + name: 'SomeThread', + }, + }; + return graphql(jsSchema, testQuery).then((res) => { + expect(res.data).toEqual(expectedResData); + }); + }); + + test('decorateToCatchUndefined throws even if default resolvers are preserved', () => { + const shorthand = ` + type Thread { + name: String + } + type RootQuery { + thread(name: String): Thread + } + schema { + query: RootQuery + } + `; + const resolve = { + RootQuery: { + thread(_root: any, _args: { [key: string]: any }) { + return { name: (): any => undefined }; + }, + }, + }; + + const jsSchema = makeExecutableSchema({ + typeDefs: shorthand, + resolvers: resolve, + allowUndefinedInResolve: false, + }); + const testQuery = `{ + thread { + name + } + }`; + return graphql(jsSchema, testQuery).then((res) => { + expect(res.errors[0].originalError.message).toBe( + 'Resolver for "Thread.name" returned undefined', + ); + }); + }); + + test('will use default resolver when returning function properties ', () => { + const shorthand = ` + type Thread { + name: String + } + type RootQuery { + thread(name: String): Thread + } + schema { + query: RootQuery + } + `; + const resolve = { + RootQuery: { + thread(_root: any, args: { [key: string]: any }) { + return { name: () => args['name'] }; + }, + }, + }; + + const jsSchema = makeExecutableSchema({ + typeDefs: shorthand, + resolvers: resolve, + allowUndefinedInResolve: false, + }); + const testQuery = `{ + thread(name: "SomeThread") { + name + } + }`; + const expectedResData = { + thread: { + name: 'SomeThread', + }, + }; + return graphql(jsSchema, testQuery).then((res) => { + expect(res.data).toEqual(expectedResData); + }); + }); + + test('will not throw errors on undefined by default', () => { + const shorthand = ` + type RootQuery { + species(name: String): String + stuff: String + } + schema { + query: RootQuery + } + `; + const resolve = { + RootQuery: { + species: (): string => undefined, + stuff: () => 'stuff', + }, + }; + + const logger = new Logger(); + const jsSchema = makeExecutableSchema({ + typeDefs: shorthand, + resolvers: resolve, + logger, + }); + const testQuery = '{ species, stuff }'; + const expectedResData = { species: null as string, stuff: 'stuff' }; + return graphql(jsSchema, testQuery).then((res) => { + expect(logger.errors.length).toEqual(0); + expect(res.data).toEqual(expectedResData); + }); + }); +}); + +describe('Add error logging to schema', () => { + test('throws an error if no logger is provided', () => { + expect(() => + addErrorLoggingToSchema(({} as unknown) as GraphQLSchema), + ).toThrow('Must provide a logger'); + }); + test('throws an error if logger.log is not a function', () => { + expect(() => + addErrorLoggingToSchema( + ({} as unknown) as GraphQLSchema, + ({ log: '1' } as unknown) as ILogger, + ), + ).toThrow('Logger.log must be a function'); + }); +}); + +describe('Attaching connectors to schema', () => { + describe('Schema level resolver', () => { + test('actually runs', () => { + const jsSchema = makeExecutableSchema({ + typeDefs: testSchema, + resolvers: testResolvers, + }); + const rootResolver = () => ({ species: 'ROOT' }); + addSchemaLevelResolver(jsSchema, rootResolver); + const query = `{ + species(name: "strix") + }`; + return graphql(jsSchema, query).then((res) => { + expect(res.data['species']).toBe('ROOTstrix'); + }); + }); + + test('can wrap fields that do not have a resolver defined', () => { + const jsSchema = makeExecutableSchema({ + typeDefs: testSchema, + resolvers: testResolvers, + }); + const rootResolver = () => ({ stuff: 'stuff' }); + addSchemaLevelResolver(jsSchema, rootResolver); + const query = `{ + stuff + }`; + return graphql(jsSchema, query).then((res) => { + expect(res.data['stuff']).toBe('stuff'); + }); + }); + + test('runs only once per query', () => { + const simpleResolvers = { + RootQuery: { + usecontext: (_r: any, _a: { [key: string]: any }, ctx: any) => + ctx.usecontext, + useTestConnector: (_r: any, _a: { [key: string]: any }, ctx: any) => + ctx.connectors.TestConnector.get(), + useContextConnector: ( + _r: any, + _a: { [key: string]: any }, + ctx: any, + ) => ctx.connectors.ContextConnector.get(), + species: (root: any, { name }: { name: string }) => + (root.species as string) + name, + }, + }; + const jsSchema = makeExecutableSchema({ + typeDefs: testSchema, + resolvers: simpleResolvers, + }); + let count = 0; + const rootResolver = () => { + if (count === 0) { + count += 1; + return { stuff: 'stuff', species: 'some ' }; + } + return { stuff: 'EEE', species: 'EEE' }; + }; + addSchemaLevelResolver(jsSchema, rootResolver); + const query = `{ + species(name: "strix") + stuff + }`; + const expected = { + species: 'some strix', + stuff: 'stuff', + }; + return graphql(jsSchema, query).then((res) => { + expect(res.data).toEqual(expected); + }); + }); + + test('runs twice for two queries', () => { + const simpleResolvers = { + RootQuery: { + usecontext: (_r: any, _a: { [key: string]: any }, ctx: any) => + ctx.usecontext, + useTestConnector: (_r: any, _a: { [key: string]: any }, ctx: any) => + ctx.connectors.TestConnector.get(), + useContextConnector: ( + _r: any, + _a: { [key: string]: any }, + ctx: any, + ) => ctx.connectors.ContextConnector.get(), + species: (root: any, { name }: { name: string }) => + (root.species as string) + name, + }, + }; + const jsSchema = makeExecutableSchema({ + typeDefs: testSchema, + resolvers: simpleResolvers, + }); + let count = 0; + const rootResolver = () => { + if (count === 0) { + count += 1; + return { stuff: 'stuff', species: 'some ' }; + } + if (count === 1) { + count += 1; + return { stuff: 'stuff2', species: 'species2 ' }; + } + return { stuff: 'EEE', species: 'EEE' }; + }; + addSchemaLevelResolver(jsSchema, rootResolver); + const query = `{ + species(name: "strix") + stuff + }`; + const expected = { + species: 'some strix', + stuff: 'stuff', + }; + const expected2 = { + species: 'species2 strix', + stuff: 'stuff2', + }; + return graphql(jsSchema, query).then((res) => { + expect(res.data).toEqual(expected); + return graphql(jsSchema, query).then((res2) => + expect(res2.data).toEqual(expected2), + ); + }); + }); + + test('can attach things to context', () => { + const jsSchema = makeExecutableSchema({ + typeDefs: testSchema, + resolvers: testResolvers, + }); + const rootResolver = (_o: any, _a: { [key: string]: any }, ctx: any) => { + ctx['usecontext'] = 'ABC'; + }; + addSchemaLevelResolver(jsSchema, rootResolver); + const query = `{ + usecontext + }`; + const expected = { + usecontext: 'ABC', + }; + return graphql(jsSchema, query, {}, {}).then((res) => { + expect(res.data).toEqual(expected); + }); + }); +}); + +describe('attachDirectiveResolvers on field', () => { + const testSchemaWithDirectives = ` + directive @upper on FIELD_DEFINITION + directive @lower on FIELD_DEFINITION + directive @default(value: String!) on FIELD_DEFINITION + directive @catchError on FIELD_DEFINITION + + type TestObject { + hello: String @upper + } + type RootQuery { + hello: String @upper + withDefault: String @default(value: "some default_value") + object: TestObject + asyncResolver: String @upper + multiDirectives: String @upper @lower + throwError: String @catchError + } + schema { + query: RootQuery + } + `; + + const testObject = { + hello: 'giau. tran minh', + }; + + const testResolversDirectives = { + RootQuery: { + hello: () => 'giau. tran minh', + object: () => testObject, + asyncResolver: async () => Promise.resolve('giau. tran minh'), + multiDirectives: () => 'Giau. Tran Minh', + throwError: () => { + throw new Error('This error for testing'); + }, + }, + }; + + const directiveResolvers: IDirectiveResolvers = { + lower( + next: NextResolverFn, + _src: any, + _args: { [argName: string]: any }, + _context: any, + ) { + return next().then((str) => { + if (typeof str === 'string') { + return str.toLowerCase(); + } + return str; + }); + }, + upper( + next: NextResolverFn, + _src: any, + _args: { [argName: string]: any }, + _context: any, + ) { + return next().then((str) => { + if (typeof str === 'string') { + return str.toUpperCase(); + } + return str; + }); + }, + default( + next: NextResolverFn, + _src: any, + args: { [argName: string]: any }, + _context: any, + ) { + return next().then((res) => { + if (undefined === res) { + return args.value; + } + return res; + }); + }, + catchError( + next: NextResolverFn, + _src: any, + _args: { [argName: string]: any }, + _context: any, + ) { + return next().catch((error) => error.message); + }, + }; + + test('throws error if directiveResolvers argument is an array', () => { + const jsSchema = makeExecutableSchema({ + typeDefs: testSchema, + resolvers: testResolvers, + }); + expect(() => + attachDirectiveResolvers(jsSchema, ([ + 1, + ] as unknown) as IDirectiveResolvers), + ).toThrowError( + 'Expected directiveResolvers to be of type object, got Array', + ); + }); + + test('throws error if directiveResolvers argument is not an object', () => { + const jsSchema = makeExecutableSchema({ + typeDefs: testSchema, + resolvers: testResolvers, + }); + return expect(() => + attachDirectiveResolvers( + jsSchema, + ('a' as unknown) as IDirectiveResolvers, + ), + ).toThrowError( + 'Expected directiveResolvers to be of type object, got string', + ); + }); + + test('upper String from resolvers', () => { + const schema = makeExecutableSchema({ + typeDefs: testSchemaWithDirectives, + resolvers: testResolversDirectives, + directiveResolvers, + }); + const query = `{ + hello + }`; + const expected = { + hello: 'GIAU. TRAN MINH', + }; + return graphql(schema, query, {}, {}).then((res) => { + expect(res.data).toEqual(expected); + }); + }); + + test('using default resolver for object property', () => { + const schema = makeExecutableSchema({ + typeDefs: testSchemaWithDirectives, + resolvers: testResolversDirectives, + directiveResolvers, + }); + const query = `{ + object { + hello + } + }`; + const expected = { + object: { + hello: 'GIAU. TRAN MINH', + }, + }; + return graphql(schema, query, {}, {}).then((res) => { + expect(res.data).toEqual(expected); + }); + }); + + test('passes in directive arguments to the directive resolver', () => { + const schema = makeExecutableSchema({ + typeDefs: testSchemaWithDirectives, + resolvers: testResolversDirectives, + directiveResolvers, + }); + const query = `{ + withDefault + }`; + const expected = { + withDefault: 'some default_value', + }; + return graphql(schema, query, {}, {}).then((res) => { + expect(res.data).toEqual(expected); + }); + }); + + test('No effect if missing directive resolvers', () => { + const schema = makeExecutableSchema({ + typeDefs: testSchemaWithDirectives, + resolvers: testResolversDirectives, + directiveResolvers: {}, // Empty resolver + }); + const query = `{ + hello + }`; + const expected = { + hello: 'giau. tran minh', + }; + return graphql(schema, query, {}, {}).then((res) => { + expect(res.data).toEqual(expected); + }); + }); + + test('If resolver return Promise, keep using it', () => { + const schema = makeExecutableSchema({ + typeDefs: testSchemaWithDirectives, + resolvers: testResolversDirectives, + directiveResolvers, + }); + const query = `{ + asyncResolver + }`; + const expected = { + asyncResolver: 'GIAU. TRAN MINH', + }; + return graphql(schema, query, {}, {}).then((res) => { + expect(res.data).toEqual(expected); + }); + }); + + test('Multi directives apply with LTR order', () => { + const schema = makeExecutableSchema({ + typeDefs: testSchemaWithDirectives, + resolvers: testResolversDirectives, + directiveResolvers, + }); + const query = `{ + multiDirectives + }`; + const expected = { + multiDirectives: 'giau. tran minh', + }; + return graphql(schema, query, {}, {}).then((res) => { + expect(res.data).toEqual(expected); + }); + }); + + test('Allow to catch error from next resolver', () => { + const schema = makeExecutableSchema({ + typeDefs: testSchemaWithDirectives, + resolvers: testResolversDirectives, + directiveResolvers, + }); + const query = `{ + throwError + }`; + const expected = { + throwError: 'This error for testing', + }; + return graphql(schema, query, {}, {}).then((res) => { + expect(res.data).toEqual(expected); + }); + }); +}); + +describe('can specify lexical parser options', () => { + test("can specify 'noLocation' option", () => { + const schema = makeExecutableSchema({ + typeDefs: ` + type RootQuery { + test: String + } + schema { + query: RootQuery + } + `, + resolvers: {}, + parseOptions: { + noLocation: true, + }, + }); + + expect(schema.astNode.loc).toBeUndefined(); + }); + + test("can specify 'experimentalFragmentVariables' option", () => { + const typeDefs = ` + type Query { + version: Int + } + `; + + const resolvers = { + Query: { + version: () => 1, + }, + }; + + expect(() => { + makeExecutableSchema({ + typeDefs, + resolvers, + parseOptions: { + experimentalFragmentVariables: true, + }, + }); + }).not.toThrowError(); + }); + + // Note that the experimentalFragmentVariables option requires a client side transform + // to hoist the parsed variables into queries, see https://github.com/graphql/graphql-js/pull/1141 + // and so this really has nothing to do with schema creation or execution. + test("can use 'experimentalFragmentVariables' option", async () => { + const typeDefs = ` + type Query { + hello(phrase: String): String + } + `; + + const resolvers = { + Query: { + hello: (_root: any, args: any) => `hello ${args.phrase as string}`, + }, + }; + + const jsSchema = makeExecutableSchema({ + typeDefs, + resolvers, + }); + + const query = ` + fragment Hello($phrase: String = "world") on Query { + hello(phrase: $phrase) + } + query { + ...Hello + } + `; + + const parsedQuery = parse(query, { experimentalFragmentVariables: true }); + + const hoist = (document: DocumentNode) => { + let variableDefs: Array = []; + + document.definitions.forEach((def) => { + if (def.kind === Kind.FRAGMENT_DEFINITION) { + variableDefs = variableDefs.concat(def.variableDefinitions); + } + }); + + return { + kind: Kind.DOCUMENT, + definitions: parsedQuery.definitions.map((def) => ({ + ...def, + variableDefinitions: variableDefs, + })), + }; + }; + + const hoistedQuery = hoist(parsedQuery); + + const result = await execute(jsSchema, hoistedQuery); + expect(result.data).toEqual({ hello: 'hello world' }); + + const result2 = await execute(jsSchema, hoistedQuery, null, null, { + phrase: 'world again!', + }); + expect(result2.data).toEqual({ hello: 'hello world again!' }); + }); +}); + +describe('interfaces', () => { + const testSchemaWithInterfaces = ` + interface Node { + id: ID! + } + type User implements Node { + id: ID! + name: String! + } + type Query { + node: Node! + user: User! + } + schema { + query: Query + } + `; + const user = { id: 1, type: 'User', name: 'Kim' }; + const queryResolver = { + node: () => user, + user: () => user, + }; + const query = `query { + node { id __typename } + user { id name } + }`; + + test('throws if there is no interface resolveType resolver', () => { + const resolvers = { + Query: queryResolver, + }; + try { + makeExecutableSchema({ + typeDefs: testSchemaWithInterfaces, + resolvers, + resolverValidationOptions: { requireResolversForResolveType: true }, + }); + } catch (error) { + expect(error.message).toEqual( + 'Type "Node" is missing a "__resolveType" resolver. Pass false into "resolverValidationOptions.requireResolversForResolveType" to disable this error.', + ); + return; + } + throw new Error('Should have had an error.'); + }); + test('does not throw if there is an interface resolveType resolver', async () => { + const resolvers = { + Query: queryResolver, + Node: { + __resolveType: ({ type }: { type: string }) => type, + }, + }; + const schema = makeExecutableSchema({ + typeDefs: testSchemaWithInterfaces, + resolvers, + resolverValidationOptions: { requireResolversForResolveType: true }, + }); + const response = await graphql(schema, query); + expect(response.errors).not.toBeDefined(); + }); + test('does not warn if requireResolversForResolveType is disabled and there are missing resolvers', () => { + const resolvers = { + Query: queryResolver, + }; + makeExecutableSchema({ + typeDefs: testSchemaWithInterfaces, + resolvers, + resolverValidationOptions: { requireResolversForResolveType: false }, + }); + }); +}); + +describe('interface resolver inheritance', () => { + test('copies resolvers from the interfaces', async () => { + const testSchemaWithInterfaceResolvers = ` + interface Node { + id: ID! + } + type User implements Node { + id: ID! + name: String! + } + type Query { + user: User! + } + schema { + query: Query + } + `; + const user = { id: 1, name: 'Ada', type: 'User' }; + const resolvers = { + Node: { + __resolveType: ({ type }: { type: string }) => type, + id: ({ id }: { id: number }) => `Node:${id.toString()}`, + }, + User: { + name: ({ name }: { name: string }) => `User:${name}`, + }, + Query: { + user: () => user, + }, + }; + const schema = makeExecutableSchema({ + typeDefs: testSchemaWithInterfaceResolvers, + resolvers, + inheritResolversFromInterfaces: true, + resolverValidationOptions: { + requireResolversForAllFields: true, + requireResolversForResolveType: true, + }, + }); + const query = '{ user { id name } }'; + const response = await graphql(schema, query); + expect(response).toEqual({ + data: { + user: { + id: 'Node:1', + name: 'User:Ada', + }, + }, + }); + }); + + test('respects interface order and existing resolvers', async () => { + const testSchemaWithInterfaceResolvers = ` + interface Node { + id: ID! + } + interface Person { + id: ID! + name: String! + } + type Replicant implements Node, Person { + id: ID! + name: String! + } + type Cyborg implements Person, Node { + id: ID! + name: String! + } + type Query { + cyborg: Cyborg! + replicant: Replicant! + } + schema { + query: Query + } + `; + const cyborg = { id: 1, name: 'Alex Murphy', type: 'Cyborg' }; + const replicant = { id: 2, name: 'Rachael Tyrell', type: 'Replicant' }; + const resolvers = { + Node: { + __resolveType: ({ type }: { type: string }) => type, + id: ({ id }: { id: number }) => `Node:${id.toString()}`, + }, + Person: { + __resolveType: ({ type }: { type: string }) => type, + id: ({ id }: { id: number }) => `Person:${id.toString()}`, + name: ({ name }: { name: string }) => `Person:${name}`, + }, + Query: { + cyborg: () => cyborg, + replicant: () => replicant, + }, + }; + const schema = makeExecutableSchema({ + parseOptions: { allowLegacySDLImplementsInterfaces: true }, + typeDefs: testSchemaWithInterfaceResolvers, + resolvers, + inheritResolversFromInterfaces: true, + resolverValidationOptions: { + requireResolversForAllFields: true, + requireResolversForResolveType: true, + }, + }); + const query = '{ cyborg { id name } replicant { id name }}'; + const response = await graphql(schema, query); + expect(response).toEqual({ + data: { + cyborg: { + id: 'Node:1', + name: 'Person:Alex Murphy', + }, + replicant: { + id: 'Person:2', + name: 'Person:Rachael Tyrell', + }, + }, + }); + }); +}); + +describe('unions', () => { + const testSchemaWithUnions = ` + type Post { + title: String! + } + type Page { + title: String! + } + union Displayable = Page | Post + type Query { + page: Page! + post: Post! + displayable: [Displayable!]! + } + schema { + query: Query + } + `; + const post = { title: 'I am a post', type: 'Post' }; + const page = { title: 'I am a page', type: 'Page' }; + const queryResolver = { + page: () => page, + post: () => post, + displayable: () => [post, page], + }; + const query = `query { + post { title } + page { title } + displayable { + ... on Post { title } + ... on Page { title } + } + }`; + + test('throws if there is no union resolveType resolver', () => { + const resolvers = { + Query: queryResolver, + }; + try { + makeExecutableSchema({ + typeDefs: testSchemaWithUnions, + resolvers, + resolverValidationOptions: { requireResolversForResolveType: true }, + }); + } catch (error) { + expect(error.message).toEqual( + 'Type "Displayable" is missing a "__resolveType" resolver. Pass false into "resolverValidationOptions.requireResolversForResolveType" to disable this error.', + ); + return; + } + throw new Error('Should have had an error.'); + }); + + test('does not throw if there is a resolveType resolver', async () => { + const resolvers = { + Query: queryResolver, + Displayable: { + __resolveType: ({ type }: { type: string }) => type, + }, + }; + const schema = makeExecutableSchema({ + typeDefs: testSchemaWithUnions, + resolvers, + resolverValidationOptions: { requireResolversForResolveType: true }, + }); + const response = await graphql(schema, query); + expect(response.errors).not.toBeDefined(); + }); + + test('does not warn if requireResolversForResolveType is disabled', () => { + const resolvers = { + Query: queryResolver, + }; + makeExecutableSchema({ + typeDefs: testSchemaWithUnions, + resolvers, + resolverValidationOptions: { requireResolversForResolveType: false }, + }); + }); +}); +}); diff --git a/packages/schema/tsconfig.json b/packages/schema/tsconfig.json new file mode 100644 index 00000000..780ddafb --- /dev/null +++ b/packages/schema/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "resolveJsonModule": true, + "importHelpers": true, + "experimentalDecorators": true, + "module": "esnext", + "target": "es2018", + "lib": ["esnext"], + "suppressImplicitAnyIndexErrors": true, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "sourceMap": true, + "declaration": true, + "outDir": "./dist/", + "noImplicitAny": false, + "noImplicitThis": true, + "alwaysStrict": true, + "noImplicitReturns": true, + "noUnusedLocals": false, + "noUnusedParameters": false + }, + "exclude": ["node_modules", "tests", "dist"] +} diff --git a/packages/types/.gitignore b/packages/types/.gitignore new file mode 100644 index 00000000..b2931f95 --- /dev/null +++ b/packages/types/.gitignore @@ -0,0 +1,72 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# next.js build output +.next + + +dist +build +temp +.idea + +test-results/ +junit.xml + +*.tgz \ No newline at end of file diff --git a/packages/types/.npmignore b/packages/types/.npmignore new file mode 100644 index 00000000..4ff88439 --- /dev/null +++ b/packages/types/.npmignore @@ -0,0 +1,13 @@ +src +node_modules +tests +!dist +.circleci +.prettierrc +bump.js +jest.config.js +tsconfig.json +yarn.lock +yarn-error.log +bundle-test +*.tgz \ No newline at end of file diff --git a/packages/types/package.json b/packages/types/package.json new file mode 100644 index 00000000..1702bdaf --- /dev/null +++ b/packages/types/package.json @@ -0,0 +1,33 @@ +{ + "name": "@graphql-toolkit/types", + "version": "0.10.2", + "description": "Common types for GraphQL Toolkit", + "repository": "git@github.com:ardatan/graphql-toolkit.git", + "author": "Kamil Kisiela ", + "license": "MIT", + "scripts": { + "lint": "tslint src/**/*.ts", + "clean": "rimraf dist", + "prebuild": "yarn clean", + "build": "bob", + "prepack": "bob-update-version", + "test": "jest --passWithNoTests --no-watchman --config ../../jest.config.js" + }, + "sideEffects": false, + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "typings": "dist/index.d.ts", + "typescript": { + "definition": "dist/index.d.ts" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0" + }, + "buildOptions": { + "input": "./src/index.ts" + }, + "publishConfig": { + "access": "public", + "directory": "dist" + } +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts new file mode 100644 index 00000000..e82f7d15 --- /dev/null +++ b/packages/types/src/index.ts @@ -0,0 +1,54 @@ +import { GraphQLResolveInfo, GraphQLIsTypeOfFn, GraphQLTypeResolver, GraphQLScalarType } from 'graphql'; + +export interface IResolvers { + [key: string]: + | (() => any) + | IResolverObject + | IResolverOptions + | GraphQLScalarType + | IEnumResolver; +} + +export interface IResolverObject { + [key: string]: + | IFieldResolver + | IResolverOptions + | IResolverObject; +} + +export type IFieldResolver> = ( + source: TSource, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => any; + +export interface IResolverOptions { + fragment?: string; + resolve?: IFieldResolver; + subscribe?: IFieldResolver; + __resolveType?: GraphQLTypeResolver; + __isTypeOf?: GraphQLIsTypeOfFn; +} + +export interface IEnumResolver { + [key: string]: string | number; +} + +export type NextResolverFn = () => Promise; + +export type DirectiveResolverFn = ( + next: NextResolverFn, + source: TSource, + args: { [argName: string]: any }, + context: TContext, + info: GraphQLResolveInfo +) => any; + +export interface IDirectiveResolvers { + [directiveName: string]: DirectiveResolverFn; +} + +export interface ILogger { + log: (error: Error) => void; +} diff --git a/packages/types/tsconfig.json b/packages/types/tsconfig.json new file mode 100644 index 00000000..6ae81d4b --- /dev/null +++ b/packages/types/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "importHelpers": true, + "experimentalDecorators": true, + "module": "esnext", + "target": "es2018", + "lib": ["esnext"], + "suppressImplicitAnyIndexErrors": true, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "sourceMap": true, + "declaration": true, + "outDir": "./dist/", + "noImplicitAny": false, + "noImplicitThis": true, + "alwaysStrict": true, + "noImplicitReturns": true, + "noUnusedLocals": false, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "noUnusedParameters": false + }, + "include": ["src"] +} diff --git a/packages/version/.gitignore b/packages/version/.gitignore new file mode 100644 index 00000000..b2931f95 --- /dev/null +++ b/packages/version/.gitignore @@ -0,0 +1,72 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# next.js build output +.next + + +dist +build +temp +.idea + +test-results/ +junit.xml + +*.tgz \ No newline at end of file diff --git a/packages/version/.npmignore b/packages/version/.npmignore new file mode 100644 index 00000000..4ff88439 --- /dev/null +++ b/packages/version/.npmignore @@ -0,0 +1,13 @@ +src +node_modules +tests +!dist +.circleci +.prettierrc +bump.js +jest.config.js +tsconfig.json +yarn.lock +yarn-error.log +bundle-test +*.tgz \ No newline at end of file diff --git a/packages/version/package.json b/packages/version/package.json new file mode 100644 index 00000000..979795c8 --- /dev/null +++ b/packages/version/package.json @@ -0,0 +1,33 @@ +{ + "name": "@graphql-toolkit/version", + "version": "0.10.2", + "description": "Check version of GraphQL", + "repository": "git@github.com:ardatan/graphql-toolkit.git", + "author": "Kamil Kisiela ", + "license": "MIT", + "scripts": { + "lint": "tslint src/**/*.ts", + "clean": "rimraf dist", + "prebuild": "yarn clean", + "build": "bob", + "prepack": "bob-update-version", + "test": "jest --passWithNoTests --no-watchman --config ../../jest.config.js" + }, + "sideEffects": false, + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "typings": "dist/index.d.ts", + "typescript": { + "definition": "dist/index.d.ts" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0" + }, + "buildOptions": { + "input": "./src/index.ts" + }, + "publishConfig": { + "access": "public", + "directory": "dist" + } +} diff --git a/packages/version/src/index.ts b/packages/version/src/index.ts new file mode 100644 index 00000000..3105c508 --- /dev/null +++ b/packages/version/src/index.ts @@ -0,0 +1,17 @@ +import { versionInfo, getOperationRootType, lexicographicSortSchema, printError } from 'graphql'; + +let version: number; + +if (versionInfo != null && versionInfo.major >= 15) { + version = 15; +} else if (getOperationRootType != null) { + version = 14; +} else if (lexicographicSortSchema != null) { + version = 13; +} else if (printError != null) { + version = 12; +} else { + version = 11; +} + +export { version }; diff --git a/packages/version/tsconfig.json b/packages/version/tsconfig.json new file mode 100644 index 00000000..6ae81d4b --- /dev/null +++ b/packages/version/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "importHelpers": true, + "experimentalDecorators": true, + "module": "esnext", + "target": "es2018", + "lib": ["esnext"], + "suppressImplicitAnyIndexErrors": true, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "sourceMap": true, + "declaration": true, + "outDir": "./dist/", + "noImplicitAny": false, + "noImplicitThis": true, + "alwaysStrict": true, + "noImplicitReturns": true, + "noUnusedLocals": false, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "noUnusedParameters": false + }, + "include": ["src"] +} diff --git a/yarn.lock b/yarn.lock index 4f271460..8e70b784 100644 --- a/yarn.lock +++ b/yarn.lock @@ -78,16 +78,6 @@ resolved "https://registry.yarnpkg.com/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.24.tgz#3ce939cb127fb8aaa3ffc1e90dff9b8af9f2e3dc" integrity sha512-8GqG48m1XqyXh4mIZrtB5xOhUwSsh1WsrrsaZQOEYYql3YN9DEu9OOSg0ILzXHZo/h2Q74777YE4YzlArQzQEQ== -"@ardatan/bob@0.2.7": - version "0.2.7" - resolved "https://registry.yarnpkg.com/@ardatan/bob/-/bob-0.2.7.tgz#ce93a2da622e551587afdb4f1f0dfe8d119e42e7" - integrity sha512-cDtYTnXwDclUj/3qofLlufY0wE0eFYNotx4aFJdHWAgHwNtHbrcpCuZtc7qJ6B/Luuw7QWYT+QulQPZ0xg5fgw== - dependencies: - rollup "1.27.13" - rollup-plugin-auto-external "2.0.0" - rollup-plugin-generate-package-json "3.2.0" - rollup-plugin-typescript2 "0.25.3" - "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" @@ -1806,11 +1796,6 @@ dependencies: deepmerge "*" -"@types/estree@*": - version "0.0.42" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.42.tgz#8d0c1f480339efedb3e46070e22dd63e0430dd11" - integrity sha512-K1DPVvnBCPxzD+G51/cxVIoc2X8uUVl1zpJeE6iKcgHMj4+tbat5Xu4TjV7v2QSDbIeAfLi2hIk+u2+s0MlpUQ== - "@types/events@*": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" @@ -1938,7 +1923,21 @@ "@types/koa-compose" "*" "@types/node" "*" -"@types/lodash@4.14.149": +"@types/lodash.get@4.4.6": + version "4.4.6" + resolved "https://registry.yarnpkg.com/@types/lodash.get/-/lodash.get-4.4.6.tgz#0c7ac56243dae0f9f09ab6f75b29471e2e777240" + integrity sha512-E6zzjR3GtNig8UJG/yodBeJeIOtgPkMgsLjDU3CbgCAPC++vJ0eCMnJhVpRZb/ENqEFlov1+3K9TKtY4UdWKtQ== + dependencies: + "@types/lodash" "*" + +"@types/lodash.set@4.3.6": + version "4.3.6" + resolved "https://registry.yarnpkg.com/@types/lodash.set/-/lodash.set-4.3.6.tgz#33e635c2323f855359225df6a5c8c6f1f1908264" + integrity sha512-ZeGDDlnRYTvS31Laij0RsSaguIUSBTYIlJFKL3vm3T2OAZAQj2YpSvVWJc0WiG4jqg9fGX6PAPGvDqBcHfSgFg== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*", "@types/lodash@4.14.149": version "4.14.149" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.149.tgz#1342d63d948c6062838fbf961012f74d4e638440" integrity sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ== @@ -2819,6 +2818,18 @@ bluebird@^3.5.1, bluebird@^3.5.3, bluebird@^3.5.5: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== +bob-the-bundler@0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/bob-the-bundler/-/bob-the-bundler-0.3.4.tgz#c886d9974119222b2ba7076da60415c642216222" + integrity sha512-sSeG+CShMiCbp14dUd8QI60zNAk9PV8DvyJa/YU0mhiK8pFSAF/F5lkeY2F/3aGK7BnhYiqCZcRXVcX1fsSpSg== + dependencies: + globby "11.0.0" + mv "2.1.1" + rollup "2.1.0" + rollup-plugin-auto-external "2.0.0" + rollup-plugin-generate-package-json "3.2.0" + rollup-plugin-typescript2 "0.26.0" + body-parser@1.19.0, body-parser@^1.18.3: version "1.19.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" @@ -4123,11 +4134,6 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" -extract-files@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/extract-files/-/extract-files-7.0.0.tgz#3dc7853320ff7876ec62d6e98f2f4e6f3e6282f6" - integrity sha512-3AUlT7TD+DbQXNe3t70QrgJU6Wgcp7rk1Zm0vqWz8OYnw4vxihgG0TgZ2SIGrVqScc4WfOu7B4a0BezGJ0YqvQ== - extsprintf@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" @@ -4264,13 +4270,13 @@ finalhandler@~1.1.2: statuses "~1.5.0" unpipe "~1.0.0" -find-cache-dir@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.2.0.tgz#e7fe44c1abc1299f516146e563108fd1006c1874" - integrity sha512-1JKclkYYsf1q9WIJKLZa9S9muC+08RIjzAlLrK4QcYLJMS6mk9yombQ9qf+zJ7H9LS800k0s44L4sDq9VYzqyg== +find-cache-dir@^3.2.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.1.tgz#89b33fad4a4670daa94f855f7fbe31d6d84fe880" + integrity sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ== dependencies: commondir "^1.0.1" - make-dir "^3.0.0" + make-dir "^3.0.2" pkg-dir "^4.1.0" find-up@^1.0.0: @@ -4440,7 +4446,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fsevents@^2.1.2: +fsevents@^2.1.2, fsevents@~2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.2.tgz#4c0a1fb34bc68e543b4b82a9ec392bfbda840805" integrity sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA== @@ -4600,6 +4606,17 @@ glob-to-regexp@^0.3.0: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= +glob@^6.0.1: + version "6.0.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" + integrity sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI= + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" @@ -4681,20 +4698,6 @@ graphql-tag@^2.10.1, graphql-tag@^2.9.2: resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.1.tgz#10aa41f1cd8fae5373eaf11f1f67260a3cad5e02" integrity sha512-jApXqWBzNXQ8jYa/HLkZJaVw9jgwNqZkywa2zfFn16Iv1Zb7ELNHkJaXHR7Quvd5SIGsy6Ny7SUKATgnu05uEg== -graphql-tools-fork@9.0.1: - version "9.0.1" - resolved "https://registry.yarnpkg.com/graphql-tools-fork/-/graphql-tools-fork-9.0.1.tgz#fc8df40c108bdba3268999dea355cc614c765038" - integrity sha512-kM6mUNVekgnWKtVqLGQ9HvQqQ3zZVPZRg1esltBoohsbUMaChl+9QkjBjoMxnZPnbTGOOGGagopNBQALIBysNg== - dependencies: - apollo-link "^1.2.13" - apollo-link-http-common "^0.2.15" - deprecated-decorator "^0.1.6" - extract-files "^7.0.0" - form-data "^3.0.0" - iterall "^1.3.0" - node-fetch "^2.6.0" - uuid "^7.0.2" - graphql-tools@^4.0.0: version "4.0.6" resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-4.0.6.tgz#0e729e73db05ade3df10a2f92511be544972a844" @@ -5426,7 +5429,7 @@ istanbul-reports@^3.0.0: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -iterall@^1.1.3, iterall@^1.2.1, iterall@^1.3.0: +iterall@^1.1.3, iterall@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea" integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg== @@ -6150,7 +6153,7 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= -lodash.get@^4, lodash.get@^4.4.2: +lodash.get@4.4.2, lodash.get@^4, lodash.get@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= @@ -6205,7 +6208,7 @@ lodash.once@^4.0.0: resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= -lodash.set@^4.3.2: +lodash.set@4.3.2, lodash.set@^4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM= @@ -6336,6 +6339,13 @@ make-dir@^3.0.0: dependencies: semver "^6.0.0" +make-dir@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.0.2.tgz#04a1acbf22221e1d6ef43559f43e05a90dbb4392" + integrity sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w== + dependencies: + semver "^6.0.0" + make-error@1.x, make-error@^1, make-error@^1.1.1: version "1.3.5" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" @@ -6512,7 +6522,7 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -minimatch@^3.0.4: +"minimatch@2 || 3", minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== @@ -6605,7 +6615,7 @@ mkdirp@1.x: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.3.tgz#4cf2e30ad45959dddea53ad97d518b6c8205e1ea" integrity sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g== -mkdirp@^0.5.3: +mkdirp@^0.5.3, mkdirp@~0.5.1: version "0.5.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.4.tgz#fd01504a6797ec5c9be81ff43d204961ed64a512" integrity sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw== @@ -6669,6 +6679,15 @@ mute-stream@~0.0.4: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== +mv@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/mv/-/mv-2.1.1.tgz#ae6ce0d6f6d5e0a4f7d893798d03c1ea9559b6a2" + integrity sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI= + dependencies: + mkdirp "~0.5.1" + ncp "~2.0.0" + rimraf "~2.4.0" + mz@^2.5.0: version "2.7.0" resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" @@ -6700,6 +6719,11 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= +ncp@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3" + integrity sha1-GVoh1sRuNh0vsSgbo4uR6d9727M= + negotiator@0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" @@ -6737,7 +6761,7 @@ node-fetch@2.1.2: resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.1.2.tgz#ab884e8e7e57e38a944753cec706f788d1768bb5" integrity sha1-q4hOjn5X44qUR1POxwb3iNF2i7U= -node-fetch@2.6.0, node-fetch@^2.1.2, node-fetch@^2.2.0, node-fetch@^2.3.0, node-fetch@^2.5.0, node-fetch@^2.6.0: +node-fetch@2.6.0, node-fetch@^2.1.2, node-fetch@^2.2.0, node-fetch@^2.3.0, node-fetch@^2.5.0: version "2.6.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== @@ -7890,10 +7914,10 @@ resolve@1.1.7: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@1.12.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6" - integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w== +resolve@1.15.1, resolve@^1.15.1: + version "1.15.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8" + integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w== dependencies: path-parse "^1.0.6" @@ -7904,13 +7928,6 @@ resolve@1.x, resolve@^1.10.0, resolve@^1.3.2: dependencies: path-parse "^1.0.6" -resolve@^1.15.1: - version "1.15.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8" - integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w== - dependencies: - path-parse "^1.0.6" - restore-cursor@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" @@ -7960,6 +7977,13 @@ rimraf@^3.0.0: dependencies: glob "^7.1.3" +rimraf@~2.4.0: + version "2.4.5" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.4.5.tgz#ee710ce5d93a8fdb856fb5ea8ff0e2d75934b2da" + integrity sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto= + dependencies: + glob "^6.0.1" + rollup-plugin-auto-external@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/rollup-plugin-auto-external/-/rollup-plugin-auto-external-2.0.0.tgz#98fd137d66c1cbe0f4e245b31560a72dbde896aa" @@ -7978,32 +8002,30 @@ rollup-plugin-generate-package-json@3.2.0: read-pkg "^5.2.0" write-pkg "^4.0.0" -rollup-plugin-typescript2@0.25.3: - version "0.25.3" - resolved "https://registry.yarnpkg.com/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.25.3.tgz#a5fb2f0f85488789334ce540abe6c7011cbdf40f" - integrity sha512-ADkSaidKBovJmf5VBnZBZe+WzaZwofuvYdzGAKTN/J4hN7QJCFYAq7IrH9caxlru6T5qhX41PNFS1S4HqhsGQg== +rollup-plugin-typescript2@0.26.0: + version "0.26.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.26.0.tgz#cee2b44d51d9623686656d76dc30a73c4de91672" + integrity sha512-lUK7XZVG77tu8dmv1L/0LZFlavED/5Yo6e4iMMl6fdox/yKdj4IFRRPPJEXNdmEaT1nDQQeCi7b5IwKHffMNeg== dependencies: - find-cache-dir "^3.0.0" + find-cache-dir "^3.2.0" fs-extra "8.1.0" - resolve "1.12.0" - rollup-pluginutils "2.8.1" + resolve "1.15.1" + rollup-pluginutils "2.8.2" tslib "1.10.0" -rollup-pluginutils@2.8.1: - version "2.8.1" - resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.1.tgz#8fa6dd0697344938ef26c2c09d2488ce9e33ce97" - integrity sha512-J5oAoysWar6GuZo0s+3bZ6sVZAC0pfqKz68De7ZgDi5z63jOVZn1uJL/+z1jeKHNbGII8kAyHF5q8LnxSX5lQg== +rollup-pluginutils@2.8.2: + version "2.8.2" + resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e" + integrity sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ== dependencies: estree-walker "^0.6.1" -rollup@1.27.13: - version "1.27.13" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.27.13.tgz#d6d3500512daacbf8de54d2800de62d893085b90" - integrity sha512-hDi7M07MpmNSDE8YVwGVFA8L7n8jTLJ4lG65nMAijAyqBe//rtu4JdxjUBE7JqXfdpqxqDTbCDys9WcqdpsQvw== - dependencies: - "@types/estree" "*" - "@types/node" "*" - acorn "^7.1.0" +rollup@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.1.0.tgz#552e248e397a06b9c6db878c0564ca4ee06729c9" + integrity sha512-gfE1455AEazVVTJoeQtcOq/U6GSxwoj4XPSWVsuWmgIxj7sBQNLDOSA82PbdMe+cP8ql8fR1jogPFe8Wg8g4SQ== + optionalDependencies: + fsevents "~2.1.2" rsvp@^4.8.4: version "4.8.5" @@ -9170,11 +9192,6 @@ uuid@^3.0.1, uuid@^3.1.0, uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866" integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ== -uuid@^7.0.2: - version "7.0.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.2.tgz#7ff5c203467e91f5e0d85cfcbaaf7d2ebbca9be6" - integrity sha512-vy9V/+pKG+5ZTYKf+VcphF5Oc6EFiu3W8Nv3P3zIh0EqVI80ZxOzuPfe9EHjkFNvf8+xuTHVeei4Drydlx4zjw== - v8-to-istanbul@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-4.0.1.tgz#d6a2a3823b8ff49bdf2167ff2a45d82dff81d02f" From c46c57775d08838d98d8e8e15e3bf185243ec93b Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Mon, 6 Apr 2020 08:47:57 +0200 Subject: [PATCH 2/9] Range tslib --- packages/core/package.json | 2 +- packages/loaders/apollo-engine/package.json | 2 +- packages/loaders/code-file/package.json | 2 +- packages/loaders/graphql-file/package.json | 2 +- packages/loaders/json-file/package.json | 2 +- packages/loaders/module/package.json | 2 +- packages/loaders/prisma/package.json | 2 +- packages/loaders/url/package.json | 2 +- packages/schema-merging/package.json | 2 +- packages/schema/package.json | 4 ++-- packages/types/package.json | 2 +- packages/version/package.json | 2 +- 12 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 24b34c3c..ed067f0a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -44,7 +44,7 @@ "lodash": "4.17.15", "p-limit": "2.3.0", "resolve-from": "5.0.0", - "tslib": "1.11.1", + "tslib": "^1.11.1", "unixify": "1.0.0", "valid-url": "1.0.9" }, diff --git a/packages/loaders/apollo-engine/package.json b/packages/loaders/apollo-engine/package.json index ff5af32a..4d4edb9e 100644 --- a/packages/loaders/apollo-engine/package.json +++ b/packages/loaders/apollo-engine/package.json @@ -29,7 +29,7 @@ "dependencies": { "@graphql-toolkit/common": "0.10.2", "apollo-language-server": "1.21.0", - "tslib": "1.11.1" + "tslib": "^1.11.1" }, "publishConfig": { "access": "public", diff --git a/packages/loaders/code-file/package.json b/packages/loaders/code-file/package.json index a7263909..5425e084 100644 --- a/packages/loaders/code-file/package.json +++ b/packages/loaders/code-file/package.json @@ -29,7 +29,7 @@ "dependencies": { "@graphql-toolkit/common": "0.10.2", "@graphql-toolkit/graphql-tag-pluck": "0.10.2", - "tslib": "1.11.1" + "tslib": "^1.11.1" }, "publishConfig": { "access": "public", diff --git a/packages/loaders/graphql-file/package.json b/packages/loaders/graphql-file/package.json index 27765632..c77123f0 100644 --- a/packages/loaders/graphql-file/package.json +++ b/packages/loaders/graphql-file/package.json @@ -28,7 +28,7 @@ }, "dependencies": { "@graphql-toolkit/common": "0.10.2", - "tslib": "1.11.1" + "tslib": "^1.11.1" }, "publishConfig": { "access": "public", diff --git a/packages/loaders/json-file/package.json b/packages/loaders/json-file/package.json index 4e605b99..9abb6767 100644 --- a/packages/loaders/json-file/package.json +++ b/packages/loaders/json-file/package.json @@ -28,7 +28,7 @@ }, "dependencies": { "@graphql-toolkit/common": "0.10.2", - "tslib": "1.11.1" + "tslib": "^1.11.1" }, "publishConfig": { "access": "public", diff --git a/packages/loaders/module/package.json b/packages/loaders/module/package.json index 07d4c1c5..d2c58703 100644 --- a/packages/loaders/module/package.json +++ b/packages/loaders/module/package.json @@ -28,7 +28,7 @@ }, "dependencies": { "@graphql-toolkit/common": "0.10.2", - "tslib": "1.11.1" + "tslib": "^1.11.1" }, "publishConfig": { "access": "public", diff --git a/packages/loaders/prisma/package.json b/packages/loaders/prisma/package.json index 03e70cd5..96028497 100644 --- a/packages/loaders/prisma/package.json +++ b/packages/loaders/prisma/package.json @@ -30,7 +30,7 @@ "@graphql-toolkit/common": "0.10.2", "@graphql-toolkit/url-loader": "0.10.2", "prisma-yml": "1.34.10", - "tslib": "1.11.1" + "tslib": "^1.11.1" }, "publishConfig": { "access": "public", diff --git a/packages/loaders/url/package.json b/packages/loaders/url/package.json index 532ae6c1..bc3f064f 100644 --- a/packages/loaders/url/package.json +++ b/packages/loaders/url/package.json @@ -37,7 +37,7 @@ "@graphql-toolkit/common": "0.10.2", "@graphql-toolkit/schema": "0.10.2", "cross-fetch": "3.0.4", - "tslib": "1.11.1", + "tslib": "^1.11.1", "valid-url": "1.0.9" }, "publishConfig": { diff --git a/packages/schema-merging/package.json b/packages/schema-merging/package.json index bf493bbf..6b5b1597 100644 --- a/packages/schema-merging/package.json +++ b/packages/schema-merging/package.json @@ -34,7 +34,7 @@ "@graphql-toolkit/schema": "0.10.2", "@graphql-toolkit/types": "0.10.2", "deepmerge": "4.2.2", - "tslib": "1.11.1" + "tslib": "^1.11.1" }, "publishConfig": { "access": "public", diff --git a/packages/schema/package.json b/packages/schema/package.json index e8fade85..86f6fe6e 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -21,7 +21,7 @@ "definition": "dist/index.d.ts" }, "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0" + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" }, "buildOptions": { "input": "./src/index.ts" @@ -33,7 +33,7 @@ "@graphql-toolkit/common": "0.10.2", "@graphql-toolkit/types": "0.10.2", "@graphql-toolkit/version": "0.10.2", - "tslib": "1.11.1" + "tslib": "^1.11.1" }, "publishConfig": { "access": "public", diff --git a/packages/types/package.json b/packages/types/package.json index 1702bdaf..cd189102 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -21,7 +21,7 @@ "definition": "dist/index.d.ts" }, "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0" + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" }, "buildOptions": { "input": "./src/index.ts" diff --git a/packages/version/package.json b/packages/version/package.json index 979795c8..42f2f14d 100644 --- a/packages/version/package.json +++ b/packages/version/package.json @@ -21,7 +21,7 @@ "definition": "dist/index.d.ts" }, "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0" + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" }, "buildOptions": { "input": "./src/index.ts" From 378f2d0ac8b8b2b551f64981e29a4bb0e11c9c04 Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Mon, 6 Apr 2020 08:56:02 +0200 Subject: [PATCH 3/9] Just 13 --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a37877d3..03908204 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,7 +20,7 @@ jobs: - name: Use Node uses: actions/setup-node@master with: - node-version: '13.x' + node-version: 13 - name: Get yarn cache id: yarn-cache run: echo "::set-output name=dir::$(yarn cache dir)" From 8cbcff46aea4f3c6a63d459dd90e4913b929fa4d Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Mon, 6 Apr 2020 09:05:00 +0200 Subject: [PATCH 4/9] Release Action --- .github/workflows/release.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..82f006ff --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,30 @@ +on: + push: + tags: + - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 + +name: Create Release + +jobs: + build: + name: Create Release on GitHub + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@master + - name: Create Release + id: create_release + uses: actions/create-release@latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + body: | + > Do you want The Guild to keep your codebase up to date and run your build on each GraphQL Toolkit commit so we will make sure not to break your app? + > Contact us here: [the-guild.dev/connected-build](http://bit.ly/connected-toolkit) + > Chat with us [on discord](http://bit.ly/guild-chat) + + List of changes: + draft: false + prerelease: false From c740c0a83f5a093d7b301a8f760de0a83aef8924 Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Mon, 6 Apr 2020 09:09:09 +0200 Subject: [PATCH 5/9] Just version --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 82f006ff..dd959af5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ github.ref }} - release_name: Release ${{ github.ref }} + release_name: ${{ github.ref }} body: | > Do you want The Guild to keep your codebase up to date and run your build on each GraphQL Toolkit commit so we will make sure not to break your app? > Contact us here: [the-guild.dev/connected-build](http://bit.ly/connected-toolkit) From f5bde2823f8b61c22f580895b77f653bc62fa7ad Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Mon, 6 Apr 2020 09:09:25 +0200 Subject: [PATCH 6/9] Use node 12 to release new versions --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 03908204..d58b241c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,7 +20,7 @@ jobs: - name: Use Node uses: actions/setup-node@master with: - node-version: 13 + node-version: 12 - name: Get yarn cache id: yarn-cache run: echo "::set-output name=dir::$(yarn cache dir)" From 4173883c2475778990da11a58b6aa3215fb99f81 Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Mon, 6 Apr 2020 09:20:02 +0200 Subject: [PATCH 7/9] --contents dist --- package.json | 4 ++-- yarn.lock | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index fd9bc34d..20219ec7 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,8 @@ "prerelease": "yarn test-and-build", "prerelease:canary": "yarn test-and-build", "release": "lerna publish --exact --force-publish=\"*\"", - "release:canary": "lerna publish --force-publish=\"*\" --preid \"alpha-`git rev-parse --short HEAD`\" --canary --exact", - "ci:release:canary": "lerna publish --force-publish=\"*\" --preid \"alpha-`git rev-parse --short HEAD`\" --canary --exact --yes" + "release:canary": "lerna publish --contents dist --force-publish=\"*\" --preid \"alpha-`git rev-parse --short HEAD`\" --canary --exact", + "ci:release:canary": "lerna publish --contents dist --force-publish=\"*\" --preid \"alpha-`git rev-parse --short HEAD`\" --canary --exact --yes" }, "husky": { "hooks": { diff --git a/yarn.lock b/yarn.lock index 8e70b784..49f87936 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8951,7 +8951,7 @@ tslib@1.10.0, tslib@^1, tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== -tslib@1.11.1: +tslib@^1.11.1: version "1.11.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35" integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA== From b213e52132720fda552f2cfc9c30096cffe0da1c Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Mon, 6 Apr 2020 09:22:34 +0200 Subject: [PATCH 8/9] Use yarn cache in windows --- .github/workflows/main.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d58b241c..06bf0f22 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -73,10 +73,8 @@ jobs: - name: Get yarn cache id: yarn-cache run: echo "::set-output name=dir::$(yarn cache dir)" - if: matrix.os == 'ubuntu-latest' - name: Yarn Cache uses: actions/cache@v1 - if: matrix.os == 'ubuntu-latest' with: path: ${{ steps.yarn-cache.outputs.dir }} key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} From 7c5e91b1687aa5ce1e3db019afb96cc0ad7809c3 Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Mon, 6 Apr 2020 12:05:17 +0200 Subject: [PATCH 9/9] lowdash --- packages/core/src/utils/import-parser/definition.ts | 5 ++++- packages/core/src/utils/import-parser/index.ts | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/core/src/utils/import-parser/definition.ts b/packages/core/src/utils/import-parser/definition.ts index c655b791..4f989e18 100644 --- a/packages/core/src/utils/import-parser/definition.ts +++ b/packages/core/src/utils/import-parser/definition.ts @@ -1,4 +1,7 @@ -import { keyBy, uniqBy, includes, reverse } from 'lodash'; +import keyBy from 'lodash/keyBy'; +import uniqBy from 'lodash/uniqBy'; +import includes from 'lodash/includes'; +import reverse from 'lodash/reverse'; import { NamedTypeNode, DirectiveNode, diff --git a/packages/core/src/utils/import-parser/index.ts b/packages/core/src/utils/import-parser/index.ts index 9785dea5..90322605 100644 --- a/packages/core/src/utils/import-parser/index.ts +++ b/packages/core/src/utils/import-parser/index.ts @@ -1,5 +1,9 @@ import { DefinitionNode, parse, ObjectTypeDefinitionNode, DocumentNode, Kind } from 'graphql'; -import { groupBy, keyBy, isEqual, uniqBy, flatten } from 'lodash'; +import groupBy from 'lodash/groupBy'; +import keyBy from 'lodash/keyBy'; +import isEqual from 'lodash/isEqual'; +import uniqBy from 'lodash/uniqBy'; +import flatten from 'lodash/flatten'; import { LoadTypedefsOptions } from '../../load/typedefs'; import { loadFile, loadFileSync } from '../../load/load-typedefs/load-file';