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
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// src/lib/decorators.ts
|
|
2
|
+
|
|
3
|
+
// 1. HTTP Methods
|
|
4
|
+
export type HttpMethod = 'get' | 'post' | 'put' | 'delete';
|
|
5
|
+
|
|
6
|
+
// Context metadata symbol for standard decorators
|
|
7
|
+
const META_KEY = Symbol('adorn:route');
|
|
8
|
+
export const SCHEMA_META = Symbol('adorn:schema');
|
|
9
|
+
export const AUTH_META = Symbol('adorn:auth');
|
|
10
|
+
|
|
11
|
+
export interface RouteDefinition {
|
|
12
|
+
method: HttpMethod;
|
|
13
|
+
path: string;
|
|
14
|
+
methodName: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// -- Method Decorator --
|
|
18
|
+
// Using Standard TC39 Signature
|
|
19
|
+
export function Get(path: string) {
|
|
20
|
+
return function (originalMethod: any, context: ClassMethodDecoratorContext) {
|
|
21
|
+
// In standard decorators, we can attach metadata to the class prototype via context
|
|
22
|
+
context.addInitializer(function () {
|
|
23
|
+
const routes: RouteDefinition[] = (this as any)[META_KEY] || [];
|
|
24
|
+
routes.push({
|
|
25
|
+
method: 'get',
|
|
26
|
+
path,
|
|
27
|
+
methodName: String(context.name),
|
|
28
|
+
});
|
|
29
|
+
(this as any)[META_KEY] = routes;
|
|
30
|
+
});
|
|
31
|
+
return originalMethod;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function Post(path: string) {
|
|
36
|
+
return function (originalMethod: any, context: ClassMethodDecoratorContext) {
|
|
37
|
+
context.addInitializer(function () {
|
|
38
|
+
const routes: RouteDefinition[] = (this as any)[META_KEY] || [];
|
|
39
|
+
routes.push({
|
|
40
|
+
method: 'post',
|
|
41
|
+
path,
|
|
42
|
+
methodName: String(context.name),
|
|
43
|
+
});
|
|
44
|
+
(this as any)[META_KEY] = routes;
|
|
45
|
+
});
|
|
46
|
+
return originalMethod;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function Put(path: string) {
|
|
51
|
+
return function (originalMethod: any, context: ClassMethodDecoratorContext) {
|
|
52
|
+
context.addInitializer(function () {
|
|
53
|
+
const routes: RouteDefinition[] = (this as any)[META_KEY] || [];
|
|
54
|
+
routes.push({
|
|
55
|
+
method: 'put',
|
|
56
|
+
path,
|
|
57
|
+
methodName: String(context.name),
|
|
58
|
+
});
|
|
59
|
+
(this as any)[META_KEY] = routes;
|
|
60
|
+
});
|
|
61
|
+
return originalMethod;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function Delete(path: string) {
|
|
66
|
+
return function (originalMethod: any, context: ClassMethodDecoratorContext) {
|
|
67
|
+
context.addInitializer(function () {
|
|
68
|
+
const routes: RouteDefinition[] = (this as any)[META_KEY] || [];
|
|
69
|
+
routes.push({
|
|
70
|
+
method: 'delete',
|
|
71
|
+
path,
|
|
72
|
+
methodName: String(context.name),
|
|
73
|
+
});
|
|
74
|
+
(this as any)[META_KEY] = routes;
|
|
75
|
+
});
|
|
76
|
+
return originalMethod;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// -- Class Decorator --
|
|
81
|
+
export function Controller(basePath: string) {
|
|
82
|
+
return function (target: any, context: ClassDecoratorContext) {
|
|
83
|
+
// We attach the base path to the class constructor
|
|
84
|
+
context.addInitializer(function () {
|
|
85
|
+
(this as any)._basePath = basePath;
|
|
86
|
+
});
|
|
87
|
+
return target;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// -- DTO Field Decorators --
|
|
92
|
+
// Since we can't decorate parameters, we decorate fields in a class
|
|
93
|
+
// e.g. class GetUserParams { @FromPath id: string }
|
|
94
|
+
|
|
95
|
+
export function FromQuery(name?: string) {
|
|
96
|
+
return function (target: undefined, context: ClassFieldDecoratorContext) {
|
|
97
|
+
context.addInitializer(function () {
|
|
98
|
+
const meta = (this as any)[SCHEMA_META] || {};
|
|
99
|
+
meta[context.name] = { type: 'query' };
|
|
100
|
+
(this as any)[SCHEMA_META] = meta;
|
|
101
|
+
});
|
|
102
|
+
return function (initialValue: any) { return initialValue; };
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function FromPath(name?: string) {
|
|
107
|
+
return function (target: undefined, context: ClassFieldDecoratorContext) {
|
|
108
|
+
context.addInitializer(function () {
|
|
109
|
+
const meta = (this as any)[SCHEMA_META] || {};
|
|
110
|
+
meta[context.name] = { type: 'path' };
|
|
111
|
+
(this as any)[SCHEMA_META] = meta;
|
|
112
|
+
});
|
|
113
|
+
return function (initialValue: any) { return initialValue; };
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function FromBody() {
|
|
118
|
+
return function (target: undefined, context: ClassFieldDecoratorContext) {
|
|
119
|
+
context.addInitializer(function () {
|
|
120
|
+
const meta = (this as any)[SCHEMA_META] || {};
|
|
121
|
+
meta[context.name] = { type: 'body' };
|
|
122
|
+
(this as any)[SCHEMA_META] = meta;
|
|
123
|
+
});
|
|
124
|
+
return function (initialValue: any) { return initialValue; };
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// -- Authentication Decorator --
|
|
129
|
+
export function Authorized(role?: string) {
|
|
130
|
+
return function (target: any, context: ClassMethodDecoratorContext | ClassDecoratorContext) {
|
|
131
|
+
context.addInitializer(function () {
|
|
132
|
+
(this as any)[AUTH_META] = role || 'default';
|
|
133
|
+
});
|
|
134
|
+
return target;
|
|
135
|
+
};
|
|
136
|
+
}
|
package/swagger.json
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
{
|
|
2
|
+
"openapi": "3.0.0",
|
|
3
|
+
"info": {
|
|
4
|
+
"title": "Adorn API",
|
|
5
|
+
"version": "2.0.0"
|
|
6
|
+
},
|
|
7
|
+
"paths": {
|
|
8
|
+
"/advanced/{tenantId}/users": {
|
|
9
|
+
"get": {
|
|
10
|
+
"operationId": "listUsers",
|
|
11
|
+
"parameters": [
|
|
12
|
+
{
|
|
13
|
+
"name": "search",
|
|
14
|
+
"in": "query",
|
|
15
|
+
"required": false,
|
|
16
|
+
"schema": {
|
|
17
|
+
"type": "string"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "tenantId",
|
|
22
|
+
"in": "path",
|
|
23
|
+
"required": true,
|
|
24
|
+
"schema": {
|
|
25
|
+
"type": "string"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "page",
|
|
30
|
+
"in": "query",
|
|
31
|
+
"required": true,
|
|
32
|
+
"schema": {
|
|
33
|
+
"type": "integer"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "limit",
|
|
38
|
+
"in": "query",
|
|
39
|
+
"required": true,
|
|
40
|
+
"schema": {
|
|
41
|
+
"type": "integer"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"responses": {
|
|
46
|
+
"200": {
|
|
47
|
+
"description": "Success",
|
|
48
|
+
"content": {
|
|
49
|
+
"application/json": {
|
|
50
|
+
"schema": {
|
|
51
|
+
"type": "array",
|
|
52
|
+
"items": {
|
|
53
|
+
"type": "object",
|
|
54
|
+
"properties": {
|
|
55
|
+
"id": {
|
|
56
|
+
"type": "string"
|
|
57
|
+
},
|
|
58
|
+
"name": {
|
|
59
|
+
"type": "string"
|
|
60
|
+
},
|
|
61
|
+
"email": {
|
|
62
|
+
"type": "string"
|
|
63
|
+
},
|
|
64
|
+
"isActive": {
|
|
65
|
+
"type": "boolean"
|
|
66
|
+
},
|
|
67
|
+
"createdAt": {
|
|
68
|
+
"type": "string"
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"required": [
|
|
72
|
+
"id",
|
|
73
|
+
"name",
|
|
74
|
+
"email",
|
|
75
|
+
"isActive",
|
|
76
|
+
"createdAt"
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"/advanced/": {
|
|
87
|
+
"post": {
|
|
88
|
+
"operationId": "create",
|
|
89
|
+
"parameters": [],
|
|
90
|
+
"requestBody": {
|
|
91
|
+
"content": {
|
|
92
|
+
"application/json": {
|
|
93
|
+
"schema": {
|
|
94
|
+
"type": "object",
|
|
95
|
+
"properties": {
|
|
96
|
+
"name": {
|
|
97
|
+
"type": "string"
|
|
98
|
+
},
|
|
99
|
+
"email": {
|
|
100
|
+
"type": "string"
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"required": [
|
|
104
|
+
"name",
|
|
105
|
+
"email"
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
"responses": {
|
|
112
|
+
"200": {
|
|
113
|
+
"description": "Success",
|
|
114
|
+
"content": {
|
|
115
|
+
"application/json": {
|
|
116
|
+
"schema": {
|
|
117
|
+
"type": "object",
|
|
118
|
+
"properties": {
|
|
119
|
+
"id": {
|
|
120
|
+
"type": "string"
|
|
121
|
+
},
|
|
122
|
+
"name": {
|
|
123
|
+
"type": "string"
|
|
124
|
+
},
|
|
125
|
+
"email": {
|
|
126
|
+
"type": "string"
|
|
127
|
+
},
|
|
128
|
+
"isActive": {
|
|
129
|
+
"type": "boolean"
|
|
130
|
+
},
|
|
131
|
+
"createdAt": {
|
|
132
|
+
"type": "string"
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
"required": [
|
|
136
|
+
"id",
|
|
137
|
+
"name",
|
|
138
|
+
"email",
|
|
139
|
+
"isActive",
|
|
140
|
+
"createdAt"
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
"/users/{userId}": {
|
|
150
|
+
"get": {
|
|
151
|
+
"operationId": "getUser",
|
|
152
|
+
"parameters": [
|
|
153
|
+
{
|
|
154
|
+
"name": "userId",
|
|
155
|
+
"in": "path",
|
|
156
|
+
"required": true,
|
|
157
|
+
"schema": {
|
|
158
|
+
"type": "string"
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"name": "details",
|
|
163
|
+
"in": "query",
|
|
164
|
+
"required": false,
|
|
165
|
+
"schema": {
|
|
166
|
+
"type": "integer",
|
|
167
|
+
"enum": [
|
|
168
|
+
null,
|
|
169
|
+
null
|
|
170
|
+
]
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
],
|
|
174
|
+
"responses": {
|
|
175
|
+
"200": {
|
|
176
|
+
"description": "Success",
|
|
177
|
+
"content": {
|
|
178
|
+
"application/json": {
|
|
179
|
+
"schema": {
|
|
180
|
+
"type": "string"
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
"/users/": {
|
|
189
|
+
"post": {
|
|
190
|
+
"operationId": "createUser",
|
|
191
|
+
"parameters": [],
|
|
192
|
+
"requestBody": {
|
|
193
|
+
"content": {
|
|
194
|
+
"application/json": {
|
|
195
|
+
"schema": {
|
|
196
|
+
"type": "object",
|
|
197
|
+
"properties": {
|
|
198
|
+
"name": {
|
|
199
|
+
"type": "string"
|
|
200
|
+
},
|
|
201
|
+
"email": {
|
|
202
|
+
"type": "string"
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
"required": [
|
|
206
|
+
"name",
|
|
207
|
+
"email"
|
|
208
|
+
]
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
"responses": {
|
|
214
|
+
"200": {
|
|
215
|
+
"description": "Success",
|
|
216
|
+
"content": {
|
|
217
|
+
"application/json": {
|
|
218
|
+
"schema": {
|
|
219
|
+
"type": "string"
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
"components": {
|
|
229
|
+
"schemas": {},
|
|
230
|
+
"securitySchemes": {
|
|
231
|
+
"bearerAuth": {
|
|
232
|
+
"type": "http",
|
|
233
|
+
"scheme": "bearer",
|
|
234
|
+
"bearerFormat": "JWT"
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
import { spawn, ChildProcessWithoutNullStreams } from "node:child_process";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { projectRoot, runTsNodeScript } from "./utils.js";
|
|
5
|
+
|
|
6
|
+
const BASE_URL = "http://localhost:3000";
|
|
7
|
+
|
|
8
|
+
let serverProcess: ChildProcessWithoutNullStreams | undefined;
|
|
9
|
+
|
|
10
|
+
beforeAll(
|
|
11
|
+
async () => {
|
|
12
|
+
await runTsNodeScript("src/cli/generate-swagger.ts");
|
|
13
|
+
await runTsNodeScript("src/cli/generate-routes.ts");
|
|
14
|
+
serverProcess = spawn(process.execPath, [path.join(projectRoot, "scripts", "run-example.js")], {
|
|
15
|
+
cwd: projectRoot,
|
|
16
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
await waitForServerReady(serverProcess);
|
|
20
|
+
},
|
|
21
|
+
30000
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
afterAll(() => {
|
|
25
|
+
if (serverProcess && !serverProcess.killed) {
|
|
26
|
+
serverProcess.kill();
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("adorn-api example server", () => {
|
|
31
|
+
it("lists users from the advanced endpoint", async () => {
|
|
32
|
+
const response = await fetch(`${BASE_URL}/advanced/tenant-12/users?page=1&limit=5`);
|
|
33
|
+
expect(response.status).toBe(200);
|
|
34
|
+
const body = await response.json();
|
|
35
|
+
expect(Array.isArray(body)).toBe(true);
|
|
36
|
+
expect(body[0]).toMatchObject({
|
|
37
|
+
id: "1",
|
|
38
|
+
name: "Alice",
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("returns user details from the user controller", async () => {
|
|
43
|
+
const response = await fetch(`${BASE_URL}/users/abc?details=true`);
|
|
44
|
+
expect(response.status).toBe(200);
|
|
45
|
+
const text = await response.text();
|
|
46
|
+
expect(text).toContain("Getting user abc");
|
|
47
|
+
expect(text).toContain("details: true");
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
async function waitForServerReady(proc: ChildProcessWithoutNullStreams, timeoutMs = 20000) {
|
|
52
|
+
const deadline = Date.now() + timeoutMs;
|
|
53
|
+
while (Date.now() < deadline) {
|
|
54
|
+
if (proc.exitCode !== null) {
|
|
55
|
+
throw new Error("Example server exited before becoming ready");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const response = await fetch(`${BASE_URL}/docs`);
|
|
60
|
+
if (response.ok) {
|
|
61
|
+
await response.text();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// ignore errors while the server is starting
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
throw new Error("Timed out waiting for the example server to become ready");
|
|
72
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// src/controllers/advanced.controller.ts
|
|
2
|
+
import { Controller, Get, Post, FromBody, FromPath, PaginationQuery, EntityResponse, CreateInput } from "../../../src/index.js";
|
|
3
|
+
import { User } from "../entities/user.entity.js";
|
|
4
|
+
|
|
5
|
+
// --- 1. Advanced Request DTOs ---
|
|
6
|
+
|
|
7
|
+
// INHERITANCE: UserListRequest automatically gets 'page' and 'limit' from PaginationQuery
|
|
8
|
+
// AND the generator will find them because we scan the Type properties.
|
|
9
|
+
export class UserListRequest extends PaginationQuery {
|
|
10
|
+
// Implicitly @FromQuery because it's a GET request and extends a class
|
|
11
|
+
search?: string;
|
|
12
|
+
|
|
13
|
+
@FromPath()
|
|
14
|
+
tenantId!: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// COMPOSITION: Using the Type Helper for safety, but class for Decorators
|
|
18
|
+
// We implement the Type Helper to ensure our Class matches the Entity rule
|
|
19
|
+
export class CreateUserDto implements CreateInput<User, 'name' | 'email'> {
|
|
20
|
+
@FromBody()
|
|
21
|
+
name!: string;
|
|
22
|
+
|
|
23
|
+
@FromBody()
|
|
24
|
+
email!: string;
|
|
25
|
+
|
|
26
|
+
// If I miss a field here that is required in CreateInput, TS throws an error at edit time.
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// --- 2. The Controller ---
|
|
30
|
+
|
|
31
|
+
@Controller("advanced")
|
|
32
|
+
export class AdvancedController {
|
|
33
|
+
|
|
34
|
+
@Get("/{tenantId}/users")
|
|
35
|
+
// Generic Return Type: The generator must resolve EntityResponse<User[]> -> User schema
|
|
36
|
+
public async listUsers(req: UserListRequest): Promise<EntityResponse<User[]>> {
|
|
37
|
+
return [
|
|
38
|
+
{ id: "1", name: "Alice", email: "a@a.com", isActive: true, createdAt: "now" }
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Post("/")
|
|
43
|
+
public async create(req: CreateUserDto): Promise<EntityResponse<User>> {
|
|
44
|
+
return {
|
|
45
|
+
id: "123",
|
|
46
|
+
name: req.name,
|
|
47
|
+
email: req.email,
|
|
48
|
+
isActive: true,
|
|
49
|
+
createdAt: "now"
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// src/controllers/user.controller.ts
|
|
2
|
+
import { Controller, Get, Post, FromQuery, FromPath, FromBody } from "../../../src/index.js";
|
|
3
|
+
|
|
4
|
+
// --- DTO Definitions (The substitute for Parameter Decorators) ---
|
|
5
|
+
export class GetUserRequest {
|
|
6
|
+
@FromPath()
|
|
7
|
+
userId!: string;
|
|
8
|
+
|
|
9
|
+
@FromQuery()
|
|
10
|
+
details?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class CreateUserRequest {
|
|
14
|
+
@FromBody()
|
|
15
|
+
name!: string;
|
|
16
|
+
|
|
17
|
+
@FromBody()
|
|
18
|
+
email!: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// --- The Controller ---
|
|
22
|
+
@Controller("users")
|
|
23
|
+
export class UserController {
|
|
24
|
+
|
|
25
|
+
// Strong typing: 'req' is checked at edit time.
|
|
26
|
+
@Get("/{userId}")
|
|
27
|
+
public async getUser(req: GetUserRequest): Promise<string> {
|
|
28
|
+
return `Getting user ${req.userId} with details: ${req.details}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Post("/")
|
|
32
|
+
public async createUser(req: CreateUserRequest): Promise<void> {
|
|
33
|
+
console.log(`Creating user ${req.name}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// src/middleware/auth.middleware.ts
|
|
2
|
+
import { Request, Response, NextFunction } from "express";
|
|
3
|
+
|
|
4
|
+
export function authenticationMiddleware(req: Request, res: Response, next: NextFunction) {
|
|
5
|
+
const token = req.headers.authorization;
|
|
6
|
+
if (!token) {
|
|
7
|
+
res.status(401).json({ message: "No token provided" });
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (token === "Bearer secret") {
|
|
12
|
+
next();
|
|
13
|
+
} else {
|
|
14
|
+
res.status(403).json({ message: "Invalid token" });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
// WARNING: This file was auto-generated by adorn-api. Do not edit.
|
|
4
|
+
import { Express, Request, Response } from 'express';
|
|
5
|
+
import { UserListRequest } from './controllers/advanced.controller.js';
|
|
6
|
+
import { CreateUserDto } from './controllers/advanced.controller.js';
|
|
7
|
+
import { AdvancedController } from './controllers/advanced.controller.js';
|
|
8
|
+
import { GetUserRequest } from './controllers/user.controller.js';
|
|
9
|
+
import { CreateUserRequest } from './controllers/user.controller.js';
|
|
10
|
+
import { UserController } from './controllers/user.controller.js';
|
|
11
|
+
|
|
12
|
+
export function RegisterRoutes(app: Express) {
|
|
13
|
+
|
|
14
|
+
app.get('/advanced/:tenantId/users', async (req: Request, res: Response) => {
|
|
15
|
+
const controller = new AdvancedController();
|
|
16
|
+
try {
|
|
17
|
+
|
|
18
|
+
const input: any = {};
|
|
19
|
+
// Map Query
|
|
20
|
+
Object.assign(input, req.query);
|
|
21
|
+
// Map Params
|
|
22
|
+
Object.assign(input, req.params);
|
|
23
|
+
// Map Body
|
|
24
|
+
Object.assign(input, req.body);
|
|
25
|
+
|
|
26
|
+
// In a real app, you would run 'zod' or 'class-validator' here on 'input'
|
|
27
|
+
|
|
28
|
+
const response = await controller.listUsers(input);
|
|
29
|
+
res.status(200).json(response);
|
|
30
|
+
} catch (err: any) {
|
|
31
|
+
console.error(err);
|
|
32
|
+
res.status(500).send(err.message);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
app.post('/advanced/', async (req: Request, res: Response) => {
|
|
37
|
+
const controller = new AdvancedController();
|
|
38
|
+
try {
|
|
39
|
+
|
|
40
|
+
const input: any = {};
|
|
41
|
+
// Map Query
|
|
42
|
+
Object.assign(input, req.query);
|
|
43
|
+
// Map Params
|
|
44
|
+
Object.assign(input, req.params);
|
|
45
|
+
// Map Body
|
|
46
|
+
Object.assign(input, req.body);
|
|
47
|
+
|
|
48
|
+
// In a real app, you would run 'zod' or 'class-validator' here on 'input'
|
|
49
|
+
|
|
50
|
+
const response = await controller.create(input);
|
|
51
|
+
res.status(200).json(response);
|
|
52
|
+
} catch (err: any) {
|
|
53
|
+
console.error(err);
|
|
54
|
+
res.status(500).send(err.message);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
app.get('/users/:userId', async (req: Request, res: Response) => {
|
|
59
|
+
const controller = new UserController();
|
|
60
|
+
try {
|
|
61
|
+
|
|
62
|
+
const input: any = {};
|
|
63
|
+
// Map Query
|
|
64
|
+
Object.assign(input, req.query);
|
|
65
|
+
// Map Params
|
|
66
|
+
Object.assign(input, req.params);
|
|
67
|
+
// Map Body
|
|
68
|
+
Object.assign(input, req.body);
|
|
69
|
+
|
|
70
|
+
// In a real app, you would run 'zod' or 'class-validator' here on 'input'
|
|
71
|
+
|
|
72
|
+
const response = await controller.getUser(input);
|
|
73
|
+
res.status(200).json(response);
|
|
74
|
+
} catch (err: any) {
|
|
75
|
+
console.error(err);
|
|
76
|
+
res.status(500).send(err.message);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
app.post('/users/', async (req: Request, res: Response) => {
|
|
81
|
+
const controller = new UserController();
|
|
82
|
+
try {
|
|
83
|
+
|
|
84
|
+
const input: any = {};
|
|
85
|
+
// Map Query
|
|
86
|
+
Object.assign(input, req.query);
|
|
87
|
+
// Map Params
|
|
88
|
+
Object.assign(input, req.params);
|
|
89
|
+
// Map Body
|
|
90
|
+
Object.assign(input, req.body);
|
|
91
|
+
|
|
92
|
+
// In a real app, you would run 'zod' or 'class-validator' here on 'input'
|
|
93
|
+
|
|
94
|
+
const response = await controller.createUser(input);
|
|
95
|
+
res.status(200).json(response);
|
|
96
|
+
} catch (err: any) {
|
|
97
|
+
console.error(err);
|
|
98
|
+
res.status(500).send(err.message);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// tests/example-app/server.ts
|
|
2
|
+
// Example Express server using adorn-api
|
|
3
|
+
import express, { Express } from "express";
|
|
4
|
+
import bodyParser from "body-parser";
|
|
5
|
+
import { readFileSync } from "fs";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { dirname, join } from "path";
|
|
8
|
+
import swaggerUi from "swagger-ui-express";
|
|
9
|
+
|
|
10
|
+
// Import generated routes
|
|
11
|
+
import { RegisterRoutes } from "./routes.js";
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
|
|
16
|
+
const app: Express = express();
|
|
17
|
+
|
|
18
|
+
app.use(bodyParser.json());
|
|
19
|
+
|
|
20
|
+
// Register the Generated Routes
|
|
21
|
+
RegisterRoutes(app);
|
|
22
|
+
|
|
23
|
+
// Serve Swagger UI
|
|
24
|
+
const swaggerDoc = JSON.parse(readFileSync(join(__dirname, "../../swagger.json"), "utf-8"));
|
|
25
|
+
app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerDoc));
|
|
26
|
+
|
|
27
|
+
app.listen(3000, () => {
|
|
28
|
+
console.log("🚀 Example server running on http://localhost:3000");
|
|
29
|
+
console.log("📄 Swagger running on http://localhost:3000/docs");
|
|
30
|
+
});
|