diff --git a/classes.json b/classes.json new file mode 100644 index 0000000..f851983 --- /dev/null +++ b/classes.json @@ -0,0 +1,86 @@ +[ + { + "name": "MongodbCollectionRecordStorageBllImpl", + "documentation": "", + "type": "typeof MongodbCollectionRecordStorageBllImpl", + "constructors": [ + { + "parameters": [ + { + "name": "options", + "documentation": "", + "type": "{ dbClient?: MongoClient; }" + } + ], + "returnType": "MongodbCollectionRecordStorageBllImpl", + "documentation": "" + } + ] + }, + { + "name": "MongodbCollectionRecordQueryBllImpl", + "documentation": "", + "type": "typeof MongodbCollectionRecordQueryBllImpl", + "constructors": [ + { + "parameters": [ + { + "name": "options", + "documentation": "", + "type": "{ dbClient?: MongoClient; }" + } + ], + "returnType": "MongodbCollectionRecordQueryBllImpl", + "documentation": "" + } + ] + }, + { + "name": "RecordBllImpl", + "documentation": "", + "type": "typeof RecordBllImpl", + "constructors": [ + { + "parameters": [ + { + "name": "options", + "documentation": "", + "type": "{ recordStorageBll?: RecordStorageBll; recordQueryBll?: RecordQueryBll; }" + } + ], + "returnType": "RecordBllImpl", + "documentation": "" + } + ] + }, + { + "name": "RecordAuthBll", + "documentation": "", + "type": "typeof RecordAuthBll", + "constructors": [ + { + "parameters": [], + "returnType": "RecordAuthBll", + "documentation": "" + } + ] + }, + { + "name": "RecordAPI", + "documentation": "Function2 2023423i", + "type": "typeof RecordAPI", + "constructors": [ + { + "parameters": [ + { + "name": "options", + "documentation": "", + "type": "{ recordBll?: RecordStorageBll & RecordQueryBll; }" + } + ], + "returnType": "RecordAPI", + "documentation": "" + } + ] + } +] \ No newline at end of file diff --git a/docs/api.yml b/docs/api.yml index 6b085c3..bb5fd6d 100644 --- a/docs/api.yml +++ b/docs/api.yml @@ -167,6 +167,11 @@ paths: summary: Query Record in JSON Array Mode tags: - Record API + params: + - name: xxx + in: path + - name: yyy + in: header requestBody: content: application/json: diff --git a/src/api/doc.ts b/src/api/doc.ts index 14c16b0..96b2b75 100644 --- a/src/api/doc.ts +++ b/src/api/doc.ts @@ -3,6 +3,7 @@ import * as path from 'path' import * as yaml from 'js-yaml' import * as config from 'config' import { before, controller, get } from '../http-server/decorator' +import { generateOpenApiDoc } from '../http-server/openapi' import * as createHttpError from 'http-errors' let openapi: any @@ -19,9 +20,10 @@ let openapi: any export class VersionAPI { @get('/') async doc() { - if (!openapi) { - openapi = yaml.load(fs.readFileSync(path.resolve(__dirname, '../../docs/api.yml'), {encoding: 'utf-8'})) - } - return openapi + // if (!openapi) { + // openapi = yaml.load(fs.readFileSync(path.resolve(__dirname, '../../docs/api.yml'), {encoding: 'utf-8'})) + // } + // return openapi + return generateOpenApiDoc() } } diff --git a/src/api/record.ts b/src/api/record.ts index 9c05b64..d48e365 100644 --- a/src/api/record.ts +++ b/src/api/record.ts @@ -65,6 +65,10 @@ export function resultMW(): MiddlewareFn { } } +/** Function2 2023423i + * @summary ggg + * @description ggghhh + */ @controller('/api/record') @before(async (ctx) => { let token = ctx.get('authorization') as string || '' @@ -159,12 +163,21 @@ export class RecordAPI { return records } + /** + * @summary abc create record api + * @description bcd balala + * @response + * schema: + * type: object + * properties: + * result: + */ @post('/create') @validator({ type: 'object', required: ['spaceId', 'entityId'], properties: { - id: { type: 'string' }, + id: { type: 'string', summary: 'xxx', description: 'yyy' }, spaceId: { type: 'string' }, entityId: { type: 'string' }, cf: { type: 'object' }, @@ -183,6 +196,9 @@ export class RecordAPI { return record } + /** + * @description bcc l999 + */ @post('/update') @validator({ type: 'object', diff --git a/src/api/version.ts b/src/api/version.ts index bf58f2a..61c02b8 100644 --- a/src/api/version.ts +++ b/src/api/version.ts @@ -31,6 +31,17 @@ try { }) export class VersionAPI { @get('/') + @skipLogger() + async version() { + return version + } + @get('/') + @skipLogger() + async version() { + return version + } + @get('/') + @skipLogger() async version() { return version } diff --git a/src/http-server/decorator.ts b/src/http-server/decorator.ts index d8a517b..6f051b2 100644 --- a/src/http-server/decorator.ts +++ b/src/http-server/decorator.ts @@ -49,7 +49,7 @@ interface RouteMeta { propertyName: string } -const controllerMap = new Map() +export const controllerMap = new Map() // const controllers: ControllerMeta[] = [] export function controller(prefix = '/') { diff --git a/src/http-server/jsdoc.ts b/src/http-server/jsdoc.ts new file mode 100644 index 0000000..36db070 --- /dev/null +++ b/src/http-server/jsdoc.ts @@ -0,0 +1,107 @@ +import * as ts from "typescript" +import * as fs from "fs" + +interface DocEntry { + name?: string + fileName?: string + documentation?: string + type?: string + constructors?: DocEntry[] + parameters?: DocEntry[] + returnType?: string +} + +/** Generate documentation for all classes in a set of .ts files */ +function generateDocumentation( + fileNames: string[], + options: ts.CompilerOptions +): void { + // Build a program using the set of root file names in fileNames + const program = ts.createProgram(fileNames, options) + + // Get the checker, we will use it to find more about classes + const checker = program.getTypeChecker() + const output: DocEntry[] = [] + + // Visit every sourceFile in the program + for (const sourceFile of program.getSourceFiles()) { + if (!sourceFile.isDeclarationFile) { + // Walk the tree to search for classes + ts.forEachChild(sourceFile, visit) + } + } + + // // print out the doc + fs.writeFileSync("classes.json", JSON.stringify(output, undefined, 4)) + + return + + /** visit nodes finding exported classes */ + function visit(node: ts.Node) { + // Only consider exported nodes + if (!isNodeExported(node)) { + return + } + + if (ts.isClassDeclaration(node) && node.name) { + // This is a top level class, get its symbol + const symbol = checker.getSymbolAtLocation(node.name) + if (symbol) { + output.push(serializeClass(symbol)) + } + // No need to walk any further, class expressions/inner declarations + // cannot be exported + } else if (ts.isModuleDeclaration(node)) { + // This is a namespace, visit its children + ts.forEachChild(node, visit) + } + } + + /** Serialize a symbol into a json object */ + function serializeSymbol(symbol: ts.Symbol): DocEntry { + return { + name: symbol.getName(), + documentation: ts.displayPartsToString(symbol.getDocumentationComment(checker)), + type: checker.typeToString( + checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration!) + ) + } + } + + /** Serialize a class symbol information */ + function serializeClass(symbol: ts.Symbol) { + const details = serializeSymbol(symbol) + + // Get the construct signatures + const constructorType = checker.getTypeOfSymbolAtLocation( + symbol, + symbol.valueDeclaration! + ) + details.constructors = constructorType + .getConstructSignatures() + .map(serializeSignature) + return details + } + + /** Serialize a signature (call or construct) */ + function serializeSignature(signature: ts.Signature) { + return { + parameters: signature.parameters.map(serializeSymbol), + returnType: checker.typeToString(signature.getReturnType()), + documentation: ts.displayPartsToString(signature.getDocumentationComment(checker)) + } + } + + /** True if this is visible outside this file, false otherwise */ + function isNodeExported(node: ts.Node): boolean { + return ( + (ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Export) !== 0 || + (!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile) + ) + } +} + +generateDocumentation(process.argv.slice(2), { + target: ts.ScriptTarget.ES5, + module: ts.ModuleKind.CommonJS +}) \ No newline at end of file diff --git a/src/http-server/openapi.ts b/src/http-server/openapi.ts new file mode 100644 index 0000000..bc79321 --- /dev/null +++ b/src/http-server/openapi.ts @@ -0,0 +1,129 @@ +import * as path from 'path' +import * as fs from 'fs' +import * as glob from 'glob' +import * as typescript from 'typescript' +import { getRouter, controllerMap } from './decorator' + +export function generateOpenApiDoc() { + const result: any = { + openapi: '3.0.0', + info: { + title: 'package.name', // openapi title or package name + description: 'package.description', // openapi description or package description + version: 'package.version', // openapi version or package version + contact: { + name: 'package.author.name', + email: 'package.author.email', // openapi contact email or package maintainer or package author + }, + license: { + name: 'Apache 2.0', // openapi license or package + url: 'http://www.apache.org/licenses/LICENSE-2.0.html', // openapi license or package + }, + }, + tags: [], + paths: {}, + } + + Array.from(controllerMap.values()).forEach(controllerMeta => { + if (!controllerMeta.prefix) return + Object.values(controllerMeta.methodMap).forEach(methodMeta => { + const fullpath = path.join(controllerMeta.prefix, methodMeta.path).replace(/\/$/, '') + const pathObj: any = result.paths[fullpath] = result.paths[fullpath] || {} + const requestBodySchema = methodMeta.validator?.schema + const responseBodySchema = {} + pathObj[methodMeta.verb] = { + summary: 'methodMeta.name', + description: '', + consumes: ['application/json'], + produces: ['application/json'], + parameters: {}, + requestBody: { + description: '', + content: { + 'application/json': { + schema: requestBodySchema, + // examples: { [string]: { summary: '', value: obj } ... }, + }, + }, + }, + responses: { + default: { + description: '', + content: { + 'application/json': { + schema: responseBodySchema, + } + } + } + } + } + }) + }) + + console.log(JSON.stringify(result, null, 2)) + return result +} + +export function generateTypeDoc() { + const p = typescript.createProgram(['src/app.ts', 'src/api/record.ts'], {}) + // const file = 'src/api/record.ts' + // const filepath = path.resolve(__dirname, '../../', file) + // const p = typescript.createSourceFile( + // file, + // fs.readFileSync(filepath).toString(), + // typescript.ScriptTarget.ES2017, + // ) + + const checker = p.getTypeChecker() + p.getSourceFiles().forEach(sourceFile => { + if (sourceFile.isDeclarationFile) return + console.log('sourceFile', sourceFile.fileName) + // console.log(p) + typescript.forEachChild(sourceFile, (node) => { + // console.log('----', node.kind) + // typescript.isClassDeclaration(node) || typescript.isModuleDeclaration(node) || + if (!isNodeExported(node)) return + // console.log((node as any).name) + // if ((node as any).name) { + if (!typescript.isClassDeclaration(node)) return + + const symbol = checker.getSymbolAtLocation((node as any).name) + if (!symbol) return + const className = symbol.getName() + const commentParts = symbol.getDocumentationComment(null) + const jsDocTags = symbol.getJsDocTags() + // console.log('++++ commentParts', commentParts) // summary + // console.log('++++ jsDocTags', jsDocTags) // summary => summary + console.log(`class ${className}: ${commentParts.map(p => p.text).join('__')} jsDoc: ${JSON.stringify(jsDocTags)}`) + + typescript.forEachChild(node, (node) => { + if (!typescript.isMethodDeclaration(node)) return + + const symbol = checker.getSymbolAtLocation(node.name) + if (!symbol) return + const methodName = symbol.getName() + const commentParts = symbol.getDocumentationComment(null) + const jsDocTags = symbol.getJsDocTags() + console.log(`${className}.${methodName}: ${commentParts.map(p => p.text).join('__')} jsDoc: ${JSON.stringify(jsDocTags)}`) + + }) + + // if (commentParts.length) { + // console.log('----commentPart', commentParts[0].text) + // // console.log('----', node.kind, (node as any).name) + // console.log(commentParts) + // } + // } + // } + }) + }) +} + +function isNodeExported(node: typescript.Node): boolean { + return ( + (typescript.getCombinedModifierFlags(node as typescript.Declaration) & typescript.ModifierFlags.Export) !== 0 || + (!!node.parent && node.parent.kind === typescript.SyntaxKind.SourceFile) + ) +} + +generateTypeDoc()