adorn-api 1.0.0

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.
Files changed (50) hide show
  1. package/README.md +249 -0
  2. package/dist/cli/generate-routes.js +101 -0
  3. package/dist/cli/generate-swagger.js +197 -0
  4. package/dist/controllers/advanced.controller.js +131 -0
  5. package/dist/controllers/user.controller.js +121 -0
  6. package/dist/entities/user.entity.js +1 -0
  7. package/dist/index.js +2 -0
  8. package/dist/lib/common.js +62 -0
  9. package/dist/lib/decorators.js +116 -0
  10. package/dist/middleware/auth.middleware.js +13 -0
  11. package/dist/routes.js +80 -0
  12. package/dist/server.js +18 -0
  13. package/dist/src/cli/generate-routes.js +105 -0
  14. package/dist/src/cli/generate-swagger.js +197 -0
  15. package/dist/src/index.js +4 -0
  16. package/dist/src/lib/common.js +62 -0
  17. package/dist/src/lib/decorators.js +116 -0
  18. package/dist/src/routes.js +80 -0
  19. package/dist/src/server.js +18 -0
  20. package/dist/tests/example-app/controllers/advanced.controller.js +130 -0
  21. package/dist/tests/example-app/controllers/controllers/advanced.controller.js +131 -0
  22. package/dist/tests/example-app/controllers/controllers/user.controller.js +121 -0
  23. package/dist/tests/example-app/controllers/user.controller.js +121 -0
  24. package/dist/tests/example-app/entities/entities/user.entity.js +1 -0
  25. package/dist/tests/example-app/entities/user.entity.js +1 -0
  26. package/dist/tests/example-app/middleware/auth.middleware.js +13 -0
  27. package/dist/tests/example-app/middleware/middleware/auth.middleware.js +13 -0
  28. package/dist/tests/example-app/routes.js +80 -0
  29. package/dist/tests/example-app/server.js +23 -0
  30. package/package.json +34 -0
  31. package/scripts/run-example.js +32 -0
  32. package/src/cli/generate-routes.ts +123 -0
  33. package/src/cli/generate-swagger.ts +216 -0
  34. package/src/index.js +20 -0
  35. package/src/index.ts +4 -0
  36. package/src/lib/common.js +68 -0
  37. package/src/lib/common.ts +35 -0
  38. package/src/lib/decorators.js +128 -0
  39. package/src/lib/decorators.ts +136 -0
  40. package/swagger.json +238 -0
  41. package/tests/e2e.test.ts +72 -0
  42. package/tests/example-app/controllers/advanced.controller.ts +52 -0
  43. package/tests/example-app/controllers/user.controller.ts +35 -0
  44. package/tests/example-app/entities/user.entity.ts +8 -0
  45. package/tests/example-app/middleware/auth.middleware.ts +16 -0
  46. package/tests/example-app/routes.ts +102 -0
  47. package/tests/example-app/server.ts +30 -0
  48. package/tests/generators.test.ts +48 -0
  49. package/tests/utils.ts +46 -0
  50. package/tsconfig.json +20 -0
package/README.md ADDED
@@ -0,0 +1,249 @@
1
+ # Adorn API
2
+
3
+ A TypeScript API framework using **Standard TC39 Decorators** (not experimental decorators) to build type-safe, self-documenting APIs with automatic OpenAPI/Swagger generation.
4
+
5
+ ## Features
6
+
7
+ - ✅ **Standard Decorators**: Uses TC39 Stage 3 decorators (no experimental flags needed)
8
+ - ✅ **Type-Safe DTOs**: Full TypeScript type checking at edit time
9
+ - ✅ **Automatic Swagger Generation**: Generates OpenAPI 3.0 documentation from your code
10
+ - ✅ **Runtime Route Generation**: Automatically creates Express routes
11
+ - ✅ **Inheritance Support**: Extend base DTO classes with full type information
12
+ - ✅ **Generic Response Types**: `EntityResponse<T>`, `CreateInput<T>`, etc.
13
+ - ✅ **Authentication**: Built-in `@Authorized` decorator
14
+ - ✅ **Union Types & Enums**: Automatically converted to Swagger enums
15
+ - ✅ **Nested Objects**: Recursive type resolution for complex DTOs
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install adorn-api
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ ### 1. Create a Controller
26
+
27
+ ```typescript
28
+ // src/controllers/user.controller.ts
29
+ import { Controller, Get, Post, FromQuery, FromPath, FromBody } from "adorn-api";
30
+
31
+ class GetUserRequest {
32
+ @FromPath()
33
+ userId!: string;
34
+
35
+ @FromQuery()
36
+ details?: boolean;
37
+ }
38
+
39
+ @Controller("users")
40
+ export class UserController {
41
+ @Get("/{userId}")
42
+ public async getUser(req: GetUserRequest): Promise<string> {
43
+ return `Getting user ${req.userId} with details: ${req.details}`;
44
+ }
45
+ }
46
+ ```
47
+
48
+ ### 2. Generate Swagger and Routes
49
+
50
+ ```bash
51
+ npx adorn-api gen
52
+ ```
53
+
54
+ This generates:
55
+ - `swagger.json` - OpenAPI 3.0 specification
56
+ - `src/routes.ts` - Express route handlers
57
+
58
+ ### 3. Start Your Server
59
+
60
+ ```typescript
61
+ // src/server.ts
62
+ import express from "express";
63
+ import { RegisterRoutes } from "./routes.js";
64
+
65
+ const app = express();
66
+ app.use(express.json());
67
+
68
+ RegisterRoutes(app);
69
+
70
+ app.listen(3000, () => {
71
+ console.log("Server running on http://localhost:3000");
72
+ });
73
+ ```
74
+
75
+ Visit http://localhost:3000/docs to see your Swagger UI.
76
+
77
+ ## Advanced Usage
78
+
79
+ ### Inheritance & Generics
80
+
81
+ ```typescript
82
+ import { PaginationQuery, EntityResponse, CreateInput } from "../lib/common.js";
83
+
84
+ // Inherit pagination properties
85
+ class UserListRequest extends PaginationQuery {
86
+ search?: string;
87
+
88
+ @FromPath()
89
+ tenantId!: string;
90
+ }
91
+
92
+ // Use generic type helpers
93
+ class CreateUserDto implements CreateInput<User, 'name' | 'email'> {
94
+ @FromBody()
95
+ name!: string;
96
+
97
+ @FromBody()
98
+ email!: string;
99
+ }
100
+
101
+ @Controller("advanced")
102
+ export class AdvancedController {
103
+ @Get("/{tenantId}/users")
104
+ public async listUsers(req: UserListRequest): Promise<EntityResponse<User[]>> {
105
+ return [/* ... */];
106
+ }
107
+ }
108
+ ```
109
+
110
+ ### Authentication
111
+
112
+ ```typescript
113
+ import { Authorized } from "../lib/decorators.js";
114
+
115
+ @Controller("profile")
116
+ export class ProfileController {
117
+ @Authorized("admin")
118
+ @Post("/update")
119
+ public async update(req: UpdateProfileDto) {
120
+ // Only accessible with valid Bearer token
121
+ return { success: true };
122
+ }
123
+ }
124
+ ```
125
+
126
+ ## Project Structure
127
+
128
+ ```
129
+ adorn-api/
130
+ ├── src/
131
+ │ ├── lib/
132
+ │ │ ├── decorators.ts # Standard decorators (@Get, @Post, @Controller, etc.)
133
+ │ │ └── common.ts # Common types (PaginationQuery, EntityResponse, etc.)
134
+ │ ├── cli/
135
+ │ │ ├── generate-swagger.ts # Swagger/OpenAPI generator
136
+ │ │ └── generate-routes.ts # Express route generator
137
+ │ └── index.ts # Main library entry point
138
+ ├── tests/
139
+ │ └── example-app/ # Example application using adorn-api
140
+ │ ├── controllers/ # Example controllers
141
+ │ ├── entities/ # Example entities
142
+ │ ├── middleware/ # Example middleware
143
+ │ ├── routes.ts # Generated routes (auto-generated)
144
+ │ └── server.ts # Example Express server
145
+ ├── swagger.json # Generated OpenAPI spec
146
+ └── package.json
147
+ ```
148
+
149
+ ## Development
150
+
151
+ ### Available Scripts
152
+
153
+ - `npm run build` - Compile TypeScript to JavaScript
154
+ - `npm run gen:spec` - Generate Swagger documentation only
155
+ - `npm run gen:routes` - Generate Express routes only
156
+ - `npm run gen` - Generate both Swagger and routes
157
+ - `npm run example` - Run the example application
158
+
159
+ ### Testing the Library
160
+
161
+ ```bash
162
+ # Generate from example app
163
+ npm run gen
164
+
165
+ # Run the example server
166
+ npm run example
167
+ ```
168
+
169
+ ## How It Works
170
+
171
+ ### 1. Decorators (src/lib/decorators.ts)
172
+
173
+ Uses **Standard TC39 Decorators** with `context.addInitializer()` to attach metadata:
174
+
175
+ ```typescript
176
+ export function Get(path: string) {
177
+ return function (originalMethod: any, context: ClassMethodDecoratorContext) {
178
+ context.addInitializer(function () {
179
+ // Store route metadata
180
+ const routes = (this as any)[META_KEY] || [];
181
+ routes.push({ method: 'get', path, methodName: String(context.name) });
182
+ (this as any)[META_KEY] = routes;
183
+ });
184
+ return originalMethod;
185
+ };
186
+ }
187
+ ```
188
+
189
+ ### 2. Swagger Generator (src/cli/generate-swagger.ts)
190
+
191
+ Uses **ts-morph** to statically analyze TypeScript code:
192
+
193
+ - Parses `@Controller` and `@Get`/`@Post` decorators
194
+ - Resolves DTO types including inheritance and generics
195
+ - Converts TypeScript types to JSON Schema
196
+ - Handles union types (enums), nested objects, and Promise unwrapping
197
+ - Generates OpenAPI 3.0 specification
198
+
199
+ ### 3. Route Generator (src/cli/generate-routes.ts)
200
+
201
+ Generates actual Express route handlers:
202
+
203
+ ```typescript
204
+ // Generated code in src/routes.ts
205
+ app.get('/users/:userId', async (req: Request, res: Response) => {
206
+ const controller = new UserController();
207
+ const input: any = {};
208
+ Object.assign(input, req.query);
209
+ Object.assign(input, req.params);
210
+ Object.assign(input, req.body);
211
+
212
+ const response = await controller.getUser(input);
213
+ res.status(200).json(response);
214
+ });
215
+ ```
216
+
217
+ ## Why Standard Decorators?
218
+
219
+ 1. **Future-Proof**: Uses the official TC39 decorator proposal (Stage 3)
220
+ 2. **No Experimental Flags**: Works with `"experimentalDecorators": false`
221
+ 3. **Better Type Safety**: Leverages TypeScript's type system instead of runtime reflection
222
+ 4. **Cleaner API**: Single-parameter DTO pattern is more explicit than parameter decorators
223
+
224
+ ## Comparison with tsoa
225
+
226
+ | Feature | tsoa (Legacy) | adorn-api |
227
+ |---------|---------------|-----------|
228
+ | Decorators | Experimental (`emitDecoratorMetadata`) | Standard TC39 |
229
+ | Parameter Decorators | `@Body() body: string` | DTO classes with `@FromBody()` |
230
+ | Type Safety | Runtime reflection | Edit-time type checking |
231
+ | Inheritance | Limited | Full support |
232
+ | Generics | Complex | Native TypeScript |
233
+ | Future Compatibility | Deprecated in future TS | Officially supported |
234
+
235
+ ## Next Steps
236
+
237
+ To make this production-ready:
238
+
239
+ 1. **Validation**: Integrate Zod or class-validator in the route generator
240
+ 2. **Error Handling**: Add centralized error handling middleware
241
+ 3. **Database Integration**: Add ORM support (Prisma, TypeORM, etc.)
242
+ 4. **Testing**: Add unit and integration test utilities
243
+ 5. **CORS**: Add CORS configuration
244
+ 6. **Rate Limiting**: Add rate limiting middleware
245
+ 7. **Logging**: Add structured logging (Winston, Pino)
246
+
247
+ ## License
248
+
249
+ ISC
@@ -0,0 +1,101 @@
1
+ // src/cli/generate-routes.ts
2
+ import { Project } from "ts-morph";
3
+ import * as fs from "fs";
4
+ const PROJECT_ROOT = "./tsconfig.json";
5
+ const OUTPUT_FILE = "./src/routes.ts"; // We generate this file!
6
+ const project = new Project({ tsConfigFilePath: PROJECT_ROOT });
7
+ let routeCode = `/* tslint:disable */
8
+ /* eslint-disable */
9
+ // WARNING: This file was auto-generated by adorn-api. Do not edit.
10
+ import { Express, Request, Response } from 'express';
11
+ `;
12
+ // Helper to keep track of imports we need to add to the generated file
13
+ const imports = new Set();
14
+ function processController(classDec) {
15
+ const className = classDec.getName();
16
+ if (!className)
17
+ return "";
18
+ // Track import (assuming controllers are in ./controllers/)
19
+ const sourceFile = classDec.getSourceFile();
20
+ const baseName = sourceFile.getBaseName().replace('.ts', '.js');
21
+ const relativePath = sourceFile.getDirectoryPath().replace(process.cwd() + "/src", ".");
22
+ imports.add(`import { ${className} } from '${relativePath}/${baseName}';`);
23
+ const controllerDec = classDec.getDecorators().find(d => d.getName() === "Controller");
24
+ if (!controllerDec)
25
+ return "";
26
+ const basePath = controllerDec.getArguments()[0]?.getText().replace(/['"]/g, "") || "/";
27
+ let methodBlocks = "";
28
+ classDec.getMethods().forEach(method => {
29
+ const getDec = method.getDecorator("Get");
30
+ const postDec = method.getDecorator("Post");
31
+ const putDec = method.getDecorator("Put");
32
+ const deleteDec = method.getDecorator("Delete");
33
+ const decorator = getDec || postDec || putDec || deleteDec;
34
+ if (!decorator)
35
+ return;
36
+ const httpMethod = getDec ? "get" : postDec ? "post" : putDec ? "put" : "delete";
37
+ const pathArg = decorator.getArguments()[0]?.getText().replace(/['"]/g, "") || "/";
38
+ const fullPath = `/${basePath}${pathArg}`.replace("//", "/").replace(/{/g, ":").replace(/}/g, ""); // Convert {id} to :id for Express
39
+ const methodName = method.getName();
40
+ const params = method.getParameters();
41
+ // Authentication check
42
+ const authDec = method.getDecorator("Authorized");
43
+ const controllerAuthDec = classDec.getDecorator("Authorized");
44
+ const hasAuth = !!authDec || !!controllerAuthDec;
45
+ const middlewareArgs = hasAuth ? "authenticationMiddleware, " : "";
46
+ // Logic to instantiate the DTO
47
+ let paramInstantiation = "";
48
+ if (params.length > 0) {
49
+ const paramName = params[0].getName();
50
+ const paramType = params[0].getType().getSymbol()?.getName();
51
+ // We assume the DTO is a class we can instantiate, or a shape we construct
52
+ // Here we map request parts to the DTO
53
+ paramInstantiation = `
54
+ const input: any = {};
55
+ // Map Query
56
+ Object.assign(input, req.query);
57
+ // Map Params
58
+ Object.assign(input, req.params);
59
+ // Map Body
60
+ Object.assign(input, req.body);
61
+
62
+ // In a real app, you would run 'zod' or 'class-validator' here on 'input'
63
+ `;
64
+ }
65
+ methodBlocks += `
66
+ app.${httpMethod}('${fullPath}', ${middlewareArgs}async (req: Request, res: Response) => {
67
+ const controller = new ${className}();
68
+ try {
69
+ ${paramInstantiation}
70
+ const response = await controller.${methodName}(${params.length > 0 ? 'input' : ''});
71
+ res.status(200).json(response);
72
+ } catch (err: any) {
73
+ console.error(err);
74
+ res.status(500).send(err.message);
75
+ }
76
+ });
77
+ `;
78
+ });
79
+ return methodBlocks;
80
+ }
81
+ const sourceFiles = project.getSourceFiles("src/controllers/**/*.ts");
82
+ let allRoutes = "";
83
+ sourceFiles.forEach(file => {
84
+ file.getClasses().forEach(c => {
85
+ allRoutes += processController(c);
86
+ });
87
+ });
88
+ // Add authentication middleware import if needed
89
+ if (allRoutes.includes('authenticationMiddleware')) {
90
+ routeCode += `import { authenticationMiddleware } from './middleware/auth.middleware.js';\n`;
91
+ }
92
+ // Prepend imports
93
+ routeCode += Array.from(imports).join('\n');
94
+ routeCode += `
95
+
96
+ export function RegisterRoutes(app: Express) {
97
+ ${allRoutes}
98
+ }
99
+ `;
100
+ fs.writeFileSync(OUTPUT_FILE, routeCode);
101
+ console.log(`✅ Generated Routes at ${OUTPUT_FILE}`);
@@ -0,0 +1,197 @@
1
+ // src/cli/generate-swagger.ts
2
+ import { Project, SyntaxKind } from "ts-morph";
3
+ import * as fs from "fs";
4
+ const PROJECT_ROOT = "./tsconfig.json";
5
+ const OUTPUT_FILE = "./swagger.json";
6
+ const project = new Project({ tsConfigFilePath: PROJECT_ROOT });
7
+ const openApiSpec = {
8
+ openapi: "3.0.0",
9
+ info: { title: "Adorn API", version: "2.0.0" },
10
+ paths: {},
11
+ components: {
12
+ schemas: {},
13
+ securitySchemes: {
14
+ bearerAuth: {
15
+ type: "http",
16
+ scheme: "bearer",
17
+ bearerFormat: "JWT"
18
+ }
19
+ }
20
+ },
21
+ };
22
+ // --- Helper: Deep Type Resolver ---
23
+ // This converts TypeScript Types (Generics, Interfaces, Primitives) into JSON Schema
24
+ function resolveSchema(type, collectedSchemas) {
25
+ // 1. Handle Primitives
26
+ if (type.isString() || type.isStringLiteral())
27
+ return { type: "string" };
28
+ if (type.isNumber() || type.isNumberLiteral())
29
+ return { type: "integer" };
30
+ if (type.isBoolean() || type.isBooleanLiteral())
31
+ return { type: "boolean" };
32
+ if (type.isArray()) {
33
+ const arrayType = type.getArrayElementType();
34
+ return {
35
+ type: "array",
36
+ items: arrayType ? resolveSchema(arrayType, collectedSchemas) : {},
37
+ };
38
+ }
39
+ // 2. Handle Union Types (Enums)
40
+ if (type.isUnion()) {
41
+ const unionTypes = type.getUnionTypes();
42
+ // Extract literal values (e.g., "active", 100)
43
+ const literals = unionTypes
44
+ .map(t => t.isLiteral() ? t.getLiteralValue() : null)
45
+ .filter(val => val !== null);
46
+ // If we found literals, it's an Enum
47
+ if (literals.length > 0) {
48
+ const isString = typeof literals[0] === 'string';
49
+ return {
50
+ type: isString ? 'string' : 'integer',
51
+ enum: literals
52
+ };
53
+ }
54
+ // If it's a mix of objects (e.g. User | Admin), OpenApi uses 'oneOf'
55
+ // Simplified for this demo: take the first non-null type
56
+ return resolveSchema(unionTypes[0], collectedSchemas);
57
+ }
58
+ // 3. Handle Objects (Classes, Interfaces, Nested Literals)
59
+ if (type.isObject()) {
60
+ // Recursion Guard: If we've seen this type name before in components, reference it!
61
+ // (This prevents infinite loops and reduces swagger size)
62
+ const symbol = type.getSymbol();
63
+ const typeName = symbol?.getName();
64
+ // Common excluded types
65
+ if (typeName === "Promise") {
66
+ return resolveSchema(type.getTypeArguments()[0], collectedSchemas);
67
+ }
68
+ if (typeName === "Date")
69
+ return { type: "string", format: "date-time" };
70
+ // If it's a named class/interface (e.g. "Address"), and we haven't processed it,
71
+ // we could add it to components.schemas. For now, we inline it for simplicity.
72
+ const properties = type.getProperties();
73
+ const schema = { type: "object", properties: {}, required: [] };
74
+ properties.forEach((prop) => {
75
+ const propName = prop.getName();
76
+ if (propName.startsWith("_"))
77
+ return; // Skip privates
78
+ // Get the type of the property using getTypeAtLocation
79
+ const declarations = prop.getDeclarations();
80
+ if (declarations.length > 0) {
81
+ const propType = prop.getTypeAtLocation(declarations[0]);
82
+ if (propType) {
83
+ // RECURSION HERE: Pass the property type back into resolveSchema
84
+ schema.properties[propName] = resolveSchema(propType, collectedSchemas);
85
+ if (!prop.isOptional()) {
86
+ schema.required.push(propName);
87
+ }
88
+ }
89
+ }
90
+ });
91
+ return schema;
92
+ }
93
+ return { type: "string" }; // Fallback
94
+ }
95
+ function processController(classDec) {
96
+ const controllerDec = classDec.getDecorators().find((d) => d.getName() === "Controller");
97
+ if (!controllerDec)
98
+ return;
99
+ const basePath = controllerDec.getArguments()[0]?.getText().replace(/['"]/g, "") || "/";
100
+ classDec.getMethods().forEach((method) => {
101
+ const getDec = method.getDecorator("Get");
102
+ const postDec = method.getDecorator("Post");
103
+ const putDec = method.getDecorator("Put");
104
+ const deleteDec = method.getDecorator("Delete");
105
+ const decorator = getDec || postDec || putDec || deleteDec;
106
+ if (!decorator)
107
+ return;
108
+ const httpMethod = getDec ? "get" : postDec ? "post" : putDec ? "put" : "delete";
109
+ const pathArg = decorator.getArguments()[0]?.getText().replace(/['"]/g, "") || "/";
110
+ const fullPath = `/${basePath}${pathArg}`.replace("//", "/");
111
+ const parameters = [];
112
+ const requestBody = { content: {} };
113
+ // --- 1. Request Analysis (Input DTOs) ---
114
+ const params = method.getParameters();
115
+ if (params.length > 0) {
116
+ const param = params[0];
117
+ const paramType = param.getType(); // This resolves generics and inheritance!
118
+ // Iterate ALL properties of the type (inherited included)
119
+ paramType.getProperties().forEach(prop => {
120
+ const propName = prop.getName();
121
+ // We need to check Decorators.
122
+ // Note: In mapped types/generics, getting decorators is hard.
123
+ // We fallback to checking the source declaration.
124
+ const declarations = prop.getDeclarations();
125
+ let isQuery = false;
126
+ let isBody = false;
127
+ let isPath = false;
128
+ // Check decorators on the definition
129
+ declarations.forEach(decl => {
130
+ if (decl.getKind() === SyntaxKind.PropertyDeclaration) {
131
+ const pDecl = decl;
132
+ if (pDecl.getDecorator("FromQuery"))
133
+ isQuery = true;
134
+ if (pDecl.getDecorator("FromPath"))
135
+ isPath = true;
136
+ if (pDecl.getDecorator("FromBody"))
137
+ isBody = true;
138
+ }
139
+ });
140
+ // IMPLICIT RULES:
141
+ // If it's a GET, and not path, default to Query.
142
+ // If it's a POST, and not path/query, default to Body.
143
+ if (!isQuery && !isPath && !isBody) {
144
+ if (httpMethod === "get")
145
+ isQuery = true;
146
+ else
147
+ isBody = true;
148
+ }
149
+ const propType = prop.getTypeAtLocation(param.getSourceFile());
150
+ const propTypeSchema = resolveSchema(propType, openApiSpec.components.schemas);
151
+ if (isPath) {
152
+ parameters.push({ name: propName, in: "path", required: true, schema: propTypeSchema });
153
+ }
154
+ else if (isQuery) {
155
+ parameters.push({ name: propName, in: "query", required: !prop.isOptional(), schema: propTypeSchema });
156
+ }
157
+ else if (isBody) {
158
+ if (!requestBody.content["application/json"]) {
159
+ requestBody.content["application/json"] = { schema: { type: "object", properties: {}, required: [] } };
160
+ }
161
+ const bodySchema = requestBody.content["application/json"].schema;
162
+ bodySchema.properties[propName] = propTypeSchema;
163
+ if (!prop.isOptional())
164
+ bodySchema.required.push(propName);
165
+ }
166
+ });
167
+ }
168
+ // --- 2. Authentication Check ---
169
+ const authDec = method.getDecorator("Authorized");
170
+ const controllerAuthDec = classDec.getDecorator("Authorized");
171
+ const isAuth = !!authDec || !!controllerAuthDec;
172
+ // --- 3. Response Analysis (Return Type) ---
173
+ const returnType = method.getReturnType(); // e.g. Promise<EntityResponse<User>>
174
+ const responseSchema = resolveSchema(returnType, openApiSpec.components.schemas);
175
+ if (!openApiSpec.paths[fullPath])
176
+ openApiSpec.paths[fullPath] = {};
177
+ openApiSpec.paths[fullPath][httpMethod] = {
178
+ operationId: method.getName(),
179
+ parameters,
180
+ requestBody: Object.keys(requestBody.content).length ? requestBody : undefined,
181
+ security: isAuth ? [{ bearerAuth: [] }] : undefined,
182
+ responses: {
183
+ 200: {
184
+ description: "Success",
185
+ content: {
186
+ "application/json": { schema: responseSchema }
187
+ }
188
+ }
189
+ }
190
+ };
191
+ });
192
+ }
193
+ console.log("🔍 Scanning...");
194
+ const sourceFiles = project.getSourceFiles("src/**/*.ts");
195
+ sourceFiles.forEach(file => file.getClasses().forEach(processController));
196
+ fs.writeFileSync(OUTPUT_FILE, JSON.stringify(openApiSpec, null, 2));
197
+ console.log(`✅ Generated ${OUTPUT_FILE}`);
@@ -0,0 +1,131 @@
1
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
2
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
3
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
4
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
5
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
6
+ var _, done = false;
7
+ for (var i = decorators.length - 1; i >= 0; i--) {
8
+ var context = {};
9
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
10
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
11
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
12
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
13
+ if (kind === "accessor") {
14
+ if (result === void 0) continue;
15
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
16
+ if (_ = accept(result.get)) descriptor.get = _;
17
+ if (_ = accept(result.set)) descriptor.set = _;
18
+ if (_ = accept(result.init)) initializers.unshift(_);
19
+ }
20
+ else if (_ = accept(result)) {
21
+ if (kind === "field") initializers.unshift(_);
22
+ else descriptor[key] = _;
23
+ }
24
+ }
25
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
26
+ done = true;
27
+ };
28
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
29
+ var useValue = arguments.length > 2;
30
+ for (var i = 0; i < initializers.length; i++) {
31
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
32
+ }
33
+ return useValue ? value : void 0;
34
+ };
35
+ // src/controllers/advanced.controller.ts
36
+ import { Controller, Get, Post, FromBody, FromPath } from "../lib/decorators.js";
37
+ import { PaginationQuery } from "../lib/common.js";
38
+ // --- 1. Advanced Request DTOs ---
39
+ // INHERITANCE: UserListRequest automatically gets 'page' and 'limit' from PaginationQuery
40
+ // AND the generator will find them because we scan the Type properties.
41
+ let UserListRequest = (() => {
42
+ let _classSuper = PaginationQuery;
43
+ let _tenantId_decorators;
44
+ let _tenantId_initializers = [];
45
+ let _tenantId_extraInitializers = [];
46
+ return class UserListRequest extends _classSuper {
47
+ static {
48
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
49
+ _tenantId_decorators = [FromPath()];
50
+ __esDecorate(null, null, _tenantId_decorators, { kind: "field", name: "tenantId", static: false, private: false, access: { has: obj => "tenantId" in obj, get: obj => obj.tenantId, set: (obj, value) => { obj.tenantId = value; } }, metadata: _metadata }, _tenantId_initializers, _tenantId_extraInitializers);
51
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
52
+ }
53
+ // Implicitly @FromQuery because it's a GET request and extends a class
54
+ search;
55
+ tenantId = __runInitializers(this, _tenantId_initializers, void 0);
56
+ constructor() {
57
+ super(...arguments);
58
+ __runInitializers(this, _tenantId_extraInitializers);
59
+ }
60
+ };
61
+ })();
62
+ export { UserListRequest };
63
+ // COMPOSITION: Using the Type Helper for safety, but class for Decorators
64
+ // We implement the Type Helper to ensure our Class matches the Entity rule
65
+ let CreateUserDto = (() => {
66
+ let _name_decorators;
67
+ let _name_initializers = [];
68
+ let _name_extraInitializers = [];
69
+ let _email_decorators;
70
+ let _email_initializers = [];
71
+ let _email_extraInitializers = [];
72
+ return class CreateUserDto {
73
+ static {
74
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
75
+ _name_decorators = [FromBody()];
76
+ _email_decorators = [FromBody()];
77
+ __esDecorate(null, null, _name_decorators, { kind: "field", name: "name", static: false, private: false, access: { has: obj => "name" in obj, get: obj => obj.name, set: (obj, value) => { obj.name = value; } }, metadata: _metadata }, _name_initializers, _name_extraInitializers);
78
+ __esDecorate(null, null, _email_decorators, { kind: "field", name: "email", static: false, private: false, access: { has: obj => "email" in obj, get: obj => obj.email, set: (obj, value) => { obj.email = value; } }, metadata: _metadata }, _email_initializers, _email_extraInitializers);
79
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
80
+ }
81
+ name = __runInitializers(this, _name_initializers, void 0);
82
+ email = (__runInitializers(this, _name_extraInitializers), __runInitializers(this, _email_initializers, void 0));
83
+ constructor() {
84
+ __runInitializers(this, _email_extraInitializers);
85
+ }
86
+ };
87
+ })();
88
+ export { CreateUserDto };
89
+ // --- 2. The Controller ---
90
+ let AdvancedController = (() => {
91
+ let _classDecorators = [Controller("advanced")];
92
+ let _classDescriptor;
93
+ let _classExtraInitializers = [];
94
+ let _classThis;
95
+ let _instanceExtraInitializers = [];
96
+ let _listUsers_decorators;
97
+ let _create_decorators;
98
+ var AdvancedController = class {
99
+ static { _classThis = this; }
100
+ static {
101
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
102
+ _listUsers_decorators = [Get("/{tenantId}/users")];
103
+ _create_decorators = [Post("/")];
104
+ __esDecorate(this, null, _listUsers_decorators, { kind: "method", name: "listUsers", static: false, private: false, access: { has: obj => "listUsers" in obj, get: obj => obj.listUsers }, metadata: _metadata }, null, _instanceExtraInitializers);
105
+ __esDecorate(this, null, _create_decorators, { kind: "method", name: "create", static: false, private: false, access: { has: obj => "create" in obj, get: obj => obj.create }, metadata: _metadata }, null, _instanceExtraInitializers);
106
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
107
+ AdvancedController = _classThis = _classDescriptor.value;
108
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
109
+ __runInitializers(_classThis, _classExtraInitializers);
110
+ }
111
+ async listUsers(req) {
112
+ return [
113
+ { id: "1", name: "Alice", email: "a@a.com", isActive: true, createdAt: "now" }
114
+ ];
115
+ }
116
+ async create(req) {
117
+ return {
118
+ id: "123",
119
+ name: req.name,
120
+ email: req.email,
121
+ isActive: true,
122
+ createdAt: "now"
123
+ };
124
+ }
125
+ constructor() {
126
+ __runInitializers(this, _instanceExtraInitializers);
127
+ }
128
+ };
129
+ return AdvancedController = _classThis;
130
+ })();
131
+ export { AdvancedController };