diff --git a/package-lock.json b/package-lock.json index 01b9606..c717450 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4097,7 +4097,7 @@ "@graphql-mesh/openapi": "^0.97.4", "@graphql-mesh/transform-filter-schema": "^0.96.2", "@graphql-mesh/transform-naming-convention": "^0.96.2", - "@graphql-mesh/transform-prefix": "^0.96.2", + "@graphql-mesh/transform-rename": "^0.96.2", "@graphql-mesh/transform-type-merging": "^0.96.2", "@graphql-tools/schema": "^10.0.2", "@graphql-tools/utils": "^10.0.12", @@ -5269,7 +5269,9 @@ } }, "packages/graphql-mesh/node_modules/@graphql-mesh/store": { - "version": "0.96.3", + "version": "0.96.6", + "resolved": "https://registry.npmjs.org/@graphql-mesh/store/-/store-0.96.6.tgz", + "integrity": "sha512-NjLCmZgz78UBeIfjs7lMIcyGGKuoc3off8cop/q85pQLsQp7RVv6AxWAA1xMVhE7OxP3zaM2LT/xTDl3CJW3SQ==", "license": "MIT", "dependencies": { "@graphql-inspector/core": "5.0.2" @@ -5279,8 +5281,8 @@ }, "peerDependencies": { "@graphql-mesh/cross-helpers": "^0.4.1", - "@graphql-mesh/types": "^0.96.3", - "@graphql-mesh/utils": "^0.96.3", + "@graphql-mesh/types": "^0.96.6", + "@graphql-mesh/utils": "^0.96.6", "@graphql-tools/utils": "^9.2.1 || ^10.0.0", "graphql": "*", "tslib": "^2.4.0" @@ -5327,8 +5329,10 @@ "tslib": "^2.5.0" } }, - "packages/graphql-mesh/node_modules/@graphql-mesh/transform-prefix": { - "version": "0.96.3", + "packages/graphql-mesh/node_modules/@graphql-mesh/transform-rename": { + "version": "0.96.7", + "resolved": "https://registry.npmjs.org/@graphql-mesh/transform-rename/-/transform-rename-0.96.7.tgz", + "integrity": "sha512-v1oUlx6HXIqwhPnGGLQ/MgoHYF1yQVUCVB154nNghYlQtOCkMhktK+45QXAdVho398icyRyVAY+N1sl6oFynPA==", "license": "MIT", "dependencies": { "@graphql-tools/delegate": "^10.0.0", @@ -5339,8 +5343,8 @@ "node": ">=16.0.0" }, "peerDependencies": { - "@graphql-mesh/types": "^0.96.3", - "@graphql-mesh/utils": "^0.96.3", + "@graphql-mesh/types": "^0.96.6", + "@graphql-mesh/utils": "^0.96.6", "@graphql-tools/utils": "^9.2.1 || ^10.0.0", "graphql": "*", "tslib": "^2.4.0" @@ -5364,7 +5368,9 @@ } }, "packages/graphql-mesh/node_modules/@graphql-mesh/types": { - "version": "0.96.3", + "version": "0.96.6", + "resolved": "https://registry.npmjs.org/@graphql-mesh/types/-/types-0.96.6.tgz", + "integrity": "sha512-l3KF+hLAXpoaX3K+oT7ZQzI7VyBoSOPo6cfhPMkZ09fbc9SiWyqGTPnqJpIUcpZS5VfV/WkSiXjxZC/qAQd5Nw==", "license": "MIT", "dependencies": { "@graphql-tools/batch-delegate": "^9.0.0", @@ -5375,14 +5381,16 @@ "node": ">=16.0.0" }, "peerDependencies": { - "@graphql-mesh/store": "^0.96.3", + "@graphql-mesh/store": "^0.96.6", "@graphql-tools/utils": "^9.2.1 || ^10.0.0", "graphql": "*", "tslib": "^2.4.0" } }, "packages/graphql-mesh/node_modules/@graphql-mesh/utils": { - "version": "0.96.3", + "version": "0.96.6", + "resolved": "https://registry.npmjs.org/@graphql-mesh/utils/-/utils-0.96.6.tgz", + "integrity": "sha512-zLb7aEIGCZV8duKOHQ5g+L+r7KVS6E6zNv+LeGNA7TeUqkh/tMObQUyn2PjkYglWbRq/rIYDZwy9LhXw1CB+6g==", "license": "MIT", "dependencies": { "@graphql-mesh/string-interpolation": "^0.5.3", @@ -5399,7 +5407,7 @@ }, "peerDependencies": { "@graphql-mesh/cross-helpers": "^0.4.1", - "@graphql-mesh/types": "^0.96.3", + "@graphql-mesh/types": "^0.96.6", "@graphql-tools/utils": "^9.2.1 || ^10.0.0", "graphql": "*", "tslib": "^2.4.0" diff --git a/packages/graphql-mesh/directives/prefixSchema.ts b/packages/graphql-mesh/directives/prefixSchema.ts index 7e75bce..7b3654c 100644 --- a/packages/graphql-mesh/directives/prefixSchema.ts +++ b/packages/graphql-mesh/directives/prefixSchema.ts @@ -2,7 +2,7 @@ import { GraphQLSchema } from 'graphql' import { mapSchema } from '@graphql-tools/utils' import { MapperKind } from '@graphql-tools/utils' import { getDirective } from '@graphql-tools/utils' -import { GraphQLObjectType } from 'graphql' +import { GraphQLObjectType, GraphQLInterfaceType } from 'graphql' import { MeshTransform } from '@graphql-mesh/types' export default class PrefixSchemaDirectiveTransform implements MeshTransform { @@ -15,6 +15,14 @@ export default class PrefixSchemaDirectiveTransform implements MeshTransform { const config = type.toConfig() return new GraphQLObjectType(config) } + }, + [MapperKind.INTERFACE_TYPE]: type => { + const prefixDirective = getDirective(schema, type, 'prefixSchema')?.[0] + if (prefixDirective) { + type.name = prefixDirective.prefix + type.name + const config = type.toConfig() + return new GraphQLInterfaceType(config) + } } }) } diff --git a/packages/graphql-mesh/package-lock.json b/packages/graphql-mesh/package-lock.json index 94635d8..663e1a7 100644 --- a/packages/graphql-mesh/package-lock.json +++ b/packages/graphql-mesh/package-lock.json @@ -13,7 +13,7 @@ "@graphql-mesh/openapi": "^0.97.4", "@graphql-mesh/transform-filter-schema": "^0.96.2", "@graphql-mesh/transform-naming-convention": "^0.96.2", - "@graphql-mesh/transform-prefix": "^0.96.2", + "@graphql-mesh/transform-rename": "^0.96.2", "@graphql-mesh/transform-type-merging": "^0.96.2", "@graphql-tools/schema": "^10.0.2", "@graphql-tools/utils": "^10.0.12", @@ -1608,7 +1608,9 @@ } }, "node_modules/@graphql-mesh/store": { - "version": "0.96.3", + "version": "0.96.6", + "resolved": "https://registry.npmjs.org/@graphql-mesh/store/-/store-0.96.6.tgz", + "integrity": "sha512-NjLCmZgz78UBeIfjs7lMIcyGGKuoc3off8cop/q85pQLsQp7RVv6AxWAA1xMVhE7OxP3zaM2LT/xTDl3CJW3SQ==", "license": "MIT", "dependencies": { "@graphql-inspector/core": "5.0.2" @@ -1618,8 +1620,8 @@ }, "peerDependencies": { "@graphql-mesh/cross-helpers": "^0.4.1", - "@graphql-mesh/types": "^0.96.3", - "@graphql-mesh/utils": "^0.96.3", + "@graphql-mesh/types": "^0.96.6", + "@graphql-mesh/utils": "^0.96.6", "@graphql-tools/utils": "^9.2.1 || ^10.0.0", "graphql": "*", "tslib": "^2.4.0" @@ -1683,8 +1685,10 @@ "tslib": "^2.5.0" } }, - "node_modules/@graphql-mesh/transform-prefix": { - "version": "0.96.3", + "node_modules/@graphql-mesh/transform-rename": { + "version": "0.96.7", + "resolved": "https://registry.npmjs.org/@graphql-mesh/transform-rename/-/transform-rename-0.96.7.tgz", + "integrity": "sha512-v1oUlx6HXIqwhPnGGLQ/MgoHYF1yQVUCVB154nNghYlQtOCkMhktK+45QXAdVho398icyRyVAY+N1sl6oFynPA==", "license": "MIT", "dependencies": { "@graphql-tools/delegate": "^10.0.0", @@ -1695,8 +1699,8 @@ "node": ">=16.0.0" }, "peerDependencies": { - "@graphql-mesh/types": "^0.96.3", - "@graphql-mesh/utils": "^0.96.3", + "@graphql-mesh/types": "^0.96.6", + "@graphql-mesh/utils": "^0.96.6", "@graphql-tools/utils": "^9.2.1 || ^10.0.0", "graphql": "*", "tslib": "^2.4.0" @@ -1720,7 +1724,9 @@ } }, "node_modules/@graphql-mesh/types": { - "version": "0.96.3", + "version": "0.96.6", + "resolved": "https://registry.npmjs.org/@graphql-mesh/types/-/types-0.96.6.tgz", + "integrity": "sha512-l3KF+hLAXpoaX3K+oT7ZQzI7VyBoSOPo6cfhPMkZ09fbc9SiWyqGTPnqJpIUcpZS5VfV/WkSiXjxZC/qAQd5Nw==", "license": "MIT", "dependencies": { "@graphql-tools/batch-delegate": "^9.0.0", @@ -1731,14 +1737,16 @@ "node": ">=16.0.0" }, "peerDependencies": { - "@graphql-mesh/store": "^0.96.3", + "@graphql-mesh/store": "^0.96.6", "@graphql-tools/utils": "^9.2.1 || ^10.0.0", "graphql": "*", "tslib": "^2.4.0" } }, "node_modules/@graphql-mesh/utils": { - "version": "0.96.3", + "version": "0.96.6", + "resolved": "https://registry.npmjs.org/@graphql-mesh/utils/-/utils-0.96.6.tgz", + "integrity": "sha512-zLb7aEIGCZV8duKOHQ5g+L+r7KVS6E6zNv+LeGNA7TeUqkh/tMObQUyn2PjkYglWbRq/rIYDZwy9LhXw1CB+6g==", "license": "MIT", "dependencies": { "@graphql-mesh/string-interpolation": "^0.5.3", @@ -1755,7 +1763,7 @@ }, "peerDependencies": { "@graphql-mesh/cross-helpers": "^0.4.1", - "@graphql-mesh/types": "^0.96.3", + "@graphql-mesh/types": "^0.96.6", "@graphql-tools/utils": "^9.2.1 || ^10.0.0", "graphql": "*", "tslib": "^2.4.0" diff --git a/packages/graphql-mesh/package.json b/packages/graphql-mesh/package.json index 1186107..56a6125 100644 --- a/packages/graphql-mesh/package.json +++ b/packages/graphql-mesh/package.json @@ -15,7 +15,7 @@ "@graphql-mesh/openapi": "^0.97.4", "@graphql-mesh/transform-filter-schema": "^0.96.2", "@graphql-mesh/transform-naming-convention": "^0.96.2", - "@graphql-mesh/transform-prefix": "^0.96.2", + "@graphql-mesh/transform-rename": "^0.96.2", "@graphql-mesh/transform-type-merging": "^0.96.2", "@graphql-tools/schema": "^10.0.2", "@graphql-tools/utils": "^10.0.12", diff --git a/packages/graphql-mesh/patches/@graphql-mesh+transform-rename+0.96.7.patch b/packages/graphql-mesh/patches/@graphql-mesh+transform-rename+0.96.7.patch new file mode 100644 index 0000000..e8091ea --- /dev/null +++ b/packages/graphql-mesh/patches/@graphql-mesh+transform-rename+0.96.7.patch @@ -0,0 +1,14 @@ +diff --git a/node_modules/@graphql-mesh/transform-rename/cjs/bareRename.js b/node_modules/@graphql-mesh/transform-rename/cjs/bareRename.js +index 7e430cf..76a818e 100644 +--- a/node_modules/@graphql-mesh/transform-rename/cjs/bareRename.js ++++ b/node_modules/@graphql-mesh/transform-rename/cjs/bareRename.js +@@ -84,6 +84,9 @@ class BareRename { + if (!mapKeys?.length) + return null; + return mapKeys.reduce((newName, mapKey) => { ++ if (!newName) { ++ return null ++ } + if (mapKeyIsString) { + const str = map.get(mapKey); + // avoid re-iterating over strings that have already been renamed diff --git a/packages/graphql-mesh/patches/@graphql-tools+stitch+9.0.3.patch b/packages/graphql-mesh/patches/@graphql-tools+stitch+9.0.3.patch index dc5c546..750d9d1 100644 --- a/packages/graphql-mesh/patches/@graphql-tools+stitch+9.0.3.patch +++ b/packages/graphql-mesh/patches/@graphql-tools+stitch+9.0.3.patch @@ -12,10 +12,12 @@ index 5e752ad..3b487f5 100644 const currentNamedType = (0, graphql_1.getNamedType)(c.type); if (finalNamedType.toString() !== currentNamedType.toString()) { diff --git a/node_modules/@graphql-tools/stitch/cjs/typeCandidates.js b/node_modules/@graphql-tools/stitch/cjs/typeCandidates.js -index c915942..6f02609 100644 +old mode 100644 +new mode 100755 +index c915942..fd09fc4 --- a/node_modules/@graphql-tools/stitch/cjs/typeCandidates.js +++ b/node_modules/@graphql-tools/stitch/cjs/typeCandidates.js -@@ -119,6 +119,25 @@ function buildTypes({ typeCandidates, directives, stitchingInfo, rootTypeNames, +@@ -119,6 +119,29 @@ function buildTypes({ typeCandidates, directives, stitchingInfo, rootTypeNames, (typeof mergeTypes === 'function' && mergeTypes(typeCandidates[typeName], typeName)) || (Array.isArray(mergeTypes) && mergeTypes.includes(typeName)) || (stitchingInfo != null && typeName in stitchingInfo.mergedTypes)) { @@ -29,7 +31,11 @@ index c915942..6f02609 100644 + let finalI = candidatesI[0]; + const otherCandidates = candidatesI.slice(1, candidatesI.length).concat(candidatesObj); + otherCandidates.forEach((otherCandidate) => { -+ finalI.type._fields = { ...finalI.type._fields, ...otherCandidate.type._fields }; ++ Object.keys(otherCandidate.type._fields).forEach(field => { ++ if (!Object.keys(finalI.type._fields).includes(field)) { ++ finalI.type._fields[field] = otherCandidate.type._fields[field]; ++ } ++ }) + }); + typeCandidates[typeName] = [finalI]; + } @@ -41,7 +47,7 @@ index c915942..6f02609 100644 typeMap[typeName] = (0, mergeCandidates_js_1.mergeCandidates)(typeName, typeCandidates[typeName], typeMergingOptions); } else { -@@ -128,6 +147,42 @@ function buildTypes({ typeCandidates, directives, stitchingInfo, rootTypeNames, +@@ -128,6 +151,58 @@ function buildTypes({ typeCandidates, directives, stitchingInfo, rootTypeNames, typeMap[typeName] = candidateSelector(typeCandidates[typeName]).type; } } @@ -53,31 +59,47 @@ index c915942..6f02609 100644 + */ + Object.values(typeMap).forEach((type) => { + if (type.constructor.name === "GraphQLObjectType") { -+ if (type.astNode.interfaces.length !== 0) { -+ type._interfaces = []; -+ type.astNode.interfaces.forEach((i) => { -+ const typeI = typeMap[i.name.value] -+ type._interfaces.push(typeI); -+ type._fields = {...type.getFields(), ...typeI.getFields()} -+ }); -+ } -+ if (type.astNode.interfaces.length >= 2) { -+ const firstFields = type._interfaces[0]._fields -+ type._interfaces.slice(1, type._interfaces.length).forEach((i) => { -+ Object.keys(i._fields).forEach((field) => { -+ if (field in firstFields) { -+ if (i._fields[field].type.constructor.name !== firstFields[field].type.constructor.name || -+ i._fields[field].type.constructor.name === "GraphQLObjectType") { -+ i._fields[field] = firstFields[field] -+ type._fields[field] = firstFields[field] -+ } -+ } -+ else { -+ firstFields[field] = i._fields[field] -+ } ++ const typeInterfaces = type.getInterfaces() ++ ++ if (typeInterfaces.length !== 0) { ++ type._fields = type.getFields() ++ ++ typeInterfaces.forEach(i => { ++ const iFields = typeMap[i.name].getFields() ++ Object.keys(iFields).forEach(keyName => { ++ type._fields[keyName] = iFields[keyName] + }) + }) + } ++ ++ if (typeInterfaces.length >= 2) { ++ let uniqueFields = {} ++ let duplicateFields = {} ++ ++ typeInterfaces.forEach(i => { ++ const iFields = typeMap[i.name].getFields() ++ Object.keys(iFields).forEach(keyName => { ++ if (uniqueFields[keyName] === undefined) { ++ uniqueFields[keyName] = "defined" ++ } ++ else { ++ duplicateFields[keyName] = iFields[keyName] ++ } ++ }) ++ }) ++ Object.keys(duplicateFields).forEach(field => { ++ if (type.getFields()[field] !== undefined) { ++ type._fields[field] = duplicateFields[field] ++ } ++ typeInterfaces.forEach(i => { ++ const iFields = typeMap[i.name].getFields() ++ ++ if (iFields[field] !== undefined) { ++ typeMap[i.name]._fields[field] = duplicateFields[field] ++ } ++ }) ++ }) ++ } + } + }); + diff --git a/packages/graphql-mesh/scripts/build-local-image.sh b/packages/graphql-mesh/scripts/build-local-image.sh index dc85da2..9c5e06d 100755 --- a/packages/graphql-mesh/scripts/build-local-image.sh +++ b/packages/graphql-mesh/scripts/build-local-image.sh @@ -7,5 +7,5 @@ cp -r ../../patches/* ./patches # build the docker image docker build -t graphql-mesh . # Remove the patches from the docker context -rm -rf patches/@graphql-tools+batch-execute+*.patch patches/@graphql-tools+executor+*.patch patches/graphql+*.patch +rm -rf patches/@graphql-tools+batch-execute+*.patch patches/@graphql-tools+executor+*.patch patches/graphql+*.patch patches/@graphql-tools+merge+*.patch diff --git a/packages/graphql-mesh/scripts/download-sources.ts b/packages/graphql-mesh/scripts/download-sources.ts index b4dd548..23ea0b2 100644 --- a/packages/graphql-mesh/scripts/download-sources.ts +++ b/packages/graphql-mesh/scripts/download-sources.ts @@ -1,5 +1,5 @@ import { readFileOrUrl, DefaultLogger } from '@graphql-mesh/utils' -import { getConfig } from '../utils/config' +import { getConfig } from '../utils/parseYamlConfig' import { writeFileSync, existsSync, mkdirSync } from 'node:fs' import { fetch } from '@whatwg-node/fetch' const logger = new DefaultLogger() @@ -10,17 +10,12 @@ const config = getConfig() const sources = config?.sources?.filter((source) => source?.handler?.openapi) || [] const swaggers = sources.map((source) => source?.handler?.openapi?.source) || [] -/** - * Get the name of generated from the source object - * @param {Record} source - * @returns {string | undefined} - */ const getFileName = (url: string): string | undefined => { return sources.find((source) => source?.handler?.openapi?.source === url)?.name } /** - * Download the swagger from the given URL and save it to the sources folder + * Download one swagger from the given URL and save it to the sources folder * @param {string} url */ const downSwaggerFromUrl = async (url: string | undefined, index: string): Promise => { diff --git a/packages/graphql-mesh/serve.ts b/packages/graphql-mesh/serve.ts index e23c8ad..442a14d 100644 --- a/packages/graphql-mesh/serve.ts +++ b/packages/graphql-mesh/serve.ts @@ -1,6 +1,6 @@ import { createServer } from 'node:http' import { createBuiltMeshHTTPHandler } from './.mesh' -import { getConfig } from './utils/config' +import { getConfig } from './utils/parseYamlConfig' const config = getConfig() const PORT = config.serve?.port ?? 4000 diff --git a/packages/graphql-mesh/types.d.ts b/packages/graphql-mesh/types.d.ts index 36d09fa..af6c52c 100644 --- a/packages/graphql-mesh/types.d.ts +++ b/packages/graphql-mesh/types.d.ts @@ -1,19 +1,19 @@ import { type OpenAPIV3 } from 'openapi-types' import { YamlConfig } from '@graphql-mesh/types' -type SwaggerName = string type Spec = OpenAPIV3.Document type Path = keyof Spec['paths'] -type OperationId = string type Resolvers = YamlConfig.Config['additionalResolvers'] | {} -/** - * Catalog of all operations - */ -type Catalog = Record - +type CatalogContent = { operationIds: string[]; type: string; swaggers: string[] } +type Catalog = Record +type XLink = { + rel: string + type: string + hrefPattern: string +} type ConfigExtension = { - /** Additional type definitions */ + // Additional type definitions typeDefs: string - /** Resolvers */ + // Additional resolvers resolvers: Resolvers } diff --git a/packages/graphql-mesh/utils/ConfigFromSwaggers.ts b/packages/graphql-mesh/utils/ConfigFromSwaggers.ts old mode 100644 new mode 100755 index 48c8e61..95ec03b --- a/packages/graphql-mesh/utils/ConfigFromSwaggers.ts +++ b/packages/graphql-mesh/utils/ConfigFromSwaggers.ts @@ -1,13 +1,17 @@ import { globSync } from 'glob' import { readFileSync } from 'node:fs' -import { Catalog, Spec, SwaggerName, ConfigExtension } from '../types' -import { getConfig, getSourceName, getSourceOpenapiEnpoint } from './config' -import { getAvailableTypes } from './swaggers' +import { Catalog, Spec, ConfigExtension } from '../types' +import { + getConfig, + getSourceName, + getSourceOpenapiEnpoint, + getSourceTransforms +} from './parseYamlConfig' import { mergeObjects } from './helpers' -import { generateTypeDefsAndResolversFromSwagger } from './swaggers' +import { generateTypeDefsAndResolversFromSwagger } from './generateTypeDefsAndResolvers' export default class ConfigFromSwaggers { - swaggers: SwaggerName[] = [] + swaggers: string[] = [] specs: Spec[] = [] catalog: Catalog = {} interfacesWithChildren: { [key: string]: string[] } = {} @@ -27,7 +31,16 @@ export default class ConfigFromSwaggers { content?.['application/json']?.schema['$ref'] ?? content?.['*/*']?.schema['$ref'] const schema = ref?.replace('#/components/schemas/', '') if (schema) { - acc[path] = [query?.operationId || '', schema, this.swaggers[i]] + if (!acc[path]) { + acc[path] = { + operationIds: [query.operationId], + type: schema, + swaggers: [this.swaggers[i]] + } + } else { + acc[path].operationIds.push(query.operationId) + acc[path].swaggers.push(this.swaggers[i]) + } } }) return acc @@ -65,7 +78,25 @@ export default class ConfigFromSwaggers { } createTypeDefsAndResolvers() { - const availableTypes = getAvailableTypes(this.specs) + if (this.config.sources) { + this.specs.forEach((spec, index) => { + // Suffix each schema name by the swagger version if there is a "rename" transform + if ( + spec.components && + this.config.sources[index]?.transforms?.find( + (transform) => transform.rename !== undefined + ) + ) { + const xVersion = spec.info.version.split('.')[0] + const schemasWithSuffix = {} + Object.entries(spec.components.schemas).forEach(([key, schema]) => { + schemasWithSuffix[`${key}_v${xVersion}`] = schema + }) + spec.components.schemas = schemasWithSuffix + } + }) + } + const availableTypes = this.getAvailableTypes(this.specs) const typeDefs = /* GraphQL */ ` type LinkItem { rel: String @@ -108,14 +139,13 @@ export default class ConfigFromSwaggers { ?.operationHeaders || {}) } } - } + }, + transforms: getSourceTransforms(source, this.config) })) || [] ) } - /* - * Get sources that are not openapi - */ + // Get sources that are not openapi getOtherSources() { return ( this.config.sources?.filter( @@ -124,12 +154,7 @@ export default class ConfigFromSwaggers { ) } - /** - * Get additional type definitions, resolvers, sources from swaggers and default config - * - * @returns {ConfigExtension} - defaultConfig, additionalTypeDefs, additionalResolvers, sources - */ - + // Create Mesh config getMeshConfigFromSwaggers(): { defaultConfig: any additionalTypeDefs: string @@ -144,4 +169,8 @@ export default class ConfigFromSwaggers { sources: [...this.getOpenApiSources(), ...this.getOtherSources()] } } + + // Get all schema names from all swaggers + getAvailableTypes = (specs: Spec[]) => + specs.flatMap((spec) => Object.keys(spec.components?.schemas ?? {})) } diff --git a/packages/graphql-mesh/utils/generateTypeDefsAndResolvers.ts b/packages/graphql-mesh/utils/generateTypeDefsAndResolvers.ts new file mode 100755 index 0000000..5798246 --- /dev/null +++ b/packages/graphql-mesh/utils/generateTypeDefsAndResolvers.ts @@ -0,0 +1,349 @@ +import { Spec, ConfigExtension, Resolvers, Catalog, XLink, CatalogContent } from '../types' +import { getSourceName } from './parseYamlConfig' +import { + trimLinks, + anonymizePathAndGetParams, + sortSwaggersByVersionDesc, + getActionsItems, + isSpecialKey, + getHighestVersionAvailable +} from './helpers' + +/** + * This function creates, for one Swagger file, the additional typeDefs and resolvers required to handle HATEOAS links and the prefixation of some schemas + * @param spec - A Swagger file + * @param availableTypes - An exhaustive list of the types available across the entire conf + * @param interfacesWithChildren - An exhaustive list of the all the interfaces, with their children + * @param catalog - An object, where the keys are operation paths and the values are: {the corresponding operationId, the type returned by the operation, the list of swaggers containing this path} + * @param config - The default config + * @returns an object with two elements: the additional typeDefs and the additional resolvers + */ +export const generateTypeDefsAndResolversFromSwagger = ( + spec: Spec, + availableTypes: string[], + interfacesWithChildren: { [key: string]: string[] }, + catalog: Catalog, + config: any +): ConfigExtension => { + if (!spec.components) { + console.warn('No components found in the swagger file') + return { typeDefs: '', resolvers: {} } + } + + const { schemas } = spec.components + if (!schemas) { + console.warn('No schemas found in the swagger file') + return { typeDefs: '', resolvers: {} } + } + + let typeDefs = '' + const resolvers: Resolvers = {} + + Object.entries(schemas).forEach(([schemaKey, schemaValue]) => { + Object.entries(schemaValue) + .filter(isSpecialKey) + .forEach(([key, value]) => { + /** + * Prefix-schema processing: + * Add a prefixSchema directive for each schema having the "x-graphql-prefix-schema-with" key + */ + if (key === 'x-graphql-prefix-schema-with') { + const schemaType = Object.keys(interfacesWithChildren).includes(schemaKey) + ? 'interface' + : 'type' + + typeDefs += `extend ${schemaType} ${schemaKey} @prefixSchema(prefix: "${value}") { dummy: String }\n` + // If it's an interface, prefix each of its children too + if (schemaType === 'interface') { + interfacesWithChildren[schemaKey].forEach((children) => { + const parentVersion = schemaKey.split('_')[schemaKey.split('_').length - 1] + if (parentVersion !== schemaKey) { + children += `_${parentVersion}` + } + if (!Object.keys(interfacesWithChildren).includes(children)) { + typeDefs += `extend type ${children} { dummy: String }\n` + } + }) + } + } + + /** + * xLinks processing: + * Add additional properties for each schema having the "x-links" key + */ + if (key === 'x-links') { + const trimedSchemaKey = trimLinks(schemaKey) + const schemaType = Object.keys(interfacesWithChildren).includes(trimedSchemaKey) + ? 'interface' + : 'type' + + let _linksItems = '' + const _actionsItems = getActionsItems(schemas) + + let subTypeDefs = `extend ${schemaType} ${trimedSchemaKey} {\n` + const subResolver = {} + + const xLinkList: XLink[] = value + let matchedLinkItems: CatalogContent = { + operationIds: undefined, + type: undefined, + swaggers: undefined + } + + for (const xLink of xLinkList) { + // Replace illegal characters + const xLinkName = xLink.rel.replaceAll('-', '_').replaceAll(' ', '') + const xLinkPath = xLink.hrefPattern + + const matchedPath = Object.keys(catalog).filter( + (key) => + anonymizePathAndGetParams(key).anonymizedPath === + anonymizePathAndGetParams(xLinkPath).anonymizedPath + )[0] + + if (matchedPath) { + matchedLinkItems = catalog[matchedPath] + } + + const paramsToSend = anonymizePathAndGetParams(matchedPath).params + const operationIds = matchedLinkItems.operationIds + const sourceSwaggers = matchedLinkItems.swaggers + + const highestVersion = getHighestVersionAvailable(availableTypes, matchedLinkItems.type) + // If the matched type is suffixed/versioned + if (matchedLinkItems.type && highestVersion >= 0) { + _linksItems += /* GraphQL */ ` + ${xLinkName} + { + href + } + ` + // Add a new link for the highest version available & all the smaller versions available too + for (let currentVersion = highestVersion; currentVersion >= 0; currentVersion--) { + if (availableTypes.includes(`${matchedLinkItems.type}_v${currentVersion}`)) { + const currentLink = `${xLinkName}_v${currentVersion}` + + subTypeDefs += `${currentLink}: ${matchedLinkItems.type}_v${currentVersion}\n` + + subResolver[currentLink] = { + selectionSet: + _actionsItems !== '' + ? /* GraphQL */ ` + { + _links { + ${_linksItems} + } + _actions { + ${_actionsItems} + } + }` + : /* GraphQL */ ` + { + _links { + ${_linksItems} + } + }`, + + resolve: (root: any, args: any, context: any, info: any) => { + const hateoasLink: any = Object.entries(root._links).find( + (item) => item[0] === xLinkName + )?.[1] + + if (hateoasLink?.href) { + root = { ...root, followLink: hateoasLink.href } + } + + if (paramsToSend.length) { + paramsToSend.forEach((param) => { + args[param] = '0' + }) + } + + const availableSwaggers = sourceSwaggers + .filter((swagger) => swagger.includes(`@${currentVersion}`)) + .sort(sortSwaggersByVersionDesc) + + const versionedSource = getSourceName(availableSwaggers[0], config) + const versionedQueryName = `${Object.keys(context[versionedSource]).find( + (key) => key.includes('Query') + )}` + + const operationId = operationIds[sourceSwaggers.indexOf(availableSwaggers[0])] + const versionedOperationId = `${operationId}_v${currentVersion}` + + return context[versionedSource][versionedQueryName][versionedOperationId]({ + root, + args, + context, + info + }) + } + } + } + } + } + // If the matched type is not suffixed/not versioned + else if ( + matchedLinkItems.type && + availableTypes.includes(matchedLinkItems.type) && + highestVersion === -1 + ) { + _linksItems += /* GraphQL */ ` + ${xLinkName} + { + href + } + ` + + subTypeDefs += `${xLinkName}: ${matchedLinkItems.type}\n` + + subResolver[xLinkName] = { + selectionSet: + _actionsItems !== '' + ? /* GraphQL */ ` + { + _links { + ${_linksItems} + } + _actions { + ${_actionsItems} + } + }` + : /* GraphQL */ ` + { + _links { + ${_linksItems} + } + }`, + + resolve: (root: any, args: any, context: any, info: any) => { + const hateoasLink: any = Object.entries(root._links).find( + (item) => item[0] === xLinkName + )?.[1] + + if (hateoasLink?.href) { + root = { ...root, followLink: hateoasLink.href } + } + + if (paramsToSend.length) { + paramsToSend.forEach((param) => { + args[param] = '0' + }) + } + + return context[sourceSwaggers[0]] + ? context[sourceSwaggers[0]].Query[operationIds[0]]({ + root, + args, + context, + info + }) + : context[ + sourceSwaggers[0] + .split('/') + [sourceSwaggers[0].split('/').length - 1].split('.')[0] + ].Query[operationIds[0]]({ root, args, context, info }) + } + } + } + } + + // Resolvers for _linksList and _actionsList + if (Object.keys(subResolver).length) { + subTypeDefs += /* GraphQL */ `_linksList: [LinkItem]\n` + subResolver['_linksList'] = { + selectionSet: /* GraphQL */ ` + { + _links { + ${_linksItems} + } + }`, + resolve: (root: any) => { + return Object.keys(root?._links || {}) + .filter((key) => root._links[key]?.href) + .map((key) => ({ + rel: key, + href: root._links[key]?.href + })) + } + } + if (_actionsItems !== '') { + subTypeDefs += /* GraphQL */ `_actionsList: [ActionItem]\n` + subResolver['_actionsList'] = { + selectionSet: /* GraphQL */ ` + { + _actions { + ${_actionsItems} + } + }`, + resolve: (root: any) => { + return ( + Object.keys(root?._actions || {}) + .filter((key) => root._actions[key]?.action) + .map((key) => ({ + rel: key, + action: root._actions[key]?.action + })) || [] + ) + } + } + } + } + + subTypeDefs += '}\n' + + // Delete the additional typeDefs section if no new fields have been added + subTypeDefs = subTypeDefs.replace(`extend ${schemaType} ${trimedSchemaKey} {\n}\n`, '') + + if (matchedLinkItems.swaggers) { + typeDefs += subTypeDefs + resolvers[trimedSchemaKey] = subResolver + } + + // Special handling for interfaces + if ( + schemaType === 'interface' && + typeDefs !== '' && + resolvers[trimedSchemaKey] !== undefined + ) { + let currentKey = trimedSchemaKey + const parentVersion = trimedSchemaKey.split('_')[trimedSchemaKey.split('_').length - 1] + + // If an interface has new typeDefs, its children need to have these new typeDefs too + interfacesWithChildren[trimedSchemaKey].forEach((childKey) => { + if (parentVersion !== trimedSchemaKey) { + childKey += `_${parentVersion}` + } + if (!Object.keys(interfacesWithChildren).includes(childKey)) { + typeDefs += typeDefs + .match(/[\s\S]*(^[\s\S]*{[\s\S]*)/m)![1] + .replace('interface', 'type') + .replace(new RegExp(` ${currentKey} `, 'g'), ` ${childKey} `) + + resolvers[childKey] ??= {} + for (const prop in resolvers[trimedSchemaKey]) { + resolvers[childKey][prop] = resolvers[trimedSchemaKey][prop] + } + + currentKey = childKey + } + }) + + // Interfaces need to have the additional '__resolveType' property + resolvers[trimedSchemaKey].__resolveType = (res, _, schema) => { + if (res.__typename) { + return res.__typename + } + const returnTypeName = schema.returnType.name + const parentVersion = returnTypeName.split('_')[returnTypeName.split('_').length - 1] + return parentVersion !== trimedSchemaKey + ? `${interfacesWithChildren[schema.returnType.name][1]}_${parentVersion}` + : `${interfacesWithChildren[schema.returnType.name][1]}` + } + } + } + }) + }) + + return { typeDefs, resolvers } +} diff --git a/packages/graphql-mesh/utils/helpers.ts b/packages/graphql-mesh/utils/helpers.ts new file mode 100644 index 0000000..26c7257 --- /dev/null +++ b/packages/graphql-mesh/utils/helpers.ts @@ -0,0 +1,93 @@ +import type { OpenAPIV3 } from 'openapi-types' + +export const mergeObjects = (obj1: any, obj2: any) => { + for (const key in obj2) { + obj1[key] = + key in obj1 && typeof obj1[key] === 'object' ? mergeObjects(obj1[key], obj2[key]) : obj2[key] + } + return obj1 +} + +export const isSpecialKey = ([key, _value]) => + key === 'x-links' || key === 'x-graphql-prefix-schema-with' + +export const getActionsItems = (schemas: OpenAPIV3.ComponentsObject) => { + let _actionsItems = '' + Object.entries(schemas).forEach(([, schemaValue]) => { + const actions = schemaValue['x-actions'] || [] + if (actions.length) { + _actionsItems += actions.reduce((acc, item) => { + return ( + acc + + /* GraphQL */ ` + ${item.rel} + { + action + } + ` + ) + }, '') + } + }) + return _actionsItems +} + +/** + * Remove "Links" from a string + * Ex: trimLinks(VehicleLinks) = Vehicle ; trimLinks(ProductsLinks_v1) = Products_v1 + * @param schemaName - A string + * @returns the input string without "Links" + */ +export const trimLinks = (schemaName: string) => + schemaName.substring(schemaName.length - 2, schemaName.length - 1) === '_v' + ? schemaName.replace(/Links_/, '_') + : schemaName.replace(/Links/, '') + +/** + * Remove the parameters from an URL path and store them in a variable + * @param path - An URL path + * @returns an object with 2 elements: the anonymized URL path and its parameters + */ +export const anonymizePathAndGetParams = (path: string) => { + const params: string[] = path?.match(/\{(.*?)\}/g) ?? [] + + return { + anonymizedPath: path?.replace(/\/(\{[^}]+\})/g, '/{}'), + params: params.map((param) => param.replace(/[{}]/g, '')) + } +} + +// Compare 2 swaggers with the same major version +export const sortSwaggersByVersionDesc = (swaggerNameA: string, swaggerNameB: string) => { + return getVersionY(swaggerNameB) !== getVersionY(swaggerNameA) + ? getVersionY(swaggerNameB) - getVersionY(swaggerNameA) + : getVersionZ(swaggerNameB) - getVersionZ(swaggerNameA) +} +function getVersionY(swaggerName: string) { + return parseInt(swaggerName.split('@')[1].split('.')[1]) +} +function getVersionZ(swaggerName: string) { + return parseInt(swaggerName.split('@')[1].split('.')[2].split('_')[0]) +} + +/** + * For one spcific type, get its highest available version + * @param availableTypes - An exhaustive list of every available types + * @param type - The name of a specific type (not versioned) + * @returns a positive integer representing the highest available version of the type || -1 if no versions of the type exist + */ +export const getHighestVersionAvailable = (availableTypes: string[], type: string) => { + if (!availableTypes.some((t) => t.startsWith(`${type}_v`))) { + return -1 + } else { + const sortedTypeVersions = availableTypes + .filter((t) => t.startsWith(`${type}_v`)) + .sort((a, b) => b.localeCompare(a)) + return parseInt( + sortedTypeVersions[0].substring( + sortedTypeVersions[0].length - 1, + sortedTypeVersions[0].length + ) + ) + } +} diff --git a/packages/graphql-mesh/utils/helpers/index.ts b/packages/graphql-mesh/utils/helpers/index.ts deleted file mode 100644 index 190d85f..0000000 --- a/packages/graphql-mesh/utils/helpers/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -export const mergeObjects = (obj1: any, obj2: any) => { - for (const key in obj2) { - obj1[key] = - key in obj1 && typeof obj1[key] === 'object' ? mergeObjects(obj1[key], obj2[key]) : obj2[key] - } - return obj1 -} - -export const trimLinks = (str: string) => str.replace(/Links$/, '') - -/** - * Anonymize path and get params - * @param path {string} - * @returns - */ -export const anonymizePathAndGetParams = (path: string) => { - const params: string[] = path?.match(/\{(.*?)\}/g) ?? [] - - return { - anonymizedPath: path?.replace(/\/(\{[^}]+\})/g, '/{}'), - params: params.map((param) => param.replace(/[{}]/g, '')) - } -} diff --git a/packages/graphql-mesh/utils/config/index.ts b/packages/graphql-mesh/utils/parseYamlConfig.ts similarity index 66% rename from packages/graphql-mesh/utils/config/index.ts rename to packages/graphql-mesh/utils/parseYamlConfig.ts index edd32d7..d61e4ba 100644 --- a/packages/graphql-mesh/utils/config/index.ts +++ b/packages/graphql-mesh/utils/parseYamlConfig.ts @@ -3,28 +3,19 @@ import { DefaultLogger } from '@graphql-mesh/utils' import { load } from 'js-yaml' import { readFileSync } from 'node:fs' import { resolve } from 'node:path' + const logger = new DefaultLogger() -/** - * Load config file from yaml or ts - * @returns Config - */ +// Load the config.yaml file export const getConfig = (): YamlConfig.Config => { logger.info('Loading config file') let config: YamlConfig.Config - // Load yaml config file + try { const configPath = resolve('./config.yaml') config = load(readFileSync(configPath, { encoding: 'utf-8' })) } catch (e) {} - // Load ts config file - try { - if (!config) { - config = require('../../config').default - } - } catch (e) {} - if (!config) { throw new Error('No config file found') } @@ -33,12 +24,7 @@ export const getConfig = (): YamlConfig.Config => { return config } -/** - * Get endpoint from openapi source in config - * @param source {string} - * @param config - * @returns - */ +// Get the endpoint of a specific openapi source export const getSourceOpenapiEnpoint = ( source: string, config: YamlConfig.Config @@ -47,13 +33,14 @@ export const getSourceOpenapiEnpoint = ( return data?.handler.openapi?.endpoint } -/** Get source name from config - * @param source {string} - source name - * @param config {YamlConfig.Config} - config object - * @returns {string} - source name - * - */ +// Get the name of a specific source export const getSourceName = (source: string, config: YamlConfig.Config): string => { const data = config.sources?.find((item) => source.includes(item.name)) - return data?.name || source + return data?.name +} + +// Get the list of transforms of a specific source +export const getSourceTransforms = (source: string, config: YamlConfig.Config) => { + const data = config.sources?.find((item) => source.includes(item.name)) + return data?.transforms } diff --git a/packages/graphql-mesh/utils/swaggers/index.ts b/packages/graphql-mesh/utils/swaggers/index.ts deleted file mode 100755 index 8ce2874..0000000 --- a/packages/graphql-mesh/utils/swaggers/index.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { Spec, ConfigExtension, Resolvers } from '../../types' -import { getSourceName } from '../config' -import { trimLinks, anonymizePathAndGetParams } from '../helpers' -import type { OpenAPIV3 } from 'openapi-types' - -/** - * This function creates, for a Swagger file, the additional typeDefs for each schema having at least one x-link, and one resolver for each x-link - * @param swagger, one unique Swagger file - * @param availableTypes, a list of the types that can be extended via additionalTypeDefs - * @returns an object with two elements: the additional typeDefs and resolvers of the Swagger file - */ -export const generateTypeDefsAndResolversFromSwagger = ( - spec: Spec, - availableTypes: string[], - interfacesWithChildren: { [key: string]: string[] }, - catalog: { [key: string]: [string, string, string] }, - config: any -): ConfigExtension => { - if (!spec.components) { - console.warn('No components found in the swagger file') - return { typeDefs: '', resolvers: {} } - } - - const { schemas } = spec.components - if (!schemas) { - console.warn('No schemas found in the swagger file') - return { typeDefs: '', resolvers: {} } - } - - let typeDefs = '' - const resolvers: Resolvers = {} - const _actionsItems = getActionsItems(schemas) - - const isSpecialKey = ([key, _value]) => - key === 'x-links' || key === 'x-graphql-prefix-schema-with' - - Object.entries(schemas).forEach(([schemaKey, schemaValue]) => { - Object.entries(schemaValue) - .filter(isSpecialKey) - .forEach(([, value]) => { - const trimedSchemaKey = trimLinks(schemaKey) - const schemaType = Object.keys(interfacesWithChildren).includes(trimedSchemaKey) - ? 'interface' - : 'type' - - // "x-graphql-prefix-schema-with" extension - if (typeof value === 'string') { - typeDefs += `extend ${schemaType} ${schemaKey} @prefixSchema(prefix: "${value}") { dummy: String }\n` - if (schemaType === 'interface') { - interfacesWithChildren[schemaKey].forEach((children) => { - typeDefs += `extend type ${children} { dummy: String }\n` - }) - } - } - - // "x-links" extension - if (typeof value === 'object') { - typeDefs += `extend ${schemaType} ${trimedSchemaKey} {\n` - - const xLinksList: { - rel: string - type: string - hrefPattern: string - }[] = value - - let matchedSwagger = 'NOT_FOUND' - - const subResolver: object = {} - let _linksItems = '' - - for (const xLink of xLinksList) { - const xLinkName = xLink.rel.replaceAll('-', '_').replaceAll(' ', '') - const xLinkPath = xLink.hrefPattern - - let matchedName = 'NOT_FOUND' - let matchedType = 'NOT_FOUND' - - const { params: _, anonymizedPath: anonymizedPathFromLink } = - anonymizePathAndGetParams(xLinkPath) - - const matchedPath = Object.keys(catalog).filter( - (key) => anonymizePathAndGetParams(key).anonymizedPath === anonymizedPathFromLink - )[0] - - if (matchedPath) { - ;[matchedName, matchedType, matchedSwagger] = catalog[matchedPath] - if (!availableTypes.includes(matchedType)) { - matchedType = 'NOT_FOUND' - } - } - - const paramsToSend = anonymizePathAndGetParams(matchedPath).params - const query = matchedName - const source = getSourceName(matchedSwagger, config) - - if ( - matchedType !== 'NOT_FOUND' && - !(trimedSchemaKey !== matchedType && xLinkName === 'self') - ) { - typeDefs += `${xLinkName}: ${matchedType}\n` - - _linksItems += /* GraphQL */ ` - ${xLinkName} - { - href - } - ` - - subResolver[xLinkName] = { - selectionSet: - _actionsItems !== '' - ? /* GraphQL */ ` - { - _links { - ${_linksItems} - } - _actions { - ${_actionsItems} - } - }` - : /* GraphQL */ ` - { - _links { - ${_linksItems} - } - }`, - - resolve: (root: any, args: any, context: any, info: any) => { - const hateoasLink: any = Object.entries(root._links).find( - (item) => item[0] === xLinkName - )?.[1] - - if (hateoasLink?.href) { - root = { ...root, followLink: hateoasLink.href } - } - - if (paramsToSend.length) { - paramsToSend.forEach((param) => { - args[param] = '0' - }) - } - - return context[source].Query[query]({ root, args, context, info }) - } - } - } - } - - // Resolvers for _linksList and _actionsList - if (Object.keys(subResolver).length) { - typeDefs += /* GraphQL */ `_linksList: [LinkItem]\n` - subResolver['_linksList'] = { - selectionSet: /* GraphQL */ ` - { - _links { - ${_linksItems} - } - }`, - resolve: (root: any) => { - return Object.keys(root?._links || {}) - .filter((key) => root._links[key]?.href) - .map((key) => ({ - rel: key, - href: root._links[key]?.href - })) - } - } - if (_actionsItems !== '') { - typeDefs += /* GraphQL */ `_actionsList: [ActionItem]\n` - subResolver['_actionsList'] = { - selectionSet: /* GraphQL */ ` - { - _actions { - ${_actionsItems} - } - }`, - resolve: (root: any) => { - return ( - Object.keys(root?._actions || {}) - .filter((key) => root._actions[key]?.action) - .map((key) => ({ - rel: key, - action: root._actions[key]?.action - })) || [] - ) - } - } - } - } - - typeDefs += '}\n' - - // Delete the typedefs section if no new fields have been added - typeDefs = typeDefs.replace(`extend ${schemaType} ${trimedSchemaKey} {\n}\n`, '') - - if (matchedSwagger !== 'NOT_FOUND') { - resolvers[trimedSchemaKey] = subResolver - } - - // If an interface has new links, its children need to have these links too - if ( - schemaType === 'interface' && - typeDefs !== '' && - resolvers[trimedSchemaKey] !== undefined - ) { - let currentKey = trimedSchemaKey - - interfacesWithChildren[trimedSchemaKey].forEach((childKey) => { - if (!Object.keys(interfacesWithChildren).includes(childKey)) { - typeDefs += typeDefs - .match(/[\s\S]*(^[\s\S]*{[\s\S]*)/m)![1] - .replace('interface', 'type') - .replace(new RegExp(` ${currentKey} `, 'g'), ` ${childKey} `) - - resolvers[childKey] ??= {} - for (const prop in resolvers[trimedSchemaKey]) { - resolvers[childKey][prop] = resolvers[trimedSchemaKey][prop] - } - - currentKey = childKey - } - }) - - resolvers[trimedSchemaKey].__resolveType = (res, _, schema) => { - if (res.__typename) { - return res.__typename - } - return interfacesWithChildren[schema.returnType.name][1] - } - } - } - }) - }) - - return { typeDefs, resolvers } -} - -export const getActionsItems = (schemas: OpenAPIV3.ComponentsObject) => { - let _actionsItems = '' - Object.entries(schemas).forEach(([, schemaValue]) => { - const actions = schemaValue['x-actions'] || [] - if (actions.length) { - _actionsItems += actions.reduce((acc, item) => { - return ( - acc + - /* GraphQL */ ` - ${item.rel} - { - action - } - ` - ) - }, '') - } - }) - - return _actionsItems -} - -export const getAvailableTypes = (specs: Spec[]) => - specs.flatMap((spec) => Object.keys(spec.components?.schemas ?? {})) diff --git a/patches/@graphql-tools+executor+1.2.1.patch b/patches/@graphql-tools+executor+1.2.1.patch index 1a8998b..8d34986 100644 --- a/patches/@graphql-tools+executor+1.2.1.patch +++ b/patches/@graphql-tools+executor+1.2.1.patch @@ -1,29 +1,34 @@ diff --git a/node_modules/@graphql-tools/executor/cjs/execution/execute.js b/node_modules/@graphql-tools/executor/cjs/execution/execute.js old mode 100644 new mode 100755 -index 791e3df..4636e0f +index 791e3df..b6a78f0 --- a/node_modules/@graphql-tools/executor/cjs/execution/execute.js +++ b/node_modules/@graphql-tools/executor/cjs/execution/execute.js -@@ -629,7 +629,8 @@ function completeAbstractValue(exeContext, returnType, fieldNodes, info, path, r +@@ -629,7 +629,10 @@ function completeAbstractValue(exeContext, returnType, fieldNodes, info, path, r } function ensureValidRuntimeType(runtimeTypeName, exeContext, returnType, fieldNodes, info, result) { if (runtimeTypeName == null) { - throw (0, utils_1.createGraphQLError)(`Abstract type "${returnType.name}" must resolve to an Object type at runtime for field "${info.parentType.name}.${info.fieldName}". Either the "${returnType.name}" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.`, { nodes: fieldNodes }); -+ // FIXME: Find a better way to solve the case when runtimeTypeName is null -+ runtimeTypeName = returnType.astNode.directives[0].arguments[1].value.value.split(':')[0].replace('{', '').replaceAll('"', '') ++ const suffix = returnType.name.split('_')[1] ++ runtimeTypeName = Object.values(JSON.parse(returnType.astNode.directives ++ .find(d => d.name.value === "discriminator").arguments ++ .find(a => a.name.value === "mapping").value.value))[0] + '_' + suffix } // releases before 16.0.0 supported returning `GraphQLObjectType` from `resolveType` // TODO: remove in 17.0.0 release -@@ -640,7 +641,11 @@ function ensureValidRuntimeType(runtimeTypeName, exeContext, returnType, fieldNo +@@ -640,6 +643,15 @@ function ensureValidRuntimeType(runtimeTypeName, exeContext, returnType, fieldNo throw (0, utils_1.createGraphQLError)(`Abstract type "${returnType.name}" must resolve to an Object type at runtime for field "${info.parentType.name}.${info.fieldName}" with ` + `value ${(0, utils_1.inspect)(result)}, received "${(0, utils_1.inspect)(runtimeTypeName)}".`); } -- const runtimeType = exeContext.schema.getType(runtimeTypeName); -+ // Add-on to resolve a type from the mapping argument of a discriminator directive ++ // Resolve type from mapping of discriminator directive when possible + const mappedTypes = JSON.parse(returnType.astNode.directives + .find(d => d.name.value === "discriminator").arguments + .find(a => a.name.value === "mapping").value.value) -+ const runtimeType = Object.keys(mappedTypes).includes(runtimeTypeName) ? exeContext.schema.getType(mappedTypes[runtimeTypeName]) : exeContext.schema.getType(runtimeTypeName); ++ Object.keys(mappedTypes).forEach((type) => { ++ if (runtimeTypeName.includes(type)) { ++ runtimeTypeName = runtimeTypeName.replace(type, mappedTypes[type]) ++ } ++ }) + const runtimeType = exeContext.schema.getType(runtimeTypeName); if (runtimeType == null) { throw (0, utils_1.createGraphQLError)(`Abstract type "${returnType.name}" was resolved to a type "${runtimeTypeName}" that does not exist inside the schema.`, { nodes: fieldNodes }); - } diff --git a/patches/@graphql-tools+merge+9.0.3.patch b/patches/@graphql-tools+merge+9.0.3.patch new file mode 100644 index 0000000..702f2d2 --- /dev/null +++ b/patches/@graphql-tools+merge+9.0.3.patch @@ -0,0 +1,16 @@ +diff --git a/node_modules/@graphql-tools/merge/cjs/typedefs-mergers/fields.js b/node_modules/@graphql-tools/merge/cjs/typedefs-mergers/fields.js +index 442943d..e794bcc 100644 +--- a/node_modules/@graphql-tools/merge/cjs/typedefs-mergers/fields.js ++++ b/node_modules/@graphql-tools/merge/cjs/typedefs-mergers/fields.js +@@ -48,7 +48,7 @@ function preventConflicts(type, a, b, ignoreNullability = false) { + const t1 = (0, utils_js_1.extractType)(a.type); + const t2 = (0, utils_js_1.extractType)(b.type); + if (t1.name.value !== t2.name.value) { +- throw new Error(`Field "${b.name.value}" already defined with a different type. Declared as "${t1.name.value}", but you tried to override with "${t2.name.value}"`); ++ return a; + } + if (!safeChangeForFieldType(a.type, b.type, !ignoreNullability)) { + throw new Error(`Field '${type.name.value}.${a.name.value}' changed type from '${aType}' to '${bType}'`); +diff --git a/node_modules/@graphql-tools/merge/cjs/typedefs-mergers/type.js b/node_modules/@graphql-tools/merge/cjs/typedefs-mergers/type.js +old mode 100644 +new mode 100755