diff --git a/.changeset/short-ladybugs-develop.md b/.changeset/short-ladybugs-develop.md new file mode 100644 index 0000000000..24c616da95 --- /dev/null +++ b/.changeset/short-ladybugs-develop.md @@ -0,0 +1,9 @@ +--- +'@graphql-tools/merge': major +'@graphql-tools/schema': minor +--- + +Breaking Change; + +Move `mergeSchemas` from `@graphql-tools/merge` to `@graphql-tools/schema` package to prevent circular dependency between them. + diff --git a/packages/load/package.json b/packages/load/package.json index 9c6f2d76f3..c23d9bc37a 100644 --- a/packages/load/package.json +++ b/packages/load/package.json @@ -35,8 +35,8 @@ }, "dependencies": { "@graphql-tools/utils": "8.0.2", - "@graphql-tools/merge": "^7.0.0", - "import-from": "4.0.0", + "@graphql-tools/merge": "7.0.0", + "@graphql-tools/schema": "8.0.3", "p-limit": "3.1.0", "tslib": "~2.3.0" }, diff --git a/packages/load/src/load-typedefs.ts b/packages/load/src/load-typedefs.ts index b3c613e2a7..5ad64dae14 100644 --- a/packages/load/src/load-typedefs.ts +++ b/packages/load/src/load-typedefs.ts @@ -76,8 +76,7 @@ export function loadTypedefsSync>( ): Source[] { const { ignore, pointerOptionMap } = normalizePointers(pointerOrPointers); - options.ignore = asArray(options.ignore || []); - options.ignore.push(...ignore); + options.ignore = asArray(options.ignore || []).concat(ignore); applyDefaultOptions(options); diff --git a/packages/load/src/schema.ts b/packages/load/src/schema.ts index d6f4c0c9ea..3e7ab14845 100644 --- a/packages/load/src/schema.ts +++ b/packages/load/src/schema.ts @@ -1,7 +1,7 @@ import { loadTypedefs, LoadTypedefsOptions, UnnormalizedTypeDefPointer, loadTypedefsSync } from './load-typedefs'; import { GraphQLSchema, BuildSchemaOptions, DocumentNode, Source as GraphQLSource, print } from 'graphql'; import { OPERATION_KINDS } from './documents'; -import { mergeSchemasAsync, mergeSchemas, MergeSchemasConfig } from '@graphql-tools/merge'; +import { mergeSchemasAsync, mergeSchemas, MergeSchemasConfig } from '@graphql-tools/schema'; import { Source } from '@graphql-tools/utils'; export type LoadSchemaOptions = BuildSchemaOptions & @@ -25,15 +25,15 @@ export async function loadSchema( options: LoadSchemaOptions ): Promise { const sources = await loadTypedefs(schemaPointers, { - filterKinds: OPERATION_KINDS, ...options, + filterKinds: OPERATION_KINDS, }); const { schemas, typeDefs } = collectSchemasAndTypeDefs(sources); const mergeSchemasOptions: MergeSchemasConfig = { + ...options, schemas, typeDefs, - ...options, }; const schema = await mergeSchemasAsync(mergeSchemasOptions); @@ -60,13 +60,12 @@ export function loadSchemaSync( }); const { schemas, typeDefs } = collectSchemasAndTypeDefs(sources); - const mergeSchemasOptions: MergeSchemasConfig = { + + const schema = mergeSchemas({ schemas, typeDefs, ...options, - }; - - const schema = mergeSchemas(mergeSchemasOptions); + }); if (options?.includeSources) { includeSources(schema, sources); diff --git a/packages/load/src/utils/custom-loader.ts b/packages/load/src/utils/custom-loader.ts index 156964e6de..72a8d4ea38 100644 --- a/packages/load/src/utils/custom-loader.ts +++ b/packages/load/src/utils/custom-loader.ts @@ -1,8 +1,9 @@ -import importFrom from 'import-from'; +import { createRequire } from 'module'; export function getCustomLoaderByPath(path: string, cwd: string) { try { - const requiredModule: any = importFrom(cwd, path); + const requireFn = createRequire(cwd); + const requiredModule = requireFn(path); if (requiredModule) { if (requiredModule.default && typeof requiredModule.default === 'function') { diff --git a/packages/load/tests/loaders/schema/schema-from-typedefs.spec.ts b/packages/load/tests/loaders/schema/schema-from-typedefs.spec.ts index b5f71ffd6d..9dd30121ce 100644 --- a/packages/load/tests/loaders/schema/schema-from-typedefs.spec.ts +++ b/packages/load/tests/loaders/schema/schema-from-typedefs.spec.ts @@ -3,6 +3,7 @@ import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader'; import { CodeFileLoader } from '@graphql-tools/code-file-loader'; import { runTests, useMonorepo } from '../../../../testing/utils'; import path from 'path'; +import { inspect } from 'util'; const monorepo = useMonorepo({ dirname: __dirname @@ -10,7 +11,7 @@ const monorepo = useMonorepo({ function assertNonMaybe(input: T): asserts input is Exclude{ if (input == null) { - throw new Error("Value should be neither null nor undefined.") + throw new Error(`Value should be neither null nor undefined. But received: ${inspect(input)}`) } } diff --git a/packages/loaders/code-file/package.json b/packages/loaders/code-file/package.json index c7929c8204..61b81d802a 100644 --- a/packages/loaders/code-file/package.json +++ b/packages/loaders/code-file/package.json @@ -26,9 +26,6 @@ "typescript": { "definition": "dist/index.d.ts" }, - "devDependencies": { - "@types/is-glob": "4.0.2" - }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0" }, @@ -36,7 +33,6 @@ "@graphql-tools/utils": "8.0.2", "@graphql-tools/graphql-tag-pluck": "^7.0.2", "globby": "^11.0.3", - "is-glob": "^4.0.1", "tslib": "~2.3.0", "unixify": "^1.0.0" }, diff --git a/packages/loaders/git/package.json b/packages/loaders/git/package.json index 7db6d7167b..b76d4d3536 100644 --- a/packages/loaders/git/package.json +++ b/packages/loaders/git/package.json @@ -38,6 +38,7 @@ "unixify": "^1.0.0" }, "devDependencies": { + "@types/is-glob": "4.0.2", "@types/micromatch": "4.0.1" }, "publishConfig": { diff --git a/packages/loaders/graphql-file/package.json b/packages/loaders/graphql-file/package.json index 5ddf0f90ae..633f531ee5 100644 --- a/packages/loaders/graphql-file/package.json +++ b/packages/loaders/graphql-file/package.json @@ -32,14 +32,10 @@ "buildOptions": { "input": "./src/index.ts" }, - "devDependencies": { - "@types/is-glob": "4.0.2" - }, "dependencies": { "@graphql-tools/import": "^6.2.6", "@graphql-tools/utils": "8.0.2", "globby": "^11.0.3", - "is-glob": "^4.0.1", "unixify": "^1.0.0", "tslib": "~2.3.0" }, diff --git a/packages/loaders/json-file/package.json b/packages/loaders/json-file/package.json index 5be8a6d5e0..5fc2ded501 100644 --- a/packages/loaders/json-file/package.json +++ b/packages/loaders/json-file/package.json @@ -31,6 +31,8 @@ }, "dependencies": { "@graphql-tools/utils": "8.0.2", + "globby": "^11.0.3", + "unixify": "^1.0.0", "tslib": "~2.3.0" }, "publishConfig": { diff --git a/packages/merge/package.json b/packages/merge/package.json index 35f9ea3e6b..fe10f08e5c 100644 --- a/packages/merge/package.json +++ b/packages/merge/package.json @@ -33,7 +33,6 @@ "input": "./src/index.ts" }, "dependencies": { - "@graphql-tools/schema": "^8.0.3", "@graphql-tools/utils": "8.0.2", "tslib": "~2.3.0" }, diff --git a/packages/merge/src/index.ts b/packages/merge/src/index.ts index 4e20e8ee4f..49d7c2dfa6 100644 --- a/packages/merge/src/index.ts +++ b/packages/merge/src/index.ts @@ -1,4 +1,3 @@ export * from './merge-resolvers'; export * from './typedefs-mergers'; -export * from './merge-schemas'; export * from './extensions'; diff --git a/packages/merge/src/merge-schemas.ts b/packages/merge/src/merge-schemas.ts deleted file mode 100644 index b20181e3db..0000000000 --- a/packages/merge/src/merge-schemas.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { GraphQLSchema, DocumentNode, buildASTSchema, BuildSchemaOptions, buildSchema } from 'graphql'; -import { addResolversToSchema } from '@graphql-tools/schema'; -import { mergeTypeDefs, Config } from './typedefs-mergers/merge-typedefs'; -import { mergeResolvers } from './merge-resolvers'; -import { - IResolvers, - IResolverValidationOptions, - asArray, - getResolversFromSchema, - TypeSource, -} from '@graphql-tools/utils'; -import { mergeExtensions, extractExtensionsFromSchema, applyExtensions, SchemaExtensions } from './extensions'; - -/** - * Configuration object for schema merging - */ -export interface MergeSchemasConfig extends Config, BuildSchemaOptions { - /** - * The schemas to be merged - */ - schemas: GraphQLSchema[]; - /** - * Additional type definitions to also merge - */ - typeDefs?: TypeSource; - /** - * Additional resolvers to also merge - */ - resolvers?: Resolvers | Resolvers[]; - /** - * Options to validate the resolvers being merged, if provided - */ - resolverValidationOptions?: IResolverValidationOptions; -} - -const defaultResolverValidationOptions: Partial = { - requireResolversForArgs: 'ignore', - requireResolversForNonScalar: 'ignore', - requireResolversForAllFields: 'ignore', - requireResolversForResolveType: 'ignore', - requireResolversToMatchSchema: 'ignore', -}; - -/** - * Synchronously merges multiple schemas, typeDefinitions and/or resolvers into a single schema. - * @param config Configuration object - */ -export function mergeSchemas(config: MergeSchemasConfig) { - const typeDefs = mergeTypeDefs([config.schemas, config.typeDefs || []], config); - const extractedResolvers: IResolvers[] = []; - const extractedExtensions: SchemaExtensions[] = []; - for (const schema of config.schemas) { - extractedResolvers.push(getResolversFromSchema(schema)); - extractedExtensions.push(extractExtensionsFromSchema(schema)); - } - extractedResolvers.push(...ensureResolvers(config)); - - const resolvers = mergeResolvers(extractedResolvers, config); - const extensions = mergeExtensions(extractedExtensions); - - return makeSchema({ resolvers, typeDefs, extensions }, config); -} - -/** - * Asynchronously merges multiple schemas, typeDefinitions and/or resolvers into a single schema. - * @param config Configuration object - */ -export async function mergeSchemasAsync(config: MergeSchemasConfig) { - const [typeDefs, resolvers, extensions] = await Promise.all([ - mergeTypeDefs([config.schemas, config.typeDefs || []], config), - Promise.all(config.schemas.map(async schema => getResolversFromSchema(schema))).then(extractedResolvers => - mergeResolvers([...extractedResolvers, ...ensureResolvers(config)], config) - ), - Promise.all(config.schemas.map(async schema => extractExtensionsFromSchema(schema))).then(extractedExtensions => - mergeExtensions(extractedExtensions) - ), - ]); - - return makeSchema({ resolvers, typeDefs, extensions }, config); -} - -function ensureResolvers(config: MergeSchemasConfig) { - return config.resolvers ? asArray(config.resolvers) : []; -} - -function makeSchema( - { - resolvers, - typeDefs, - extensions, - }: { resolvers: IResolvers; typeDefs: string | DocumentNode; extensions: SchemaExtensions }, - config: MergeSchemasConfig -) { - let schema = typeof typeDefs === 'string' ? buildSchema(typeDefs, config) : buildASTSchema(typeDefs, config); - - // add resolvers - if (resolvers) { - schema = addResolversToSchema({ - schema, - resolvers, - resolverValidationOptions: { - ...defaultResolverValidationOptions, - ...(config.resolverValidationOptions || {}), - }, - }); - } - - // extensions - applyExtensions(schema, extensions); - - return schema; -} diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts index 4d090541b2..68212c5ac9 100644 --- a/packages/schema/src/index.ts +++ b/packages/schema/src/index.ts @@ -5,3 +5,4 @@ export { checkForResolveTypeResolver } from './checkForResolveTypeResolver'; export { extendResolversFromInterfaces } from './extendResolversFromInterfaces'; export * from './makeExecutableSchema'; export * from './types'; +export * from './merge-schemas'; diff --git a/packages/schema/src/makeExecutableSchema.ts b/packages/schema/src/makeExecutableSchema.ts index 0afb7b4a91..2d38f06a7a 100644 --- a/packages/schema/src/makeExecutableSchema.ts +++ b/packages/schema/src/makeExecutableSchema.ts @@ -1,11 +1,11 @@ import { buildASTSchema, buildSchema, GraphQLSchema } from 'graphql'; -import { pruneSchema } from '@graphql-tools/utils'; +import { asArray, pruneSchema } from '@graphql-tools/utils'; import { addResolversToSchema } from './addResolversToSchema'; import { assertResolversPresent } from './assertResolversPresent'; import { IExecutableSchemaDefinition } from './types'; -import { mergeResolvers, mergeTypeDefs } from '@graphql-tools/merge'; +import { applyExtensions, mergeExtensions, mergeResolvers, mergeTypeDefs } from '@graphql-tools/merge'; /** * Builds a schema from the provided type definitions and resolvers. @@ -59,6 +59,7 @@ export function makeExecutableSchema({ inheritResolversFromInterfaces = false, pruningOptions, updateResolversInPlace = false, + extensions, }: IExecutableSchemaDefinition) { // Validate and clean up arguments if (typeof resolverValidationOptions !== 'object') { @@ -100,5 +101,10 @@ export function makeExecutableSchema({ assertResolversPresent(schema, resolverValidationOptions); } + if (extensions) { + extensions = mergeExtensions(asArray(extensions)); + applyExtensions(schema, extensions); + } + return schema; } diff --git a/packages/schema/src/merge-schemas.ts b/packages/schema/src/merge-schemas.ts new file mode 100644 index 0000000000..503dec97d7 --- /dev/null +++ b/packages/schema/src/merge-schemas.ts @@ -0,0 +1,58 @@ +import { GraphQLSchema } from 'graphql'; +import { extractExtensionsFromSchema, SchemaExtensions } from '@graphql-tools/merge'; +import { IResolvers, asArray, getResolversFromSchema } from '@graphql-tools/utils'; +import { makeExecutableSchema } from './makeExecutableSchema'; +import { IExecutableSchemaDefinition } from './types'; + +/** + * Configuration object for schema merging + */ +export type MergeSchemasConfig = Partial> & + IExecutableSchemaDefinition['parseOptions'] & { + /** + * 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 extractedResolvers: IResolvers[] = []; + const extractedExtensions: SchemaExtensions[] = asArray(config.extensions || []); + + const schemas = config.schemas || []; + for (const schema of config.schemas || []) { + extractedResolvers.push(getResolversFromSchema(schema)); + extractedExtensions.push(extractExtensionsFromSchema(schema)); + } + + return makeExecutableSchema({ + parseOptions: config, + ...config, + typeDefs: asArray(config.typeDefs || []).concat(schemas), + resolvers: asArray(config.resolvers || []).concat(extractedResolvers), + extensions: asArray(config.extensions || []).concat(extractedExtensions), + }); +} + +/** + * Asynchronously merges multiple schemas, typeDefinitions and/or resolvers into a single schema. + * @param config Configuration object + */ +export async function mergeSchemasAsync(config: MergeSchemasConfig) { + const schemas = config.schemas || []; + const [extractedResolvers, extractedExtensions] = await Promise.all([ + Promise.all(schemas.map(async schema => getResolversFromSchema(schema))), + Promise.all(schemas.map(async schema => extractExtensionsFromSchema(schema))), + ]); + + return makeExecutableSchema({ + ...config, + typeDefs: asArray(config.typeDefs || []).concat(config.schemas || []), + resolvers: asArray(config.resolvers || []).concat(extractedResolvers), + extensions: asArray(config.extensions || []).concat(extractedExtensions), + }); +} diff --git a/packages/schema/src/types.ts b/packages/schema/src/types.ts index 47b9f882cb..12277d05f4 100644 --- a/packages/schema/src/types.ts +++ b/packages/schema/src/types.ts @@ -5,6 +5,8 @@ import { GraphQLParseOptions, PruneSchemaOptions, } from '@graphql-tools/utils'; +import { SchemaExtensions } from '@graphql-tools/merge'; +import { BuildSchemaOptions } from 'graphql'; /** * Configuration object for creating an executable schema @@ -26,7 +28,7 @@ export interface IExecutableSchemaDefinition { * Additional options for parsing the type definitions if they are provided * as a string */ - parseOptions?: GraphQLParseOptions; + parseOptions?: BuildSchemaOptions & GraphQLParseOptions; /** * GraphQL object types that implement interfaces will inherit any missing * resolvers from their interface types defined in the `resolvers` object @@ -40,4 +42,8 @@ export interface IExecutableSchemaDefinition { * Do not create a schema again and use the one from `buildASTSchema` */ updateResolversInPlace?: boolean; + /** + * Schema extensions + */ + extensions?: SchemaExtensions | Array; } diff --git a/packages/merge/tests/merge-schemas.spec.ts b/packages/schema/tests/merge-schemas.spec.ts similarity index 100% rename from packages/merge/tests/merge-schemas.spec.ts rename to packages/schema/tests/merge-schemas.spec.ts diff --git a/website/docs/schema-merging.mdx b/website/docs/schema-merging.mdx index 276afd3337..6c0d6a0cb2 100644 --- a/website/docs/schema-merging.mdx +++ b/website/docs/schema-merging.mdx @@ -4,14 +4,14 @@ title: Schema merging sidebar_label: Schema merging --- -Schema merging (`@graphql-tools/merge`) consolidates the type definitions and resolvers from many local schema instances into a single executable schema. This is useful for building a single local service schema from many individually-managed parts. This should not be confused with [schema stitching](/docs/stitch-combining-schemas), which builds a combined proxy schema atop numerous subservice APIs. +Schema merging (`@graphql-tools/merge` and `@graphql-tools/schema`) consolidates the type definitions and resolvers from many local schema instances into a single executable schema. This is useful for building a single local service schema from many individually-managed parts. This should not be confused with [schema stitching](/docs/stitch-combining-schemas), which builds a combined proxy schema atop numerous subservice APIs. ## Getting started You can use `mergeSchemas` to merge `GraphQLSchema` objects together with extra `typeDefs` and `resolvers`. ```ts -const { mergeSchemas } = require('@graphql-tools/merge'); +const { mergeSchemas } = require('@graphql-tools/schema'); const mergedSchema = mergeSchemas({ schemas: [ diff --git a/yarn.lock b/yarn.lock index bb69b54a90..77b784a910 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6969,11 +6969,6 @@ import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: parent-module "^1.0.0" resolve-from "^4.0.0" -import-from@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/import-from/-/import-from-4.0.0.tgz#2710b8d66817d232e16f4166e319248d3d5492e2" - integrity sha512-P9J71vT5nLlDeV8FHs5nNxaLbrpfAV5cF5srvbZfpwpcJoM/xZR3hiv+q+SAnuSmuGbXMWud063iIMx/V/EWZQ== - import-from@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/import-from/-/import-from-3.0.0.tgz#055cfec38cd5a27d8057ca51376d7d3bf0891966"