import path from 'path'; import * as plugins from './smartswagger.plugins'; interface RedocProps { 'x-tagGroups'?: {name: string, tags: string[]}[]; } type IExtendedApiDoc = plugins.openapiTypes.OpenAPIV3.Document & RedocProps; export class Smartswagger { // STATIC /** * creates a new Smartswagger instance from an external document * @param urlArg a url arg that contains an original swagger.json in the response * @returns an instance of */ public static async createFromUrl(urlArg: string) { const jsonResponse = await plugins.nodeFetch(urlArg, { headers: { accept: 'application/json', }, }); const apiDoc = await jsonResponse.json(); const newSMartswaggerInstance = new Smartswagger(apiDoc); return newSMartswaggerInstance; } public static async createNew(titleArg: string = 'an OpenApiDoc') { const newSMartswaggerInstance = new Smartswagger({ openapi: '3.0.0', paths: {}, info: { title: titleArg, version: '1.0.0' } }); return newSMartswaggerInstance; } // INSTANCE /** * the basic info of the api doc */ public baseInfo: plugins.openapiTypes.OpenAPIV3.InfoObject; public apiDoc: IExtendedApiDoc; constructor(apiDocArg: IExtendedApiDoc) { this.apiDoc = apiDocArg; } /** * dereferences the document at hand */ public async deref() { this.apiDoc = (await plugins.swaggerParser.dereference(this.apiDoc)) as IExtendedApiDoc; } public async addServer(serverArg: plugins.openapiTypes.OpenAPIV3.ServerObject) { await this.deref(); this.apiDoc.servers = this.apiDoc.servers || []; this.apiDoc.servers.push(serverArg); } /** * merge a document from url * @param documentToMergeArg * @param basePathArg */ public async mergeDocument(documentToMergeArg: IExtendedApiDoc, basePathArg: string = '', tagArg?: string) { console.log(`merging document with name ${documentToMergeArg.info?.title}`); await this.deref(); // lets get a dereferenced version of the document we want to merge const documentToMerge = (await plugins.swaggerParser.dereference( documentToMergeArg )) as IExtendedApiDoc; for (const path of Object.keys(documentToMerge.paths)) { const pathToMerge = plugins.path.join(basePathArg, path); this.apiDoc.paths = this.apiDoc.paths || {}; this.apiDoc.paths[pathToMerge] = documentToMerge.paths[path]; if (tagArg) { if (this.apiDoc.paths[pathToMerge].post) { this.apiDoc.paths[pathToMerge].post.tags = this.apiDoc.paths[pathToMerge].post.tags || []; this.apiDoc.paths[pathToMerge].post.tags.push(tagArg); } if (this.apiDoc.paths[pathToMerge].get) { this.apiDoc.paths[pathToMerge].get.tags = this.apiDoc.paths[pathToMerge].get.tags || []; this.apiDoc.paths[pathToMerge].get.tags.push(tagArg); } if (this.apiDoc.paths[pathToMerge].put) { this.apiDoc.paths[pathToMerge].put.tags = this.apiDoc.paths[pathToMerge].put.tags || []; this.apiDoc.paths[pathToMerge].put.tags.push(tagArg); } if (this.apiDoc.paths[pathToMerge].delete) { this.apiDoc.paths[pathToMerge].delete.tags = this.apiDoc.paths[pathToMerge].delete.tags || []; this.apiDoc.paths[pathToMerge].delete.tags.push(tagArg); } } } // merge tag groups this.apiDoc['x-tagGroups'] = this.apiDoc['x-tagGroups'] || []; if (documentToMerge['x-tagGroups']) { for (const xTagGroup of documentToMerge['x-tagGroups']) { this.apiDoc['x-tagGroups'].push(xTagGroup); } } console.log('merged!'); // set custom tag arg. if (tagArg) { this.apiDoc['x-tagGroups'].push({name: tagArg, tags: [tagArg]}); } } /** * merges a document from url */ public async mergeDocumentFromUrl(documentUrlArg: string, basePathArg: string = '', tagArg?: string) { console.log(`getting document at ${documentUrlArg} for merging...`); const documentResponse = await plugins.nodeFetch(documentUrlArg, { headers: { 'accept-encoding': 'application/json', }, }).catch(err => { console.log(err); }); if (!documentResponse || documentResponse.status > 299) { return; } const documentString = await documentResponse.text(); const apiDoc: IExtendedApiDoc = JSON.parse(documentString); console.log(`document successfully fetched!`); await this.mergeDocument(apiDoc, basePathArg, tagArg); } /** * merge multiple documents in parallel * @param urlArrayArg */ public async mergeManyDocumentsFromUrl(urlArrayArg: { url: string; basePath?: string, tagArg?: string }[]) { const promiseArray: Promise[] = []; for (const urlArg of urlArrayArg) { promiseArray.push(this.mergeDocumentFromUrl(urlArg.url, urlArg.basePath, urlArg.tagArg)); } await Promise.all(promiseArray); } /** * merges a component to routes based on regex */ public mergeComponentToRoutes( routeDescriptor: { includeGlobArray: string[]; excludeGlobArray: string[]; }, componentArg: plugins.openapiTypes.OpenAPIV3.ComponentsObject ) { for (const pathCandidateRoute of Object.keys(this.apiDoc.paths)) { let included: boolean = false; let excluded: boolean = false; // We are using glob espressions here due to easier path expressions for (const globExpression of routeDescriptor.includeGlobArray) { if (plugins.matcher.isMatch(pathCandidateRoute, globExpression)) { included = true; break; } } if (included) { for (const globExpression of routeDescriptor.excludeGlobArray) { if (plugins.matcher.isMatch(pathCandidateRoute, globExpression)) { excluded = true; break; } } } if (included && !excluded) { // lets do the actual component inclusion const pathCandidate = this.apiDoc.paths[pathCandidateRoute]; const instrumentMethod = (methodArg: typeof pathCandidate.get) => { if (!methodArg) { return; } if (componentArg.securitySchemes) { methodArg.security = methodArg.security || []; for (const securityScheme of Object.keys(componentArg.securitySchemes)) { methodArg.security.push({ [securityScheme]: [] }) } } }; instrumentMethod(pathCandidate.get); instrumentMethod(pathCandidate.post); instrumentMethod(pathCandidate.put); instrumentMethod(pathCandidate.delete); } } } private cacheResult: string = ''; private cacheCreationUnixTimestamp: number; /** * returns an express middleware using 'swagger-ui-express' */ public getSlashApiUiMiddleware() { return (req: plugins.smartexpress.Request, res: plugins.smartexpress.Response) => { res.setHeader('content-type', 'text/html'); res.write(` ${this.apiDoc.info?.title || 'no name set'} - SwaggerUI
`); res.end(); }; } public getSlashRedocMiddleware() { return (req: plugins.smartexpress.Request, res: plugins.smartexpress.Response) => { res.setHeader('content-type', 'text/html'); res.write(` ${this.apiDoc.info?.title || 'no name set'} - Redoc `); res.end(); }; } public getSlashApiSchemaMiddleware() { return async (req: plugins.smartexpress.Request, res: plugins.smartexpress.Response) => { res.header('content-type', 'application/json'); res.write(JSON.stringify(this.apiDoc)); res.end(); }; } }