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.
- package/README.md +249 -0
- package/dist/cli/generate-routes.js +101 -0
- package/dist/cli/generate-swagger.js +197 -0
- package/dist/controllers/advanced.controller.js +131 -0
- package/dist/controllers/user.controller.js +121 -0
- package/dist/entities/user.entity.js +1 -0
- package/dist/index.js +2 -0
- package/dist/lib/common.js +62 -0
- package/dist/lib/decorators.js +116 -0
- package/dist/middleware/auth.middleware.js +13 -0
- package/dist/routes.js +80 -0
- package/dist/server.js +18 -0
- package/dist/src/cli/generate-routes.js +105 -0
- package/dist/src/cli/generate-swagger.js +197 -0
- package/dist/src/index.js +4 -0
- package/dist/src/lib/common.js +62 -0
- package/dist/src/lib/decorators.js +116 -0
- package/dist/src/routes.js +80 -0
- package/dist/src/server.js +18 -0
- package/dist/tests/example-app/controllers/advanced.controller.js +130 -0
- package/dist/tests/example-app/controllers/controllers/advanced.controller.js +131 -0
- package/dist/tests/example-app/controllers/controllers/user.controller.js +121 -0
- package/dist/tests/example-app/controllers/user.controller.js +121 -0
- package/dist/tests/example-app/entities/entities/user.entity.js +1 -0
- package/dist/tests/example-app/entities/user.entity.js +1 -0
- package/dist/tests/example-app/middleware/auth.middleware.js +13 -0
- package/dist/tests/example-app/middleware/middleware/auth.middleware.js +13 -0
- package/dist/tests/example-app/routes.js +80 -0
- package/dist/tests/example-app/server.js +23 -0
- package/package.json +34 -0
- package/scripts/run-example.js +32 -0
- package/src/cli/generate-routes.ts +123 -0
- package/src/cli/generate-swagger.ts +216 -0
- package/src/index.js +20 -0
- package/src/index.ts +4 -0
- package/src/lib/common.js +68 -0
- package/src/lib/common.ts +35 -0
- package/src/lib/decorators.js +128 -0
- package/src/lib/decorators.ts +136 -0
- package/swagger.json +238 -0
- package/tests/e2e.test.ts +72 -0
- package/tests/example-app/controllers/advanced.controller.ts +52 -0
- package/tests/example-app/controllers/user.controller.ts +35 -0
- package/tests/example-app/entities/user.entity.ts +8 -0
- package/tests/example-app/middleware/auth.middleware.ts +16 -0
- package/tests/example-app/routes.ts +102 -0
- package/tests/example-app/server.ts +30 -0
- package/tests/generators.test.ts +48 -0
- package/tests/utils.ts +46 -0
- 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 };
|