koa-ts-core 0.3.9 → 0.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -6
- package/dist/core/swagger_collector.d.ts +6 -1
- package/dist/exceptions/index.d.ts +1 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/types/core.d.ts +3 -0
- package/dist/types/meta_data.d.ts +4 -26
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
- **全局异常处理**: 统一捕获异常,规范化错误响应。
|
|
15
15
|
- **统一响应体**: 提供 `successRsp`, `errorRsp` 等工具函数,统一 API 返回格式。
|
|
16
16
|
- **阶段式中间件 (Phase Middleware)**: 独创的生命周期阶段管理,允许在 AsyncContext、Auth、Routing 等任意阶段前后注入中间件。
|
|
17
|
-
- **环境配置**: 自动加载 `.env.
|
|
17
|
+
- **环境配置**: 自动加载 `env/` 目录下的配置文件,支持通用配置(`.common.env`)与环境特定配置(`.development.env` 等)的自动合并。
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
@@ -49,7 +49,16 @@ async function bootstrap() {
|
|
|
49
49
|
enabled: true,
|
|
50
50
|
path: "/swagger-ui",
|
|
51
51
|
title: "My API Service",
|
|
52
|
-
version: "1.0.0"
|
|
52
|
+
version: "1.0.0",
|
|
53
|
+
// 安全配置 (如 Bearer JWT)
|
|
54
|
+
securitySchemes: {
|
|
55
|
+
BearerAuth: {
|
|
56
|
+
type: "http",
|
|
57
|
+
scheme: "bearer",
|
|
58
|
+
bearerFormat: "JWT"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
security: [{ BearerAuth: [] }] // 全局启用
|
|
53
62
|
},
|
|
54
63
|
|
|
55
64
|
// 错误处理
|
|
@@ -237,10 +246,11 @@ createKoaApp({
|
|
|
237
246
|
|
|
238
247
|
## 环境变量
|
|
239
248
|
|
|
240
|
-
约定根目录下 `env/`
|
|
241
|
-
|
|
242
|
-
- `.
|
|
243
|
-
- `.
|
|
249
|
+
约定根目录下 `env/` 文件夹,优先级配置:`环境特定 > 通用`。
|
|
250
|
+
|
|
251
|
+
- `.common.env` (所有环境通用的配置)
|
|
252
|
+
- `.{env}.env` (特定环境配置,如 `.development.env`)
|
|
253
|
+
- `.{env}.env.local` (可选:本地覆盖配置)
|
|
244
254
|
|
|
245
255
|
---
|
|
246
256
|
|
|
@@ -8,6 +8,9 @@ type TagSpecOptions = {
|
|
|
8
8
|
export declare class SwaggerCollector {
|
|
9
9
|
private static instance;
|
|
10
10
|
private paths;
|
|
11
|
+
private securitySchemes;
|
|
12
|
+
private security;
|
|
13
|
+
setSecurity(securitySchemes: Record<string, OpenAPIV3.SecuritySchemeObject | OpenAPIV3.ReferenceObject>, security: OpenAPIV3.SecurityRequirementObject[]): void;
|
|
11
14
|
static getInstance(): SwaggerCollector;
|
|
12
15
|
/**
|
|
13
16
|
* 添加路由定义到 Swagger Spec
|
|
@@ -32,10 +35,12 @@ export declare class SwaggerCollector {
|
|
|
32
35
|
version: string;
|
|
33
36
|
description: string;
|
|
34
37
|
};
|
|
35
|
-
paths:
|
|
38
|
+
paths: OpenAPIV3.PathsObject<{}, {}>;
|
|
36
39
|
components: {
|
|
37
40
|
schemas: {};
|
|
41
|
+
securitySchemes: Record<string, OpenAPIV3.SecuritySchemeObject | OpenAPIV3.ReferenceObject>;
|
|
38
42
|
};
|
|
43
|
+
security: OpenAPIV3.SecurityRequirementObject[];
|
|
39
44
|
};
|
|
40
45
|
}
|
|
41
46
|
export {};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { default as BaseException } from "./base";
|
|
2
|
-
export { default as
|
|
2
|
+
export { default as ForbiddenException } from "./forbidden_exceptions";
|
package/dist/index.cjs.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";require("reflect-metadata");var e=require("async_hooks"),t=require("fs"),s=require("path"),r=require("@koa/router"),o=require("koa"),a=require("portfinder"),n=require("dotenv"),c=require("log4js"),i=require("koa-bodyparser"),d=require("koa2-cors"),p=require("crypto"),u=require("koa2-swagger-ui");const l=["get","post","put","delete","patch","options"],h=0,g="index",f="get_",m=[g,f,...l],y=Symbol("route_metadata");function w(e){const t=e.trim();if(!t)return"";return`/${t.replace(/^\/+/,"")}`}function x(e,t,s=!1){return(r,o,a)=>{if("function"!=typeof a.value)throw new Error(`@Router/@AuthRouter 只能用于方法:${o}`);let n=Reflect.getMetadata(y,r.constructor);n||(n=new Map,Reflect.defineMetadata(y,n,r.constructor));const c=function(e,t){return e||(t===f?"get":l.includes(t)?t:"get")}(e,o),i=function(e,t){return null!=e?w(e):m.includes(t)?"":w(t)}(t,o),d={handler:a.value,path:i,method:c,functionName:o,authRequired:s,middlewares:[],className:r.constructor.name,apiPrePath:""};return n.set(o,d),a}}const v=new e.AsyncLocalStorage,P=()=>{const e=v.getStore();if(!e)throw new Error("context is not exist");return e},k=e=>{const{success:t=!0,errorCode:s=-1,message:r="success",errorMessage:o="error request",data:a=null,statusCode:n=200}=e??{},c=P(),i={code:t?h:s,message:t?r:o,...null!=a?{data:a}:{}};c.trackId&&(i.trackId=c.trackId),c.body=i,c.status=n},b=(e,t)=>{k({success:!1,errorMessage:t?.message,errorCode:t?.code,data:t?.data,statusCode:e})};class q extends Error{constructor(e){super(e?.message||"An unexpected error occurred"),this.name=this.constructor.name,this.code=e?.code??-1,this.statusCode=e?.statusCode??500,"function"==typeof Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor),this.stack=this.stack??new Error(this.message).stack}get toRspOptions(){return{code:this.code,message:this.message,data:this.stack}}}class E extends q{constructor(e){super({statusCode:403,message:e})}}const M=()=>{const e=process.cwd();if(t.existsSync(s.resolve(e,"controller")))return e;const r=s.resolve(e,"src");if(t.existsSync(r))return r;const o=s.resolve(e,"dist");if(t.existsSync(o))return o;const a=s.resolve(e,"build");return t.existsSync(a)?a:r},S=()=>{const e=M();return{controllerModule:s.resolve(e,"controller"),validateModule:s.resolve(e,"validate"),viewModule:s.resolve(e,"view"),docModule:s.resolve(e,"doc")}},R=e=>{if(t.existsSync(e))try{const t=require(e);return t.default||t}catch(t){throw console.error(`[koa-ts-core] Failed to load module: ${e}`,t),t}};function A(e,r){t.existsSync(e)&&t.readdirSync(e,{withFileTypes:!0}).forEach(t=>{const o=s.resolve(e,t.name);t.isFile()?r({path:e,name:t.name,wholePath:o},t):t.isDirectory()&&A(o,r)})}const N=e=>e.split(s.sep).join("/"),I=e=>{const t=N(e).replace(/^\/+/,"");return t?`/${t}`:""},O=(e,t)=>{const s=N(e.path).split("/"),r=s.lastIndexOf(t);if(-1===r){const s=e.path.indexOf(t);if(-1===s)throw new Error(`'${t}' not found in path: ${e.path}`);const r=e.path.substring(s+t.length),o=I(r),a=e.name?.split(".")?.[0];return{pathPrefix:o,moduleName:a,name:e.name}}const o=s.slice(r+1).join("/"),a=I(o),n=e.name?.split(".")?.[0];return{pathPrefix:a,moduleName:n,name:e.name}};function C(e){try{return t.statSync(e).isDirectory()}catch{return!1}}var $;exports.MiddlewarePhase=void 0,($=exports.MiddlewarePhase||(exports.MiddlewarePhase={})).AsyncContext="AsyncContext",$.ErrorHandling="ErrorHandling",$.Logging="Logging",$.Security="Security",$.BodyParsing="BodyParsing",$.Auth="Auth",$.Routing="Routing";const _=new r,j=async e=>{if(!process.env.APP_PORT)try{const e=await a.getPortPromise({port:8e3,stopPort:9999});process.env.APP_PORT=String(e)}catch(e){console.warn("[koa-ts-core] Failed to find available port automatically.")}const t=e.listen;e.listen=function(...e){if(0===e.length&&process.env.APP_PORT){const t=Number(process.env.APP_PORT);isNaN(t)||e.push(t)}const s=t.apply(this,e);return s.on("listening",()=>(e=>{const t=e.address();if(t)if("string"==typeof t)console.log(`Server listening on ${t}`);else{const{port:e}=t;console.log(`Server running at http://localhost:${e}`)}})(s)),s}},D=Symbol("validate_metadata"),T=Symbol("swagger_tags"),L=Symbol("swagger_operation"),B=Symbol("swagger_parameters"),U=Symbol("swagger_body"),H=Symbol("swagger_responses"),V={DOCUMENTATION_DIR:s.join(process.cwd(),"doc")},F=()=>{const e=[];return C(V.DOCUMENTATION_DIR)?(A(V.DOCUMENTATION_DIR,t=>{if(!t.name.endsWith(".ts")&&!t.name.endsWith(".js"))return;const{pathPrefix:s}=O(t,"doc"),r=((e,t)=>{const s=R(e.wholePath);if(!s)return{configs:[],desc:"",name:"",pathPrefix:t,apiPrePath:""};const r=s.prototype,o=Object.getOwnPropertyNames(r).filter(e=>"function"==typeof r[e]&&"constructor"!==e),a=e.path.indexOf("doc"),n=a>=0?I(e.path.substring(a+3)):"";let c=n;if(e.name&&!e.name.startsWith("index.")){const t=e.name.split(".")[0];c=`${n}/${t}`}const i=o.map(e=>{const t=r[e];if("function"==typeof t){const e=t();return e.path=`${c}${e.path}`,e}return null}).filter(e=>!!e);let d="";if(i[0]?.path){const e=i[0].path.lastIndexOf("/");e>-1&&(d=i[0].path.substring(0,e))}return{configs:i,desc:s.desc,name:s.name,pathPrefix:t,apiPrePath:d}})(t,s);r.configs.length>0&&e.push(r)}),e):[]};class z{constructor(){this.paths={}}static getInstance(){return this.instance||(this.instance=new z),this.instance}addRoute(e,t,s,r){const o=r.replace(/:([a-zA-Z0-9_]+)/g,"{$1}");this.paths[o]||(this.paths[o]={});const a=e.prototype,n=Reflect.getMetadata(L,a,t)||{},c=n.summary,i=n.description,d=Reflect.getMetadata(T,e)||[e.name],p=Reflect.getMetadata(B,a,t)||[],u=Reflect.getMetadata(U,a,t);let l;u&&(l={description:u.description,required:u.required,content:{"application/json":{schema:u.schema||{type:"object"}}}});const h=Reflect.getMetadata(H,a,t)||[],g={};h.forEach(e=>{g[e.status]={description:e.description||"",content:e.schema?{"application/json":{schema:e.schema}}:void 0}}),0===Object.keys(g).length&&(g[200]={description:"Success"}),this.paths[o][s.toLowerCase()]={tags:d,summary:c,description:i,parameters:p,requestBody:l,responses:g}}loadDocMetadata(){F().forEach(e=>{const t=e.desc||e.name;e.configs.forEach(e=>{const s=("/"+e.path).replace(/\/+/g,"/"),r=s.replace(/:([a-zA-Z0-9_]+)/g,"{$1}"),o=e.method.toLowerCase();this.paths[r]||(this.paths[r]={}),this.paths[r][o]||(this.paths[r][o]={});const a=this.paths[r][o];a.summary||(a.summary=e.description),a.tags&&0!==a.tags.length||(a.tags=[t]);const n=s.match(/:([a-zA-Z0-9_]+)/g);if(n&&(a.parameters||(a.parameters=[]),n.forEach(e=>{const t=e.substring(1);a.parameters.find(e=>e.name===t&&"path"===e.in)||a.parameters.push({name:t,in:"path",required:!0,schema:{type:"string"},description:t})})),e.request?.query&&(a.parameters||(a.parameters=[]),Object.keys(e.request.query).forEach(t=>{if(!a.parameters.find(e=>e.name===t&&"query"===e.in)){const s=e.request?.query?.[t];s&&"object"==typeof s&&a.parameters.push({name:t,in:"query",description:s.description,required:!1!==s.required,schema:s.schema??{type:s.type??"string"},example:s.example})}})),e.request?.header&&(a.parameters||(a.parameters=[]),Object.keys(e.request.header).forEach(t=>{if("content-type"===t.toLowerCase())return;if(!a.parameters.find(e=>e.name===t&&"header"===e.in)){const s=e.request?.header?.[t];s&&"object"==typeof s&&a.parameters.push({name:t,in:"header",description:s.description,required:!1!==s.required,schema:s.schema??{type:s.type??"string"},example:s.example})}})),e.request?.body&&Object.keys(e.request.body).length>0&&!a.requestBody){const t=e.request.body;a.requestBody={description:t.description,required:t.required,content:{"application/json":{schema:{type:"object",properties:t.schema},example:t.example}}}}e.response?.body&&(a.responses||(a.responses={}),a.responses[200]||(a.responses[200]={description:"Success",content:{"application/json":{schema:{type:"object",example:e.response.body}}}}))})})}tagSpecByPrefix(e,t={}){const{level:s=2,onlyIfEmpty:r=!0}=t,o=new Set(["get","post","put","delete","patch","head","options","trace"]),a=new Set((e.tags||[]).map(e=>e.name)),n=e.paths||{};for(const[e,t]of Object.entries(n)){if(!t)continue;const n="/"+e.split("/").filter(Boolean).slice(0,s).join("/");for(const[e,s]of Object.entries(t)){if(!o.has(e))continue;if(!s||"object"!=typeof s)continue;const t=s,c=Array.isArray(t.tags)&&t.tags.length>0;r&&c||(t.tags=[n]),a.add(n)}}return e.tags=Array.from(a).map(e=>({name:e})),e}generateSpec(){return{openapi:"3.0.0",info:{title:"API Documentation",version:"1.0.0",description:"Auto generated by koa-ts-core"},paths:this.paths,components:{schemas:{}}}}}const W=(e,t,s,r,o)=>{if(!r||0===r.size)return;const{pathPrefix:a,moduleName:n,name:c}=O(t,"controller"),i=`${a}/${n}`,d=R(`${e}${a}/${c}`),p=e=>async(t,s)=>{const r=e(t);r instanceof Promise&&await r,await s()};r.forEach(e=>{const{method:t,path:r,handler:n,functionName:c,authRequired:u,middlewares:l}=e,h=[...l],g=Reflect.getMetadata(D,s.prototype,c);g?h.push(p(g)):d&&"function"==typeof d[c]&&h.push(p(d[c].bind(d))),h.push(((e,t)=>async(s,r)=>{let a=o(e,s);if(a instanceof Promise&&(a=await a),!a)throw new E("Access denied");return t(s,r)})(u,n));const f=Z(r,i,c,h,t);z.getInstance().addRoute(s,c,t,f),e.path=f,e.apiPrePath=a})},Z=(e,t,s,r,o)=>{const a=`${t}${e}`;if(s===g){const e=`${t}/index`;_[o](e,...r)}return _[o](a,...r),a};let K=null;const Q=()=>(K||(K=c.configure({appenders:{console:{type:"console"},file:{type:"dateFile",filename:"logs/app.log",pattern:".yyyy-MM-dd",compress:!0,numBackups:7,alwaysIncludePattern:!0}},categories:{default:{appenders:["console","file"],level:"info"}}})),K),X=(e,t)=>v.run(e,()=>t()),Y=e=>{const{generator:t,exposeHeader:s=!0,headerName:r="x-request-id"}=e;return!1===t?async(e,t)=>{await t()}:async(e,o)=>{const a=await t(e);a&&(e.trackId=a,s&&e.set(r,a)),await o()}},G=async(e,t)=>{const s=Date.now(),r=Q().getLogger(),o=e.runtimeLog;o&&r.info("[REQUEST] %s %s %s - ip: %s - ua: %s",e.trackId,e.method,e.url,e.ip,e.get("user-agent")||"-");try{e.hook?.(e,"request"),await t(),e.hook?.(e,"response")}catch(t){const a=Date.now()-s;throw o&&r.error("[ERROR] %s %s %s - status: %s - cost: %dms - message: %s - stack: %s",e.trackId,e.method,e.url,t.status||500,a,t.message||"unknown error",t.stack||""),t}const a=Date.now()-s;o&&r.info("[RESPONSE] %s %s %s - status: %d - cost: %dms - length: %s",e.trackId,e.method,e.url,e.status,a,e.length||e.response.length||"-")},J=async(e,t)=>{e.extra={get:e.query,post:e.request.body??{}},await t()};(()=>{const e=process.env.NODE_ENV||"production";let t;const r=process.env.ENV_DIR;if(r){if(t=s.isAbsolute(r)?r:s.resolve(process.cwd(),r),!C(t))throw new Error(`[koa-ts-core] ENV_DIR is set but directory not found: ${t}`)}else{const e=M();let r=s.resolve(e,"env");if(C(r)||(r=s.resolve(e,"..","env")),!C(r))return void console.warn(`[koa-ts-core] env directory not found: tried ${s.resolve(e,"env")} and ${s.resolve(e,"..","env")}`);t=r}console.log("[koa-ts-core] load env path:",t),n.config({path:s.resolve(t,".env.local")});const o={development:".development.env",test:".test.env",production:".production.env"}[e];o&&n.config({path:s.resolve(t,`${o}.local`)}),o&&n.config({path:s.resolve(t,o)}),n.config({path:s.resolve(t,".common.env")})})(),global.isDev="development"===process.env.NODE_ENV;const ee=e=>{const{koaInstance:t,auth:s,error:r,log:a,hooks:n,phaseMiddlewares:c,koa2Cors:i,trackId:d,swagger:u,bodyParser:l}=e;return{app:t??new o,auth:{handler:s?.handler},error:{handler:r?.handler,exposeStack:r?.exposeStack??"production"!==process.env.NODE_ENV},log:{log4:a?.log4??!1,runtimeLog:a?.runtimeLog??!0},hooks:{register:n?.register},phaseMiddlewares:c??new Map,koa2Cors:i,trackId:{generator:!1!==d?.generator&&(async e=>{const t=d?.headerName??"x-request-id",s=e.get(t);return s||p.randomUUID()}),exposeHeader:d?.exposeHeader??!0,headerName:d?.headerName??"x-request-id"},swagger:u,bodyParser:l}},te=(e,t)=>{const{log:s,hooks:r}=t;e.context.log4=(e=>{if(!1!==e)return"boolean"==typeof e&&!0===e?Q():e(c)})(s.log4),e.context.runtimeLog=s.runtimeLog,e.context.hook=r.register,e.context.validated={params:{},get:{},post:{}}},se=e=>{(e=>{const{controllerModule:t,validateModule:s}=S();console.log("load controller path:",t),A(t,t=>{if(!t.name.endsWith(".ts")&&!t.name.endsWith(".js"))return;const r=R(t.wholePath);if(!r)return;const o=Reflect.getMetadata(y,r);o instanceof Map&&0!==o.size&&W(s,t,r,o,(s,r)=>{if(!s)return!0;if(!e)throw new Error(`Route requires auth but no authCheckCallback provided. File: ${t.wholePath}`);return e({filePath:t,ctx:r})})})})(e),_.get("/",async e=>{e.body={code:200,message:"hello koa-ts-cli https://www.npmjs.com/package/koa-ts-core"}})};exports.AuthRouter=function(e,t){return x(e,t,!0)},exports.BaseException=q,exports.INDEX_ROUTE=g,exports.ParamException=E,exports.ROUTE_METADATA_KEY=y,exports.Router=function(e,t){return x(e,t,!1)},exports.contextStore=v,exports.createKoaApp=async e=>{const t=ee(e),{app:s}=t;if(te(s,t),se(t.auth.handler),t.swagger?.enabled){z.getInstance().loadDocMetadata();const e=z.getInstance().generateSpec();t.swagger.title&&(e.info.title=t.swagger.title),t.swagger.description&&(e.info.description=t.swagger.description),t.swagger.version&&(e.info.version=t.swagger.version);const r=t.swagger.path||"/swagger-ui";s.use(u.koaSwagger({routePrefix:r,swaggerOptions:{spec:z.getInstance().tagSpecByPrefix(e,{level:2,onlyIfEmpty:!1})}})),console.log(`[koa-ts-core] Swagger UI available at ${r}`)}return((e,t,s)=>{const{errorConfig:r,koa2Cors:o,trackConfig:a,bodyParser:n}=s,c={[exports.MiddlewarePhase.AsyncContext]:[X],[exports.MiddlewarePhase.ErrorHandling]:[(p=r?.handler,async(e,t)=>{try{await t()}catch(t){if(e.hook?.(e,"error"),p)p(t,e);else{const e=t.statusCode||t.status||500,s=t.code??-1;b(e,{message:t.message,code:s,data:"production"!==process.env.NODE_ENV?t.stack:void 0})}}})],[exports.MiddlewarePhase.Logging]:[Y(a),G],[exports.MiddlewarePhase.Security]:[...o?[o(d)]:[]],[exports.MiddlewarePhase.BodyParsing]:[i(n),J],[exports.MiddlewarePhase.Auth]:[],[exports.MiddlewarePhase.Routing]:[_.routes()]};var p;const u=[exports.MiddlewarePhase.AsyncContext,exports.MiddlewarePhase.ErrorHandling,exports.MiddlewarePhase.Logging,exports.MiddlewarePhase.Security,exports.MiddlewarePhase.BodyParsing,exports.MiddlewarePhase.Auth,exports.MiddlewarePhase.Routing];for(const s of u){const r=t.get(s),o=c[s]??[];if(r?.before)for(const t of r.before)e.use(t);for(const t of o)e.use(t);if(r?.use)for(const t of r.use)e.use(t);if(r?.after)for(const t of r.after)e.use(t)}})(s,t.phaseMiddlewares,{errorConfig:t.error,koa2Cors:t.koa2Cors,trackConfig:t.trackId,bodyParser:t.bodyParser}),await j(s),"production"===process.env.NODE_ENV&&console.log(`[koa-ts-core] version: ${require("../package.json").version}`),[s,_]},exports.errorRsp=b,exports.getCurrentContext=P,exports.getSrcModulePaths=S,exports.successRsp=e=>{const t=Array.isArray(e?.data);k({success:!0,message:e?.message,data:t?{list:e?.data}:e?.data})},exports.unSuccessRsp=e=>{k({success:!1,errorCode:e?.code,errorMessage:e?.message,data:e?.data})};
|
|
1
|
+
"use strict";require("reflect-metadata");var e=require("async_hooks"),t=require("fs"),s=require("path"),r=require("@koa/router"),o=require("koa"),a=require("portfinder"),n=require("dotenv"),c=require("log4js"),i=require("koa-bodyparser"),d=require("koa2-cors"),u=require("crypto"),p=require("koa2-swagger-ui");const l=["get","post","put","delete","patch","options"],g=0,h="index",m="get_",f=[h,m,...l],y=Symbol("route_metadata");function w(e){const t=e.trim();if(!t)return"";return`/${t.replace(/^\/+/,"")}`}function x(e,t,s=!1){return(r,o,a)=>{if("function"!=typeof a.value)throw new Error(`@Router/@AuthRouter 只能用于方法:${o}`);let n=Reflect.getMetadata(y,r.constructor);n||(n=new Map,Reflect.defineMetadata(y,n,r.constructor));const c=function(e,t){return e||(t===m?"get":l.includes(t)?t:"get")}(e,o),i=function(e,t){return null!=e?w(e):f.includes(t)?"":w(t)}(t,o),d={handler:a.value,path:i,method:c,functionName:o,authRequired:s,middlewares:[],className:r.constructor.name,apiPrePath:""};return n.set(o,d),a}}const v=new e.AsyncLocalStorage,P=()=>{const e=v.getStore();if(!e)throw new Error("context is not exist");return e},k=e=>{const{success:t=!0,errorCode:s=-1,message:r="success",errorMessage:o="error request",data:a=null,statusCode:n=200}=e??{},c=P(),i={code:t?g:s,message:t?r:o,...null!=a?{data:a}:{}};c.trackId&&(i.trackId=c.trackId),c.body=i,c.status=n},S=(e,t)=>{k({success:!1,errorMessage:t?.message,errorCode:t?.code,data:t?.data,statusCode:e})};class M extends Error{constructor(e){super(e?.message||"An unexpected error occurred"),this.name=this.constructor.name,this.code=e?.code??-1,this.statusCode=e?.statusCode??500,"function"==typeof Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor),this.stack=this.stack??new Error(this.message).stack}get toRspOptions(){return{code:this.code,message:this.message,data:this.stack}}}class E extends M{constructor(e){super({statusCode:403,message:e})}}const b=()=>{const e=process.cwd();if(t.existsSync(s.resolve(e,"controller")))return e;const r=s.resolve(e,"src");if(t.existsSync(r))return r;const o=s.resolve(e,"dist");if(t.existsSync(o))return o;const a=s.resolve(e,"build");return t.existsSync(a)?a:r},R=()=>{const e=b();return{controllerModule:s.resolve(e,"controller"),validateModule:s.resolve(e,"validate"),viewModule:s.resolve(e,"view"),docModule:s.resolve(e,"doc")}},I=e=>{if(t.existsSync(e))try{const t=require(e);return t.default||t}catch(t){throw console.error(`[koa-ts-core] Failed to load module: ${e}`,t),t}};function A(e,r){t.existsSync(e)&&t.readdirSync(e,{withFileTypes:!0}).forEach(t=>{const o=s.resolve(e,t.name);t.isFile()?r({path:e,name:t.name,wholePath:o},t):t.isDirectory()&&A(o,r)})}const N=e=>e.split(s.sep).join("/"),q=e=>{const t=N(e).replace(/^\/+/,"");return t?`/${t}`:""},O=(e,t)=>{const s=N(e.path).split("/"),r=s.lastIndexOf(t);if(-1===r){const s=e.path.indexOf(t);if(-1===s)throw new Error(`'${t}' not found in path: ${e.path}`);const r=e.path.substring(s+t.length),o=q(r),a=e.name?.split(".")?.[0];return{pathPrefix:o,moduleName:a,name:e.name}}const o=s.slice(r+1).join("/"),a=q(o),n=e.name?.split(".")?.[0];return{pathPrefix:a,moduleName:n,name:e.name}};function C(e){try{return t.statSync(e).isDirectory()}catch{return!1}}var _;exports.MiddlewarePhase=void 0,(_=exports.MiddlewarePhase||(exports.MiddlewarePhase={})).AsyncContext="AsyncContext",_.ErrorHandling="ErrorHandling",_.Logging="Logging",_.Security="Security",_.BodyParsing="BodyParsing",_.Auth="Auth",_.Routing="Routing";const $=new r,D=async e=>{if(!process.env.APP_PORT)try{const e=await a.getPortPromise({port:8e3,stopPort:9999});process.env.APP_PORT=String(e)}catch(e){console.warn("[koa-ts-core] Failed to find available port automatically.")}const t=e.listen;e.listen=function(...e){if(0===e.length&&process.env.APP_PORT){const t=Number(process.env.APP_PORT);isNaN(t)||e.push(t)}const s=t.apply(this,e);return s.on("listening",()=>(e=>{const t=e.address();if(t)if("string"==typeof t)console.log(`Server listening on ${t}`);else{const{port:e}=t;console.log(`Server running at http://localhost:${e}`)}})(s)),s}},j=Symbol("validate_metadata"),T=Symbol("swagger_tags"),B=Symbol("swagger_operation"),L=Symbol("swagger_parameters"),U=Symbol("swagger_body"),F=Symbol("swagger_responses"),H={DOCUMENTATION_DIR:s.join(process.cwd(),"doc")},V=()=>{const e=[];return C(H.DOCUMENTATION_DIR)?(A(H.DOCUMENTATION_DIR,t=>{if(!t.name.endsWith(".ts")&&!t.name.endsWith(".js"))return;const{pathPrefix:s}=O(t,"doc"),r=((e,t)=>{const s=I(e.wholePath);if(!s)return{configs:[],desc:"",name:"",pathPrefix:t,apiPrePath:""};const r=s.prototype,o=Object.getOwnPropertyNames(r).filter(e=>"function"==typeof r[e]&&"constructor"!==e),a=e.path.indexOf("doc"),n=a>=0?q(e.path.substring(a+3)):"";let c=n;if(e.name&&!e.name.startsWith("index.")){const t=e.name.split(".")[0];c=`${n}/${t}`}const i=o.map(e=>{const t=r[e];if("function"==typeof t){const e=t();return e.path=`${c}${e.path}`,e}return null}).filter(e=>!!e);let d="";if(i[0]?.path){const e=i[0].path.lastIndexOf("/");e>-1&&(d=i[0].path.substring(0,e))}return{configs:i,desc:s.desc,name:s.name,pathPrefix:t,apiPrePath:d}})(t,s);r.configs.length>0&&e.push(r)}),e):[]};class z{constructor(){this.paths={},this.securitySchemes={},this.security=[]}setSecurity(e,t){this.securitySchemes=e,this.security=t}static getInstance(){return this.instance||(this.instance=new z),this.instance}addRoute(e,t,s,r){const o=r.replace(/:([a-zA-Z0-9_]+)/g,"{$1}");this.paths[o]||(this.paths[o]={});const a=e.prototype,n=Reflect.getMetadata(B,a,t)||{},c=n.summary,i=n.description,d=Reflect.getMetadata(T,e)||[e.name],u=Reflect.getMetadata(L,a,t)||[],p=Reflect.getMetadata(U,a,t);let l;p&&(l={description:p.description,required:p.required,content:{"application/json":{schema:p.schema||{type:"object"}}}});const g=Reflect.getMetadata(F,a,t)||[],h={};g.forEach(e=>{h[e.status]={description:e.description||"",content:e.schema?{"application/json":{schema:e.schema}}:void 0}}),0===Object.keys(h).length&&(h[200]={description:"Success"});const m=s.toLowerCase(),f=this.paths[o]||{};this.paths[o]=f,f[m]={tags:d,summary:c,description:i,parameters:u,requestBody:l,responses:h}}loadDocMetadata(){V().forEach(e=>{const t=e.desc||e.name;e.configs.forEach(e=>{const s=("/"+e.path).replace(/\/+/g,"/"),r=s.replace(/:([a-zA-Z0-9_]+)/g,"{$1}"),o=e.method.toLowerCase();this.paths[r]||(this.paths[r]={});const a=this.paths[r];a[o]||(a[o]={});const n=a[o];!n.summary&&e.summary&&(n.summary=e.summary),!n.description&&e.description&&(n.description=e.description),n.tags&&0!==n.tags.length||(e.tags&&e.tags.length>0?n.tags=e.tags:n.tags=[t]);const c=s.match(/:([a-zA-Z0-9_]+)/g);c&&(n.parameters||(n.parameters=[]),c.forEach(e=>{const t=e.substring(1);n.parameters.find(e=>e.name===t&&"path"===e.in)||n.parameters.push({name:t,in:"path",required:!0,schema:{type:"string"},description:t})})),e.parameters&&(n.parameters||(n.parameters=[]),e.parameters.forEach(e=>{if("name"in e&&"in"in e){const t=n.parameters.findIndex(t=>t.name===e.name&&t.in===e.in);t>-1?n.parameters[t]=e:n.parameters.push(e)}else n.parameters.push(e)})),e.requestBody&&(n.requestBody,n.requestBody=e.requestBody),e.responses&&(n.responses||(n.responses={}),Object.assign(n.responses,e.responses)),e.security&&(n.security||(n.security=[]),n.security.push(...e.security))})})}tagSpecByPrefix(e,t={}){const{level:s=2,onlyIfEmpty:r=!0}=t,o=new Set(["get","post","put","delete","patch","head","options","trace"]),a=new Set((e.tags||[]).map(e=>e.name)),n=e.paths||{};for(const[e,t]of Object.entries(n)){if(!t)continue;const n="/"+e.split("/").filter(Boolean).slice(0,s).join("/");for(const[e,s]of Object.entries(t)){if(!o.has(e))continue;if(!s||"object"!=typeof s)continue;const t=s,c=Array.isArray(t.tags)&&t.tags.length>0;r&&c||(t.tags=[n]),a.add(n)}}return e.tags=Array.from(a).map(e=>({name:e})),e}generateSpec(){return{openapi:"3.0.0",info:{title:"API Documentation",version:"1.0.0",description:"Auto generated by koa-ts-core"},paths:this.paths,components:{schemas:{},securitySchemes:this.securitySchemes},security:this.security}}}const W=(e,t,s,r,o)=>{if(!r||0===r.size)return;const{pathPrefix:a,moduleName:n,name:c}=O(t,"controller"),i=`${a}/${n}`,d=I(`${e}${a}/${c}`),u=e=>async(t,s)=>{const r=e(t);r instanceof Promise&&await r,await s()};r.forEach(e=>{const{method:t,path:r,handler:n,functionName:c,authRequired:p,middlewares:l}=e,g=[...l],h=Reflect.getMetadata(j,s.prototype,c);h?g.push(u(h)):d&&"function"==typeof d[c]&&g.push(u(d[c].bind(d))),g.push(((e,t)=>async(s,r)=>{let a=o(e,s);if(a instanceof Promise&&(a=await a),!a)throw new E("Access denied");return t(s,r)})(p,n));const m=Z(r,i,c,g,t);z.getInstance().addRoute(s,c,t,m),e.path=m,e.apiPrePath=a})},Z=(e,t,s,r,o)=>{const a=`${t}${e}`;if(s===h){const e=`${t}/index`;$[o](e,...r)}return $[o](a,...r),a};let K=null;const Q=()=>(K||(K=c.configure({appenders:{console:{type:"console"},file:{type:"dateFile",filename:"logs/app.log",pattern:".yyyy-MM-dd",compress:!0,numBackups:7,alwaysIncludePattern:!0}},categories:{default:{appenders:["console","file"],level:"info"}}})),K),X=(e,t)=>v.run(e,()=>t()),Y=e=>{const{generator:t,exposeHeader:s=!0,headerName:r="x-request-id"}=e;return!1===t?async(e,t)=>{await t()}:async(e,o)=>{const a=await t(e);a&&(e.trackId=a,s&&e.set(r,a)),await o()}},G=async(e,t)=>{const s=Date.now(),r=Q().getLogger(),o=e.runtimeLog;o&&r.info("[REQUEST] %s %s %s - ip: %s - ua: %s",e.trackId,e.method,e.url,e.ip,e.get("user-agent")||"-");try{e.hook?.(e,"request"),await t(),e.hook?.(e,"response")}catch(t){const a=Date.now()-s;throw o&&r.error("[ERROR] %s %s %s - status: %s - cost: %dms - message: %s - stack: %s",e.trackId,e.method,e.url,t.status||500,a,t.message||"unknown error",t.stack||""),t}const a=Date.now()-s;o&&r.info("[RESPONSE] %s %s %s - status: %d - cost: %dms - length: %s",e.trackId,e.method,e.url,e.status,a,e.length||e.response.length||"-")},J=async(e,t)=>{e.extra={get:e.query,post:e.request.body??{}},await t()};(()=>{const e=process.env.NODE_ENV||"production";let t;const r=process.env.ENV_DIR;if(r){if(t=s.isAbsolute(r)?r:s.resolve(process.cwd(),r),!C(t))throw new Error(`[koa-ts-core] ENV_DIR is set but directory not found: ${t}`)}else{const e=b();let r=s.resolve(e,"env");if(C(r)||(r=s.resolve(e,"..","env")),!C(r))return void console.warn(`[koa-ts-core] env directory not found: tried ${s.resolve(e,"env")} and ${s.resolve(e,"..","env")}`);t=r}console.log("[koa-ts-core] load env path:",t);const o={development:".development.env",test:".test.env",production:".production.env"}[e];n.config({path:s.resolve(t,".common.env")}),o&&n.config({path:s.resolve(t,o)})})(),global.isDev="development"===process.env.NODE_ENV;const ee=e=>{const{koaInstance:t,auth:s,error:r,log:a,hooks:n,phaseMiddlewares:c,koa2Cors:i,trackId:d,swagger:p,bodyParser:l}=e;return{app:t??new o,auth:{handler:s?.handler},error:{handler:r?.handler,exposeStack:r?.exposeStack??"production"!==process.env.NODE_ENV},log:{log4:a?.log4??!1,runtimeLog:a?.runtimeLog??!0},hooks:{register:n?.register},phaseMiddlewares:c??new Map,koa2Cors:i,trackId:{generator:!1!==d?.generator&&(async e=>{const t=d?.headerName??"x-request-id",s=e.get(t);return s||u.randomUUID()}),exposeHeader:d?.exposeHeader??!0,headerName:d?.headerName??"x-request-id"},swagger:p,bodyParser:l}},te=(e,t)=>{const{log:s,hooks:r}=t;e.context.log4=(e=>{if(!1!==e)return"boolean"==typeof e&&!0===e?Q():e(c)})(s.log4),e.context.runtimeLog=s.runtimeLog,e.context.hook=r.register,e.context.validated={params:{},get:{},post:{}}},se=e=>{(e=>{const{controllerModule:t,validateModule:s}=R();console.log("load controller path:",t),A(t,t=>{if(!t.name.endsWith(".ts")&&!t.name.endsWith(".js"))return;const r=I(t.wholePath);if(!r)return;const o=Reflect.getMetadata(y,r);o instanceof Map&&0!==o.size&&W(s,t,r,o,(s,r)=>{if(!s)return!0;if(!e)throw new Error(`Route requires auth but no authCheckCallback provided. File: ${t.wholePath}`);return e({filePath:t,ctx:r})})})})(e),$.get("/",async e=>{e.body={code:200,message:"hello koa-ts-cli https://www.npmjs.com/package/koa-ts-core"}})};exports.AuthRouter=function(e,t){return x(e,t,!0)},exports.BaseException=M,exports.ForbiddenException=E,exports.INDEX_ROUTE=h,exports.ROUTE_METADATA_KEY=y,exports.Router=function(e,t){return x(e,t,!1)},exports.contextStore=v,exports.createKoaApp=async e=>{const t=ee(e),{app:s}=t;if(te(s,t),se(t.auth.handler),t.swagger?.enabled){z.getInstance().loadDocMetadata();const e=z.getInstance().generateSpec();t.swagger.title&&(e.info.title=t.swagger.title),t.swagger.description&&(e.info.description=t.swagger.description),t.swagger.version&&(e.info.version=t.swagger.version),(t.swagger.securitySchemes||t.swagger.security)&&(z.getInstance().setSecurity(t.swagger.securitySchemes||{},t.swagger.security||[]),Object.assign(e,z.getInstance().generateSpec()));const r=t.swagger.path||"/swagger-ui",o=z.getInstance().tagSpecByPrefix(e,{level:2,onlyIfEmpty:!1});s.use(p.koaSwagger({routePrefix:r,swaggerOptions:{spec:o}})),console.log(`[koa-ts-core] Swagger UI available at ${r}`)}return((e,t,s)=>{const{errorConfig:r,koa2Cors:o,trackConfig:a,bodyParser:n}=s,c={[exports.MiddlewarePhase.AsyncContext]:[X],[exports.MiddlewarePhase.ErrorHandling]:[(u=r?.handler,async(e,t)=>{try{await t()}catch(t){if(e.hook?.(e,"error"),u)u(t,e);else{const e=t.statusCode||t.status||500,s=t.code??-1;S(e,{message:t.message,code:s,data:"production"!==process.env.NODE_ENV?t.stack:void 0})}}})],[exports.MiddlewarePhase.Logging]:[Y(a),G],[exports.MiddlewarePhase.Security]:[...o?[o(d)]:[]],[exports.MiddlewarePhase.BodyParsing]:[i(n),J],[exports.MiddlewarePhase.Auth]:[],[exports.MiddlewarePhase.Routing]:[$.routes()]};var u;const p=[exports.MiddlewarePhase.AsyncContext,exports.MiddlewarePhase.ErrorHandling,exports.MiddlewarePhase.Logging,exports.MiddlewarePhase.Security,exports.MiddlewarePhase.BodyParsing,exports.MiddlewarePhase.Auth,exports.MiddlewarePhase.Routing];for(const s of p){const r=t.get(s),o=c[s]??[];if(r?.before)for(const t of r.before)e.use(t);for(const t of o)e.use(t);if(r?.use)for(const t of r.use)e.use(t);if(r?.after)for(const t of r.after)e.use(t)}})(s,t.phaseMiddlewares,{errorConfig:t.error,koa2Cors:t.koa2Cors,trackConfig:t.trackId,bodyParser:t.bodyParser}),await D(s),"production"===process.env.NODE_ENV&&console.log(`[koa-ts-core] version: ${require("../package.json").version}`),[s,$]},exports.errorRsp=S,exports.getCurrentContext=P,exports.getSrcModulePaths=R,exports.successRsp=e=>{const t=Array.isArray(e?.data);k({success:!0,message:e?.message,data:t?{list:e?.data}:e?.data})},exports.unSuccessRsp=e=>{k({success:!1,errorCode:e?.code,errorMessage:e?.message,data:e?.data})};
|
package/dist/index.esm.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import"reflect-metadata";import{AsyncLocalStorage as e}from"async_hooks";import{statSync as t,existsSync as o,readdirSync as s}from"fs";import{resolve as r,sep as a,join as n,isAbsolute as c}from"path";import i from"@koa/router";import p from"koa";import d from"portfinder";import u from"dotenv";import l from"log4js";import h from"koa-bodyparser";import g from"koa2-cors";import{randomUUID as m}from"crypto";import{koaSwagger as f}from"koa2-swagger-ui";const y=["get","post","put","delete","patch","options"],w=0,k="index",v="get_",b=[k,v,...y],P=Symbol("route_metadata");function x(e){const t=e.trim();if(!t)return"";return`/${t.replace(/^\/+/,"")}`}function E(e,t,o=!1){return(s,r,a)=>{if("function"!=typeof a.value)throw new Error(`@Router/@AuthRouter 只能用于方法:${r}`);let n=Reflect.getMetadata(P,s.constructor);n||(n=new Map,Reflect.defineMetadata(P,n,s.constructor));const c=function(e,t){return e||(t===v?"get":y.includes(t)?t:"get")}(e,r),i=function(e,t){return null!=e?x(e):b.includes(t)?"":x(t)}(t,r),p={handler:a.value,path:i,method:c,functionName:r,authRequired:o,middlewares:[],className:s.constructor.name,apiPrePath:""};return n.set(r,p),a}}function q(e,t){return E(e,t,!1)}function R(e,t){return E(e,t,!0)}const N=new e,I=()=>{const e=N.getStore();if(!e)throw new Error("context is not exist");return e},M=e=>{const{success:t=!0,errorCode:o=-1,message:s="success",errorMessage:r="error request",data:a=null,statusCode:n=200}=e??{},c=I(),i={code:t?w:o,message:t?s:r,...null!=a?{data:a}:{}};c.trackId&&(i.trackId=c.trackId),c.body=i,c.status=n},O=e=>{const t=Array.isArray(e?.data);M({success:!0,message:e?.message,data:t?{list:e?.data}:e?.data})},S=e=>{M({success:!1,errorCode:e?.code,errorMessage:e?.message,data:e?.data})},C=(e,t)=>{M({success:!1,errorMessage:t?.message,errorCode:t?.code,data:t?.data,statusCode:e})};class $ extends Error{constructor(e){super(e?.message||"An unexpected error occurred"),this.name=this.constructor.name,this.code=e?.code??-1,this.statusCode=e?.statusCode??500,"function"==typeof Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor),this.stack=this.stack??new Error(this.message).stack}get toRspOptions(){return{code:this.code,message:this.message,data:this.stack}}}class A extends ${constructor(e){super({statusCode:403,message:e})}}const _=()=>{const e=process.cwd();if(o(r(e,"controller")))return e;const t=r(e,"src");if(o(t))return t;const s=r(e,"dist");if(o(s))return s;const a=r(e,"build");return o(a)?a:t},j=()=>{const e=_();return{controllerModule:r(e,"controller"),validateModule:r(e,"validate"),viewModule:r(e,"view"),docModule:r(e,"doc")}},D=e=>{if(o(e))try{const t=require(e);return t.default||t}catch(t){throw console.error(`[koa-ts-core] Failed to load module: ${e}`,t),t}};function T(e,t){o(e)&&s(e,{withFileTypes:!0}).forEach(o=>{const s=r(e,o.name);o.isFile()?t({path:e,name:o.name,wholePath:s},o):o.isDirectory()&&T(s,t)})}const L=e=>e.split(a).join("/"),B=e=>{const t=L(e).replace(/^\/+/,"");return t?`/${t}`:""},H=(e,t)=>{const o=L(e.path).split("/"),s=o.lastIndexOf(t);if(-1===s){const o=e.path.indexOf(t);if(-1===o)throw new Error(`'${t}' not found in path: ${e.path}`);const s=e.path.substring(o+t.length),r=B(s),a=e.name?.split(".")?.[0];return{pathPrefix:r,moduleName:a,name:e.name}}const r=o.slice(s+1).join("/"),a=B(r),n=e.name?.split(".")?.[0];return{pathPrefix:a,moduleName:n,name:e.name}};function V(e){try{return t(e).isDirectory()}catch{return!1}}var F;!function(e){e.AsyncContext="AsyncContext",e.ErrorHandling="ErrorHandling",e.Logging="Logging",e.Security="Security",e.BodyParsing="BodyParsing",e.Auth="Auth",e.Routing="Routing"}(F||(F={}));const z=new i,U=async e=>{if(!process.env.APP_PORT)try{const e=await d.getPortPromise({port:8e3,stopPort:9999});process.env.APP_PORT=String(e)}catch(e){console.warn("[koa-ts-core] Failed to find available port automatically.")}const t=e.listen;e.listen=function(...e){if(0===e.length&&process.env.APP_PORT){const t=Number(process.env.APP_PORT);isNaN(t)||e.push(t)}const o=t.apply(this,e);return o.on("listening",()=>(e=>{const t=e.address();if(t)if("string"==typeof t)console.log(`Server listening on ${t}`);else{const{port:e}=t;console.log(`Server running at http://localhost:${e}`)}})(o)),o}},W=Symbol("validate_metadata"),Z=Symbol("swagger_tags"),Q=Symbol("swagger_operation"),G=Symbol("swagger_parameters"),J=Symbol("swagger_body"),K=Symbol("swagger_responses"),X={DOCUMENTATION_DIR:n(process.cwd(),"doc")},Y=()=>{const e=[];return V(X.DOCUMENTATION_DIR)?(T(X.DOCUMENTATION_DIR,t=>{if(!t.name.endsWith(".ts")&&!t.name.endsWith(".js"))return;const{pathPrefix:o}=H(t,"doc"),s=((e,t)=>{const o=D(e.wholePath);if(!o)return{configs:[],desc:"",name:"",pathPrefix:t,apiPrePath:""};const s=o.prototype,r=Object.getOwnPropertyNames(s).filter(e=>"function"==typeof s[e]&&"constructor"!==e),a=e.path.indexOf("doc"),n=a>=0?B(e.path.substring(a+3)):"";let c=n;if(e.name&&!e.name.startsWith("index.")){const t=e.name.split(".")[0];c=`${n}/${t}`}const i=r.map(e=>{const t=s[e];if("function"==typeof t){const e=t();return e.path=`${c}${e.path}`,e}return null}).filter(e=>!!e);let p="";if(i[0]?.path){const e=i[0].path.lastIndexOf("/");e>-1&&(p=i[0].path.substring(0,e))}return{configs:i,desc:o.desc,name:o.name,pathPrefix:t,apiPrePath:p}})(t,o);s.configs.length>0&&e.push(s)}),e):[]};class ee{constructor(){this.paths={}}static getInstance(){return this.instance||(this.instance=new ee),this.instance}addRoute(e,t,o,s){const r=s.replace(/:([a-zA-Z0-9_]+)/g,"{$1}");this.paths[r]||(this.paths[r]={});const a=e.prototype,n=Reflect.getMetadata(Q,a,t)||{},c=n.summary,i=n.description,p=Reflect.getMetadata(Z,e)||[e.name],d=Reflect.getMetadata(G,a,t)||[],u=Reflect.getMetadata(J,a,t);let l;u&&(l={description:u.description,required:u.required,content:{"application/json":{schema:u.schema||{type:"object"}}}});const h=Reflect.getMetadata(K,a,t)||[],g={};h.forEach(e=>{g[e.status]={description:e.description||"",content:e.schema?{"application/json":{schema:e.schema}}:void 0}}),0===Object.keys(g).length&&(g[200]={description:"Success"}),this.paths[r][o.toLowerCase()]={tags:p,summary:c,description:i,parameters:d,requestBody:l,responses:g}}loadDocMetadata(){Y().forEach(e=>{const t=e.desc||e.name;e.configs.forEach(e=>{const o=("/"+e.path).replace(/\/+/g,"/"),s=o.replace(/:([a-zA-Z0-9_]+)/g,"{$1}"),r=e.method.toLowerCase();this.paths[s]||(this.paths[s]={}),this.paths[s][r]||(this.paths[s][r]={});const a=this.paths[s][r];a.summary||(a.summary=e.description),a.tags&&0!==a.tags.length||(a.tags=[t]);const n=o.match(/:([a-zA-Z0-9_]+)/g);if(n&&(a.parameters||(a.parameters=[]),n.forEach(e=>{const t=e.substring(1);a.parameters.find(e=>e.name===t&&"path"===e.in)||a.parameters.push({name:t,in:"path",required:!0,schema:{type:"string"},description:t})})),e.request?.query&&(a.parameters||(a.parameters=[]),Object.keys(e.request.query).forEach(t=>{if(!a.parameters.find(e=>e.name===t&&"query"===e.in)){const o=e.request?.query?.[t];o&&"object"==typeof o&&a.parameters.push({name:t,in:"query",description:o.description,required:!1!==o.required,schema:o.schema??{type:o.type??"string"},example:o.example})}})),e.request?.header&&(a.parameters||(a.parameters=[]),Object.keys(e.request.header).forEach(t=>{if("content-type"===t.toLowerCase())return;if(!a.parameters.find(e=>e.name===t&&"header"===e.in)){const o=e.request?.header?.[t];o&&"object"==typeof o&&a.parameters.push({name:t,in:"header",description:o.description,required:!1!==o.required,schema:o.schema??{type:o.type??"string"},example:o.example})}})),e.request?.body&&Object.keys(e.request.body).length>0&&!a.requestBody){const t=e.request.body;a.requestBody={description:t.description,required:t.required,content:{"application/json":{schema:{type:"object",properties:t.schema},example:t.example}}}}e.response?.body&&(a.responses||(a.responses={}),a.responses[200]||(a.responses[200]={description:"Success",content:{"application/json":{schema:{type:"object",example:e.response.body}}}}))})})}tagSpecByPrefix(e,t={}){const{level:o=2,onlyIfEmpty:s=!0}=t,r=new Set(["get","post","put","delete","patch","head","options","trace"]),a=new Set((e.tags||[]).map(e=>e.name)),n=e.paths||{};for(const[e,t]of Object.entries(n)){if(!t)continue;const n="/"+e.split("/").filter(Boolean).slice(0,o).join("/");for(const[e,o]of Object.entries(t)){if(!r.has(e))continue;if(!o||"object"!=typeof o)continue;const t=o,c=Array.isArray(t.tags)&&t.tags.length>0;s&&c||(t.tags=[n]),a.add(n)}}return e.tags=Array.from(a).map(e=>({name:e})),e}generateSpec(){return{openapi:"3.0.0",info:{title:"API Documentation",version:"1.0.0",description:"Auto generated by koa-ts-core"},paths:this.paths,components:{schemas:{}}}}}const te=(e,t,o,s,r)=>{if(!s||0===s.size)return;const{pathPrefix:a,moduleName:n,name:c}=H(t,"controller"),i=`${a}/${n}`,p=D(`${e}${a}/${c}`),d=e=>async(t,o)=>{const s=e(t);s instanceof Promise&&await s,await o()};s.forEach(e=>{const{method:t,path:s,handler:n,functionName:c,authRequired:u,middlewares:l}=e,h=[...l],g=Reflect.getMetadata(W,o.prototype,c);g?h.push(d(g)):p&&"function"==typeof p[c]&&h.push(d(p[c].bind(p))),h.push(((e,t)=>async(o,s)=>{let a=r(e,o);if(a instanceof Promise&&(a=await a),!a)throw new A("Access denied");return t(o,s)})(u,n));const m=oe(s,i,c,h,t);ee.getInstance().addRoute(o,c,t,m),e.path=m,e.apiPrePath=a})},oe=(e,t,o,s,r)=>{const a=`${t}${e}`;if(o===k){const e=`${t}/index`;z[r](e,...s)}return z[r](a,...s),a};let se=null;const re=()=>(se||(se=l.configure({appenders:{console:{type:"console"},file:{type:"dateFile",filename:"logs/app.log",pattern:".yyyy-MM-dd",compress:!0,numBackups:7,alwaysIncludePattern:!0}},categories:{default:{appenders:["console","file"],level:"info"}}})),se),ae=(e,t)=>N.run(e,()=>t()),ne=e=>{const{generator:t,exposeHeader:o=!0,headerName:s="x-request-id"}=e;return!1===t?async(e,t)=>{await t()}:async(e,r)=>{const a=await t(e);a&&(e.trackId=a,o&&e.set(s,a)),await r()}},ce=async(e,t)=>{const o=Date.now(),s=re().getLogger(),r=e.runtimeLog;r&&s.info("[REQUEST] %s %s %s - ip: %s - ua: %s",e.trackId,e.method,e.url,e.ip,e.get("user-agent")||"-");try{e.hook?.(e,"request"),await t(),e.hook?.(e,"response")}catch(t){const a=Date.now()-o;throw r&&s.error("[ERROR] %s %s %s - status: %s - cost: %dms - message: %s - stack: %s",e.trackId,e.method,e.url,t.status||500,a,t.message||"unknown error",t.stack||""),t}const a=Date.now()-o;r&&s.info("[RESPONSE] %s %s %s - status: %d - cost: %dms - length: %s",e.trackId,e.method,e.url,e.status,a,e.length||e.response.length||"-")},ie=async(e,t)=>{e.extra={get:e.query,post:e.request.body??{}},await t()};(()=>{const e=process.env.NODE_ENV||"production";let t;const o=process.env.ENV_DIR;if(o){if(t=c(o)?o:r(process.cwd(),o),!V(t))throw new Error(`[koa-ts-core] ENV_DIR is set but directory not found: ${t}`)}else{const e=_();let o=r(e,"env");if(V(o)||(o=r(e,"..","env")),!V(o))return void console.warn(`[koa-ts-core] env directory not found: tried ${r(e,"env")} and ${r(e,"..","env")}`);t=o}console.log("[koa-ts-core] load env path:",t),u.config({path:r(t,".env.local")});const s={development:".development.env",test:".test.env",production:".production.env"}[e];s&&u.config({path:r(t,`${s}.local`)}),s&&u.config({path:r(t,s)}),u.config({path:r(t,".common.env")})})(),global.isDev="development"===process.env.NODE_ENV;const pe=e=>{const{koaInstance:t,auth:o,error:s,log:r,hooks:a,phaseMiddlewares:n,koa2Cors:c,trackId:i,swagger:d,bodyParser:u}=e;return{app:t??new p,auth:{handler:o?.handler},error:{handler:s?.handler,exposeStack:s?.exposeStack??"production"!==process.env.NODE_ENV},log:{log4:r?.log4??!1,runtimeLog:r?.runtimeLog??!0},hooks:{register:a?.register},phaseMiddlewares:n??new Map,koa2Cors:c,trackId:{generator:!1!==i?.generator&&(async e=>{const t=i?.headerName??"x-request-id",o=e.get(t);return o||m()}),exposeHeader:i?.exposeHeader??!0,headerName:i?.headerName??"x-request-id"},swagger:d,bodyParser:u}},de=(e,t)=>{const{log:o,hooks:s}=t;e.context.log4=(e=>{if(!1!==e)return"boolean"==typeof e&&!0===e?re():e(l)})(o.log4),e.context.runtimeLog=o.runtimeLog,e.context.hook=s.register,e.context.validated={params:{},get:{},post:{}}},ue=e=>{(e=>{const{controllerModule:t,validateModule:o}=j();console.log("load controller path:",t),T(t,t=>{if(!t.name.endsWith(".ts")&&!t.name.endsWith(".js"))return;const s=D(t.wholePath);if(!s)return;const r=Reflect.getMetadata(P,s);r instanceof Map&&0!==r.size&&te(o,t,s,r,(o,s)=>{if(!o)return!0;if(!e)throw new Error(`Route requires auth but no authCheckCallback provided. File: ${t.wholePath}`);return e({filePath:t,ctx:s})})})})(e),z.get("/",async e=>{e.body={code:200,message:"hello koa-ts-cli https://www.npmjs.com/package/koa-ts-core"}})},le=async e=>{const t=pe(e),{app:o}=t;if(de(o,t),ue(t.auth.handler),t.swagger?.enabled){ee.getInstance().loadDocMetadata();const e=ee.getInstance().generateSpec();t.swagger.title&&(e.info.title=t.swagger.title),t.swagger.description&&(e.info.description=t.swagger.description),t.swagger.version&&(e.info.version=t.swagger.version);const s=t.swagger.path||"/swagger-ui";o.use(f({routePrefix:s,swaggerOptions:{spec:ee.getInstance().tagSpecByPrefix(e,{level:2,onlyIfEmpty:!1})}})),console.log(`[koa-ts-core] Swagger UI available at ${s}`)}return((e,t,o)=>{const{errorConfig:s,koa2Cors:r,trackConfig:a,bodyParser:n}=o,c={[F.AsyncContext]:[ae],[F.ErrorHandling]:[(i=s?.handler,async(e,t)=>{try{await t()}catch(t){if(e.hook?.(e,"error"),i)i(t,e);else{const e=t.statusCode||t.status||500,o=t.code??-1;C(e,{message:t.message,code:o,data:"production"!==process.env.NODE_ENV?t.stack:void 0})}}})],[F.Logging]:[ne(a),ce],[F.Security]:[...r?[r(g)]:[]],[F.BodyParsing]:[h(n),ie],[F.Auth]:[],[F.Routing]:[z.routes()]};var i;const p=[F.AsyncContext,F.ErrorHandling,F.Logging,F.Security,F.BodyParsing,F.Auth,F.Routing];for(const o of p){const s=t.get(o),r=c[o]??[];if(s?.before)for(const t of s.before)e.use(t);for(const t of r)e.use(t);if(s?.use)for(const t of s.use)e.use(t);if(s?.after)for(const t of s.after)e.use(t)}})(o,t.phaseMiddlewares,{errorConfig:t.error,koa2Cors:t.koa2Cors,trackConfig:t.trackId,bodyParser:t.bodyParser}),await U(o),"production"===process.env.NODE_ENV&&console.log(`[koa-ts-core] version: ${require("../package.json").version}`),[o,z]};export{R as AuthRouter,$ as BaseException,k as INDEX_ROUTE,F as MiddlewarePhase,A as ParamException,P as ROUTE_METADATA_KEY,q as Router,N as contextStore,le as createKoaApp,C as errorRsp,I as getCurrentContext,j as getSrcModulePaths,O as successRsp,S as unSuccessRsp};
|
|
1
|
+
import"reflect-metadata";import{AsyncLocalStorage as e}from"async_hooks";import{statSync as t,existsSync as s,readdirSync as o}from"fs";import{resolve as r,sep as a,join as n,isAbsolute as c}from"path";import i from"@koa/router";import u from"koa";import d from"portfinder";import p from"dotenv";import l from"log4js";import g from"koa-bodyparser";import h from"koa2-cors";import{randomUUID as m}from"crypto";import{koaSwagger as f}from"koa2-swagger-ui";const y=["get","post","put","delete","patch","options"],w=0,k="index",v="get_",P=[k,v,...y],b=Symbol("route_metadata");function E(e){const t=e.trim();if(!t)return"";return`/${t.replace(/^\/+/,"")}`}function x(e,t,s=!1){return(o,r,a)=>{if("function"!=typeof a.value)throw new Error(`@Router/@AuthRouter 只能用于方法:${r}`);let n=Reflect.getMetadata(b,o.constructor);n||(n=new Map,Reflect.defineMetadata(b,n,o.constructor));const c=function(e,t){return e||(t===v?"get":y.includes(t)?t:"get")}(e,r),i=function(e,t){return null!=e?E(e):P.includes(t)?"":E(t)}(t,r),u={handler:a.value,path:i,method:c,functionName:r,authRequired:s,middlewares:[],className:o.constructor.name,apiPrePath:""};return n.set(r,u),a}}function S(e,t){return x(e,t,!1)}function R(e,t){return x(e,t,!0)}const I=new e,N=()=>{const e=I.getStore();if(!e)throw new Error("context is not exist");return e},M=e=>{const{success:t=!0,errorCode:s=-1,message:o="success",errorMessage:r="error request",data:a=null,statusCode:n=200}=e??{},c=N(),i={code:t?w:s,message:t?o:r,...null!=a?{data:a}:{}};c.trackId&&(i.trackId=c.trackId),c.body=i,c.status=n},O=e=>{const t=Array.isArray(e?.data);M({success:!0,message:e?.message,data:t?{list:e?.data}:e?.data})},C=e=>{M({success:!1,errorCode:e?.code,errorMessage:e?.message,data:e?.data})},A=(e,t)=>{M({success:!1,errorMessage:t?.message,errorCode:t?.code,data:t?.data,statusCode:e})};class $ extends Error{constructor(e){super(e?.message||"An unexpected error occurred"),this.name=this.constructor.name,this.code=e?.code??-1,this.statusCode=e?.statusCode??500,"function"==typeof Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor),this.stack=this.stack??new Error(this.message).stack}get toRspOptions(){return{code:this.code,message:this.message,data:this.stack}}}class _ extends ${constructor(e){super({statusCode:403,message:e})}}const D=()=>{const e=process.cwd();if(s(r(e,"controller")))return e;const t=r(e,"src");if(s(t))return t;const o=r(e,"dist");if(s(o))return o;const a=r(e,"build");return s(a)?a:t},q=()=>{const e=D();return{controllerModule:r(e,"controller"),validateModule:r(e,"validate"),viewModule:r(e,"view"),docModule:r(e,"doc")}},j=e=>{if(s(e))try{const t=require(e);return t.default||t}catch(t){throw console.error(`[koa-ts-core] Failed to load module: ${e}`,t),t}};function T(e,t){s(e)&&o(e,{withFileTypes:!0}).forEach(s=>{const o=r(e,s.name);s.isFile()?t({path:e,name:s.name,wholePath:o},s):s.isDirectory()&&T(o,t)})}const B=e=>e.split(a).join("/"),L=e=>{const t=B(e).replace(/^\/+/,"");return t?`/${t}`:""},H=(e,t)=>{const s=B(e.path).split("/"),o=s.lastIndexOf(t);if(-1===o){const s=e.path.indexOf(t);if(-1===s)throw new Error(`'${t}' not found in path: ${e.path}`);const o=e.path.substring(s+t.length),r=L(o),a=e.name?.split(".")?.[0];return{pathPrefix:r,moduleName:a,name:e.name}}const r=s.slice(o+1).join("/"),a=L(r),n=e.name?.split(".")?.[0];return{pathPrefix:a,moduleName:n,name:e.name}};function V(e){try{return t(e).isDirectory()}catch{return!1}}var F;!function(e){e.AsyncContext="AsyncContext",e.ErrorHandling="ErrorHandling",e.Logging="Logging",e.Security="Security",e.BodyParsing="BodyParsing",e.Auth="Auth",e.Routing="Routing"}(F||(F={}));const z=new i,U=async e=>{if(!process.env.APP_PORT)try{const e=await d.getPortPromise({port:8e3,stopPort:9999});process.env.APP_PORT=String(e)}catch(e){console.warn("[koa-ts-core] Failed to find available port automatically.")}const t=e.listen;e.listen=function(...e){if(0===e.length&&process.env.APP_PORT){const t=Number(process.env.APP_PORT);isNaN(t)||e.push(t)}const s=t.apply(this,e);return s.on("listening",()=>(e=>{const t=e.address();if(t)if("string"==typeof t)console.log(`Server listening on ${t}`);else{const{port:e}=t;console.log(`Server running at http://localhost:${e}`)}})(s)),s}},W=Symbol("validate_metadata"),Z=Symbol("swagger_tags"),Q=Symbol("swagger_operation"),G=Symbol("swagger_parameters"),J=Symbol("swagger_body"),K=Symbol("swagger_responses"),X={DOCUMENTATION_DIR:n(process.cwd(),"doc")},Y=()=>{const e=[];return V(X.DOCUMENTATION_DIR)?(T(X.DOCUMENTATION_DIR,t=>{if(!t.name.endsWith(".ts")&&!t.name.endsWith(".js"))return;const{pathPrefix:s}=H(t,"doc"),o=((e,t)=>{const s=j(e.wholePath);if(!s)return{configs:[],desc:"",name:"",pathPrefix:t,apiPrePath:""};const o=s.prototype,r=Object.getOwnPropertyNames(o).filter(e=>"function"==typeof o[e]&&"constructor"!==e),a=e.path.indexOf("doc"),n=a>=0?L(e.path.substring(a+3)):"";let c=n;if(e.name&&!e.name.startsWith("index.")){const t=e.name.split(".")[0];c=`${n}/${t}`}const i=r.map(e=>{const t=o[e];if("function"==typeof t){const e=t();return e.path=`${c}${e.path}`,e}return null}).filter(e=>!!e);let u="";if(i[0]?.path){const e=i[0].path.lastIndexOf("/");e>-1&&(u=i[0].path.substring(0,e))}return{configs:i,desc:s.desc,name:s.name,pathPrefix:t,apiPrePath:u}})(t,s);o.configs.length>0&&e.push(o)}),e):[]};class ee{constructor(){this.paths={},this.securitySchemes={},this.security=[]}setSecurity(e,t){this.securitySchemes=e,this.security=t}static getInstance(){return this.instance||(this.instance=new ee),this.instance}addRoute(e,t,s,o){const r=o.replace(/:([a-zA-Z0-9_]+)/g,"{$1}");this.paths[r]||(this.paths[r]={});const a=e.prototype,n=Reflect.getMetadata(Q,a,t)||{},c=n.summary,i=n.description,u=Reflect.getMetadata(Z,e)||[e.name],d=Reflect.getMetadata(G,a,t)||[],p=Reflect.getMetadata(J,a,t);let l;p&&(l={description:p.description,required:p.required,content:{"application/json":{schema:p.schema||{type:"object"}}}});const g=Reflect.getMetadata(K,a,t)||[],h={};g.forEach(e=>{h[e.status]={description:e.description||"",content:e.schema?{"application/json":{schema:e.schema}}:void 0}}),0===Object.keys(h).length&&(h[200]={description:"Success"});const m=s.toLowerCase(),f=this.paths[r]||{};this.paths[r]=f,f[m]={tags:u,summary:c,description:i,parameters:d,requestBody:l,responses:h}}loadDocMetadata(){Y().forEach(e=>{const t=e.desc||e.name;e.configs.forEach(e=>{const s=("/"+e.path).replace(/\/+/g,"/"),o=s.replace(/:([a-zA-Z0-9_]+)/g,"{$1}"),r=e.method.toLowerCase();this.paths[o]||(this.paths[o]={});const a=this.paths[o];a[r]||(a[r]={});const n=a[r];!n.summary&&e.summary&&(n.summary=e.summary),!n.description&&e.description&&(n.description=e.description),n.tags&&0!==n.tags.length||(e.tags&&e.tags.length>0?n.tags=e.tags:n.tags=[t]);const c=s.match(/:([a-zA-Z0-9_]+)/g);c&&(n.parameters||(n.parameters=[]),c.forEach(e=>{const t=e.substring(1);n.parameters.find(e=>e.name===t&&"path"===e.in)||n.parameters.push({name:t,in:"path",required:!0,schema:{type:"string"},description:t})})),e.parameters&&(n.parameters||(n.parameters=[]),e.parameters.forEach(e=>{if("name"in e&&"in"in e){const t=n.parameters.findIndex(t=>t.name===e.name&&t.in===e.in);t>-1?n.parameters[t]=e:n.parameters.push(e)}else n.parameters.push(e)})),e.requestBody&&(n.requestBody,n.requestBody=e.requestBody),e.responses&&(n.responses||(n.responses={}),Object.assign(n.responses,e.responses)),e.security&&(n.security||(n.security=[]),n.security.push(...e.security))})})}tagSpecByPrefix(e,t={}){const{level:s=2,onlyIfEmpty:o=!0}=t,r=new Set(["get","post","put","delete","patch","head","options","trace"]),a=new Set((e.tags||[]).map(e=>e.name)),n=e.paths||{};for(const[e,t]of Object.entries(n)){if(!t)continue;const n="/"+e.split("/").filter(Boolean).slice(0,s).join("/");for(const[e,s]of Object.entries(t)){if(!r.has(e))continue;if(!s||"object"!=typeof s)continue;const t=s,c=Array.isArray(t.tags)&&t.tags.length>0;o&&c||(t.tags=[n]),a.add(n)}}return e.tags=Array.from(a).map(e=>({name:e})),e}generateSpec(){return{openapi:"3.0.0",info:{title:"API Documentation",version:"1.0.0",description:"Auto generated by koa-ts-core"},paths:this.paths,components:{schemas:{},securitySchemes:this.securitySchemes},security:this.security}}}const te=(e,t,s,o,r)=>{if(!o||0===o.size)return;const{pathPrefix:a,moduleName:n,name:c}=H(t,"controller"),i=`${a}/${n}`,u=j(`${e}${a}/${c}`),d=e=>async(t,s)=>{const o=e(t);o instanceof Promise&&await o,await s()};o.forEach(e=>{const{method:t,path:o,handler:n,functionName:c,authRequired:p,middlewares:l}=e,g=[...l],h=Reflect.getMetadata(W,s.prototype,c);h?g.push(d(h)):u&&"function"==typeof u[c]&&g.push(d(u[c].bind(u))),g.push(((e,t)=>async(s,o)=>{let a=r(e,s);if(a instanceof Promise&&(a=await a),!a)throw new _("Access denied");return t(s,o)})(p,n));const m=se(o,i,c,g,t);ee.getInstance().addRoute(s,c,t,m),e.path=m,e.apiPrePath=a})},se=(e,t,s,o,r)=>{const a=`${t}${e}`;if(s===k){const e=`${t}/index`;z[r](e,...o)}return z[r](a,...o),a};let oe=null;const re=()=>(oe||(oe=l.configure({appenders:{console:{type:"console"},file:{type:"dateFile",filename:"logs/app.log",pattern:".yyyy-MM-dd",compress:!0,numBackups:7,alwaysIncludePattern:!0}},categories:{default:{appenders:["console","file"],level:"info"}}})),oe),ae=(e,t)=>I.run(e,()=>t()),ne=e=>{const{generator:t,exposeHeader:s=!0,headerName:o="x-request-id"}=e;return!1===t?async(e,t)=>{await t()}:async(e,r)=>{const a=await t(e);a&&(e.trackId=a,s&&e.set(o,a)),await r()}},ce=async(e,t)=>{const s=Date.now(),o=re().getLogger(),r=e.runtimeLog;r&&o.info("[REQUEST] %s %s %s - ip: %s - ua: %s",e.trackId,e.method,e.url,e.ip,e.get("user-agent")||"-");try{e.hook?.(e,"request"),await t(),e.hook?.(e,"response")}catch(t){const a=Date.now()-s;throw r&&o.error("[ERROR] %s %s %s - status: %s - cost: %dms - message: %s - stack: %s",e.trackId,e.method,e.url,t.status||500,a,t.message||"unknown error",t.stack||""),t}const a=Date.now()-s;r&&o.info("[RESPONSE] %s %s %s - status: %d - cost: %dms - length: %s",e.trackId,e.method,e.url,e.status,a,e.length||e.response.length||"-")},ie=async(e,t)=>{e.extra={get:e.query,post:e.request.body??{}},await t()};(()=>{const e=process.env.NODE_ENV||"production";let t;const s=process.env.ENV_DIR;if(s){if(t=c(s)?s:r(process.cwd(),s),!V(t))throw new Error(`[koa-ts-core] ENV_DIR is set but directory not found: ${t}`)}else{const e=D();let s=r(e,"env");if(V(s)||(s=r(e,"..","env")),!V(s))return void console.warn(`[koa-ts-core] env directory not found: tried ${r(e,"env")} and ${r(e,"..","env")}`);t=s}console.log("[koa-ts-core] load env path:",t);const o={development:".development.env",test:".test.env",production:".production.env"}[e];p.config({path:r(t,".common.env")}),o&&p.config({path:r(t,o)})})(),global.isDev="development"===process.env.NODE_ENV;const ue=e=>{const{koaInstance:t,auth:s,error:o,log:r,hooks:a,phaseMiddlewares:n,koa2Cors:c,trackId:i,swagger:d,bodyParser:p}=e;return{app:t??new u,auth:{handler:s?.handler},error:{handler:o?.handler,exposeStack:o?.exposeStack??"production"!==process.env.NODE_ENV},log:{log4:r?.log4??!1,runtimeLog:r?.runtimeLog??!0},hooks:{register:a?.register},phaseMiddlewares:n??new Map,koa2Cors:c,trackId:{generator:!1!==i?.generator&&(async e=>{const t=i?.headerName??"x-request-id",s=e.get(t);return s||m()}),exposeHeader:i?.exposeHeader??!0,headerName:i?.headerName??"x-request-id"},swagger:d,bodyParser:p}},de=(e,t)=>{const{log:s,hooks:o}=t;e.context.log4=(e=>{if(!1!==e)return"boolean"==typeof e&&!0===e?re():e(l)})(s.log4),e.context.runtimeLog=s.runtimeLog,e.context.hook=o.register,e.context.validated={params:{},get:{},post:{}}},pe=e=>{(e=>{const{controllerModule:t,validateModule:s}=q();console.log("load controller path:",t),T(t,t=>{if(!t.name.endsWith(".ts")&&!t.name.endsWith(".js"))return;const o=j(t.wholePath);if(!o)return;const r=Reflect.getMetadata(b,o);r instanceof Map&&0!==r.size&&te(s,t,o,r,(s,o)=>{if(!s)return!0;if(!e)throw new Error(`Route requires auth but no authCheckCallback provided. File: ${t.wholePath}`);return e({filePath:t,ctx:o})})})})(e),z.get("/",async e=>{e.body={code:200,message:"hello koa-ts-cli https://www.npmjs.com/package/koa-ts-core"}})},le=async e=>{const t=ue(e),{app:s}=t;if(de(s,t),pe(t.auth.handler),t.swagger?.enabled){ee.getInstance().loadDocMetadata();const e=ee.getInstance().generateSpec();t.swagger.title&&(e.info.title=t.swagger.title),t.swagger.description&&(e.info.description=t.swagger.description),t.swagger.version&&(e.info.version=t.swagger.version),(t.swagger.securitySchemes||t.swagger.security)&&(ee.getInstance().setSecurity(t.swagger.securitySchemes||{},t.swagger.security||[]),Object.assign(e,ee.getInstance().generateSpec()));const o=t.swagger.path||"/swagger-ui",r=ee.getInstance().tagSpecByPrefix(e,{level:2,onlyIfEmpty:!1});s.use(f({routePrefix:o,swaggerOptions:{spec:r}})),console.log(`[koa-ts-core] Swagger UI available at ${o}`)}return((e,t,s)=>{const{errorConfig:o,koa2Cors:r,trackConfig:a,bodyParser:n}=s,c={[F.AsyncContext]:[ae],[F.ErrorHandling]:[(i=o?.handler,async(e,t)=>{try{await t()}catch(t){if(e.hook?.(e,"error"),i)i(t,e);else{const e=t.statusCode||t.status||500,s=t.code??-1;A(e,{message:t.message,code:s,data:"production"!==process.env.NODE_ENV?t.stack:void 0})}}})],[F.Logging]:[ne(a),ce],[F.Security]:[...r?[r(h)]:[]],[F.BodyParsing]:[g(n),ie],[F.Auth]:[],[F.Routing]:[z.routes()]};var i;const u=[F.AsyncContext,F.ErrorHandling,F.Logging,F.Security,F.BodyParsing,F.Auth,F.Routing];for(const s of u){const o=t.get(s),r=c[s]??[];if(o?.before)for(const t of o.before)e.use(t);for(const t of r)e.use(t);if(o?.use)for(const t of o.use)e.use(t);if(o?.after)for(const t of o.after)e.use(t)}})(s,t.phaseMiddlewares,{errorConfig:t.error,koa2Cors:t.koa2Cors,trackConfig:t.trackId,bodyParser:t.bodyParser}),await U(s),"production"===process.env.NODE_ENV&&console.log(`[koa-ts-core] version: ${require("../package.json").version}`),[s,z]};export{R as AuthRouter,$ as BaseException,_ as ForbiddenException,k as INDEX_ROUTE,F as MiddlewarePhase,b as ROUTE_METADATA_KEY,S as Router,I as contextStore,le as createKoaApp,A as errorRsp,N as getCurrentContext,q as getSrcModulePaths,O as successRsp,C as unSuccessRsp};
|
package/dist/types/core.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import cors from "koa2-cors";
|
|
|
3
3
|
import type { Log4js } from "log4js";
|
|
4
4
|
import { AuthRouterCallback } from "./route";
|
|
5
5
|
import type { Options as BodyParserOptions } from "koa-bodyparser";
|
|
6
|
+
import { OpenAPIV3 } from "openapi-types";
|
|
6
7
|
/**
|
|
7
8
|
* 中间件分阶段
|
|
8
9
|
*/
|
|
@@ -72,6 +73,8 @@ export interface SwaggerConfig {
|
|
|
72
73
|
title?: string;
|
|
73
74
|
description?: string;
|
|
74
75
|
version?: string;
|
|
76
|
+
security?: OpenAPIV3.SecurityRequirementObject[];
|
|
77
|
+
securitySchemes?: Record<string, OpenAPIV3.SecuritySchemeObject | OpenAPIV3.ReferenceObject>;
|
|
75
78
|
}
|
|
76
79
|
/**
|
|
77
80
|
* 初始化配置
|
|
@@ -1,29 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
import { OpenAPIV3 } from "openapi-types";
|
|
2
|
+
export interface IMetaData extends OpenAPIV3.OperationObject {
|
|
2
3
|
method: string;
|
|
3
|
-
description: string;
|
|
4
4
|
path: string;
|
|
5
|
-
request: Partial<Request>;
|
|
6
|
-
response: Response;
|
|
7
5
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
export interface ApiParamDef {
|
|
12
|
-
description?: string;
|
|
13
|
-
required?: boolean;
|
|
14
|
-
type?: string;
|
|
15
|
-
example?: any;
|
|
16
|
-
schema?: any;
|
|
17
|
-
}
|
|
18
|
-
export interface ApiBodyDef {
|
|
19
|
-
description?: string;
|
|
20
|
-
required?: boolean;
|
|
21
|
-
schema?: any;
|
|
22
|
-
example?: any;
|
|
23
|
-
}
|
|
24
|
-
interface Request {
|
|
25
|
-
header: Record<string, ApiParamDef>;
|
|
26
|
-
body: ApiParamDef;
|
|
27
|
-
query: Record<string, ApiParamDef>;
|
|
28
|
-
}
|
|
29
|
-
export {};
|
|
6
|
+
export type ApiParamDef = OpenAPIV3.ReferenceObject | OpenAPIV3.ParameterObject;
|
|
7
|
+
export type ApiBodyDef = OpenAPIV3.ReferenceObject | OpenAPIV3.RequestBodyObject;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koa-ts-core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
4
4
|
"main": "dist/index.cjs.js",
|
|
5
5
|
"module": "dist/index.esm.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"koa2-swagger-ui": "^5.12.0",
|
|
27
27
|
"log4js": "^6.9.1",
|
|
28
28
|
"portfinder": "^1.0.38",
|
|
29
|
-
"reflect-metadata": "^0.2.2"
|
|
29
|
+
"reflect-metadata": "^0.2.2",
|
|
30
|
+
"openapi-types": "^12.1.3"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"@rollup/plugin-commonjs": "^29.0.0",
|
|
@@ -40,7 +41,6 @@
|
|
|
40
41
|
"@types/koa__router": "^12.0.5",
|
|
41
42
|
"@types/node": "^25.0.3",
|
|
42
43
|
"koa": "^3.1.1",
|
|
43
|
-
"openapi-types": "^12.1.3",
|
|
44
44
|
"rollup": "^4.54.0",
|
|
45
45
|
"rollup-plugin-copy": "^3.5.0",
|
|
46
46
|
"rollup-plugin-copy-and-reference-dts": "0.0.2",
|