From 7737f21de14ead008a4450d4eb732fe96c2ac382 Mon Sep 17 00:00:00 2001 From: Adrien FIGARD Date: Fri, 12 Jul 2024 15:36:10 +0200 Subject: [PATCH] :bulb: Add comments --- .../graphql-mesh/utils/ConfigFromSwaggers.ts | 73 ++++++++++++++- .../utils/generateTypeDefsAndResolvers.ts | 93 ++++--------------- 2 files changed, 87 insertions(+), 79 deletions(-) diff --git a/packages/graphql-mesh/utils/ConfigFromSwaggers.ts b/packages/graphql-mesh/utils/ConfigFromSwaggers.ts index 420e6c8..2980cb0 100755 --- a/packages/graphql-mesh/utils/ConfigFromSwaggers.ts +++ b/packages/graphql-mesh/utils/ConfigFromSwaggers.ts @@ -23,6 +23,15 @@ export default class ConfigFromSwaggers { this.specs = this.swaggers.map( (swagger) => JSON.parse(readFileSync(swagger, { encoding: 'utf-8' })) ) + /** + * The following code builds a comprehensive catalog of all operations defined in a list of Swagger specifications. + * The resulting catalog is an object where: + * - Keys are operation paths (e.g. "foo/bar/endpoint"). + * - Values are objects with details about each operation, including: + * - operationIds: a list of operation IDs corresponding to this path. + * - type: the type returned by the 200 response of this path. + * - swaggers: a list of Swagger definitions where this path is present. + */ this.catalog = this.specs.reduce((acc, spec, i) => { Object.keys(spec.paths).forEach((path) => { const query = spec.paths[path]?.get @@ -47,11 +56,29 @@ export default class ConfigFromSwaggers { }, {} as Catalog) } - getAvailableTypes() { + /** + * Extracts and returns all available schema types from the Swagger specifications. + * + * @returns {string[]} An array of schema type names. + * + * This function flattens the list of schemas from all Swagger specifications + * and extracts the keys (schema names) from the components' schemas. + */ + getAvailableTypes(): string[] { return this.specs.flatMap((spec) => Object.keys(spec.components?.schemas ?? {})) } - getInterfacesWithChildren() { + /** + * Identifies and returns a map of interface along with their child types. + * + * @returns {Record} An object where keys are interface names + * and values are arrays of child schema names. + * + * This function iterates through all Swagger specifications to find schemas that + * use discriminators. For each schema with a discriminator, it collects the mapping + * of the discriminator and associates child schemas to the parent schema. + */ + getInterfacesWithChildren(): Record { this.specs.forEach((s) => { const { schemas } = s.components || {} const entries = Object.entries(schemas || {}).filter(([_, value]) => @@ -81,21 +108,33 @@ export default class ConfigFromSwaggers { return this.interfacesWithChildren } + /** + * Creates and returns GraphQL type definitions and resolvers based on the Swagger specifications. + * The function processes each Swagger specification and extracts or generates GraphQL types and resolvers. + * + * @returns {ConfigExtension} An object containing the type definitions and resolvers. + * + * This function supports configurations that allow for schema renaming based on the Swagger version. + */ createTypeDefsAndResolvers() { if (this.config.sources) { this.specs.forEach((spec, index) => { - // Suffix each schema name by the swagger version if there is a "rename" transform + // Apply naming transformations if specified in the configuration if ( spec.components && this.config.sources[index]?.transforms?.find( (transform) => transform.rename !== undefined ) ) { + // Extract the major version from the Swagger specification const xVersion = spec.info.version.split('.')[0] + // Create a new object to store schemas with version suffixes const schemasWithSuffix = {} + // Suffix each schema name with the major version number Object.entries(spec.components.schemas).forEach(([key, schema]) => { schemasWithSuffix[`${key}_v${xVersion}`] = schema }) + // Replace the original schemas with the suffixed schemas spec.components.schemas = schemasWithSuffix } }) @@ -116,6 +155,7 @@ export default class ConfigFromSwaggers { const catalog = this.catalog const config = this.config + // Reduce over the specifications to generate and accumulate type definitions and resolvers return this.specs.reduce( (acc, spec) => { const { typeDefs, resolvers } = generateTypeDefsAndResolversFromSwagger( @@ -133,6 +173,15 @@ export default class ConfigFromSwaggers { ) } + /** + * Generates and returns an array of OpenAPI sources configured for the project. + * Each source includes a name, handler configurations, and possible transformations. + * + * @returns {OpenApiSource[]} An array of configured OpenAPI sources. + * + * This function maps over the `this.swaggers` array and constructs an object for each source, + * incorporating relevant configurations such as endpoint and headers. + */ getOpenApiSources() { return ( this.swaggers.map((source) => ({ @@ -154,7 +203,14 @@ export default class ConfigFromSwaggers { ) } - // Get sources that are not openapi + /** + * Filters and returns an array of sources that are not OpenAPI sources from the configuration. + * + * @returns {OtherSource[]} An array of sources that do not use OpenAPI handlers. + * + * This function checks the `this.config.sources` array and filters out any source + * that has an OpenAPI handler configuration. + */ getOtherSources() { return ( this.config.sources?.filter( @@ -163,7 +219,14 @@ export default class ConfigFromSwaggers { ) } - // Create Mesh config + /** + * Constructs and returns a complete Mesh configuration object based on the Swagger specifications and custom sources. + * + * @returns {MeshConfig} An object containing the default configuration, additional type definitions, resolvers, and sources. + * + * This function integrates type definitions and resolvers generated from the Swagger specifications + * and combines OpenAPI sources with other custom sources from the configuration. + */ getMeshConfigFromSwaggers(): { defaultConfig: any additionalTypeDefs: string diff --git a/packages/graphql-mesh/utils/generateTypeDefsAndResolvers.ts b/packages/graphql-mesh/utils/generateTypeDefsAndResolvers.ts index bebb631..54d5dc3 100755 --- a/packages/graphql-mesh/utils/generateTypeDefsAndResolvers.ts +++ b/packages/graphql-mesh/utils/generateTypeDefsAndResolvers.ts @@ -10,13 +10,15 @@ import { } 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 + * 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 availableTypes - An exhaustive list of the types available across the entire config + * @param interfacesWithChildren - An exhaustive list of all the interfaces, with their children + * @param catalog - An object, where the keys are operation paths and the values are: {the corresponding operationIds, 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 + * @returns {ConfigExtension} An object with two elements: the additional typeDefs and the additional resolvers */ export const generateTypeDefsAndResolversFromSwagger = ( spec: Spec, @@ -25,6 +27,7 @@ export const generateTypeDefsAndResolversFromSwagger = ( catalog: Catalog, config: any ): ConfigExtension => { + // If there are no schemas in the components section, log a warning and return empty typeDefs and resolvers if (!spec.components?.schemas) { console.warn('No components found in the swagger file') return { typeDefs: '', resolvers: {} } @@ -35,20 +38,22 @@ export const generateTypeDefsAndResolversFromSwagger = ( let typeDefs = '' const resolvers: Resolvers = {} + // Iterate over each schema in the Swagger file Object.entries(schemas).forEach(([schemaKey, schemaValue]) => { Object.entries(schemaValue) + // Filter for special keys such as 'x-links' and 'x-graphql-prefix-schema-with' .filter(isSpecialKey) .forEach(([key, value]) => { - /** - * Schema prefixation processing: - * Add a prefixSchema directive for each schema having the "x-graphql-prefix-schema-with" key - */ + + // HANDLE SCHEMA PREFIXATION if (key === 'x-graphql-prefix-schema-with') { const schemaType = Object.keys(interfacesWithChildren).includes(schemaKey) ? 'interface' : 'type' + // Add a prefixSchema directive to the type definition 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) => { @@ -63,69 +68,7 @@ export const generateTypeDefsAndResolversFromSwagger = ( } } - /** - * HATEOAS links processing: - * Add additional properties for each schema having the "x-links" key - * - * Explanations: - * GraphQL Mesh can't understand HATEOAS links natively. - * To translate them to GraphQL Mesh, we have to create an additional type definition - * and an additional resolver for each HATEOAS link. - * - * Example: - * If we have one HATEOAS link defined like this... - * ``` - * "Vehicle": { - * ..., - * "properties": { - * "_links": { - * "$ref": "#/components/schemas/VehicleLinks" - * } - * } - * }, - * "VehicleLinks": { - * ..., - * "properties": { - * "linkToFollow": { - * "$ref": "#/components/schemas/XLink" - * } - * }, - * "x-links": [ - * { - * "rel": "linkToFollow", - * "type": "application/json", - * "hrefPattern": "/the/link/path" - * } - * ] - * } - * ``` - * ...we will have to create an additional type definition and and additional resolver like this: - * typeDef - * ``` - * extend type Vehicle { - * linkToFollow: CorrespondingPath - * } - * ``` - * resolver - * ``` - * { - * Vehicle: { - * linkToFollow: { - * selectionSet: ..., - * resolve: ... - * } - * } - * } - * ``` - * - * Strategy: - * The main difficulty is that, while in HATEOAS a link is referenced by its path, - * in GraphQL Mesh a "link" represented by a type definition is referenced by the - * type returned by the corresponding operation (as seen above). - * Therefore, we need to have a catalog of all the path and corresponding type - * available in our spec. Then we can map each patch to the type returned by its - * operation. - */ + // HANDLE HATEOAS LINKS PROCESSING if (key === 'x-links') { const trimedSchemaKey = trimLinks(schemaKey) const schemaType = Object.keys(interfacesWithChildren).includes(trimedSchemaKey) @@ -145,11 +88,13 @@ export const generateTypeDefsAndResolversFromSwagger = ( swaggers: undefined } + // Process each HATEOAS link in the x-links array for (const xLink of xLinkList) { - // Replace illegal characters + // Replace illegal characters in the link name const xLinkName = xLink.rel.replaceAll('-', '_').replaceAll(' ', '') const xLinkPath = xLink.hrefPattern + // Find the corresponding path in the catalog const matchedPath = Object.keys(catalog).filter( (key) => anonymizePathAndGetParams(key).anonymizedPath === @@ -179,7 +124,7 @@ export const generateTypeDefsAndResolversFromSwagger = ( href } ` - // Add a new link for the highest version available & all the smaller versions available too + // Add a new link for the highest version available and 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}`