From 6587cc0bbe6dcb2eb544da849a9a95e06bc30d28 Mon Sep 17 00:00:00 2001 From: Mike Fix Date: Wed, 10 Jan 2018 18:29:50 -0800 Subject: [PATCH 01/15] Allow passing an array of resolver functions to makeExecutable schema - Closes #486 --- package.json | 4 +- src/Interfaces.ts | 2 +- src/schemaGenerator.ts | 10 ++++- src/test/testSchemaGenerator.ts | 74 +++++++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 65d19ff2e40..31b8e734519 100644 --- a/package.json +++ b/package.json @@ -48,10 +48,12 @@ }, "homepage": "https://github.com/apollostack/graphql-tools#readme", "dependencies": { - "apollo-utilities": "^1.0.1", + "@types/lodash.merge": "^4.6.3", "apollo-link": "^1.0.0", + "apollo-utilities": "^1.0.1", "deprecated-decorator": "^0.1.6", "graphql-subscriptions": "^0.5.6", + "lodash.merge": "^4.6.0", "uuid": "^3.1.0" }, "peerDependencies": { diff --git a/src/Interfaces.ts b/src/Interfaces.ts index a91c47fcb33..96bdab3d480 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -71,7 +71,7 @@ export type IConnectors = { [key: string]: IConnector }; export interface IExecutableSchemaDefinition { typeDefs: ITypeDefinitions; - resolvers?: IResolvers; + resolvers?: IResolvers | Array; connectors?: IConnectors; logger?: ILogger; allowUndefinedInResolve?: boolean; diff --git a/src/schemaGenerator.ts b/src/schemaGenerator.ts index db17c375451..8f61ac2f784 100644 --- a/src/schemaGenerator.ts +++ b/src/schemaGenerator.ts @@ -44,6 +44,8 @@ import { import { deprecated } from 'deprecated-decorator'; +const merge = require('lodash.merge'); + // @schemaDefinition: A GraphQL type schema in shorthand // @resolvers: Definitions for resolvers to be merged with schema class SchemaError extends Error { @@ -59,7 +61,7 @@ class SchemaError extends Error { // type definitions can be a string or an array of strings. function _generateSchema( typeDefinitions: ITypeDefinitions, - resolveFunctions: IResolvers, + resolveFunctions: IResolvers | Array, logger: ILogger, // TODO: rename to allowUndefinedInResolve to be consistent allowUndefinedInResolve: boolean, @@ -78,13 +80,17 @@ function _generateSchema( throw new SchemaError('Must provide resolvers'); } + const resolvers = Array.isArray(resolveFunctions) + ? merge({}, ...resolveFunctions.filter(resolverObj => typeof resolverObj === 'object')) + : resolveFunctions; + // TODO: check that typeDefinitions is either string or array of strings const schema = buildSchemaFromTypeDefinitions(typeDefinitions); addResolveFunctionsToSchema( schema, - resolveFunctions, + resolvers, resolverValidationOptions, ); diff --git a/src/test/testSchemaGenerator.ts b/src/test/testSchemaGenerator.ts index 738582dd059..4401e724af8 100644 --- a/src/test/testSchemaGenerator.ts +++ b/src/test/testSchemaGenerator.ts @@ -662,6 +662,80 @@ describe('generating schema from shorthand', () => { ); }); + it('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 resolveFunctions = { + RootQuery: { + species: (root: any, { name }: { name: string }) => [ + { + name: `Hello ${name}!`, + wingspan: 200, + height: 30.2, + }, + ], + }, + }; + + const otherResolveFunctions = { + 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: [resolveFunctions, otherResolveFunctions], + }); + const resultPromise = graphql(jsSchema, testQuery); + return resultPromise.then(result => + assert.deepEqual(result, solution as ExecutionResult), + ); + }); + describe('scalar types', () => { it('supports passing a GraphQLScalarType in resolveFunctions', () => { // Here GraphQLJSON is used as an example of non-default GraphQLScalarType From 454ff4b5e99bd4d3c237cb635119733d8964746b Mon Sep 17 00:00:00 2001 From: Mike Fix Date: Wed, 10 Jan 2018 19:38:52 -0800 Subject: [PATCH 02/15] Implement passing an array of resolvers sets to mergeSchemas --- src/stitching/mergeSchemas.ts | 11 +- src/test/testMergeSchemas.ts | 196 ++++++++++++++++++++++++++++++++++ 2 files changed, 206 insertions(+), 1 deletion(-) diff --git a/src/stitching/mergeSchemas.ts b/src/stitching/mergeSchemas.ts index 8a0f4e84809..b00de26bc36 100644 --- a/src/stitching/mergeSchemas.ts +++ b/src/stitching/mergeSchemas.ts @@ -30,6 +30,7 @@ import { import delegateToSchema from './delegateToSchema'; import typeFromAST from './typeFromAST'; +const merge = require('lodash.merge'); const backcompatOptions = { commentDescriptions: true }; export default function mergeSchemas({ @@ -42,7 +43,7 @@ export default function mergeSchemas({ left: GraphQLNamedType, right: GraphQLNamedType, ) => GraphQLNamedType; - resolvers?: IResolvers | ((mergeInfo: MergeInfo) => IResolvers); + resolvers?: IResolvers | ((mergeInfo: MergeInfo) => IResolvers) | Array IResolvers)>; }): GraphQLSchema { if (!onTypeConflict) { onTypeConflict = defaultOnTypeConflict; @@ -177,6 +178,14 @@ export default function mergeSchemas({ if (resolvers) { if (typeof resolvers === 'function') { passedResolvers = resolvers(mergeInfo); + } else if (Array.isArray(resolvers)) { + passedResolvers = merge( + {}, + ...resolvers + .map(resolver => typeof resolver === 'function' + ? resolver(mergeInfo) + : resolver) + ); } else { passedResolvers = { ...resolvers }; } diff --git a/src/test/testMergeSchemas.ts b/src/test/testMergeSchemas.ts index cce1678ceb5..5348db68867 100644 --- a/src/test/testMergeSchemas.ts +++ b/src/test/testMergeSchemas.ts @@ -24,6 +24,7 @@ import { } from './testingSchemas'; import { forAwaitEach } from 'iterall'; import { makeExecutableSchema } from '../schemaGenerator'; +import { IResolvers } from '../Interfaces'; const testCombinations = [ { @@ -963,6 +964,201 @@ bookingById(id: "b1") { const Booking = mergedSchema.getType('Booking') as GraphQLObjectType; expect(Booking.isTypeOf).to.equal(undefined); }); + + it('should merge resolvers when passed an array of resolver objects', async () => { + const Scalars = () => ({ + TestScalar: new GraphQLScalarType({ + name: 'TestScalar', + description: undefined, + serialize: value => value, + parseValue: value => value, + parseLiteral: () => null, + }) + }); + const Enums = () => ({ + NumericEnum: { + TEST: 1 + }, + Color: { + RED: '#EA3232', + } + }); + const PropertyResolvers: IResolvers = { + Property: { + bookings: { + fragment: 'fragment PropertyFragment on Property { id }', + resolve(parent, args, context, info) { + return info.mergeInfo.delegate( + 'query', + 'bookingsByPropertyId', + { + propertyId: parent.id, + limit: args.limit ? args.limit : null, + }, + context, + info, + ); + } + } + } + }; + const LinkResolvers: (info: any) => IResolvers = (info) => ({ + Booking: { + property: { + fragment: 'fragment BookingFragment on Booking { propertyId }', + resolve(parent, args, context) { + return info.mergeInfo.delegate( + 'query', + 'propertyById', + { + id: parent.propertyId, + }, + context, + info + ); + } + } + } + }); + const Query1 = () => ({ + Query: { + color() { + return '#EA3232'; + }, + numericEnum() { + return 1; + } + } + }); + const Query2: (info: any) => IResolvers = () => ({ + Query: { + delegateInterfaceTest(parent, args, context, info) { + return info.mergeInfo.delegate( + 'query', + 'interfaceTest', + { + kind: 'ONE', + }, + context, + info, + ); + }, + delegateArgumentTest(parent, args, context, info) { + return info.mergeInfo.delegate( + 'query', + 'propertyById', + { + id: 'p1', + }, + context, + info, + ); + }, + linkTest() { + return { + test: 'test', + }; + }, + node: { + // fragment doesn't work + fragment: 'fragment NodeFragment on Node { id }', + resolve(parent, args, context, info) { + if (args.id.startsWith('p')) { + return info.mergeInfo.delegate( + 'query', + 'propertyById', + args, + context, + info, + ); + } else if (args.id.startsWith('b')) { + return info.mergeInfo.delegate( + 'query', + 'bookingById', + args, + context, + info, + ); + } else if (args.id.startsWith('c')) { + return info.mergeInfo.delegate( + 'query', + 'customerById', + args, + context, + info, + ); + } else { + throw new Error('invalid id'); + } + } + } + } + }); + + const AsyncQuery: (info: any) => IResolvers = (info) => ({ + Query: { + async nodes(parent, args, context) { + const bookings = await info.mergeInfo.delegate( + 'query', + 'bookings', + {}, + context, + info, + ); + const properties = await info.mergeInfo.delegate( + 'query', + 'properties', + {}, + context, + info, + ); + return [...bookings, ...properties]; + } + } + }); + const schema = mergeSchemas({ + schemas: [ + propertySchema, + bookingSchema, + productSchema, + scalarTest, + enumTest, + linkSchema, + loneExtend, + localSubscriptionSchema, + ], + resolvers: [ + Scalars, + Enums, + PropertyResolvers, + LinkResolvers, + Query1, + Query2, + AsyncQuery + ] + }); + + const mergedResult = await graphql( + schema, + ` + query { + dateTimeTest + test1: jsonTest(input: { foo: "bar" }) + test2: jsonTest(input: 5) + test3: jsonTest(input: "6") + } + `, + ); + const expected = { + data: { + dateTimeTest: '1987-09-25T12:00:00', + test1: { foo: 'bar' }, + test2: 5, + test3: '6' + } + }; + expect(mergedResult).to.deep.equal(expected); + }); }); describe('fragments', () => { From 0aff3037badec0effb1c4b3c35fb69c92268abd3 Mon Sep 17 00:00:00 2001 From: Mike Fix Date: Wed, 10 Jan 2018 20:13:29 -0800 Subject: [PATCH 03/15] Introduce unit or array composite type --- src/Interfaces.ts | 1 + src/schemaGenerator.ts | 3 ++- src/stitching/mergeSchemas.ts | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 96bdab3d480..f2bac1865ad 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -13,6 +13,7 @@ import { /* TODO: Add documentation */ +export type UnitOrList = Type | Array; export interface IResolverValidationOptions { requireResolversForArgs?: boolean; requireResolversForNonScalar?: boolean; diff --git a/src/schemaGenerator.ts b/src/schemaGenerator.ts index 8f61ac2f784..6d4023ad54a 100644 --- a/src/schemaGenerator.ts +++ b/src/schemaGenerator.ts @@ -40,6 +40,7 @@ import { IConnectorCls, IResolverValidationOptions, IDirectiveResolvers, + UnitOrList, } from './Interfaces'; import { deprecated } from 'deprecated-decorator'; @@ -61,7 +62,7 @@ class SchemaError extends Error { // type definitions can be a string or an array of strings. function _generateSchema( typeDefinitions: ITypeDefinitions, - resolveFunctions: IResolvers | Array, + resolveFunctions: UnitOrList, logger: ILogger, // TODO: rename to allowUndefinedInResolve to be consistent allowUndefinedInResolve: boolean, diff --git a/src/stitching/mergeSchemas.ts b/src/stitching/mergeSchemas.ts index b00de26bc36..752b8ad783f 100644 --- a/src/stitching/mergeSchemas.ts +++ b/src/stitching/mergeSchemas.ts @@ -17,7 +17,7 @@ import { parse, } from 'graphql'; import TypeRegistry from './TypeRegistry'; -import { IResolvers, MergeInfo, IFieldResolver } from '../Interfaces'; +import { IResolvers, MergeInfo, IFieldResolver, UnitOrList } from '../Interfaces'; import isEmptyObject from '../isEmptyObject'; import { extractExtensionDefinitions, @@ -43,7 +43,7 @@ export default function mergeSchemas({ left: GraphQLNamedType, right: GraphQLNamedType, ) => GraphQLNamedType; - resolvers?: IResolvers | ((mergeInfo: MergeInfo) => IResolvers) | Array IResolvers)>; + resolvers?: UnitOrList IResolvers)>; }): GraphQLSchema { if (!onTypeConflict) { onTypeConflict = defaultOnTypeConflict; From 9c13517778cee99fe3d95afa6e13cceda66c5dfb Mon Sep 17 00:00:00 2001 From: Mike Fix Date: Thu, 11 Jan 2018 08:40:06 -0800 Subject: [PATCH 04/15] Fix @types dep --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 31b8e734519..af58b0b264b 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,6 @@ }, "homepage": "https://github.com/apollostack/graphql-tools#readme", "dependencies": { - "@types/lodash.merge": "^4.6.3", "apollo-link": "^1.0.0", "apollo-utilities": "^1.0.1", "deprecated-decorator": "^0.1.6", @@ -62,6 +61,7 @@ "devDependencies": { "@types/chai": "4.0.10", "@types/graphql": "0.11.7", + "@types/lodash.merge": "^4.6.3", "@types/mocha": "^2.2.44", "@types/node": "^8.0.47", "@types/uuid": "^3.4.3", From 9041bc219dd24b85419170d07c8c99edd17a14ca Mon Sep 17 00:00:00 2001 From: Renato Benkendorf Date: Tue, 23 Jan 2018 08:35:00 -0200 Subject: [PATCH 05/15] Fix resolvers to accept and move forward args with zero or false values (#586) Fixing the args with zero value or false --- CHANGELOG.md | 2 +- src/stitching/delegateToSchema.ts | 15 ++++++--------- src/test/testMergeSchemas.ts | 28 ++++++++++++++-------------- 3 files changed, 21 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5330fd85c4..326a2a06008 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### vNEXT -* ... +* Fix `delegateToSchema.js` to accept and move forward args with zero or false values [PR #586](https://github.com/apollographql/graphql-tools/pull/586) ### v2.18.0 diff --git a/src/stitching/delegateToSchema.ts b/src/stitching/delegateToSchema.ts index f8214167e03..4e64f7a67f6 100644 --- a/src/stitching/delegateToSchema.ts +++ b/src/stitching/delegateToSchema.ts @@ -72,18 +72,15 @@ export default async function delegateToSchema( if ( operationDefinition && operationDefinition.kind === Kind.OPERATION_DEFINITION && - operationDefinition.variableDefinitions + operationDefinition.variableDefinitions && + Array.isArray(operationDefinition.variableDefinitions) ) { - operationDefinition.variableDefinitions.forEach(definition => { + for (const definition of operationDefinition.variableDefinitions) { const key = definition.variable.name.value; // (XXX) This is kinda hacky - let actualKey = key; - if (actualKey.startsWith('_')) { - actualKey = actualKey.slice(1); - } - const value = args[actualKey] || args[key] || info.variableValues[key]; - variableValues[key] = value; - }); + const actualKey = key.startsWith('_') ? key.slice(1) : key; + variableValues[key] = args[actualKey] != null ? args[actualKey] : info.variableValues[key]; + } } if (operation === 'query' || operation === 'mutation') { diff --git a/src/test/testMergeSchemas.ts b/src/test/testMergeSchemas.ts index cce1678ceb5..34b5d734951 100644 --- a/src/test/testMergeSchemas.ts +++ b/src/test/testMergeSchemas.ts @@ -1143,21 +1143,21 @@ bookingById(id: "b1") { describe('variables', () => { it('basic', async () => { const propertyFragment = ` -propertyById(id: $p1) { - id - name -} - `; + propertyById(id: $p1) { + id + name + } + `; const bookingFragment = ` -bookingById(id: $b1) { - id - customer { - name - } - startTime - endTime -} - `; + bookingById(id: $b1) { + id + customer { + name + } + startTime + endTime + } + `; const propertyResult = await graphql( propertySchema, From 57152af088a6cd77c93cd48cbdf423c6ed68aa24 Mon Sep 17 00:00:00 2001 From: Sebastian Richter Date: Tue, 23 Jan 2018 12:45:49 +0100 Subject: [PATCH 06/15] Also recreate astNode for fields (#580) * Also recreate astNode for fields In a [previous commit](https://github.com/apollographql/graphql-tools/commit/fd9f6260faa779b2bfc12f1f707cdf2b778d306b) we added the `astNode` property in the `reacreateCompositeType` function. That resulted in cache control working with schema stitching but only for GraphQL Types. By recreating the `astNode` prop also in `fieldToFieldConfig` cache control also works for fields. This is required for caching fields and hence queries. * Add ast to input field node too --- CHANGELOG.md | 1 + src/stitching/schemaRecreation.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 326a2a06008..1780d4e90d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### vNEXT +* Also recreate `astNode` property for fields, not only types, when recreating schemas. [PR #580](https://github.com/apollographql/graphql-tools/pull/580) * Fix `delegateToSchema.js` to accept and move forward args with zero or false values [PR #586](https://github.com/apollographql/graphql-tools/pull/586) ### v2.18.0 diff --git a/src/stitching/schemaRecreation.ts b/src/stitching/schemaRecreation.ts index ee8300c4db1..d760db79d76 100644 --- a/src/stitching/schemaRecreation.ts +++ b/src/stitching/schemaRecreation.ts @@ -92,6 +92,7 @@ function fieldToFieldConfig( resolve: defaultMergedResolver, description: field.description, deprecationReason: field.deprecationReason, + astNode: field.astNode, }; } @@ -140,5 +141,6 @@ function inputFieldToFieldConfig( type: registry.resolveType(field.type), defaultValue: field.defaultValue, description: field.description, + astNode: field.astNode, }; } From 66ddf7e88227c4eb9b4fb22b203d0822649dc510 Mon Sep 17 00:00:00 2001 From: Mike Fix Date: Tue, 23 Jan 2018 11:54:22 -0800 Subject: [PATCH 07/15] Address comments, remove lodash --- package.json | 2 -- src/schemaGenerator.ts | 7 ++++--- src/stitching/mergeSchemas.ts | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 04d22e7fbfc..8ed95ec5d91 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,6 @@ "apollo-utilities": "^1.0.1", "deprecated-decorator": "^0.1.6", "graphql-subscriptions": "^0.5.6", - "lodash.merge": "^4.6.0", "uuid": "^3.1.0" }, "peerDependencies": { @@ -61,7 +60,6 @@ "devDependencies": { "@types/chai": "4.0.10", "@types/graphql": "0.11.7", - "@types/lodash.merge": "^4.6.3", "@types/mocha": "^2.2.44", "@types/node": "^8.0.47", "@types/uuid": "^3.4.3", diff --git a/src/schemaGenerator.ts b/src/schemaGenerator.ts index 8f61ac2f784..7b6c99db211 100644 --- a/src/schemaGenerator.ts +++ b/src/schemaGenerator.ts @@ -43,8 +43,7 @@ import { } from './Interfaces'; import { deprecated } from 'deprecated-decorator'; - -const merge = require('lodash.merge'); +import { mergeDeep } from './stitching/mergeSchemas'; // @schemaDefinition: A GraphQL type schema in shorthand // @resolvers: Definitions for resolvers to be merged with schema @@ -81,7 +80,9 @@ function _generateSchema( } const resolvers = Array.isArray(resolveFunctions) - ? merge({}, ...resolveFunctions.filter(resolverObj => typeof resolverObj === 'object')) + ? resolveFunctions + .filter(resolverObj => typeof resolverObj === 'object') + .reduce(mergeDeep, {}) : resolveFunctions; // TODO: check that typeDefinitions is either string or array of strings diff --git a/src/stitching/mergeSchemas.ts b/src/stitching/mergeSchemas.ts index 8a0f4e84809..99d60cf6871 100644 --- a/src/stitching/mergeSchemas.ts +++ b/src/stitching/mergeSchemas.ts @@ -303,7 +303,7 @@ function isObject(item: any): Boolean { return item && typeof item === 'object' && !Array.isArray(item); } -function mergeDeep(target: any, source: any): any { +export function mergeDeep(target: any, source: any): any { let output = Object.assign({}, target); if (isObject(target) && isObject(source)) { Object.keys(source).forEach(key => { From de31c0437d3e81a753b0c969513e211d2f0e1cce Mon Sep 17 00:00:00 2001 From: Mike Fix Date: Thu, 11 Jan 2018 08:40:06 -0800 Subject: [PATCH 08/15] Fix @types dep --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f4758983b8f..04d22e7fbfc 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,6 @@ }, "homepage": "https://github.com/apollostack/graphql-tools#readme", "dependencies": { - "@types/lodash.merge": "^4.6.3", "apollo-link": "^1.0.0", "apollo-utilities": "^1.0.1", "deprecated-decorator": "^0.1.6", @@ -62,6 +61,7 @@ "devDependencies": { "@types/chai": "4.0.10", "@types/graphql": "0.11.7", + "@types/lodash.merge": "^4.6.3", "@types/mocha": "^2.2.44", "@types/node": "^8.0.47", "@types/uuid": "^3.4.3", From a6728b9ac6d12cec8067f95ee2ec93f5b595640c Mon Sep 17 00:00:00 2001 From: Renato Benkendorf Date: Tue, 23 Jan 2018 08:35:00 -0200 Subject: [PATCH 09/15] Fix resolvers to accept and move forward args with zero or false values (#586) Fixing the args with zero value or false --- CHANGELOG.md | 2 +- src/stitching/delegateToSchema.ts | 15 ++++++--------- src/test/testMergeSchemas.ts | 28 ++++++++++++++-------------- 3 files changed, 21 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5330fd85c4..326a2a06008 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### vNEXT -* ... +* Fix `delegateToSchema.js` to accept and move forward args with zero or false values [PR #586](https://github.com/apollographql/graphql-tools/pull/586) ### v2.18.0 diff --git a/src/stitching/delegateToSchema.ts b/src/stitching/delegateToSchema.ts index f8214167e03..4e64f7a67f6 100644 --- a/src/stitching/delegateToSchema.ts +++ b/src/stitching/delegateToSchema.ts @@ -72,18 +72,15 @@ export default async function delegateToSchema( if ( operationDefinition && operationDefinition.kind === Kind.OPERATION_DEFINITION && - operationDefinition.variableDefinitions + operationDefinition.variableDefinitions && + Array.isArray(operationDefinition.variableDefinitions) ) { - operationDefinition.variableDefinitions.forEach(definition => { + for (const definition of operationDefinition.variableDefinitions) { const key = definition.variable.name.value; // (XXX) This is kinda hacky - let actualKey = key; - if (actualKey.startsWith('_')) { - actualKey = actualKey.slice(1); - } - const value = args[actualKey] || args[key] || info.variableValues[key]; - variableValues[key] = value; - }); + const actualKey = key.startsWith('_') ? key.slice(1) : key; + variableValues[key] = args[actualKey] != null ? args[actualKey] : info.variableValues[key]; + } } if (operation === 'query' || operation === 'mutation') { diff --git a/src/test/testMergeSchemas.ts b/src/test/testMergeSchemas.ts index 5348db68867..fbf192d3915 100644 --- a/src/test/testMergeSchemas.ts +++ b/src/test/testMergeSchemas.ts @@ -1339,21 +1339,21 @@ bookingById(id: "b1") { describe('variables', () => { it('basic', async () => { const propertyFragment = ` -propertyById(id: $p1) { - id - name -} - `; + propertyById(id: $p1) { + id + name + } + `; const bookingFragment = ` -bookingById(id: $b1) { - id - customer { - name - } - startTime - endTime -} - `; + bookingById(id: $b1) { + id + customer { + name + } + startTime + endTime + } + `; const propertyResult = await graphql( propertySchema, From 008935bb3664400acea9143cbe4683c90edb45b0 Mon Sep 17 00:00:00 2001 From: Sebastian Richter Date: Tue, 23 Jan 2018 12:45:49 +0100 Subject: [PATCH 10/15] Also recreate astNode for fields (#580) * Also recreate astNode for fields In a [previous commit](https://github.com/apollographql/graphql-tools/commit/fd9f6260faa779b2bfc12f1f707cdf2b778d306b) we added the `astNode` property in the `reacreateCompositeType` function. That resulted in cache control working with schema stitching but only for GraphQL Types. By recreating the `astNode` prop also in `fieldToFieldConfig` cache control also works for fields. This is required for caching fields and hence queries. * Add ast to input field node too --- CHANGELOG.md | 1 + src/stitching/schemaRecreation.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 326a2a06008..1780d4e90d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### vNEXT +* Also recreate `astNode` property for fields, not only types, when recreating schemas. [PR #580](https://github.com/apollographql/graphql-tools/pull/580) * Fix `delegateToSchema.js` to accept and move forward args with zero or false values [PR #586](https://github.com/apollographql/graphql-tools/pull/586) ### v2.18.0 diff --git a/src/stitching/schemaRecreation.ts b/src/stitching/schemaRecreation.ts index ee8300c4db1..d760db79d76 100644 --- a/src/stitching/schemaRecreation.ts +++ b/src/stitching/schemaRecreation.ts @@ -92,6 +92,7 @@ function fieldToFieldConfig( resolve: defaultMergedResolver, description: field.description, deprecationReason: field.deprecationReason, + astNode: field.astNode, }; } @@ -140,5 +141,6 @@ function inputFieldToFieldConfig( type: registry.resolveType(field.type), defaultValue: field.defaultValue, description: field.description, + astNode: field.astNode, }; } From 7977b063b56d345a8895fd92a2ae38781acbfe5e Mon Sep 17 00:00:00 2001 From: Mike Fix Date: Tue, 23 Jan 2018 11:54:22 -0800 Subject: [PATCH 11/15] Address comments, remove lodash --- package.json | 2 -- src/schemaGenerator.ts | 7 ++++--- src/stitching/mergeSchemas.ts | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 04d22e7fbfc..8ed95ec5d91 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,6 @@ "apollo-utilities": "^1.0.1", "deprecated-decorator": "^0.1.6", "graphql-subscriptions": "^0.5.6", - "lodash.merge": "^4.6.0", "uuid": "^3.1.0" }, "peerDependencies": { @@ -61,7 +60,6 @@ "devDependencies": { "@types/chai": "4.0.10", "@types/graphql": "0.11.7", - "@types/lodash.merge": "^4.6.3", "@types/mocha": "^2.2.44", "@types/node": "^8.0.47", "@types/uuid": "^3.4.3", diff --git a/src/schemaGenerator.ts b/src/schemaGenerator.ts index 6d4023ad54a..a3e4f5a2c7e 100644 --- a/src/schemaGenerator.ts +++ b/src/schemaGenerator.ts @@ -44,8 +44,7 @@ import { } from './Interfaces'; import { deprecated } from 'deprecated-decorator'; - -const merge = require('lodash.merge'); +import { mergeDeep } from './stitching/mergeSchemas'; // @schemaDefinition: A GraphQL type schema in shorthand // @resolvers: Definitions for resolvers to be merged with schema @@ -82,7 +81,9 @@ function _generateSchema( } const resolvers = Array.isArray(resolveFunctions) - ? merge({}, ...resolveFunctions.filter(resolverObj => typeof resolverObj === 'object')) + ? resolveFunctions + .filter(resolverObj => typeof resolverObj === 'object') + .reduce(mergeDeep, {}) : resolveFunctions; // TODO: check that typeDefinitions is either string or array of strings diff --git a/src/stitching/mergeSchemas.ts b/src/stitching/mergeSchemas.ts index 752b8ad783f..8bf5e283698 100644 --- a/src/stitching/mergeSchemas.ts +++ b/src/stitching/mergeSchemas.ts @@ -312,7 +312,7 @@ function isObject(item: any): Boolean { return item && typeof item === 'object' && !Array.isArray(item); } -function mergeDeep(target: any, source: any): any { +export function mergeDeep(target: any, source: any): any { let output = Object.assign({}, target); if (isObject(target) && isObject(source)) { Object.keys(source).forEach(key => { From e8631fd296bc317cbb869d494d20d4a781068119 Mon Sep 17 00:00:00 2001 From: Mike Fix Date: Tue, 23 Jan 2018 12:07:13 -0800 Subject: [PATCH 12/15] Use mergeDeep instead of lodash.merge --- src/stitching/mergeSchemas.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/stitching/mergeSchemas.ts b/src/stitching/mergeSchemas.ts index 8bf5e283698..3fea5b29131 100644 --- a/src/stitching/mergeSchemas.ts +++ b/src/stitching/mergeSchemas.ts @@ -30,7 +30,6 @@ import { import delegateToSchema from './delegateToSchema'; import typeFromAST from './typeFromAST'; -const merge = require('lodash.merge'); const backcompatOptions = { commentDescriptions: true }; export default function mergeSchemas({ @@ -179,13 +178,11 @@ export default function mergeSchemas({ if (typeof resolvers === 'function') { passedResolvers = resolvers(mergeInfo); } else if (Array.isArray(resolvers)) { - passedResolvers = merge( - {}, - ...resolvers - .map(resolver => typeof resolver === 'function' - ? resolver(mergeInfo) - : resolver) - ); + passedResolvers = resolvers + .map(resolver => typeof resolver === 'function' + ? resolver(mergeInfo) + : resolver) + .reduce(mergeDeep, {}) } else { passedResolvers = { ...resolvers }; } From 0c937ae912c6f4da546988b8a5fc3463218f2867 Mon Sep 17 00:00:00 2001 From: Mike Fix Date: Tue, 23 Jan 2018 12:16:45 -0800 Subject: [PATCH 13/15] Fix build --- src/stitching/mergeSchemas.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stitching/mergeSchemas.ts b/src/stitching/mergeSchemas.ts index 3fea5b29131..1fa09621623 100644 --- a/src/stitching/mergeSchemas.ts +++ b/src/stitching/mergeSchemas.ts @@ -182,7 +182,7 @@ export default function mergeSchemas({ .map(resolver => typeof resolver === 'function' ? resolver(mergeInfo) : resolver) - .reduce(mergeDeep, {}) + .reduce(mergeDeep, {}); } else { passedResolvers = { ...resolvers }; } From 9edfc6b714d7707ff89bf25944741704ff9656fb Mon Sep 17 00:00:00 2001 From: Mikhail Novikov Date: Mon, 5 Feb 2018 11:25:30 +0200 Subject: [PATCH 14/15] Changelog and separate utility --- CHANGELOG.md | 1 + src/mergeDeep.ts | 21 +++++++++++++++++++++ src/schemaGenerator.ts | 18 +++++++++--------- src/stitching/mergeSchemas.ts | 23 +---------------------- 4 files changed, 32 insertions(+), 31 deletions(-) create mode 100644 src/mergeDeep.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f5c3adc8cb6..bd2d3204123 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Change log ### vNEXT +* `makeExecutableSchema` and `mergeSchema` now accept an array of `IResolver` [PR #576](https://github.com/apollographql/graphql-tools/pull/576) [PR #577](https://github.com/apollographql/graphql-tools/pull/577) * Fix `delegateToSchema.ts` to remove duplicate new variable definitions when delegating to schemas [PR #607](https://github.com/apollographql/graphql-tools/pull/607) * Fix duplicate subscriptions for schema stitching [PR #609](https://github.com/apollographql/graphql-tools/pull/609) diff --git a/src/mergeDeep.ts b/src/mergeDeep.ts new file mode 100644 index 00000000000..e970535cce0 --- /dev/null +++ b/src/mergeDeep.ts @@ -0,0 +1,21 @@ +export default function mergeDeep(target: any, source: any): any { + let output = Object.assign({}, target); + 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/src/schemaGenerator.ts b/src/schemaGenerator.ts index 7b6c99db211..08f2126d2e7 100644 --- a/src/schemaGenerator.ts +++ b/src/schemaGenerator.ts @@ -43,7 +43,7 @@ import { } from './Interfaces'; import { deprecated } from 'deprecated-decorator'; -import { mergeDeep } from './stitching/mergeSchemas'; +import mergeDeep from './mergeDeep'; // @schemaDefinition: A GraphQL type schema in shorthand // @resolvers: Definitions for resolvers to be merged with schema @@ -89,11 +89,7 @@ function _generateSchema( const schema = buildSchemaFromTypeDefinitions(typeDefinitions); - addResolveFunctionsToSchema( - schema, - resolvers, - resolverValidationOptions, - ); + addResolveFunctionsToSchema(schema, resolvers, resolverValidationOptions); assertResolveFunctionsPresent(schema, resolverValidationOptions); @@ -213,7 +209,10 @@ function buildSchemaFromTypeDefinitions( const backcompatOptions = { commentDescriptions: true }; // TODO fix types https://github.com/apollographql/graphql-tools/issues/542 - let schema: GraphQLSchema = (buildASTSchema as any)(astDocument, backcompatOptions); + let schema: GraphQLSchema = (buildASTSchema as any)( + astDocument, + backcompatOptions, + ); const extensionsAst = extractExtensionDefinitions(astDocument); if (extensionsAst.definitions.length > 0) { @@ -224,7 +223,6 @@ function buildSchemaFromTypeDefinitions( return schema; } - // This was changed in graphql@0.12 // See https://github.com/apollographql/graphql-tools/pull/541 // TODO fix types https://github.com/apollographql/graphql-tools/issues/542 @@ -233,7 +231,9 @@ const newExtensionDefinitionKind = 'ObjectTypeExtension'; export function extractExtensionDefinitions(ast: DocumentNode) { const extensionDefs = ast.definitions.filter( - (def: DefinitionNode) => def.kind === oldTypeExtensionDefinitionKind || (def.kind as any) === newExtensionDefinitionKind, + (def: DefinitionNode) => + def.kind === oldTypeExtensionDefinitionKind || + (def.kind as any) === newExtensionDefinitionKind, ); return Object.assign({}, ast, { diff --git a/src/stitching/mergeSchemas.ts b/src/stitching/mergeSchemas.ts index 99d60cf6871..ebad11a64d8 100644 --- a/src/stitching/mergeSchemas.ts +++ b/src/stitching/mergeSchemas.ts @@ -19,6 +19,7 @@ import { import TypeRegistry from './TypeRegistry'; import { IResolvers, MergeInfo, IFieldResolver } from '../Interfaces'; import isEmptyObject from '../isEmptyObject'; +import mergeDeep from '../mergeDeep'; import { extractExtensionDefinitions, addResolveFunctionsToSchema, @@ -299,28 +300,6 @@ function createDelegatingResolver( }; } -function isObject(item: any): Boolean { - return item && typeof item === 'object' && !Array.isArray(item); -} - -export function mergeDeep(target: any, source: any): any { - let output = Object.assign({}, target); - 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; -} - type FieldIteratorFn = ( fieldDef: GraphQLField, typeName: string, From a5a063eda3500303baa2bef74d73beb667888be2 Mon Sep 17 00:00:00 2001 From: Mikhail Novikov Date: Mon, 5 Feb 2018 11:28:08 +0200 Subject: [PATCH 15/15] Update CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd2d3204123..01f1df4df27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Change log ### vNEXT -* `makeExecutableSchema` and `mergeSchema` now accept an array of `IResolver` [PR #576](https://github.com/apollographql/graphql-tools/pull/576) [PR #577](https://github.com/apollographql/graphql-tools/pull/577) +* `makeExecutableSchema` and `mergeSchema` now accept an array of `IResolver` [PR #612](https://github.com/apollographql/graphql-tools/pull/612) [PR #576](https://github.com/apollographql/graphql-tools/pull/576) [PR #577](https://github.com/apollographql/graphql-tools/pull/577) * Fix `delegateToSchema.ts` to remove duplicate new variable definitions when delegating to schemas [PR #607](https://github.com/apollographql/graphql-tools/pull/607) * Fix duplicate subscriptions for schema stitching [PR #609](https://github.com/apollographql/graphql-tools/pull/609)