diff --git a/.changeset/fifty-experts-unite.md b/.changeset/fifty-experts-unite.md new file mode 100644 index 00000000000..1209ceb5296 --- /dev/null +++ b/.changeset/fifty-experts-unite.md @@ -0,0 +1,68 @@ +--- +'@graphql-tools/schema': major +--- + +Thanks @mattkrick and @borisno2! + +`addResolversToSchema`; + +If you are using the legacy parameters like below, you should update them to the new usage. Other than that, there is no functional change; + +```ts +// From +addResolversToSchema(schema, resolvers, resolverValidationOptions) + +// To +addResolversToSchema({ + schema, + resolvers, + resolverValidationOptions +}) +``` + +`mergeSchemas`; + +The provided `resolver` overrides the resolvers in the `schema` with the same name; + +The `hello` resolver in the `schema` would be overridden by the `hello` resolver in the `resolvers`. Before it was opposite which is not expected. + +```ts +const schema = makeExecutableSchema({ + typeDefs: ` + type Query { + hello: String + } + `, + resolvers: { + Query: { + hello: () => 'Hello world!' + } + } +}) + +mergeSchemas({ + schemas: [schema], + resolvers: { + Query: { + hello: () => 'New hello world' + } + } +}) +``` + +`makeExecutableSchema` no longer takes `parseOptions` and you can pass those options directly; + +```ts +makeExecutableSchema({ + typeDefs: ``, + parseOptions: { + assumeValid: true + } +}) + +// After +makeExecutableSchema({ + typeDefs: ``, + assumeValid: true +}) +``` diff --git a/packages/load/package.json b/packages/load/package.json index c7614368ecf..11b71f0ed09 100644 --- a/packages/load/package.json +++ b/packages/load/package.json @@ -57,6 +57,7 @@ "dependencies": { "@graphql-tools/utils": "8.9.0", "@graphql-tools/schema": "8.5.1", + "@graphql-tools/merge": "8.3.1", "p-limit": "3.1.0", "tslib": "^2.4.0" }, diff --git a/packages/load/src/schema.ts b/packages/load/src/schema.ts index d16f84049d5..1bf91ea345d 100644 --- a/packages/load/src/schema.ts +++ b/packages/load/src/schema.ts @@ -1,19 +1,13 @@ -import { loadTypedefs, LoadTypedefsOptions, UnnormalizedTypeDefPointer, loadTypedefsSync } from './load-typedefs.js'; -import { - GraphQLSchema, - BuildSchemaOptions, - DocumentNode, - Source as GraphQLSource, - print, - lexicographicSortSchema, -} from 'graphql'; -import { OPERATION_KINDS } from './documents.js'; -import { mergeSchemas, MergeSchemasConfig } from '@graphql-tools/schema'; -import { Source } from '@graphql-tools/utils'; +import { loadTypedefs, LoadTypedefsOptions, UnnormalizedTypeDefPointer, loadTypedefsSync } from './load-typedefs'; +import { GraphQLSchema, BuildSchemaOptions, Source as GraphQLSource, print, lexicographicSortSchema } from 'graphql'; +import { OPERATION_KINDS } from './documents'; +import { IExecutableSchemaDefinition, mergeSchemas } from '@graphql-tools/schema'; +import { getResolversFromSchema, IResolvers, Source, TypeSource } from '@graphql-tools/utils'; +import { extractExtensionsFromSchema, SchemaExtensions } from '@graphql-tools/merge'; export type LoadSchemaOptions = BuildSchemaOptions & LoadTypedefsOptions & - Partial & { + Partial & { /** * Adds a list of Sources in to `extensions.sources` * @@ -35,22 +29,7 @@ export async function loadSchema( ...options, filterKinds: OPERATION_KINDS, }); - - const { schemas, typeDefs } = collectSchemasAndTypeDefs(sources); - schemas.push(...(options.schemas ?? [])); - const mergeSchemasOptions: MergeSchemasConfig = { - ...options, - schemas: schemas.concat(options.schemas ?? []), - typeDefs, - }; - - const schema = typeDefs?.length === 0 && schemas?.length === 1 ? schemas[0] : mergeSchemas(mergeSchemasOptions); - - if (options?.includeSources) { - includeSources(schema, sources); - } - - return options.sort ? lexicographicSortSchema(schema) : schema; + return getSchemaFromSources(sources, options); } /** @@ -66,20 +45,7 @@ export function loadSchemaSync( filterKinds: OPERATION_KINDS, ...options, }); - - const { schemas, typeDefs } = collectSchemasAndTypeDefs(sources); - - const schema = mergeSchemas({ - schemas, - typeDefs, - ...options, - }); - - if (options?.includeSources) { - includeSources(schema, sources); - } - - return options.sort ? lexicographicSortSchema(schema) : schema; + return getSchemaFromSources(sources, options); } function includeSources(schema: GraphQLSchema, sources: Source[]) { @@ -98,20 +64,47 @@ function includeSources(schema: GraphQLSchema, sources: Source[]) { }; } -function collectSchemasAndTypeDefs(sources: Source[]) { - const schemas: GraphQLSchema[] = []; - const typeDefs: DocumentNode[] = []; +function getSchemaFromSources(sources: Source[], options: LoadSchemaOptions) { + if (sources.length === 1 && sources[0].schema != null && options.typeDefs == null && options.resolvers == null) { + return options.sort ? lexicographicSortSchema(sources[0].schema) : sources[0].schema; + } + const { typeDefs, resolvers, schemaExtensions } = collectSchemaParts(sources); + + const schema = mergeSchemas({ + ...options, + typeDefs, + resolvers, + schemaExtensions, + }); + + if (options?.includeSources) { + includeSources(schema, sources); + } + + return options.sort ? lexicographicSortSchema(schema) : schema; +} + +function collectSchemaParts(sources: Source[]) { + const typeDefs: TypeSource[] = []; + const resolvers: IResolvers[] = []; + const schemaExtensions: SchemaExtensions[] = []; for (const source of sources) { if (source.schema) { - schemas.push(source.schema); - } else if (source.document) { - typeDefs.push(source.document); + typeDefs.push(source.schema); + resolvers.push(getResolversFromSchema(source.schema)); + schemaExtensions.push(extractExtensionsFromSchema(source.schema)); + } else { + const typeDef = source.document || source.rawSDL; + if (typeDef) { + typeDefs.push(typeDef); + } } } return { - schemas, typeDefs, + resolvers, + schemaExtensions, }; } diff --git a/packages/mock/src/addMocksToSchema.ts b/packages/mock/src/addMocksToSchema.ts index d82cc6daea0..2b891de4bcb 100644 --- a/packages/mock/src/addMocksToSchema.ts +++ b/packages/mock/src/addMocksToSchema.ts @@ -245,5 +245,10 @@ export function addMocksToSchema({ }, }); - return resolvers ? addResolversToSchema(schemaWithMocks, resolvers as any) : schemaWithMocks; + return resolvers + ? addResolversToSchema({ + schema: schemaWithMocks, + resolvers: resolvers as any, + }) + : schemaWithMocks; } diff --git a/packages/mock/tests/mocking-compatibility.spec.ts b/packages/mock/tests/mocking-compatibility.spec.ts index dd5b6f96ffe..120bf636deb 100644 --- a/packages/mock/tests/mocking-compatibility.spec.ts +++ b/packages/mock/tests/mocking-compatibility.spec.ts @@ -181,7 +181,10 @@ describe('Mock retro-compatibility', () => { returnString: () => 'someString', }, }; - jsSchema = addResolversToSchema(jsSchema, resolvers); + jsSchema = addResolversToSchema({ + schema: jsSchema, + resolvers, + }); const testQuery = /* GraphQL */ ` { returnInt @@ -266,7 +269,10 @@ describe('Mock retro-compatibility', () => { }, }, }; - jsSchema = addResolversToSchema(jsSchema, resolvers); + jsSchema = addResolversToSchema({ + schema: jsSchema, + resolvers, + }); jsSchema = addMocksToSchema({ schema: jsSchema, mocks: {}, @@ -625,7 +631,10 @@ describe('Mock retro-compatibility', () => { returnMockError: () => undefined, }, }; - jsSchema = addResolversToSchema(jsSchema, resolvers); + jsSchema = addResolversToSchema({ + schema: jsSchema, + resolvers, + }); const mockMap = {}; jsSchema = addMocksToSchema({ @@ -656,7 +665,10 @@ describe('Mock retro-compatibility', () => { returnMockError: () => '10-11-2012', }, }; - jsSchema = addResolversToSchema(jsSchema, resolvers); + jsSchema = addResolversToSchema({ + schema: jsSchema, + resolvers, + }); const mockMap = {}; addMocksToSchema({ @@ -875,7 +887,10 @@ describe('Mock retro-compatibility', () => { }, }, }; - jsSchema = addResolversToSchema(jsSchema, resolvers); + jsSchema = addResolversToSchema({ + schema: jsSchema, + resolvers, + }); const testQuery = /* GraphQL */ ` { returnListOfListOfObject { @@ -1064,7 +1079,10 @@ describe('Mock retro-compatibility', () => { returnString: () => Promise.resolve('bar'), // see c) }, }; - jsSchema = addResolversToSchema(jsSchema, resolvers); + jsSchema = addResolversToSchema({ + schema: jsSchema, + resolvers, + }); jsSchema = addMocksToSchema({ schema: jsSchema, mocks: mockMap, @@ -1129,7 +1147,10 @@ describe('Mock retro-compatibility', () => { }), }, }; - jsSchema = addResolversToSchema(jsSchema, resolvers); + jsSchema = addResolversToSchema({ + schema: jsSchema, + resolvers, + }); const mockMap = { returnListOfInt: () => [5, 6, 7], Bird: () => ({ @@ -1174,7 +1195,10 @@ describe('Mock retro-compatibility', () => { }), }, }; - jsSchema = addResolversToSchema(jsSchema, resolvers); + jsSchema = addResolversToSchema({ + schema: jsSchema, + resolvers, + }); const mockMap = { Bird: () => ({ returnInt: 3, // see a) @@ -1218,7 +1242,10 @@ describe('Mock retro-compatibility', () => { returnObject: () => objProxy, }, }; - jsSchema = addResolversToSchema(jsSchema, resolvers); + jsSchema = addResolversToSchema({ + schema: jsSchema, + resolvers, + }); const mockMap = { Bird: () => ({ returnInt: 3, // see a) @@ -1295,7 +1322,10 @@ describe('Mock retro-compatibility', () => { returnString: () => null, // a) resolve of a string }, }; - jsSchema = addResolversToSchema(jsSchema, resolvers); + jsSchema = addResolversToSchema({ + schema: jsSchema, + resolvers, + }); const mockMap = { Int: () => 666, // b) mock of Int. }; diff --git a/packages/schema/src/addResolversToSchema.ts b/packages/schema/src/addResolversToSchema.ts index c019f8ba948..becec011d78 100644 --- a/packages/schema/src/addResolversToSchema.ts +++ b/packages/schema/src/addResolversToSchema.ts @@ -1,7 +1,6 @@ import { GraphQLEnumType, GraphQLSchema, - isSchema, GraphQLScalarType, GraphQLUnionType, GraphQLInterfaceType, @@ -19,7 +18,6 @@ import { import { IResolvers, - IResolverValidationOptions, IAddResolversToSchemaOptions, mapSchema, MapperKind, @@ -33,28 +31,14 @@ import { import { checkForResolveTypeResolver } from './checkForResolveTypeResolver.js'; import { extendResolversFromInterfaces } from './extendResolversFromInterfaces.js'; -export function addResolversToSchema( - schemaOrOptions: GraphQLSchema | IAddResolversToSchemaOptions, - legacyInputResolvers?: IResolvers, - legacyInputValidationOptions?: IResolverValidationOptions -): GraphQLSchema { - const options: IAddResolversToSchemaOptions = isSchema(schemaOrOptions) - ? { - schema: schemaOrOptions, - resolvers: legacyInputResolvers ?? {}, - resolverValidationOptions: legacyInputValidationOptions, - } - : schemaOrOptions; - - let { - schema, - resolvers: inputResolvers, - defaultFieldResolver, - resolverValidationOptions = {}, - inheritResolversFromInterfaces = false, - updateResolversInPlace = false, - } = options; - +export function addResolversToSchema({ + schema, + resolvers: inputResolvers, + defaultFieldResolver, + resolverValidationOptions = {}, + inheritResolversFromInterfaces = false, + updateResolversInPlace = false, +}: IAddResolversToSchemaOptions): GraphQLSchema { const { requireResolversToMatchSchema = 'error', requireResolversForResolveType } = resolverValidationOptions; const resolvers = inheritResolversFromInterfaces diff --git a/packages/schema/src/makeExecutableSchema.ts b/packages/schema/src/makeExecutableSchema.ts index 96b84c07b14..8e4da97f3f1 100644 --- a/packages/schema/src/makeExecutableSchema.ts +++ b/packages/schema/src/makeExecutableSchema.ts @@ -15,12 +15,11 @@ import { applyExtensions, mergeExtensions, mergeResolvers, mergeTypeDefs } from * of these. If a function is provided, it will be passed no arguments and * should return an array of strings or `DocumentNode`s. * - * Note: You can use `graphql-tag` to not only parse a string into a - * `DocumentNode` but also to provide additional syntax highlighting in your - * editor (with the appropriate editor plugin). + * Note: You can use GraphQL magic comment provide additional syntax + * highlighting in your editor (with the appropriate editor plugin). * * ```js - * const typeDefs = gql` + * const typeDefs = /* GraphQL *\/ ` * type Query { * posts: [Post] * author(id: Int!): Author @@ -55,11 +54,11 @@ export function makeExecutableSchema({ typeDefs, resolvers = {}, resolverValidationOptions = {}, - parseOptions = {}, inheritResolversFromInterfaces = false, pruningOptions, updateResolversInPlace = false, schemaExtensions, + ...otherOptions }: IExecutableSchemaDefinition) { // Validate and clean up arguments if (typeof resolverValidationOptions !== 'object') { @@ -74,15 +73,15 @@ export function makeExecutableSchema({ if (isSchema(typeDefs)) { schema = typeDefs; - } else if (parseOptions?.commentDescriptions) { + } else if (otherOptions?.commentDescriptions) { const mergedTypeDefs = mergeTypeDefs(typeDefs, { - ...parseOptions, + ...otherOptions, commentDescriptions: true, }); - schema = buildSchema(mergedTypeDefs, parseOptions); + schema = buildSchema(mergedTypeDefs, otherOptions); } else { - const mergedTypeDefs = mergeTypeDefs(typeDefs, parseOptions); - schema = buildASTSchema(mergedTypeDefs, parseOptions); + const mergedTypeDefs = mergeTypeDefs(typeDefs, otherOptions); + schema = buildASTSchema(mergedTypeDefs, otherOptions); } if (pruningOptions) { diff --git a/packages/schema/src/merge-schemas.ts b/packages/schema/src/merge-schemas.ts index 3a5f55e8eab..3ebdebaece0 100644 --- a/packages/schema/src/merge-schemas.ts +++ b/packages/schema/src/merge-schemas.ts @@ -7,32 +7,45 @@ import { IExecutableSchemaDefinition } from './types.js'; /** * Configuration object for schema merging */ -export type MergeSchemasConfig = Partial> & - IExecutableSchemaDefinition['parseOptions'] & { - /** - * The schemas to be merged - */ - schemas?: GraphQLSchema[]; - }; +export type MergeSchemasConfig = Partial> & { + /** + * The schemas to be merged + */ + schemas?: GraphQLSchema[]; +}; /** * Synchronously merges multiple schemas, typeDefinitions and/or resolvers into a single schema. * @param config Configuration object */ export function mergeSchemas(config: MergeSchemasConfig) { - const extractedTypeDefs: TypeSource = asArray(config.typeDefs || []); - const extractedResolvers: IResolvers[] = asArray(config.resolvers || []); - const extractedSchemaExtensions: SchemaExtensions[] = asArray(config.schemaExtensions || []); + const extractedTypeDefs: TypeSource[] = []; + const extractedResolvers: IResolvers[] = []; + const extractedSchemaExtensions: SchemaExtensions[] = []; - const schemas = config.schemas || []; - for (const schema of schemas) { - extractedTypeDefs.push(schema); - extractedResolvers.push(getResolversFromSchema(schema, true)); - extractedSchemaExtensions.push(extractExtensionsFromSchema(schema)); + if (config.schemas != null) { + for (const schema of config.schemas) { + extractedTypeDefs.push(schema); + extractedResolvers.push(getResolversFromSchema(schema)); + extractedSchemaExtensions.push(extractExtensionsFromSchema(schema)); + } + } + + if (config.typeDefs != null) { + extractedTypeDefs.push(config.typeDefs); + } + + if (config.resolvers != null) { + const additionalResolvers = asArray(config.resolvers); + extractedResolvers.push(...additionalResolvers); + } + + if (config.schemaExtensions != null) { + const additionalSchemaExtensions = asArray(config.schemaExtensions); + extractedSchemaExtensions.push(...additionalSchemaExtensions); } return makeExecutableSchema({ - parseOptions: config, ...config, typeDefs: extractedTypeDefs, resolvers: extractedResolvers, diff --git a/packages/schema/src/types.ts b/packages/schema/src/types.ts index a5a917ddbb1..c83f11a2244 100644 --- a/packages/schema/src/types.ts +++ b/packages/schema/src/types.ts @@ -11,7 +11,7 @@ import { BuildSchemaOptions } from 'graphql'; /** * Configuration object for creating an executable schema */ -export interface IExecutableSchemaDefinition { +export interface IExecutableSchemaDefinition extends BuildSchemaOptions, GraphQLParseOptions { /** * The type definitions used to create the schema */ @@ -24,11 +24,6 @@ export interface IExecutableSchemaDefinition { * Additional options for validating the provided resolvers */ resolverValidationOptions?: IResolverValidationOptions; - /** - * Additional options for parsing the type definitions if they are provided - * as a string - */ - parseOptions?: BuildSchemaOptions & GraphQLParseOptions; /** * GraphQL object types that implement interfaces will inherit any missing * resolvers from their interface types defined in the `resolvers` object diff --git a/packages/schema/tests/merge-schemas.spec.ts b/packages/schema/tests/merge-schemas.spec.ts index 416cd5dff55..82959360a88 100644 --- a/packages/schema/tests/merge-schemas.spec.ts +++ b/packages/schema/tests/merge-schemas.spec.ts @@ -40,6 +40,61 @@ describe('Merge Schemas', () => { expect(mergedSchema.extensions).toEqual({ schemaA: true, schemaB: true }); }); + it('should override resolver in schema with resolver passed into config', async () => { + const fooSchema = makeExecutableSchema({ + typeDefs: /* GraphQL */ ` + type Query { + foo: String + } + `, + resolvers: { + Query: { + foo: () => 'FOO', + }, + }, + }); + const barSchema = makeExecutableSchema({ + typeDefs: /* GraphQL */ ` + type Query { + bar: String + } + `, + resolvers: { + Query: { + bar: () => 'BAR', + }, + }, + }); + const { errors, data } = await graphql({ + schema: mergeSchemas({ + schemas: [fooSchema, barSchema], + typeDefs: /* GraphQL */ ` + type Query { + qux: String + } + `, + resolvers: { + Query: { + qux: () => 'QUX', + foo: () => 'FOO_BAR_QUX', + }, + }, + }), + source: ` + { + foo + bar + qux + } + `, + }); + expect(errors).toBeFalsy(); + assertSome(data); + expect(data['foo']).toBe('FOO_BAR_QUX'); + expect(data['bar']).toBe('BAR'); + expect(data['qux']).toBe('QUX'); + }); + it('should merge two valid executable schemas', async () => { const fooSchema = makeExecutableSchema({ typeDefs: /* GraphQL */ ` diff --git a/packages/schema/tests/schemaGenerator.test.ts b/packages/schema/tests/schemaGenerator.test.ts index 4d36a2fd5b3..375afd4007c 100644 --- a/packages/schema/tests/schemaGenerator.test.ts +++ b/packages/schema/tests/schemaGenerator.test.ts @@ -1961,9 +1961,7 @@ describe('can specify lexical parser options', () => { } `, resolvers: {}, - parseOptions: { - noLocation: true, - }, + noLocation: true, }); expect(schema.astNode!.loc).toBeUndefined(); @@ -1986,9 +1984,7 @@ describe('can specify lexical parser options', () => { makeExecutableSchema({ typeDefs, resolvers, - parseOptions: { - experimentalFragmentVariables: true, - }, + experimentalFragmentVariables: true, }); }).not.toThrowError(); }); @@ -2264,7 +2260,7 @@ describe('interface resolver inheritance', () => { }, }; const schema = makeExecutableSchema({ - parseOptions: { allowLegacySDLImplementsInterfaces: true }, + allowLegacySDLImplementsInterfaces: true, typeDefs: testSchemaWithInterfaceResolvers, resolvers, inheritResolversFromInterfaces: true, diff --git a/packages/stitch/src/stitchSchemas.ts b/packages/stitch/src/stitchSchemas.ts index 5574a6f6b12..465da7409c1 100644 --- a/packages/stitch/src/stitchSchemas.ts +++ b/packages/stitch/src/stitchSchemas.ts @@ -28,9 +28,9 @@ export function stitchSchemas = Record): GraphQLSchema { const transformedSubschemas: Array> = []; const subschemaMap: Map< @@ -63,8 +63,8 @@ export function stitchSchemas = Record> = ( declare module '@graphql-tools/utils' { interface IFieldResolverOptions { - fragment?: string; selectionSet?: string | ((node: FieldNode) => SelectionSetNode); } }