From 326dbeedf4a68dbb35f9d91d5aa1f0e449b2187b Mon Sep 17 00:00:00 2001 From: gbremond <125568626+gbremond@users.noreply.github.com> Date: Thu, 9 May 2024 17:59:43 +0200 Subject: [PATCH] Refactoring (#112) * refactor: clean up constructor process * refactor: clean up utility functions and generateSwagger * refactor: refactor generatePaths to make it safer and more generic * chore: fix package versions for npm audit fix compliance --- package-lock.json | 48 ++-- src/ServerlessAutoSwagger.ts | 291 ++++++++----------------- src/converters.ts | 138 ++++++++++++ src/helperFunctions.ts | 10 + src/types/serverless-plugin.types.d.ts | 2 + 5 files changed, 265 insertions(+), 224 deletions(-) create mode 100644 src/converters.ts diff --git a/package-lock.json b/package-lock.json index 45cbc0b..ffe699c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3926,9 +3926,9 @@ "dev": true }, "node_modules/cookiejar": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz", - "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "peer": true }, "node_modules/copyfiles": { @@ -6052,9 +6052,9 @@ "dev": true }, "node_modules/http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "peer": true }, "node_modules/http2-wrapper": { @@ -7425,9 +7425,9 @@ } }, "node_modules/json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "bin": { "json5": "lib/cli.js" @@ -10181,9 +10181,9 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, "node_modules/simple-git": { - "version": "3.15.1", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.15.1.tgz", - "integrity": "sha512-73MVa5984t/JP4JcQt0oZlKGr42ROYWC3BcUZfuHtT3IHKPspIvL0cZBnvPXF7LL3S/qVeVHVdYYmJ3LOTw4Rg==", + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.17.0.tgz", + "integrity": "sha512-JozI/s8jr3nvLd9yn2jzPVHnhVzt7t7QWfcIoDcqRIGN+f1IINGv52xoZti2kkYfoRhhRvzMSNPfogHMp97rlw==", "peer": true, "dependencies": { "@kwsites/file-exists": "^1.1.1", @@ -15091,9 +15091,9 @@ "dev": true }, "cookiejar": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz", - "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "peer": true }, "copyfiles": { @@ -16723,9 +16723,9 @@ "dev": true }, "http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "peer": true }, "http2-wrapper": { @@ -17761,9 +17761,9 @@ } }, "json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true }, "jsonfile": { @@ -19816,9 +19816,9 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, "simple-git": { - "version": "3.15.1", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.15.1.tgz", - "integrity": "sha512-73MVa5984t/JP4JcQt0oZlKGr42ROYWC3BcUZfuHtT3IHKPspIvL0cZBnvPXF7LL3S/qVeVHVdYYmJ3LOTw4Rg==", + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.17.0.tgz", + "integrity": "sha512-JozI/s8jr3nvLd9yn2jzPVHnhVzt7t7QWfcIoDcqRIGN+f1IINGv52xoZti2kkYfoRhhRvzMSNPfogHMp97rlw==", "peer": true, "requires": { "@kwsites/file-exists": "^1.1.1", diff --git a/src/ServerlessAutoSwagger.ts b/src/ServerlessAutoSwagger.ts index add3085..8b3d70b 100644 --- a/src/ServerlessAutoSwagger.ts +++ b/src/ServerlessAutoSwagger.ts @@ -5,80 +5,70 @@ import type { Options } from 'serverless'; import type { Service } from 'serverless/aws'; import type { Logging } from 'serverless/classes/Plugin'; import { getOpenApiWriter, getTypeScriptReader, makeConverter } from 'typeconv'; -import { removeStringFromArray, writeFile } from './helperFunctions'; +import { generateEmptySwagger, writeFile } from './helperFunctions'; import swaggerFunctions from './resources/functions'; import * as customPropertiesSchema from './schemas/custom-properties.schema.json'; import * as functionEventPropertiesSchema from './schemas/function-event-properties.schema.json'; import type { HttpMethod } from './types/common.types'; import type { + AutoSwaggerCustomConfig, CustomHttpApiEvent, CustomHttpEvent, CustomServerless, - HeaderParameters, - HttpResponses, - PathParameterPath, - PathParameters, - QueryStringParameters, - ServerlessCommand, + ServerlessCommands, ServerlessHooks, } from './types/serverless-plugin.types'; -import type { - Definition, - MethodSecurity, - Parameter, - Response, - SecurityDefinition, - Swagger, -} from './types/swagger.types'; +import type { Definition, MethodSecurity, SecurityDefinition, Swagger } from './types/swagger.types'; +import { formatResponses, httpEventToParameters, isHttpApiEvent, isHttpEvent } from './converters'; export default class ServerlessAutoSwagger { serverless: CustomServerless; + autoSwaggerCustomConfig: AutoSwaggerCustomConfig; + swagger: Swagger = generateEmptySwagger(); options: Options; - swagger: Swagger = { - swagger: '2.0', - info: { - title: '', - version: '1', - }, - paths: {}, - definitions: {}, - securityDefinitions: {}, - }; log: Logging['log']; - - commands: Record = {}; - hooks: ServerlessHooks = {}; + commands: ServerlessCommands; + hooks: ServerlessHooks; // IO is only injected in Serverless v3.0.0 (can experiment with `import { writeText, log, progress } from '@serverless/utils/log'; in a future PR) constructor(serverless: CustomServerless, options: Options, io?: Logging) { this.serverless = serverless; + this.autoSwaggerCustomConfig = this.serverless.service.custom?.autoswagger || {}; this.options = options; + this.log = this.setupLogging(io); + this.commands = this.getCustomCommands(); + this.hooks = this.getCustomLifecycleHooks(); + this.enrichServerlessSchema(); + } - if (io?.log) this.log = io.log; - else - this.log = { - notice: this.serverless.cli?.log ?? console.log, - error: console.error, - } as Logging['log']; - - this.registerOptions(); + private getCustomLifecycleHooks(): ServerlessHooks { + return { + 'generate-swagger:generateSwagger': this.generateSwagger, + 'before:offline:start:init': this.preDeploy, + 'before:package:cleanup': this.preDeploy, + }; + } - this.commands = { + private getCustomCommands(): ServerlessCommands { + return { 'generate-swagger': { usage: 'Generates Swagger for your API', lifecycleEvents: ['generateSwagger'], }, }; + } - this.hooks = { - 'generate-swagger:generateSwagger': this.generateSwagger, - 'before:offline:start:init': this.preDeploy, - 'before:package:cleanup': this.preDeploy, - }; + private setupLogging(io: Logging | undefined) { + if (io?.log) return io.log; + else + return { + notice: this.serverless.cli?.log ?? console.log, + error: console.error, + } as Logging['log']; } - registerOptions = () => { + enrichServerlessSchema = () => { // TODO: Test custom properties configuration this.serverless.configSchemaHandler?.defineCustomProperties(customPropertiesSchema); this.serverless.configSchemaHandler?.defineFunctionEventProperties('aws', 'http', functionEventPropertiesSchema); @@ -87,22 +77,33 @@ export default class ServerlessAutoSwagger { preDeploy = async () => { const stage = this.serverless.service.provider.stage; - const excludedStages = this.serverless.service.custom?.autoswagger?.excludeStages; - if (excludedStages?.includes(stage!)) { + const excludedStages = this.autoSwaggerCustomConfig.excludeStages; + if (excludedStages && excludedStages.includes(stage!)) { this.log.notice( `Swagger lambdas will not be deployed for stage [${stage}], as it has been marked for exclusion.` ); return; } - const generateSwaggerOnDeploy = this.serverless.service.custom?.autoswagger?.generateSwaggerOnDeploy ?? true; + const generateSwaggerOnDeploy = this.autoSwaggerCustomConfig.generateSwaggerOnDeploy ?? true; if (generateSwaggerOnDeploy) await this.generateSwagger(); this.addEndpointsAndLambda(); }; + generateSwagger = async () => { + await this.gatherTypes(); + this.gatherSwaggerOverrides(); + this.generateSecurity(); + this.generatePaths(); + + this.log.notice('Creating Swagger file...'); + const resourcesPath = await this.prepareResourceFolder(); + await this.writeSwaggerFile(resourcesPath); + }; + /** Updates this.swagger with serverless custom.autoswagger overrides */ gatherSwaggerOverrides = (): void => { - const autoswagger = this.serverless.service.custom?.autoswagger ?? {}; + const autoswagger = this.autoSwaggerCustomConfig; if (autoswagger.basePath) this.swagger.basePath = autoswagger.basePath; if (autoswagger.host) this.swagger.host = autoswagger.host; @@ -148,7 +149,7 @@ export default class ServerlessAutoSwagger { }); const { convert } = makeConverter(reader, writer); try { - const typeLocationOverride = this.serverless.service.custom?.autoswagger?.typefiles; + const typeLocationOverride = this.autoSwaggerCustomConfig.typefiles; const typesFile = typeLocationOverride || ['./src/types/api-types.d.ts']; await Promise.all( @@ -181,7 +182,7 @@ export default class ServerlessAutoSwagger { }; generateSecurity = (): void => { - const apiKeyHeaders = this.serverless.service.custom?.autoswagger?.apiKeyHeaders; + const apiKeyHeaders = this.autoSwaggerCustomConfig.apiKeyHeaders; if (apiKeyHeaders?.length) { const securityDefinitions: Record = {}; @@ -200,37 +201,44 @@ export default class ServerlessAutoSwagger { // that may be defined in a custom swagger json }; - generateSwagger = async () => { - await this.gatherTypes(); - this.gatherSwaggerOverrides(); - this.generateSecurity(); - this.generatePaths(); - - this.log.notice('Creating Swagger file...'); - + private async prepareResourceFolder() { // TODO enable user to specify swagger file path. also needs to update the swagger json endpoint. const packagePath = dirname(require.resolve('serverless-auto-swagger/package.json')); const resourcesPath = `${packagePath}/dist/resources`; await copy(resourcesPath, './swagger'); + return resourcesPath; + } - if (this.serverless.service.provider.runtime?.includes('python')) { - const swaggerStr = JSON.stringify(this.swagger, null, 2) - .replace(/true/g, 'True') - .replace(/false/g, 'False') - .replace(/null/g, 'None'); - let swaggerPythonString = `# this file was generated by serverless-auto-swagger`; - swaggerPythonString += `\ndocs = ${swaggerStr}`; - await writeFile('./swagger/swagger.py', swaggerPythonString); + private async writeSwaggerFile(resourcesPath: string) { + if (this.isPythonRuntime()) { + await this.writePythonSwaggerFile(); } else { - await copy(resourcesPath, './swagger', { - filter: (src) => src.slice(-2) === 'js', - }); + await this.writeJSSwaggerFile(resourcesPath); + } + } - const swaggerJavaScriptString = `// this file was generated by serverless-auto-swagger + private isPythonRuntime() { + return this.serverless.service.provider.runtime?.includes('python'); + } + + private async writePythonSwaggerFile() { + const swaggerStr = JSON.stringify(this.swagger, null, 2) + .replace(/true/g, 'True') + .replace(/false/g, 'False') + .replace(/null/g, 'None'); + let swaggerPythonString = `# this file was generated by serverless-auto-swagger`; + swaggerPythonString += `\ndocs = ${swaggerStr}`; + await writeFile('./swagger/swagger.py', swaggerPythonString); + } + private async writeJSSwaggerFile(resourcesPath: string) { + await copy(resourcesPath, './swagger', { + filter: (src) => src.slice(-2) === 'js', + }); + + const swaggerJavaScriptString = `// this file was generated by serverless-auto-swagger module.exports = ${JSON.stringify(this.swagger, null, 2)};`; - await writeFile('./swagger/swagger.js', swaggerJavaScriptString); - } - }; + await writeFile('./swagger/swagger.js', swaggerJavaScriptString); + } addEndpointsAndLambda = () => { this.serverless.service.functions = { @@ -239,7 +247,7 @@ export default class ServerlessAutoSwagger { }; }; - addSwaggerPath = (functionName: string, http: CustomHttpEvent | CustomHttpApiEvent | string) => { + addSwaggerPath = (functionName: string, http: CustomHttpEvent | CustomHttpApiEvent) => { if (typeof http === 'string') { // TODO they're using the shorthand - parse that into object. // You'll also have to remove the `typeof http !== 'string'` check from the function calling this one @@ -261,11 +269,11 @@ export default class ServerlessAutoSwagger { produces: http.produces ?? ['application/json'], security: http.security, // This is actually type `HttpEvent | HttpApiEvent`, but we can lie since only HttpEvent params (or shared params) are used - parameters: this.httpEventToParameters(http as CustomHttpEvent), - responses: this.formatResponses(http.responseData ?? http.responses), + parameters: httpEventToParameters(http as CustomHttpEvent), + responses: formatResponses(http.responseData ?? http.responses), }; - const apiKeyHeaders = this.serverless.service.custom?.autoswagger?.apiKeyHeaders; + const apiKeyHeaders = this.autoSwaggerCustomConfig.apiKeyHeaders; const security: MethodSecurity[] = []; @@ -284,130 +292,13 @@ export default class ServerlessAutoSwagger { const functions = this.serverless.service.functions ?? {}; Object.entries(functions).forEach(([functionName, config]) => { const events = config.events ?? []; - events - .map((event) => event.http || event.httpApi) - .filter((http) => !!http && typeof http !== 'string' && !http.exclude) - .forEach((http) => this.addSwaggerPath(functionName, http!)); - }); - }; - - formatResponses = (responseData: HttpResponses | undefined) => { - if (!responseData) { - // could throw error - return { 200: { description: '200 response' } }; - } - const formatted: Record = {}; - Object.entries(responseData).forEach(([statusCode, responseDetails]) => { - if (typeof responseDetails == 'string') { - formatted[statusCode] = { - description: responseDetails, - }; - return; - } - const response: Response = { description: responseDetails.description || `${statusCode} response` }; - if (responseDetails.bodyType) { - response.schema = { $ref: `#/definitions/${responseDetails.bodyType}` }; - } - - formatted[statusCode] = response; - }); - - return formatted; - }; - - // httpEventToSecurity = (http: EitherHttpEvent) => { - // // TODO - add security sections - // return undefined - // } - - pathToParam = (pathParam: string, paramInfoOrRequired?: PathParameterPath[string]): Parameter => { - const isObj = typeof paramInfoOrRequired === 'object'; - const required = (isObj ? paramInfoOrRequired.required : paramInfoOrRequired) ?? true; - - return { - name: pathParam, - in: 'path', - required, - description: isObj ? paramInfoOrRequired.description : undefined, - type: 'string', - }; - }; - - // The arg is actually type `HttpEvent | HttpApiEvent`, but we only use it if it has httpEvent props (or shared props), - // so we can lie to the compiler to make typing simpler - httpEventToParameters = (httpEvent: CustomHttpEvent): Parameter[] => { - const parameters: Parameter[] = []; - - if (httpEvent.bodyType) { - parameters.push({ - in: 'body', - name: 'body', - description: 'Body required in the request', - required: true, - schema: { $ref: `#/definitions/${httpEvent.bodyType}` }, + events.forEach((event) => { + if (isHttpEvent(event) && !event.http.exclude) { + this.addSwaggerPath(functionName, event.http); + } else if (isHttpApiEvent(event) && !event.httpApi.exclude) { + this.addSwaggerPath(functionName, event.httpApi); + } }); - } - - const rawPathParams: PathParameters['path'] = httpEvent.request?.parameters?.paths; - const match = httpEvent.path.match(/[^{}]+(?=})/g); - let pathParameters: string[] = match ?? []; - - if (rawPathParams) { - Object.entries(rawPathParams ?? {}).forEach(([param, paramInfo]) => { - parameters.push(this.pathToParam(param, paramInfo)); - pathParameters = removeStringFromArray(pathParameters, param); - }); - } - - // If no match, will just be [] anyway - pathParameters.forEach((param: string) => parameters.push(this.pathToParam(param))); - - if (httpEvent.headerParameters || httpEvent.request?.parameters?.headers) { - // If no headerParameters are provided, try to use the builtin headers - const rawHeaderParams: HeaderParameters = - httpEvent.headerParameters ?? - Object.entries(httpEvent.request!.parameters!.headers!).reduce( - (acc, [name, required]) => ({ ...acc, [name]: { required, type: 'string' } }), - {} - ); - - Object.entries(rawHeaderParams).forEach(([param, data]) => { - parameters.push({ - in: 'header', - name: param, - required: data.required ?? false, - type: data.type ?? 'string', - description: data.description, - }); - }); - } - - if (httpEvent.queryStringParameters || httpEvent.request?.parameters?.querystrings) { - // If no queryStringParameters are provided, try to use the builtin query strings - const rawQueryParams: QueryStringParameters = - httpEvent.queryStringParameters ?? - Object.entries(httpEvent.request!.parameters!.querystrings!).reduce( - (acc, [name, required]) => ({ ...acc, [name]: { required, type: 'string' } }), - {} - ); - - Object.entries(rawQueryParams).forEach(([param, data]) => { - parameters.push({ - in: 'query', - name: param, - type: data.type ?? 'string', - description: data.description, - required: data.required ?? false, - ...(data.type === 'array' - ? { - items: { type: data.arrayItemsType }, - collectionFormat: 'multi', - } - : {}), - }); - }); - } - - return parameters; + }); }; } diff --git a/src/converters.ts b/src/converters.ts new file mode 100644 index 0000000..89d3ed6 --- /dev/null +++ b/src/converters.ts @@ -0,0 +1,138 @@ +import { + CustomHttpApiEvent, + CustomHttpEvent, + HeaderParameters, + HttpResponses, + PathParameterPath, + PathParameters, + QueryStringParameters, + ServerlessFunctionEvent, +} from './types/serverless-plugin.types'; +import { Parameter, Response } from './types/swagger.types'; +import { removeStringFromArray } from './helperFunctions'; + +export const formatResponses = (responseData: HttpResponses | undefined): Record => { + if (!responseData) { + // could throw error + return { 200: { description: '200 response' } }; + } + const formatted: Record = {}; + Object.entries(responseData).forEach(([statusCode, responseDetails]) => { + if (typeof responseDetails == 'string') { + formatted[statusCode] = { + description: responseDetails, + }; + return; + } + const response: Response = { description: responseDetails.description || `${statusCode} response` }; + if (responseDetails.bodyType) { + response.schema = { $ref: `#/definitions/${responseDetails.bodyType}` }; + } + + formatted[statusCode] = response; + }); + + return formatted; +}; + +// httpEventToSecurity = (http: EitherHttpEvent) => { +// // TODO - add security sections +// return undefined +// } + +// The arg is actually type `HttpEvent | HttpApiEvent`, but we only use it if it has httpEvent props (or shared props), +// so we can lie to the compiler to make typing simpler +export const httpEventToParameters = (httpEvent: CustomHttpEvent): Parameter[] => { + const parameters: Parameter[] = []; + + if (httpEvent.bodyType) { + parameters.push({ + in: 'body', + name: 'body', + description: 'Body required in the request', + required: true, + schema: { $ref: `#/definitions/${httpEvent.bodyType}` }, + }); + } + + const rawPathParams: PathParameters['path'] = httpEvent.request?.parameters?.paths; + const match = httpEvent.path.match(/[^{}]+(?=})/g); + let pathParameters: string[] = match ?? []; + + if (rawPathParams) { + Object.entries(rawPathParams ?? {}).forEach(([param, paramInfo]) => { + parameters.push(pathToParam(param, paramInfo)); + pathParameters = removeStringFromArray(pathParameters, param); + }); + } + + // If no match, will just be [] anyway + pathParameters.forEach((param: string) => parameters.push(pathToParam(param))); + + if (httpEvent.headerParameters || httpEvent.request?.parameters?.headers) { + // If no headerParameters are provided, try to use the builtin headers + const rawHeaderParams: HeaderParameters = + httpEvent.headerParameters ?? + Object.entries(httpEvent.request!.parameters!.headers!).reduce( + (acc, [name, required]) => ({ ...acc, [name]: { required, type: 'string' } }), + {} + ); + + Object.entries(rawHeaderParams).forEach(([param, data]) => { + parameters.push({ + in: 'header', + name: param, + required: data.required ?? false, + type: data.type ?? 'string', + description: data.description, + }); + }); + } + + if (httpEvent.queryStringParameters || httpEvent.request?.parameters?.querystrings) { + // If no queryStringParameters are provided, try to use the builtin query strings + const rawQueryParams: QueryStringParameters = + httpEvent.queryStringParameters ?? + Object.entries(httpEvent.request!.parameters!.querystrings!).reduce( + (acc, [name, required]) => ({ ...acc, [name]: { required, type: 'string' } }), + {} + ); + + Object.entries(rawQueryParams).forEach(([param, data]) => { + parameters.push({ + in: 'query', + name: param, + type: data.type ?? 'string', + description: data.description, + required: data.required ?? false, + ...(data.type === 'array' + ? { + items: { type: data.arrayItemsType }, + collectionFormat: 'multi', + } + : {}), + }); + }); + } + + return parameters; +}; +export const pathToParam = (pathParam: string, paramInfoOrRequired?: PathParameterPath[string]): Parameter => { + const isObj = typeof paramInfoOrRequired === 'object'; + const required = (isObj ? paramInfoOrRequired.required : paramInfoOrRequired) ?? true; + + return { + name: pathParam, + in: 'path', + required, + description: isObj ? paramInfoOrRequired.description : undefined, + type: 'string', + }; +}; + +export const isHttpEvent = (event: ServerlessFunctionEvent): event is { http: CustomHttpEvent } => { + return event.http !== undefined; +}; +export const isHttpApiEvent = (event: ServerlessFunctionEvent): event is { httpApi: CustomHttpApiEvent } => { + return event.httpApi !== undefined; +}; diff --git a/src/helperFunctions.ts b/src/helperFunctions.ts index f41717b..2250b6a 100644 --- a/src/helperFunctions.ts +++ b/src/helperFunctions.ts @@ -40,3 +40,13 @@ export const recursiveFixAnyOf = (definition: Definition) => { return definition; }; +export const generateEmptySwagger = () => ({ + swagger: '2.0', + info: { + title: '', + version: '1', + }, + paths: {}, + definitions: {}, + securityDefinitions: {}, +}); diff --git a/src/types/serverless-plugin.types.d.ts b/src/types/serverless-plugin.types.d.ts index 7722a83..7e39847 100644 --- a/src/types/serverless-plugin.types.d.ts +++ b/src/types/serverless-plugin.types.d.ts @@ -148,4 +148,6 @@ export interface ServerlessCommand { usage?: string; } +export type ServerlessCommands = Record; + export type ServerlessHooks = Record Promise>;