Skip to content

Commit

Permalink
💡 Add comments
Browse files Browse the repository at this point in the history
  • Loading branch information
adbouygues committed Jul 12, 2024
1 parent f3e9e3d commit 7737f21
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 79 deletions.
73 changes: 68 additions & 5 deletions packages/graphql-mesh/utils/ConfigFromSwaggers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ export default class ConfigFromSwaggers {
this.specs = this.swaggers.map(
(swagger) => <Spec>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
Expand All @@ -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<string, string[]>} 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<string, string[]> {
this.specs.forEach((s) => {
const { schemas } = s.components || {}
const entries = Object.entries(schemas || {}).filter(([_, value]) =>
Expand Down Expand Up @@ -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
}
})
Expand All @@ -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(
Expand All @@ -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) => ({
Expand All @@ -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(
Expand All @@ -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
Expand Down
93 changes: 19 additions & 74 deletions packages/graphql-mesh/utils/generateTypeDefsAndResolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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: {} }
Expand All @@ -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) => {
Expand All @@ -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)
Expand All @@ -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 ===
Expand Down Expand Up @@ -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}`
Expand Down

0 comments on commit 7737f21

Please sign in to comment.