UNPKG

node-web-mvc

Version:
303 lines (302 loc) 14.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); /** * @module OpenApiModel * @description 用于构建当前环境的openapi.json */ const path_1 = __importDefault(require("path")); const fs_1 = __importDefault(require("fs")); const schemas_1 = __importDefault(require("./schemas")); const RequestMapping_1 = __importDefault(require("../../servlets/annotations/mapping/RequestMapping")); const RuntimeAnnotation_1 = __importDefault(require("../../servlets/annotations/annotation/RuntimeAnnotation")); const ApiImplicitParams_1 = __importDefault(require("../annotations/ApiImplicitParams")); const ParamAnnotation_1 = __importDefault(require("../../servlets/annotations/params/ParamAnnotation")); const ApiOperation_1 = __importDefault(require("../annotations/ApiOperation")); const Controller_1 = __importDefault(require("../../servlets/annotations/Controller")); const Api_1 = __importDefault(require("../annotations/Api")); const ElementType_1 = __importDefault(require("../../servlets/annotations/annotation/ElementType")); const ApiIgnore_1 = __importDefault(require("../annotations/ApiIgnore")); const ServletResponse_1 = __importDefault(require("../../servlets/annotations/params/ServletResponse")); const ServletRequest_1 = __importDefault(require("../../servlets/annotations/params/ServletRequest")); const Javascript_1 = __importDefault(require("../../interface/Javascript")); const HttpEntity_1 = __importDefault(require("../../servlets/models/HttpEntity")); const metadata_1 = require("../../servlets/annotations/annotation/metadata"); const emptyOf = (v, defaultValue) => (v === null || v === undefined || v === '') ? defaultValue : v; class OpenApiModel { /** * 驼峰命名转换成 - 符号链接 */ clampToJoinName(name) { const k = name.length; let newName = ''; for (let i = 0; i < k; i++) { const code = name.charCodeAt(i); const isUpperCase = code >= 65 && code <= 90; const joinChar = isUpperCase ? '-' : ''; newName = newName + joinChar + (name[i]).toLowerCase(); } return newName.replace(/^-/, ''); } /** * 将 -转换成小驼峰命名 */ toClamp(name) { const segments = name.split('-'); return segments.map((m, i) => { if (i === 0) { return m[0].toLowerCase() + m.slice(1); } return m[0].toUpperCase() + m.slice(1); }).join(''); } createTags(annotation) { var _a; const apiAnno = (_a = RuntimeAnnotation_1.default.getClassAnnotation(annotation.ctor, Api_1.default)) === null || _a === void 0 ? void 0 : _a.nativeAnnotation; const name = this.clampToJoinName(annotation.ctor.name); return ((apiAnno === null || apiAnno === void 0 ? void 0 : apiAnno.tags) || [{ name: name, description: (apiAnno === null || apiAnno === void 0 ? void 0 : apiAnno.description) || name, externalDocs: apiAnno === null || apiAnno === void 0 ? void 0 : apiAnno.externalDocs, }]); } readPkg(id) { if (fs_1.default.existsSync(id)) { return JSON.parse(fs_1.default.readFileSync(id).toString('utf-8')); } else { return {}; } } /** * 获取完整的swaager openapi.json */ build(contextPath) { var _a, _b; const operationIds = {}; const id = require.resolve(path_1.default.resolve('package.json')); delete require.cache[id]; // TODO: low 如果要支持mjs场景这里需要考虑如何改造 // eslint-disable-next-line @typescript-eslint/no-var-requires const pkg = this.readPkg(id); const tags = []; const paths = {}; const contributor = (pkg.contributors || [])[0] || {}; const controllers = RuntimeAnnotation_1.default.getAnnotations(Controller_1.default); const definition = new schemas_1.default(); for (const controller of controllers) { if (RuntimeAnnotation_1.default.hasClassAnnotation(controller.ctor, ApiIgnore_1.default)) { // 如果忽略该控制器 continue; } // 创建tags const controllerTags = this.createTags(controller); const actions = RuntimeAnnotation_1.default.getAnnotations(RequestMapping_1.default, controller.ctor).filter((m) => m.elementType === ElementType_1.default.METHOD); for (const action of actions) { // 构建操作 this.buildOperation(paths, action, controllerTags, definition, operationIds); } tags.push(...controllerTags); } return { info: { 'contact': { email: contributor.email || pkg.author || '', }, 'license': (_b = (_a = pkg.licenses) === null || _a === void 0 ? void 0 : _a.map) === null || _b === void 0 ? void 0 : _b.call(_a, (k) => { return { name: k.type, url: k.url, }; }), 'title': pkg.name || '', 'version': pkg.version || '1.0.0', 'description': pkg.description || '', }, tags: tags.sort((a, b) => { if (a.name > b.name) { return 1; } else if (a.name == b.name) { return 0; } else { return -1; } }), paths: paths, servers: [ { url: contextPath }, ], components: { schemas: definition.build(), }, openapi: '3.0.1', }; } /** * 创建api操作的所有paths */ buildOperation(paths, action, tags, definition, operationIds) { var _a; const apiOperation = (_a = RuntimeAnnotation_1.default.getMethodAnnotation(action.ctor, action.methodName, ApiOperation_1.default)) === null || _a === void 0 ? void 0 : _a.nativeAnnotation; const mapping = RequestMapping_1.default.getMappingInfo(action.ctor, action.methodName); if (!mapping) { return; } const utags = (apiOperation === null || apiOperation === void 0 ? void 0 : apiOperation.tags) || tags.map((tag) => tag.name); let operationId = action.methodName; if (operationIds[operationId]) { operationId = this.toClamp(`${utags[0]}-${operationId}`); } operationIds[action.methodName] = true; const returnType = action.returnType; const code = (apiOperation === null || apiOperation === void 0 ? void 0 : apiOperation.code) || '200'; const consumes = (apiOperation === null || apiOperation === void 0 ? void 0 : apiOperation.consumes) || mapping.consumes || []; const parameters = this.buildOperationParameters(action, definition); const operationDoc = { deprecated: false, operationId: operationId, tags: utags, summary: apiOperation === null || apiOperation === void 0 ? void 0 : apiOperation.value, description: apiOperation === null || apiOperation === void 0 ? void 0 : apiOperation.notes, parameters: parameters.filter((m) => this.isNotBodyParameter(m)), requestBody: this.buildOperationConsumes(action, parameters, consumes), responses: { '201': { 'description': 'Created' }, '401': { 'description': 'Unauthorized' }, '403': { 'description': 'Forbidden' }, '404': { 'description': 'Not Found' }, [code]: { 'description': 'OK', 'content': this.buildOperationProduces(returnType, mapping, definition), }, }, }; mapping.value.forEach((url) => { Object.keys(mapping.method).forEach((method) => { const path = (paths[url] = paths[url] || {}); path[method.toLowerCase()] = operationDoc; }); }); } isNotBodyParameter(m) { const paramIn = m.in; return !this.isMultipartFile(m.schema) && paramIn !== 'body' && paramIn !== 'part'; } /** * 构建api接口操作参数 * @param operation */ buildOperationParameters(action, definition) { var _a; const apiImplicitAnno = RuntimeAnnotation_1.default.getMethodAnnotation(action.ctor, action.methodName, ApiImplicitParams_1.default); const parameters = ((_a = apiImplicitAnno === null || apiImplicitAnno === void 0 ? void 0 : apiImplicitAnno.nativeAnnotation) === null || _a === void 0 ? void 0 : _a.parameters) || []; const parameterNames = action.parameters; const finalParameters = parameterNames.map((name, i) => { const parameter = (parameters.find((m) => m.name === name) || {}); const parameterAnno = RuntimeAnnotation_1.default.getMethodParamAnnotation(action.ctor, action.methodName, name, ParamAnnotation_1.default); const isRequest = !!RuntimeAnnotation_1.default.getMethodParamAnnotation(action.ctor, action.methodName, name, ServletRequest_1.default); const isResponse = !!RuntimeAnnotation_1.default.getMethodParamAnnotation(action.ctor, action.methodName, name, ServletResponse_1.default); const parameter2 = parameterAnno === null || parameterAnno === void 0 ? void 0 : parameterAnno.nativeAnnotation; const dataType = action.paramTypes[i]; if (isRequest || isResponse || Javascript_1.default.createTyper(dataType.clazz).isType(HttpEntity_1.default)) { return; } const value = emptyOf(parameter.value, parameter2 === null || parameter2 === void 0 ? void 0 : parameter2.value) || emptyOf(parameter.name, parameter2 === null || parameter2 === void 0 ? void 0 : parameter2.value) || name; const useType = parameter2 === null || parameter2 === void 0 ? void 0 : parameter2.paramAt; return { name: value || name, required: emptyOf(parameter.required, parameter2 === null || parameter2 === void 0 ? void 0 : parameter2.required), example: emptyOf(parameter.example, parameter2 === null || parameter2 === void 0 ? void 0 : parameter2.defaultValue), description: parameter.description || undefined, in: useType || 'query', dataType: dataType, type: '', schema: { $ref: null, }, }; }).filter(Boolean); return finalParameters.map((parameter) => { var _a; const info = parameter.example && (0, metadata_1.buildRuntimeType)((_a = parameter.example) === null || _a === void 0 ? void 0 : _a.constructor, null); const typeInfo = definition.typemappings.make(parameter.dataType || info); return { name: parameter.name, required: parameter.required, description: parameter.description, in: parameter.in, example: emptyOf(parameter.example, undefined), schema: typeInfo, }; }).filter(Boolean); } isMultipartFile(schema) { var _a; const ref = schema === null || schema === void 0 ? void 0 : schema.$ref; const type = schema; if (!type) { return false; } else if (ref) { return ref.indexOf('MultipartFile') > -1; } else { return type.type == 'file' || ((_a = type.items) === null || _a === void 0 ? void 0 : _a.type) == 'file'; } } buildOperationConsumes(action, parameters, consumes) { const requestBody = { content: {} }; const body = parameters.find((m) => m.in == 'body'); const multiparts = parameters.filter((m) => this.isMultipartFile(m.schema) || m.in == 'part'); if (multiparts.length > 0) { const meta = requestBody.content['multipart/form-data'] = { schema: { required: multiparts.filter((m) => m.required).map((m) => m.name), properties: {}, type: 'object', }, }; multiparts.forEach((m) => { meta.schema.properties[m.name] = Object.assign({}, m.schema); }); } if ((consumes === null || consumes === void 0 ? void 0 : consumes.length) > 0) { consumes.forEach((m) => { if (requestBody.content[m]) return; requestBody.content[m] = { schema: { type: 'string', }, }; }); } else if (body) { requestBody.content['application/json'] = { example: emptyOf(body.example, undefined), schema: body.schema, }; } if (Object.keys(requestBody.content).length < 1) { return undefined; } return requestBody; } buildOperationProduces(returnType, mapping, definition) { var _a; const produces = ((_a = mapping.produces) === null || _a === void 0 ? void 0 : _a.length) < 1 ? ['*/*'] : mapping.produces; const contents = {}; produces.forEach((name) => { const schema = returnType ? definition.typemappings.make(returnType) : undefined; contents[name] = { schema: schema || { type: 'string' }, }; }); return Object.keys(contents).length < 1 ? undefined : contents; } } exports.default = OpenApiModel;