ts5deco-express-controller
Version:
TypeScript 5 Modern Decorator Express Controller Framework
286 lines (268 loc) • 17.8 kB
JavaScript
var B=Object.defineProperty;var J=(e,t,s)=>t in e?B(e,t,{enumerable:!0,configurable:!0,writable:!0,value:s}):e[t]=s;var E=(e,t,s)=>J(e,typeof t!="symbol"?t+"":t,s);var S=new WeakMap;function G(e){return S.has(e)||S.set(e,new Map),S.get(e)}function I(e,t,s){G(s).set(e,t)}function V(e,t){return S.get(t)?.get(e)}var D={CONTROLLER:Symbol("controller"),ROUTES:Symbol("routes"),PARAMETERS:Symbol("parameters"),MIDDLEWARES:Symbol("middlewares")};function j(e,t){I(D.CONTROLLER,t,e)}function q(e){return V(D.CONTROLLER,e)}function C(e,t){I(D.ROUTES,t,e)}function y(e){return V(D.ROUTES,e)||[]}function g(e,t){let s=y(e);C(e,[...s,t])}function ne(e){return function(t,s){let a="",r=[];return typeof e=="string"?a=e:e&&(a=e.path||"",r=e.middlewares||[]),a=("/"+a).replace(/\/+/g,"/").replace(/\/$/,"")||"/",j(t,{path:a,middlewares:r}),t}}function c(e){return function(t){return function(s,a){let r="",n=[];typeof t=="string"?r=t:t&&(r=t.path||"",n=t.middlewares||[]),r=("/"+r).replace(/\/+/g,"/").replace(/\/$/,"")||"/",a.addInitializer(function(){g(this.constructor,{path:r,method:e,middlewares:n,propertyKey:a.name})})}}}var de=c("GET"),pe=c("POST"),Te=c("PUT"),ce=c("DELETE"),he=c("PATCH"),ue=c("HEAD"),le=c("OPTIONS");function me(e){return function(t,s){let a=["GET","POST","PUT","DELETE","PATCH","HEAD","OPTIONS"],r="",n=[];typeof e=="string"?r=e:e&&(r=e.path||"",n=e.middlewares||[]),r=("/"+r).replace(/\/+/g,"/").replace(/\/$/,"")||"/",s.addInitializer(function(){a.forEach(o=>{g(this.constructor,{path:r,method:o,middlewares:n,propertyKey:s.name})})})}}function v(...e){return function(t,s){s.addInitializer(function(){let r=y(this.constructor).map(n=>n.propertyKey===s.name?{...n,middlewares:[...e,...n.middlewares]}:n);r.length>0&&C(this.constructor,r)})}}function ge(e){return v(e)}function Re(e){return v(e)}function Me(e){return v(e)}function b(e){return e.replace(/{([^}]+)}/g,":$1")}function Ee(e){return e.replace(/:([^/]+)/g,"{$1}")}function L(e){let t=e.match(/{([^}]+)}/g);return t?t.map(s=>s.slice(1,-1)):[]}function Se(e){let t=(e.match(/{/g)||[]).length,s=(e.match(/}/g)||[]).length;if(t!==s)return!1;let a=0;for(let r of e)if(r==="{"){if(a++,a>1)return!1}else if(r==="}"&&(a--,a<0))return!1;return a===0}function H(e){let s=b(e).replace(/:[^/]+/g,"([^/]+)").replace(/\//g,"\\/");return new RegExp(`^${s}$`)}function De(e,t){let s=L(e),a=H(e),r=t.match(a);if(!r||r.length!==s.length+1)return{};let n={};return s.forEach((o,d)=>{let T=r[d+1];T!==void 0&&(n[o]=T)}),n}var i=class{constructor(t){this.statusCode=t}};var R=class e extends i{constructor(s,a){super(s);this.data=a}send(s){s.status(this.statusCode).json(this.data)}static ok(s){return new e(200,s)}static created(s){return new e(201,s)}static badRequest(s){return new e(400,s)}static unauthorized(s){return new e(401,s)}static forbidden(s){return new e(403,s)}static notFound(s){return new e(404,s)}static internalError(s){return new e(500,s)}};var h=class{static create(t,s){let a=new R(t,s);return a.__brand=`api:${String(t)}:${String(t)}:${t}`,a.__statusCode=t,a.__data=s,a}static ok(t){return this.create(200,t)}static created(t){return this.create(201,t)}static badRequest(t){return this.create(400,t)}static notFound(t){return this.create(404,t)}static internalError(t){return this.create(500,t)}};function O(){return new A}var A=class{constructor(){E(this,"_allowedStatuses")}ok(t){return h.ok(t)}created(t){return h.created(t)}badRequest(t){return h.badRequest(t)}notFound(t){return h.notFound(t)}internalError(t){return h.internalError(t)}};function p(e){return function(t,s={}){return function(a,r){let{middlewares:n=[]}=s,o=t,d={},f=("/"+b(t)).replace(/\/+/g,"/").replace(/\/$/,"")||"/";r.addInitializer(function(){g(this.constructor,{path:f,method:e,middlewares:n,propertyKey:r.name})})}}}function je(){return p("GET")}function qe(){return p("POST")}function Oe(){return p("PUT")}function Ne(){return p("DELETE")}function Be(){return{Get:p("GET"),Post:p("POST"),Put:p("PUT"),Delete:p("DELETE"),Patch:p("PATCH"),Head:p("HEAD"),Options:p("OPTIONS")}}function Je(){return O()}import{Router as z}from"express";function K(e){let t=z();return e.forEach(s=>{_(t,s)}),t}function _(e,t){let s=q(t),a=y(t);if(!s){console.warn(`Controller ${t.name} does not have @Controller decorator`);return}let r=new t;a.forEach(n=>{let o=Y(s.path,n.path),d=n.method.toLowerCase(),T=W(r,n.propertyKey),f=[...s.middlewares,...n.middlewares,T];typeof e[d]=="function"&&e[d](o,...f)})}function W(e,t){return async(s,a,r)=>{try{let n=await e[t](s,a,r);if(a.headersSent)return;if(n instanceof i){n.send(a);return}n!==void 0&&a.json(n)}catch(n){r(n)}}}function Y(e,t){let s=`${e}${t}`.replace(/\/+/g,"/");return s==="/"?"/":s.replace(/\/$/,"")}function Ke(e,t,s=""){let a=K(t);e.use(s,a)}var k=class e extends i{constructor(s,a){super(s);this.text=a}send(s){s.status(this.statusCode).send(this.text||"")}static ok(s){return new e(200,s||"OK")}static created(s){return new e(201,s||"Created")}static badRequest(s){return new e(400,s||"Bad Request")}static unauthorized(s){return new e(401,s||"Unauthorized")}static forbidden(s){return new e(403,s||"Forbidden")}static notFound(s){return new e(404,s||"Not Found")}static internalError(s){return new e(500,s||"Internal Server Error")}};var $=class extends i{constructor(){super(204)}send(t){t.status(204).end()}};var u=class extends i{constructor(s,a=!1){super(typeof a=="number"?a:a?301:302);E(this,"url");this.url=s}send(s){s.status(this.statusCode).redirect(this.url)}},U=class{static temporary(t){return new u(t,302)}static permanent(t){return new u(t,301)}static temporaryPreserveMethod(t){return new u(t,307)}static permanentPreserveMethod(t){return new u(t,308)}};var M=class extends i{constructor(s,a,r=!1){super(200);this.filePath=s;this.filename=a;this.asAttachment=r}send(s){this.asAttachment?this.filename?s.status(this.statusCode).download(this.filePath,this.filename):s.status(this.statusCode).download(this.filePath):this.filename?s.status(this.statusCode).sendFile(this.filePath,{headers:{"Content-Disposition":`inline; filename="${this.filename}"`}}):s.status(this.statusCode).sendFile(this.filePath)}},F=class{static inline(t,s){return new M(t,s,!1)}static attachment(t,s){return new M(t,s,!0)}};function nt(e){return e instanceof i}import m from"fs/promises";import l from"path";import{execSync as Q}from"child_process";async function xt(e){let{input:t,outputDir:s,apiTypesPath:a,utilsPath:r,generateAliases:n=!0,generateUtils:o=!0,openapiTypescriptOptions:d=[]}=e;try{await m.access(t)}catch{throw new Error(`OpenAPI specification file not found: ${t}`)}await m.mkdir(s,{recursive:!0});let T=l.join(s,"api.d.ts");console.log(`\u{1F4C4} Processing ${t}...`);let f=["npx openapi-typescript",`"${t}"`,"-o",`"${T}"`,...d].join(" ");try{Q(f,{stdio:"inherit"}),console.log(`\u2705 Generated types: ${T}`)}catch(w){throw new Error(`Failed to generate types: ${w instanceof Error?w.message:String(w)}`)}let N=await X(T);o&&r&&(await Z(r,T),console.log(`\u2705 Generated utility types: ${r}`)),n&&a&&(await ee(a,N,r),console.log(`\u2705 Generated API type aliases: ${a}`))}async function X(e){try{let s=(await m.readFile(e,"utf8")).match(/schemas:\s*{([^}]+)}/s);if(!s||!s[1])return[];let a=s[1],r=[],n=/(\w+):\s*{/g,o;for(;(o=n.exec(a))!==null;)o[1]&&r.push(o[1]);return r}catch(t){return console.warn("Warning: Could not extract schema names:",t instanceof Error?t.message:String(t)),[]}}async function Z(e,t){let a=`/**
* OpenAPI \uD0C0\uC785 \uC720\uD2F8\uB9AC\uD2F0
* openapi-typescript\uB85C \uC0DD\uC131\uB41C \uD0C0\uC785\uB4E4\uC744 \uC27D\uAC8C \uC0AC\uC6A9\uD558\uAE30 \uC704\uD55C \uD5EC\uD37C\uB4E4
*/
import type { paths, components } from './${l.relative(l.dirname(e),t).replace(/\.d\.ts$/,"").replace(/\\/g,"/")}';
/**
* OpenAPI \uC2A4\uD0A4\uB9C8\uC5D0\uC11C \uD0C0\uC785\uC744 \uCD94\uCD9C\uD558\uB294 \uD5EC\uD37C \uD0C0\uC785
* @example
* \`\`\`typescript
* type User = ExtractSchema<'User'>;
* type ErrorResponse = ExtractSchema<'Error'>;
* \`\`\`
*/
export type ExtractSchema<T extends keyof components['schemas']> =
components['schemas'][T];
/**
* OpenAPI \uACBD\uB85C\uC5D0\uC11C \uC751\uB2F5 \uD0C0\uC785\uC744 \uCD94\uCD9C\uD558\uB294 \uD5EC\uD37C \uD0C0\uC785
* @example
* \`\`\`typescript
* type GetUserResponse = ExtractResponse<'/users/{id}', 'get'>;
* type CreateUserResponse = ExtractResponse<'/users', 'post', 201>;
* \`\`\`
*/
export type ExtractResponse<
TPath extends keyof paths,
TMethod extends keyof paths[TPath],
TStatus extends number = 200
> = TPath extends keyof paths
? TMethod extends keyof paths[TPath]
? paths[TPath][TMethod] extends { responses: any }
? TStatus extends keyof paths[TPath][TMethod]['responses']
? paths[TPath][TMethod]['responses'][TStatus] extends {
content: { 'application/json': infer T }
}
? T
: never
: never
: never
: never
: never;
/**
* OpenAPI \uACBD\uB85C\uC5D0\uC11C \uC694\uCCAD \uBCF8\uBB38 \uD0C0\uC785\uC744 \uCD94\uCD9C\uD558\uB294 \uD5EC\uD37C \uD0C0\uC785
*/
export type ExtractRequestBody<
TPath extends keyof paths,
TMethod extends keyof paths[TPath]
> = TPath extends keyof paths
? TMethod extends keyof paths[TPath]
? paths[TPath][TMethod] extends {
requestBody: { content: { 'application/json': infer T } }
}
? T
: never
: never
: never;
/**
* OpenAPI \uACBD\uB85C\uC5D0\uC11C \uD30C\uB77C\uBBF8\uD130 \uD0C0\uC785\uC744 \uCD94\uCD9C\uD558\uB294 \uD5EC\uD37C \uD0C0\uC785
*/
export type ExtractParameters<
TPath extends keyof paths,
TMethod extends keyof paths[TPath]
> = TPath extends keyof paths
? TMethod extends keyof paths[TPath]
? paths[TPath][TMethod] extends { parameters: infer P }
? P
: never
: never
: never;
// \uC6D0\uBCF8 \uD0C0\uC785\uB4E4\uB3C4 re-export
export type { paths, components };
`;await m.mkdir(l.dirname(e),{recursive:!0}),await m.writeFile(e,a)}async function ee(e,t,s){let r=`/**
* API \uD0C0\uC785 \uBCC4\uCE6D
* OpenAPI \uC2A4\uD399\uC5D0\uC11C \uC0DD\uC131\uB41C \uD0C0\uC785\uB4E4\uC744 \uC27D\uAC8C \uC0AC\uC6A9\uD558\uAE30 \uC704\uD55C \uBCC4\uCE6D \uC815\uC758
*
* \uC774 \uD30C\uC77C\uC740 \uC790\uB3D9 \uC0DD\uC131\uB429\uB2C8\uB2E4. \uD544\uC694\uC5D0 \uB530\uB77C \uC218\uC815\uD558\uC5EC \uC0AC\uC6A9\uD558\uC138\uC694.
*/
${s?`import type { ExtractSchema } from './${l.relative(l.dirname(e),s).replace(/\.ts$/,"").replace(/\\/g,"/")}';`:""}
/**
* \uC77C\uBC18\uC801\uC73C\uB85C \uC0AC\uC6A9\uB418\uB294 \uD0C0\uC785 \uBCC4\uCE6D\uB4E4
* \uD544\uC694\uC5D0 \uB530\uB77C \uCD94\uAC00\uD558\uAC70\uB098 \uC218\uC815\uD558\uC138\uC694
*/
${t.map(n=>{let o=te(n);return o?`export type ${o} = ExtractSchema<'${n}'>;`:`// export type ${n} = ExtractSchema<'${n}'>;`}).join(`
`)}
/**
* \uBAA8\uB4E0 \uC2A4\uD0A4\uB9C8 \uD0C0\uC785\uB4E4 (\uCC38\uACE0\uC6A9)
* \uC544\uB798 \uD0C0\uC785\uB4E4\uC744 \uC704\uC640 \uAC19\uC774 \uBCC4\uCE6D\uC73C\uB85C \uB9CC\uB4E4\uC5B4 \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4:
*
${t.map(n=>` * - ${n}`).join(`
`)}
*/
`;await m.mkdir(l.dirname(e),{recursive:!0}),await m.writeFile(e,r)}function te(e){return{User:"User",Error:"ErrorResponse",CreateUserRequest:"CreateUserRequest",UpdateUserRequest:"UpdateUserRequest",PaginatedUserResponse:"PaginatedResponse",ApiError:"ApiError",ValidationError:"ValidationError"}[e]||null}import P from"fs/promises";import x from"path";async function gt(e="."){let t=x.resolve(e),s=x.join(t,"api");await P.mkdir(s,{recursive:!0}),await P.writeFile(x.join(s,"openapi.yaml"),`openapi: 3.0.0
info:
title: My API
version: 1.0.0
description: API for my application
servers:
- url: http://localhost:3000/api
description: Local development server
paths:
/users:
get:
summary: Get all users
operationId: getUsers
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 10
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedUserResponse'
'500':
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
post:
summary: Create a new user
operationId: createUser
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'
responses:
'201':
description: User created successfully
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'400':
description: Bad request
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/users/{id}:
get:
summary: Get a user by ID
operationId: getUserById
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'404':
description: User not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
User:
type: object
required:
- id
- name
- email
properties:
id:
type: string
example: "123"
name:
type: string
example: "John Doe"
email:
type: string
format: email
example: "john@example.com"
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
CreateUserRequest:
type: object
required:
- name
- email
- password
properties:
name:
type: string
example: "John Doe"
email:
type: string
format: email
example: "john@example.com"
password:
type: string
format: password
minLength: 8
Error:
type: object
required:
- error
- message
properties:
error:
type: string
example: "NOT_FOUND"
message:
type: string
example: "The requested resource was not found"
code:
type: string
example: "USER_NOT_FOUND"
statusCode:
type: integer
example: 404
PaginatedUserResponse:
type: object
required:
- data
- total
- page
- limit
- hasNext
- hasPrev
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
total:
type: integer
example: 100
page:
type: integer
example: 1
limit:
type: integer
example: 10
hasNext:
type: boolean
example: true
hasPrev:
type: boolean
example: false
`);let r=x.join(t,"package.json"),n=!1;try{let o=JSON.parse(await P.readFile(r,"utf8"));o.scripts||(o.scripts={}),o.scripts["generate:types"]||(o.scripts["generate:types"]="ts5deco-express-controller generate",n=!0),o.scripts.generate||(o.scripts.generate="npm run generate:types",n=!0),n&&(await P.writeFile(r,JSON.stringify(o,null,2)),console.log("\u2705 Updated package.json with recommended scripts"))}catch{console.log("\u{1F4A1} Recommended package.json scripts:"),console.log(' "generate:types": "ts5deco-express-controller generate"'),console.log(' "generate": "npm run generate:types"')}try{let o=x.join(t,".gitignore"),d="";try{d=await P.readFile(o,"utf8")}catch{}d.includes("src/types/generated/")||(d+=`
# Generated OpenAPI types
src/types/generated/
`,await P.writeFile(o,d),console.log("\u2705 Updated .gitignore to exclude generated types"))}catch{console.log("\u{1F4A1} Recommended .gitignore addition:"),console.log(" src/types/generated/")}console.log(`
\u{1F4C1} Created files:`),console.log(` ${x.relative(process.cwd(),x.join(s,"openapi.yaml"))}`)}function Mt(e){return e.replace(/{([^}]+)}/g,":$1")}function wt(e){return e.replace(/:([^/]+)/g,"{$1}")}export{me as All,ge as Authenticated,Re as Authorized,i as BaseResponse,ne as Controller,ce as Delete,M as FileResponse,F as FileResponses,de as Get,ue as Head,R as JsonResponse,D as METADATA_KEYS,$ as NoContentResponse,le as Options,he as Patch,pe as Post,Te as Put,u as RedirectResponse,U as RedirectResponses,h as ResponseFactory,k as TextResponse,A as TypedApiResponseFactory,Ne as TypedDelete,je as TypedGet,qe as TypedPost,Oe as TypedPut,v as Use,Me as Validated,g as addRouteMetadata,wt as convertExpressPath,Ee as convertExpressPathToOpenApiPath,Mt as convertOpenAPIPath,b as convertOpenApiPathToExpressPath,O as createApiResponse,H as createPathMatcher,Je as createResponseFor,K as createRouter,Be as createTypedRoutes,L as extractPathParameterNames,De as extractPathParameterValues,xt as generateTypes,q as getControllerMetadata,y as getRouteMetadata,gt as initProject,nt as isBaseResponse,Se as isValidOpenApiPath,_ as registerController,Ke as registerControllers,j as setControllerMetadata,C as setRouteMetadata};
//# sourceMappingURL=index.mjs.map