UNPKG

serverless-openapi-documenter

Version:

Generate OpenAPI v3 documentation and Postman Collections from your Serverless Config

284 lines (238 loc) 7.79 kB
"use strict"; const isEqual = require("node:util").isDeepStrictEqual; const path = require("path"); const $RefParser = require("@apidevtools/json-schema-ref-parser"); const SchemaConvertor = require("json-schema-for-openapi"); const { v4: uuid } = require("uuid"); class SchemaHandler { constructor(serverless, openAPI, logger) { this.logger = logger; this.apiGatewayModels = serverless.service?.provider?.apiGateway?.request?.schemas || {}; this.documentation = serverless.service.custom.documentation; this.openAPI = openAPI; this.modelReferences = {}; this.__standardiseModels(); try { this.logger.verbose( `Trying to resolve Ref-Parser config from: ${path.resolve( "options", "ref-parser.js" )}` ); this.refParserOptions = require(path.resolve("options", "ref-parser.js")); } catch (err) { this.refParserOptions = {}; } } /** * Standardises the models to a specific format */ __standardiseModels() { const standardModel = (model) => { if (model.schema) { return model; } const contentType = Object.keys(model.content)[0]; model.contentType = contentType; model.schema = model.content[contentType].schema; return model; }; const standardisedModels = this.documentation?.models?.map(standardModel) || []; const standardisedModelsList = this.documentation?.modelsList?.map(standardModel) || []; const standardisedGatewayModels = Object.keys(this.apiGatewayModels).flatMap((key) => { const gatewayModel = this.apiGatewayModels[key]; return standardModel(gatewayModel); }) || []; this.models = standardisedModels.concat( standardisedModelsList, standardisedGatewayModels ); } async addModelsToOpenAPI() { for (const model of this.models) { const modelName = model.name; const modelSchema = model.schema; const convertedSchemas = await this.__dereferenceAndConvert( modelSchema, modelName, model ).catch((err) => { if (err instanceof Error) throw err; else return err; }); if ( typeof convertedSchemas.schemas === "object" && !Array.isArray(convertedSchemas.schemas) && convertedSchemas.schemas !== null ) { for (const [schemaName, schemaValue] of Object.entries( convertedSchemas.schemas )) { if (schemaName === modelName) { this.modelReferences[ schemaName ] = `#/components/schemas/${modelName}`; } this.__addToComponents("schemas", schemaValue, schemaName); } } else { throw new Error( `There was an error converting the ${ model.name } schema. Model received looks like: \n\n${JSON.stringify( model )}. The convereted schema looks like \n\n${JSON.stringify( convertedSchemas )}` ); } } } async createSchema(name, schema) { let originalName = name; let finalName = name; if (this.modelReferences[name] && schema === undefined) { return this.modelReferences[name]; } const convertedSchemas = await this.__dereferenceAndConvert(schema, name, { name, schema, }).catch((err) => { throw err; }); for (const [schemaName, schemaValue] of Object.entries( convertedSchemas.schemas )) { if (this.__existsInComponents(schemaName)) { if (this.__isTheSameSchema(schemaValue, schemaName) === false) { if (schemaName === originalName) { finalName = `${schemaName}-${uuid()}`; this.__addToComponents("schemas", schemaValue, finalName); } else { this.__addToComponents("schemas", schemaValue, schemaName); } } } else { this.__addToComponents("schemas", schemaValue, schemaName); } } return `#/components/schemas/${finalName}`; } async __dereferenceAndConvert(schema, name, model) { this.logger.verbose(`dereferencing model: ${name}`); const dereferencedSchema = await this.__dereferenceSchema(schema).catch( (err) => { this.__checkForHTTPErrorsAndThrow(err, model); this.__checkForMissingPathAndThrow(err); return schema; } ); this.logger.verbose( `dereferenced model: ${JSON.stringify(dereferencedSchema)}` ); this.logger.verbose(`converting model: ${name}`); const convertedSchemas = SchemaConvertor.convert(dereferencedSchema, name); this.logger.verbose( `converted schemas: ${JSON.stringify(convertedSchemas)}` ); return convertedSchemas; } async __dereferenceSchema(schema) { const bundledSchema = await $RefParser .bundle(schema, this.refParserOptions) .catch((err) => { throw err; }); let deReferencedSchema = await $RefParser .dereference(bundledSchema, this.refParserOptions) .catch((err) => { throw err; }); // deal with schemas that have been de-referenced poorly: naive if (deReferencedSchema?.$ref === "#") { const oldRef = bundledSchema.$ref; const path = oldRef.split("/"); const pathTitle = path[path.length - 1]; const referencedProperties = deReferencedSchema.definitions[pathTitle]; Object.assign(deReferencedSchema, { ...referencedProperties }); delete deReferencedSchema.$ref; deReferencedSchema = await this.__dereferenceSchema( deReferencedSchema ).catch((err) => { throw err; }); } return deReferencedSchema; } /** * @function existsInComponents * @param {string} name - The name of the Schema * @returns {boolean} Whether it exists in components already */ __existsInComponents(name) { return Boolean(this.openAPI?.components?.schemas?.[name]); } /** * @function isTheSameSchema * @param {object} schema - The schema value * @param {string} otherSchemaName - The name of the schema * @returns {boolean} Whether the schema provided is the same one as in components already */ __isTheSameSchema(schema, otherSchemaName) { return isEqual(schema, this.openAPI.components.schemas[otherSchemaName]); } /** * @function addToComponents * @param {string} type - The component type * @param {object} schema - The schema * @param {string} name - The name of the schema */ __addToComponents(type, schema, name) { const schemaObj = { [name]: schema, }; if (this.openAPI?.components) { if (this.openAPI.components[type]) { Object.assign(this.openAPI.components[type], schemaObj); } else { Object.assign(this.openAPI.components, { [type]: schemaObj }); } } else { const components = { components: { [type]: schemaObj, }, }; Object.assign(this.openAPI, components); } } __checkForMissingPathAndThrow(error) { if (error.message === "Expected a file path, URL, or object. Got undefined") throw error; } __checkForHTTPErrorsAndThrow(error, model) { if (error.errors) { for (const err of error?.errors) { this.__HTTPError(err, model); } } else { this.__HTTPError(error, model); } } __HTTPError(error, model) { if (error.message.includes("HTTP ERROR")) { throw new Error( `There was an error dereferencing ${ model.name } schema. \n\n dereferencing message: ${ error.message } \n\n Model received: ${JSON.stringify(model)}` ); } } } module.exports = SchemaHandler;