diff --git a/.changeset/angry-bobcats-double.md b/.changeset/angry-bobcats-double.md new file mode 100644 index 00000000000..3c92d135969 --- /dev/null +++ b/.changeset/angry-bobcats-double.md @@ -0,0 +1,7 @@ +--- +'@graphql-tools/load': patch +--- + +No longer call `mergeSchemas` if a single schema is loaded. +Previously all typeDefs and resolvers were extracted and the schema was rebuilt from scratch. +But this is not necessary if there is only one schema loaded with `loadSchema` diff --git a/.changeset/gold-timers-thank.md b/.changeset/gold-timers-thank.md new file mode 100644 index 00000000000..b247536bd8e --- /dev/null +++ b/.changeset/gold-timers-thank.md @@ -0,0 +1,9 @@ +--- +'@graphql-tools/load': minor +'@graphql-tools/schema': minor +'@graphql-tools/utils': minor +--- + +`mergeSchemas` was skipping `defaultFieldResolver` and `defaultMergedResolver` by default while extracting resolvers for each given schema to reduce the overhead. But this doesn't work properly if you mix wrapped schemas and local schemas. So new `includeDefaultMergedResolver` flag is introduced in `getResolversFromSchema` to put default "proxy" resolvers in the extracted resolver map for `mergeSchemas`. + +This fixes an issue with alias issue, so nested aliased fields weren't resolved properly because of the missing `defaultMergedResolver` in the final merged schema which should come from the wrapped schema. diff --git a/.changeset/sweet-clouds-grab.md b/.changeset/sweet-clouds-grab.md new file mode 100644 index 00000000000..3b199b9ff90 --- /dev/null +++ b/.changeset/sweet-clouds-grab.md @@ -0,0 +1,5 @@ +--- +'@graphql-tools/url-loader': minor +--- + +New 'batch' flag! Now you can configure your remote schema to batch parallel queries to the upstream. diff --git a/package.json b/package.json index 778cdeb7d1b..4c041bf77b2 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "packages/**/src/**/*.{ts,tsx}": [ "eslint --fix" ], - "**/*.{ts,tsx,graphql,yml}": [ + "**/*.{ts,tsx,graphql,yml,md,mdx}": [ "prettier --write" ] }, diff --git a/packages/load/src/schema.ts b/packages/load/src/schema.ts index cd89812a28b..ec968b4a93c 100644 --- a/packages/load/src/schema.ts +++ b/packages/load/src/schema.ts @@ -37,13 +37,14 @@ export async function loadSchema( }); const { schemas, typeDefs } = collectSchemasAndTypeDefs(sources); + schemas.push(...(options.schemas ?? [])); const mergeSchemasOptions: MergeSchemasConfig = { ...options, schemas: schemas.concat(options.schemas ?? []), typeDefs, }; - const schema = mergeSchemas(mergeSchemasOptions); + const schema = typeDefs?.length === 0 && schemas?.length === 1 ? schemas[0] : mergeSchemas(mergeSchemasOptions); if (options?.includeSources) { includeSources(schema, sources); diff --git a/packages/loaders/url/src/index.ts b/packages/loaders/url/src/index.ts index 5587bca1ff7..028d78af06b 100644 --- a/packages/loaders/url/src/index.ts +++ b/packages/loaders/url/src/index.ts @@ -133,6 +133,10 @@ export interface LoadFromUrlOptions extends BaseLoaderOptions, Partial { source.schema = wrapSchema({ schema: source.schema, executor, + batch: options?.batch, }); } diff --git a/packages/loaders/url/tests/url-loader.spec.ts b/packages/loaders/url/tests/url-loader.spec.ts index 028fc76e815..c4a6350f467 100644 --- a/packages/loaders/url/tests/url-loader.spec.ts +++ b/packages/loaders/url/tests/url-loader.spec.ts @@ -31,6 +31,7 @@ import { createServer } from '@graphql-yoga/node'; import { GraphQLLiveDirectiveSDL, useLiveQuery } from '@envelop/live-query'; import { InMemoryLiveQueryStore } from '@n1ru4l/in-memory-live-query-store'; import { LiveExecutionResult } from '@n1ru4l/graphql-live-query'; +import { loadSchema } from '@graphql-tools/load'; describe('Schema URL Loader', () => { const loader = new UrlLoader(); @@ -45,7 +46,27 @@ scalar Upload type CustomQuery { """Test field comment""" a(testVariable: String): String + + complexField(complexArg: ComplexInput): ComplexType +} + +input ComplexInput { + id: ID +} + +input ComplexChildInput { + id: ID +} + +type ComplexType { + id: ID + complexChildren(complexChildArg: ComplexChildInput): [ComplexChild] +} + +type ComplexChild { + id: ID } + type Mutation { uploadFile(file: Upload, dummyVar: TestInput, secondDummyVar: String): File } @@ -69,6 +90,14 @@ input TestInput { const testResolvers = { CustomQuery: { a: (_: never, { testVariable }: { testVariable: string }) => testVariable || 'a', + complexField: (_: never, { complexArg }: { complexArg: { id: string } }) => { + return complexArg; + }, + }, + ComplexType: { + complexChildren: (_: never, { complexChildArg }: { complexChildArg: { id: string } }) => { + return [{ id: complexChildArg.id }]; + }, }, Upload: GraphQLUpload, File: { @@ -818,6 +847,36 @@ input TestInput { }); }); + it('should handle aliases properly', async () => { + const yoga = createServer({ + schema: testSchema, + port: 9876, + logging: false, + }); + httpServer = await yoga.start(); + const schema = await loadSchema(`http://0.0.0.0:9876/graphql`, { + loaders: [loader], + }); + const document = parse(/* GraphQL */ ` + query { + b: a + foo: complexField(complexArg: { id: "FOO" }) { + id + bar: complexChildren(complexChildArg: { id: "BAR" }) { + id + } + } + } + `); + const result: any = await execute({ + schema, + document, + }); + expect(result?.data?.['b']).toBe('a'); + expect(result?.data?.['foo']?.id).toBe('FOO'); + expect(result?.data?.['foo']?.bar?.[0]?.id).toBe('BAR'); + }); + describe('sync', () => { it('should handle introspection', () => { const [{ schema }] = loader.loadSync(`https://swapi-graphql.netlify.app/.netlify/functions/index`, {}); diff --git a/packages/schema/src/merge-schemas.ts b/packages/schema/src/merge-schemas.ts index 8b4ea994b1e..9742b26c469 100644 --- a/packages/schema/src/merge-schemas.ts +++ b/packages/schema/src/merge-schemas.ts @@ -27,7 +27,7 @@ export function mergeSchemas(config: MergeSchemasConfig) { const schemas = config.schemas || []; for (const schema of schemas) { extractedTypeDefs.push(schema); - extractedResolvers.push(getResolversFromSchema(schema)); + extractedResolvers.push(getResolversFromSchema(schema, true)); extractedSchemaExtensions.push(extractExtensionsFromSchema(schema)); } diff --git a/packages/utils/src/getResolversFromSchema.ts b/packages/utils/src/getResolversFromSchema.ts index c7b83b90a02..e6d4ce645a2 100644 --- a/packages/utils/src/getResolversFromSchema.ts +++ b/packages/utils/src/getResolversFromSchema.ts @@ -11,7 +11,11 @@ import { import { IResolvers } from './Interfaces'; -export function getResolversFromSchema(schema: GraphQLSchema): IResolvers { +export function getResolversFromSchema( + schema: GraphQLSchema, + // Include default merged resolvers + includeDefaultMergedResolver?: boolean +): IResolvers { const resolvers = Object.create(null); const typeMap = schema.getTypeMap(); @@ -59,11 +63,16 @@ export function getResolversFromSchema(schema: GraphQLSchema): IResolvers { resolvers[typeName][fieldName] = resolvers[typeName][fieldName] || {}; resolvers[typeName][fieldName].subscribe = field.subscribe; } - if ( - field.resolve != null && - field.resolve?.name !== 'defaultFieldResolver' && - field.resolve?.name !== 'defaultMergedResolver' - ) { + if (field.resolve != null && field.resolve?.name !== 'defaultFieldResolver') { + switch (field.resolve?.name) { + case 'defaultMergedResolver': + if (!includeDefaultMergedResolver) { + continue; + } + break; + case 'defaultFieldResolver': + continue; + } resolvers[typeName][fieldName] = resolvers[typeName][fieldName] || {}; resolvers[typeName][fieldName].resolve = field.resolve; } diff --git a/packages/wrap/tests/makeRemoteExecutableSchema.test.ts b/packages/wrap/tests/makeRemoteExecutableSchema.test.ts index 25a4ccf4e66..46f2fec2fee 100644 --- a/packages/wrap/tests/makeRemoteExecutableSchema.test.ts +++ b/packages/wrap/tests/makeRemoteExecutableSchema.test.ts @@ -17,7 +17,7 @@ describe('remote queries', () => { schema = wrapSchema(remoteSubschemaConfig); }); - test('should work', async () => { + test('should handle interfaces correctly', async () => { const query = /* GraphQL */ ` { interfaceTest(kind: ONE) { @@ -46,6 +46,28 @@ describe('remote queries', () => { const result = await graphql({ schema, source: query }); expect(result).toEqual(expected); }); + + test('should handle aliases properly', async () => { + const query = /* GraphQL */ ` + query AliasedExample { + propertyInAnArray: properties(limit: 1) { + id + name + loc: location { + title: name + } + } + } + `; + + const result: any = await graphql({ schema, source: query }); + + expect(result?.data?.['propertyInAnArray']).toHaveLength(1); + expect(result?.data?.['propertyInAnArray'][0]['id']).toBeTruthy(); + expect(result?.data?.['propertyInAnArray'][0]['name']).toBeTruthy(); + expect(result?.data?.['propertyInAnArray'][0]['loc']).toBeTruthy(); + expect(result?.data?.['propertyInAnArray'][0]['loc']['title']).toBeTruthy(); + }); }); describe('remote subscriptions', () => {