UNPKG

kobp

Version:
251 lines 8.96 kB
import { withDocument, KobpError, ServerErrorCode, Loggy } from '..'; export const METADATA_KEYS = { // compiled documents DOC_KEY: 'document', // - compiled query shape DOC_HEADERS_SHAPE_VALIDATION_KEY: 'document:validate:headers', // - compiled query shape DOC_QUERY_SHAPE_VALIDATION_KEY: 'document:validate:query', // - compiled body shape DOC_BODY_SHAPE_VALIDATION_KEY: 'document:validate:body', // - compiled param shape DOC_PARAMS_SHAPE_VALIDATION_KEY: 'document:validate:param', }; export const ALL_METADATA_KEYS = new Set(Object.values(METADATA_KEYS)); // Check if zod-to-json-schema is installed const z2js = require('zod-to-json-schema')?.zodToJsonSchema; // schema extraction utils const isZod = (o) => /^Zod/.test(`${o?._def?.typeName}`); const isAjv = (o) => Boolean(o?._ajv); /** * Attempt to extract schema from the SchemaObject | KobpParsable object * * NOTE: we are over simplify here as we believe that .schema would returns `OpenAPI31.SchemaObject` (or its compatible ones) */ export const extractSchema = (spec, required = false, mode = 'read') => { if (z2js && isZod(spec)) { const forDocuments = z2js(spec, { target: 'openApi3', }); return ['zod', forDocuments]; } if (isAjv(spec) && spec.schema) { return ['ajv', spec.schema]; } if (mode === 'read' && spec.readonlySchema) { return ['literal', spec.readonlySchema]; } if (spec.schema) { return ['literal', spec.schema]; } if (required) { // Given Loggy.error('Failed to extract schema from', JSON.stringify(spec)); throw KobpError.fromServer(ServerErrorCode.notImplemented, 'You are using invalid schema provider. If you are using Zod, please install zod-to-json-schema (https://www.npmjs.com/package/zod-to-json-schema). If your are using other interface please make sure it has `schema` property that provides OpenAPI3.1 Schema compatible object!'); } return undefined; }; export class OperationDocumentBuilder { constructor(baseDoc) { this.wrapJsonResult = true; // try extract documents from other sources this.doc = { ...(baseDoc || {}) }; } /** * replace the current document */ from(baseDoc) { this.doc = { ...(baseDoc || {}) }; return this; } deprecated(deprecated) { this.doc.deprecated = deprecated; return this; } describe(description) { this.doc.description = description; return this; } summary(summary) { this.doc.summary = summary; return this; } /** * Given authorization scheme MUST matched those defined in * the Swagger's controller (or Module's) * * @param {string} schemeName - name of the security handling example: 'bearer' | 'api-key' | 'basic' * @param {string[]} detail - detail of the security object */ authorizeWith(schemeName, detail = []) { this.doc.security = [{ [schemeName]: detail }]; return this; } useHeader(nameOrMap, doc) { return this.useParameters('header', nameOrMap, doc); } usePath(nameOrMap, doc) { return this.useParameters('path', nameOrMap, doc); } useQuery(nameOrMap, doc) { return this.useParameters('query', nameOrMap, doc); } useCookie(nameOrMap, doc) { return this.useParameters('cookie', nameOrMap, doc); } useParameters(location, nameOrMap, doc) { // ensure parameter doc exists. this.doc.parameters = this.doc.parameters || []; if (typeof nameOrMap === 'string') { this.doc.parameters.push({ ...doc, in: location, name: nameOrMap }); } else { Object.entries(nameOrMap).forEach(([name, doc]) => { this.doc.parameters.push({ ...doc, in: location, name }); }); } return this; } // used by swagger controller useParameter(location, name, doc) { const params = (this.doc.parameters || []); // safe push if (params.findIndex((p) => p.name === name && p.in === location) >= 0) { return this; } params.push({ ...doc, in: location, name }); this.doc.parameters = params; return this; } /** * This method will override the validation body's middleware injection if used. */ useBody(requestBody) { this.doc.requestBody = requestBody; return this; } /** * Server successfully processed the request */ onOk(schema, rest) { return this.onResponse(200, { description: 'Successful', }, schema ? { schema: extractSchema(schema, true)[1], ...rest, } : rest); } /** * Server accepted the request, payload is still being processed */ onOkAccepted() { return this.onResponse(202, { description: 'Accepted' }); } /** * Server successfully processed the request and is not returning any content */ onOkNoContent() { return this.onResponse(204, { description: 'Successful without content' }); } /** * Server rejected the request due to invalid user's input */ onErrorBadRequest(contentOrMessage) { return this.onErrorResponse(400, 'Bad request', contentOrMessage); } onErrorUnauthorized(contentOrMessage) { return this.onErrorResponse(401, 'Unauthorized', contentOrMessage); } onErrorForbidden(contentOrMessage) { return this.onErrorResponse(403, 'Forbidden', contentOrMessage); } onErrorNotFound(contentOrMessage) { return this.onErrorResponse(404, 'Resource not found', contentOrMessage); } onErrorInternal(contentOrMessage) { return this.onErrorResponse(500, 'Internal server error', contentOrMessage); } onErrorResponse(status, defaultMessage, contentOrMessage) { if (typeof contentOrMessage === 'string') { return this.onResponse(status, { description: defaultMessage }, { schema: { type: 'object', properties: { error: { type: 'string', default: contentOrMessage, }, }, }, }); } return this.onResponse(status, { description: defaultMessage }, contentOrMessage); } onResponse(status, doc, content) { this.doc.responses = this.doc.responses || {}; this.doc.responses[status] = { ...doc }; if (content) { if (this.wrapJsonResult && content.schema) { if (status >= 200 && status < 400) { // Success case content = { ...content, schema: { type: 'object', properties: { success: { type: 'boolean', default: true, }, data: content.schema, }, }, }; } else { // Error case content = { ...content, schema: { allOf: [ { type: 'object', properties: { success: { type: 'boolean', default: false, }, type: { type: 'string', default: 'kobp', }, }, }, content.schema, ], }, }; } } this.doc.responses[status].content = { 'application/json': content, // TODO: add wrapper? // TODO: Add other response types (Workaround, use withDocument) }; } return this; } /** * useful for swagger controller to access the output document directly * * @returns {OperationObject} OpenAPI operation object */ build() { return this.doc; } middleware() { return withDocument(this.doc); } } //# sourceMappingURL=doc.helpers.js.map