From 2cb51866d0a7fabdea3082b2795806e47b991134 Mon Sep 17 00:00:00 2001 From: mbthiam88 Date: Wed, 3 Apr 2024 10:42:24 +0200 Subject: [PATCH] Refactor/generate resolvers from sources (#4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️ refactor resolvers types generation from swaggers * 🔥 remove unsued files and improve refactoring --------- Co-authored-by: Mbaye THIAM --- packages/graphql-mesh/.meshrc.ts | 18 +- packages/graphql-mesh/package-lock.json | 3 - .../graphql-mesh/scripts/download-sources.ts | 2 +- packages/graphql-mesh/serve.ts | 2 +- packages/graphql-mesh/setup.ts | 357 ------------------ .../graphql-mesh/utils/ConfigFromSwaggers.ts | 134 +++++++ .../{helpers => utils/config}/index.ts | 38 +- .../utils/directive-typedefs/index.ts | 31 ++ packages/graphql-mesh/utils/helpers/index.ts | 26 ++ .../{config.ts => utils/swaggers/index.ts} | 136 +------ 10 files changed, 212 insertions(+), 535 deletions(-) delete mode 100644 packages/graphql-mesh/setup.ts create mode 100644 packages/graphql-mesh/utils/ConfigFromSwaggers.ts rename packages/graphql-mesh/{helpers => utils/config}/index.ts (52%) create mode 100644 packages/graphql-mesh/utils/directive-typedefs/index.ts create mode 100644 packages/graphql-mesh/utils/helpers/index.ts rename packages/graphql-mesh/{config.ts => utils/swaggers/index.ts} (66%) diff --git a/packages/graphql-mesh/.meshrc.ts b/packages/graphql-mesh/.meshrc.ts index a79ec34..b4e6f72 100644 --- a/packages/graphql-mesh/.meshrc.ts +++ b/packages/graphql-mesh/.meshrc.ts @@ -1,11 +1,9 @@ import type { Config } from '@graphql-mesh/types/typings/config' -import { - openapiSources, - additionalTypeDefs, - resolvers, - defaultConfig, - othersSources -} from './setup' +import ConfigFromSwaggers from './utils/ConfigFromSwaggers' + +const configFromSwaggers = new ConfigFromSwaggers() +const { defaultConfig, additionalResolvers, additionalTypeDefs, sources } = + configFromSwaggers.getMeshConfigFromSwaggers() const config = { ...defaultConfig, @@ -15,9 +13,9 @@ const config = { { 'directive-no-auth': {} }, ...(defaultConfig.transforms || []) ], - sources: [...openapiSources, ...othersSources], - additionalTypeDefs: [...(defaultConfig.additionalTypeDefs || []), additionalTypeDefs], - additionalResolvers: [...(defaultConfig.additionalResolvers || []), resolvers] + sources: [...sources], + additionalTypeDefs: [...(defaultConfig.additionalTypeDefs || []), ...additionalTypeDefs], + additionalResolvers: [...(defaultConfig.additionalResolvers || []), additionalResolvers] } export default config diff --git a/packages/graphql-mesh/package-lock.json b/packages/graphql-mesh/package-lock.json index b7d12b1..7335b0f 100644 --- a/packages/graphql-mesh/package-lock.json +++ b/packages/graphql-mesh/package-lock.json @@ -3542,7 +3542,6 @@ "node_modules/directive-headers": { "version": "1.0.0", "resolved": "file:local-pkg/directive-headers-1.0.0.tgz", - "integrity": "sha512-Gc8QFPyDLvQgAl1S4FMlz20JKRNbcQg28EM6x0zY1QPT84MuRj0l96DLU7jxvURI3StVAMiPATwSrMrp7WvtZA==", "peerDependencies": { "@graphql-mesh/cache-localforage": "*", "@graphql-mesh/types": "*", @@ -3554,7 +3553,6 @@ "node_modules/directive-no-auth": { "version": "1.0.0", "resolved": "file:local-pkg/directive-no-auth-1.0.0.tgz", - "integrity": "sha512-yfHfdqjWoDi4seUzZ5g1Fm124ujbNz1gppXOFtuxGs4UV2dd9tn6LaszgsUnq/GMNtyoMtuqZ0c/QHPAzwSFAQ==", "peerDependencies": { "@graphql-mesh/cache-localforage": "*", "@graphql-mesh/types": "*", @@ -3566,7 +3564,6 @@ "node_modules/directive-spl": { "version": "1.0.0", "resolved": "file:local-pkg/directive-spl-1.0.0.tgz", - "integrity": "sha512-vepLz1dzK+1Lb/RCAHIdFz+7ckxjtTW0tfh8xm+037tOWcJXklzmuxqBGk91LmOcFxFVllmeHHgGCID4qCwM8g==", "dependencies": { "antlr4ts": "^0.5.0-alpha.4" }, diff --git a/packages/graphql-mesh/scripts/download-sources.ts b/packages/graphql-mesh/scripts/download-sources.ts index 6896c04..31bad97 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 '../helpers/config' +import { getConfig } from '../utils/config' import { writeFileSync, existsSync, mkdirSync } from 'node:fs' import { fetch } from '@whatwg-node/fetch' const logger = new DefaultLogger() diff --git a/packages/graphql-mesh/serve.ts b/packages/graphql-mesh/serve.ts index cc7b236..7f91d7d 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 './helpers' +import { getConfig } from './utils/config' const config = getConfig() || {} const PORT = config.serve?.port ?? 4000 diff --git a/packages/graphql-mesh/setup.ts b/packages/graphql-mesh/setup.ts deleted file mode 100644 index fd1f4d7..0000000 --- a/packages/graphql-mesh/setup.ts +++ /dev/null @@ -1,357 +0,0 @@ -import { globSync } from 'glob' -import { readFileSync } from 'node:fs' -import type { Catalog, Spec, SwaggerName, Resolvers, ConfigExtension } from './types' -import { - getConfig, - mergeObjects, - trimLinks, - anonymizePathAndGetParams, - getOpenapiEnpoint, - getAvailableTypes -} from './helpers' - -// Load the config file -const config = getConfig() - -const SWAGGERS: SwaggerName[] = globSync('./sources/**/*.json').sort((a, b) => a.localeCompare(b)) - -const specsRaw = SWAGGERS.map( - (swagger) => JSON.parse(readFileSync(swagger, { encoding: 'utf-8' })) -) - -const catalog = specsRaw.reduce((acc, spec, i) => { - Object.keys(spec.paths).forEach((path) => { - const query = spec.paths[path]?.get - const content = query?.responses['200']?.['content'] - const ref = content?.['application/json']?.schema['$ref'] ?? content?.['*/*']?.schema['$ref'] - const schema = ref?.replace('#/components/schemas/', '') - if (schema) { - acc[path] = [query?.operationId, schema, SWAGGERS[i]] - } - }) - return acc -}, {} as Catalog) - -let interfacesWithChildren = {} -specsRaw.forEach((s) => { - const { schemas } = s.components - const entries = Object.entries(schemas).filter(([_, value]) => - Object.keys(value).includes('discriminator') - ) - for (const [schemaKey, schemaValue] of entries) { - const mapping = schemaValue['discriminator']['mapping'] ?? {} - const mappingTypes = [] - mappingTypes.push( - ...Object.keys(mapping) - .filter((k) => k !== schemaKey) - .map((k) => mapping[k].replace('#/components/schemas/', '')) - ) - if (interfacesWithChildren[schemaKey] === undefined) { - interfacesWithChildren[schemaKey] = mappingTypes - } else { - mappingTypes.forEach((type) => { - if (!interfacesWithChildren[schemaKey].includes(type)) { - interfacesWithChildren[schemaKey].push(type) - } - }) - } - } -}) - -export const openapiSources = - SWAGGERS.map((source) => ({ - name: source, - handler: { - openapi: { - source, - endpoint: getOpenapiEnpoint(source, config) || '{env.ENDPOINT}', - ignoreErrorResponses: true, - operationHeaders: { - Authorization: `{context.headers["authorization"]}` - } - } - } - })) || [] - -/** - * 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 - */ -function createTypeDefsAndResolversFromOneSwagger( - spec: Spec, - availableTypes: string[] -): ConfigExtension { - if (!spec.components) { - return { - typeDefs: '', - resolvers: {} - } - } - - const { schemas } = spec.components - - if (!schemas) { - console.warn('No schemas found in the swagger files') - - return { - typeDefs: '', - resolvers: {} - } - } - - let typeDefs = '' - - const resolvers: Resolvers = {} - - const isKeyXlink = ([key, _value]) => key === 'x-links' - - Object.entries(schemas).forEach(([schemaKey, schemaValue]) => { - Object.entries(schemaValue) - .filter(isKeyXlink) - .forEach(([, schema]) => { - const trimedSchemaKey = trimLinks(schemaKey) - const objToExtend = Object.keys(interfacesWithChildren).includes(trimedSchemaKey) - ? 'interface' - : 'type' - - typeDefs += `extend ${objToExtend} ${trimedSchemaKey} {\n` - - const xLinksList: { - rel: string - type: string - hrefPattern: string - }[] = schema - let targetedSwaggerName = 'SWAGGER_NOT_FOUND' - - let objResolver: object = {} - let _linksItems = '' - for (let x = 0; x < xLinksList.length; x++) { - const xLink = xLinksList[x] - const xLinkName = xLink.rel.replaceAll('-', '_').replaceAll(' ', '') - const xLinkPath = xLink.hrefPattern - let targetedOperationName = 'NAME_NOT_FOUND' - let targetedOperationType = 'TYPE_NOT_FOUND' - - const { params: paramsFromLink, anonymizedPath: anonymizedPathFromLink } = - anonymizePathAndGetParams(xLinkPath) - - const matchedPathsForLinks = Object.keys(catalog).filter( - (key) => anonymizePathAndGetParams(key).anonymizedPath === anonymizedPathFromLink - ) - - if (matchedPathsForLinks.length) { - ;[targetedOperationName, targetedOperationType, targetedSwaggerName] = - catalog[matchedPathsForLinks[0]] - if (!availableTypes.includes(targetedOperationType)) { - targetedOperationType = 'TYPE_NOT_FOUND' - } - } - - const paramsToSend: string[] = [] - matchedPathsForLinks.forEach((key) => - paramsToSend.push(...anonymizePathAndGetParams(key).params) - ) - - const query = targetedOperationName - const type = targetedOperationType - const source = targetedSwaggerName - - if ( - targetedOperationType !== 'TYPE_NOT_FOUND' && - !(trimedSchemaKey !== targetedOperationType && xLinkName === 'self') - ) { - typeDefs += `${xLinkName}: ${targetedOperationType}\n` - - _linksItems += /* GraphQL */ ` - ${xLinkName} - { - href - } - ` - objResolver[xLinkName] = { - selectionSet: /* 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, i) => { - args[param] = root[param] || root[paramsFromLink[i]] || '' - }) - } - - return context[source].Query[query]({ - root, - args, - context, - info - }) - } - } - } - } - - /** Resolver for _linksList */ - if (Object.keys(objResolver).length) { - typeDefs += /* GraphQL */ `_linksList: [LinkItem]\n` - objResolver['_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 - })) - } - } - } - - typeDefs += '}\n' - typeDefs = typeDefs.replace(`extend ${objToExtend} ${trimedSchemaKey} {\n}\n`, '') - - if (targetedSwaggerName !== 'SWAGGER_NOT_FOUND') { - resolvers[trimedSchemaKey] = objResolver - } - - if ( - objToExtend === 'interface' && - typeDefs !== '' && - resolvers[trimedSchemaKey] !== undefined - ) { - let varToCompare = trimedSchemaKey - interfacesWithChildren[trimedSchemaKey].forEach((type) => { - const regex = new RegExp(` ${varToCompare} `, 'g') - if (Object.keys(interfacesWithChildren).includes(type)) { - typeDefs += typeDefs - .match(/[\s\S]*(^[\s\S]*{[\s\S]*)/m)![1] - .replace('type', 'interface') - .replace(regex, ` ${type} `) - varToCompare = type - resolvers[type] ??= {} - for (const key in resolvers[trimedSchemaKey]) { - resolvers[type][key] = resolvers[trimedSchemaKey][key] - } - - interfacesWithChildren[type].forEach((t) => { - const regex2 = new RegExp(` ${varToCompare} `, 'g') - typeDefs += typeDefs - .match(/[\s\S]*(^[\s\S]*{[\s\S]*)/m)![1] - .replace('interface', 'type') - .replace(regex2, ` ${t} `) - varToCompare = t - - resolvers[t] ??= {} - for (const key in resolvers[type]) { - resolvers[t][key] = resolvers[type][key] - } - }) - } else { - typeDefs += typeDefs - .match(/[\s\S]*(^[\s\S]*{[\s\S]*)/m)![1] - .replace('interface', 'type') - .replace(regex, ` ${type} `) - varToCompare = type - - resolvers[type] ??= {} - for (const key in resolvers[trimedSchemaKey]) { - resolvers[type][key] = resolvers[trimedSchemaKey][key] - } - } - }) - - resolvers[trimedSchemaKey].__resolveType = (res, _, schema) => { - if (res.__typename !== undefined) { - return res.__typename - } else { - if (schema.parentType._fields[schema.fieldName] !== undefined) { - //TODO: - return interfacesWithChildren[ - schema.parentType._fields[schema.fieldName].type.name - ][ - interfacesWithChildren[schema.parentType._fields[schema.fieldName].type.name] - .length - 1 - ] - } else { - return interfacesWithChildren[schema.fieldName][0] - } - } - } - } - }) - }) - - return { typeDefs, resolvers } -} - -/** - * This function merges the additional typeDefs and resolvers of each Swagger file into one - * @param specsList, a list of Swagger files - * @returns an object with two elements: the merged typeDefs and the merged resolvers - */ -function createTypeDefsAndResolvers(specs: Spec[]) { - const availableTypes = getAvailableTypes(specs) - return specs.reduce( - (acc, spec) => { - const { typeDefs, resolvers } = createTypeDefsAndResolversFromOneSwagger(spec, availableTypes) - acc.typeDefs += typeDefs - acc.resolvers = mergeObjects(acc.resolvers, resolvers) - return acc - }, - { typeDefs: '', resolvers: {} } as ConfigExtension - ) -} - -const typeDefsAndResolvers = createTypeDefsAndResolvers(specsRaw) - -typeDefsAndResolvers.typeDefs += /* GraphQL */ ` - directive @SPL(query: String) on FIELD -` - -typeDefsAndResolvers.typeDefs += /* GraphQL */ ` - directive @noAuth on FIELD -` - -typeDefsAndResolvers.typeDefs += /* GraphQL */ ` - directive @upper on FIELD -` - -typeDefsAndResolvers.typeDefs += /* GraphQL */ ` - type LinkItem { - rel: String - href: String - } -` -typeDefsAndResolvers.typeDefs += /* GraphQL */ ` - input Header { - key: String - value: String - } - - directive @headers(input: [Header]) on FIELD -` - -export const additionalTypeDefs = typeDefsAndResolvers.typeDefs -export const resolvers = typeDefsAndResolvers.resolvers -export const defaultConfig = config -export const othersSources = - config.sources?.filter((source: { handler: { openapi: any } }) => !source?.handler?.openapi) || [] diff --git a/packages/graphql-mesh/utils/ConfigFromSwaggers.ts b/packages/graphql-mesh/utils/ConfigFromSwaggers.ts new file mode 100644 index 0000000..2082f37 --- /dev/null +++ b/packages/graphql-mesh/utils/ConfigFromSwaggers.ts @@ -0,0 +1,134 @@ +import { globSync } from 'glob' +import { readFileSync } from 'node:fs' +import { Catalog, Spec, SwaggerName, ConfigExtension } from '../types' +import { getConfig, getSourceOpenapiEnpoint } from './config' +import { getAvailableTypes } from './swaggers' +import { mergeObjects } from './helpers' +import { generateTypeDefsAndResolversFromSwagger } from './swaggers' +import { directiveTypeDefs } from './directive-typedefs' + +export default class ConfigFromSwaggers { + swaggers: SwaggerName[] = [] + specs: Spec[] = [] + catalog: Catalog = {} + interfacesWithChildren: { [key: string]: string[] } = {} + config: any = {} + + constructor() { + this.config = getConfig() + this.swaggers = globSync('././sources/**/*.json').sort((a, b) => a.localeCompare(b)) + this.specs = this.swaggers.map( + (swagger) => JSON.parse(readFileSync(swagger, { encoding: 'utf-8' })) + ) + this.catalog = this.specs.reduce((acc, spec, i) => { + Object.keys(spec.paths).forEach((path) => { + const query = spec.paths[path]?.get + const content = query?.responses['200']?.['content'] + const ref = + content?.['application/json']?.schema['$ref'] ?? content?.['*/*']?.schema['$ref'] + const schema = ref?.replace('#/components/schemas/', '') + if (schema) { + acc[path] = [query?.operationId, schema, this.swaggers[i]] + } + }) + return acc + }, {} as Catalog) + } + + getInterfacesWithChildren() { + this.specs.forEach((s) => { + const { schemas } = s.components + const entries = Object.entries(schemas).filter(([_, value]) => + Object.keys(value).includes('discriminator') + ) + for (const [schemaKey, schemaValue] of entries) { + const mapping = schemaValue['discriminator']['mapping'] ?? {} + const mappingTypes = [] + mappingTypes.push( + ...Object.keys(mapping) + .filter((k) => k !== schemaKey) + .map((k) => mapping[k].replace('#/components/schemas/', '')) + ) + if (this.interfacesWithChildren[schemaKey] === undefined) { + this.interfacesWithChildren[schemaKey] = mappingTypes + } else { + mappingTypes.forEach((type) => { + if (!this.interfacesWithChildren[schemaKey].includes(type)) { + this.interfacesWithChildren[schemaKey].push(type) + } + }) + } + } + }) + return this.interfacesWithChildren + } + + createTypeDefsAndResolvers() { + const availableTypes = getAvailableTypes(this.specs) + return this.specs.reduce( + (acc, spec) => { + const { typeDefs, resolvers } = generateTypeDefsAndResolversFromSwagger( + spec, + availableTypes, + this.getInterfacesWithChildren(), + this.catalog + ) + acc.typeDefs += typeDefs + acc.resolvers = mergeObjects(acc.resolvers, resolvers) + return acc + }, + { typeDefs: '', resolvers: {} } as ConfigExtension + ) + } + + getOpenApiSources() { + return ( + this.swaggers.map((source) => ({ + name: source, + handler: { + openapi: { + source, + endpoint: getSourceOpenapiEnpoint(source, this.config) || '{env.ENDPOINT}', + ignoreErrorResponses: true, + operationHeaders: { + Authorization: `{context.headers["authorization"]}` + } + } + } + })) || [] + ) + } + + /* + * Get sources that are not openapi + */ + getOtherSources() { + return ( + this.config.sources?.filter( + (source: { handler: { openapi: any } }) => !source?.handler?.openapi + ) || [] + ) + } + + /** + * Get additional type definitions, resolvers, sources from swaggers and default config + * + * @returns {ConfigExtension} - defaultConfig, additionalTypeDefs, additionalResolvers, sources + */ + + getMeshConfigFromSwaggers(): { + defaultConfig: any + additionalTypeDefs: string[] + additionalResolvers: any + sources: any[] + } { + const { typeDefs, resolvers } = this.createTypeDefsAndResolvers() + + return { + defaultConfig: this.config, + additionalTypeDefs: [typeDefs, directiveTypeDefs], + additionalResolvers: resolvers, + sources: [...this.getOpenApiSources(), ...this.getOtherSources()] + } + } +} diff --git a/packages/graphql-mesh/helpers/index.ts b/packages/graphql-mesh/utils/config/index.ts similarity index 52% rename from packages/graphql-mesh/helpers/index.ts rename to packages/graphql-mesh/utils/config/index.ts index a06f937..4388abf 100644 --- a/packages/graphql-mesh/helpers/index.ts +++ b/packages/graphql-mesh/utils/config/index.ts @@ -3,38 +3,8 @@ import { DefaultLogger } from '@graphql-mesh/utils' import { load } from 'js-yaml' import { readFileSync } from 'node:fs' import { resolve } from 'node:path' -import { Spec } from '../types' - const logger = new DefaultLogger() -export const mergeObjects = (obj1: any, obj2: any) => { - for (const key in obj2) { - if (key in obj1 && typeof obj1[key] === 'object') { - obj1[key] = mergeObjects(obj1[key], obj2[key]) - } else { - obj1[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, '')) - } -} - - /** * Load config file from yaml or ts * @returns Config @@ -51,7 +21,7 @@ export const getConfig = (): Config => { // Load ts config file try { if (!config) { - config = require('../config').default + config = require('../../config').default } } catch (e) {} @@ -63,17 +33,13 @@ export const getConfig = (): Config => { return config } - -export const getAvailableTypes = (specs: Spec[]) => - specs.flatMap((spec) => Object.keys(spec.components?.schemas ?? {})) - /** * Get endpoint from openapi source in config * @param source {string} * @param config * @returns */ -export const getOpenapiEnpoint = (source: string, config: Config): string | undefined => { +export const getSourceOpenapiEnpoint = (source: string, config: Config): string | undefined => { const data = config.sources?.find((item) => item?.handler?.openapi?.source?.includes(source.split('/').pop()) ) diff --git a/packages/graphql-mesh/utils/directive-typedefs/index.ts b/packages/graphql-mesh/utils/directive-typedefs/index.ts new file mode 100644 index 0000000..6eeeef3 --- /dev/null +++ b/packages/graphql-mesh/utils/directive-typedefs/index.ts @@ -0,0 +1,31 @@ +export const directiveTypeDefs = /* GraphQL */ ` + """ + This is a very small, lightweight, straightforward and non-evaluated expression language to sort, filter and paginate arrays of maps. + """ + directive @SPL(query: String) on FIELD + + """ + This directive is used to disable authentication for a specific operation. + """ + directive @noAuth on FIELD + + """ + This directive is used to convert the result to uppercase. + """ + directive @upper on FIELD + + type LinkItem { + rel: String + href: String + } + + input Header { + key: String + value: String + } + + """ + This directive is used to add headers to the request. + """ + directive @headers(input: [Header]) on FIELD +` diff --git a/packages/graphql-mesh/utils/helpers/index.ts b/packages/graphql-mesh/utils/helpers/index.ts new file mode 100644 index 0000000..cd337ee --- /dev/null +++ b/packages/graphql-mesh/utils/helpers/index.ts @@ -0,0 +1,26 @@ +export const mergeObjects = (obj1: any, obj2: any) => { + for (const key in obj2) { + if (key in obj1 && typeof obj1[key] === 'object') { + obj1[key] = mergeObjects(obj1[key], obj2[key]) + } else { + obj1[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/config.ts b/packages/graphql-mesh/utils/swaggers/index.ts similarity index 66% rename from packages/graphql-mesh/config.ts rename to packages/graphql-mesh/utils/swaggers/index.ts index 1af2ba0..5bc1a04 100644 --- a/packages/graphql-mesh/config.ts +++ b/packages/graphql-mesh/utils/swaggers/index.ts @@ -1,100 +1,17 @@ -import { globSync } from 'glob' -import { readFileSync } from 'node:fs' -import type { Catalog, ConfigExtension, Resolvers, Spec, SwaggerName } from './types' - -const SWAGGERS: SwaggerName[] = globSync('./specs/**/*.json').sort((a, b) => a.localeCompare(b)) - -const specsRaw = SWAGGERS.map( - (swagger) => JSON.parse(readFileSync(swagger, { encoding: 'utf-8' })) -) - -const catalog = specsRaw.reduce((acc, spec, i) => { - Object.keys(spec.paths).forEach((path) => { - const query = spec.paths[path]?.get - const content = query?.responses['200']?.['content'] - if (content) { - const ref = content['application/json']?.schema['$ref'] ?? content['*/*']?.schema['$ref'] - const schema = ref?.replace('#/components/schemas/', '') - if (schema) { - acc[path] = [query?.operationId, schema, SWAGGERS[i]] - } - } - }) - return acc -}, {} as Catalog) - -let interfacesWithChildren = {} -specsRaw.forEach((s) => { - const { schemas } = s.components - const entries = Object.entries(schemas).filter(([_, value]) => - Object.keys(value).includes('discriminator') - ) - for (const [schemaKey, schemaValue] of entries) { - const mapping = schemaValue['discriminator']['mapping'] ?? {} - const mappingTypes = [] - mappingTypes.push( - ...Object.keys(mapping) - .filter((k) => k !== schemaKey) - .map((k) => mapping[k].replace('#/components/schemas/', '')) - ) - if (interfacesWithChildren[schemaKey] === undefined) { - interfacesWithChildren[schemaKey] = mappingTypes - } else { - mappingTypes.forEach((type) => { - if (!interfacesWithChildren[schemaKey].includes(type)) { - interfacesWithChildren[schemaKey].push(type) - } - }) - } - } -}) - -const trimLinks = (str: string) => str.replace(/Links$/, '') -const mergeObjects = (obj1: Resolvers, obj2: Resolvers) => { - for (const key in obj2) { - if (key in obj1 && typeof obj1[key] === 'object') { - obj1[key] = mergeObjects(obj1[key], obj2[key]) - } else { - obj1[key] = obj2[key] - } - } - return obj1 -} -const getAvailableTypes = (specs: Spec[]) => - specs.flatMap((spec) => Object.keys(spec.components?.schemas ?? {})) -function anonymizePathAndGetParams(path: string) { - const params: string[] = path.match(/\{(.*?)\}/g) ?? [] - - return { - anonymizedPath: path.replace(/\/(\{[^}]+\})/g, '/{}'), - params: params.map((param) => param.replace(/[{}]/g, '')) - } -} - -export const sources = SWAGGERS.map((source) => ({ - name: source, - handler: { - openapi: { - source, - endpoint: '{env.ENDPOINT}', - ignoreErrorResponses: true, - operationHeaders: { - Authorization: `{context.headers["authorization"]}` - } - } - } -})) - +import { Spec, ConfigExtension, Resolvers } from '../../types' +import { trimLinks, anonymizePathAndGetParams } from '../helpers' /** * 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 */ -function createTypeDefsAndResolversFromOneSwagger( +export const generateTypeDefsAndResolversFromSwagger = ( spec: Spec, - availableTypes: string[] -): ConfigExtension { + availableTypes: string[], + interfacesWithChildren: { [key: string]: string[] }, + catalog: { [key: string]: [string, string, string] } +): ConfigExtension => { if (!spec.components) { return { typeDefs: '', @@ -315,40 +232,5 @@ function createTypeDefsAndResolversFromOneSwagger( return { typeDefs, resolvers } } -/** - * This function merges the additional typeDefs and resolvers of each Swagger file into one - * @param specsList, a list of Swagger files - * @returns an object with two elements: the merged typeDefs and the merged resolvers - */ -function createTypeDefsAndResolvers(specs: Spec[]) { - const availableTypes = getAvailableTypes(specs) - return specs.reduce( - (acc, spec) => { - const { typeDefs, resolvers } = createTypeDefsAndResolversFromOneSwagger(spec, availableTypes) - acc.typeDefs += typeDefs - acc.resolvers = mergeObjects(acc.resolvers, resolvers) - return acc - }, - { typeDefs: '', resolvers: {} } as ConfigExtension - ) -} - -const typeDefsAndResolvers = createTypeDefsAndResolvers(specsRaw) - -typeDefsAndResolvers.typeDefs += /* GraphQL */ ` - directive @SPL(query: String) on FIELD -` - -typeDefsAndResolvers.typeDefs += /* GraphQL */ ` - directive @upper on FIELD -` - -typeDefsAndResolvers.typeDefs += /* GraphQL */ ` - type LinkItem { - rel: String - href: String - } -` - -export const additionalTypeDefs = typeDefsAndResolvers.typeDefs -export const resolvers = typeDefsAndResolvers.resolvers +export const getAvailableTypes = (specs: Spec[]) => + specs.flatMap((spec) => Object.keys(spec.components?.schemas ?? {}))