adorn-api 1.0.11 → 1.0.13
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 +318 -620
- package/dist/adapter/express/auth.d.ts +5 -0
- package/dist/adapter/express/auth.d.ts.map +1 -0
- package/dist/adapter/express/bootstrap.d.ts.map +1 -1
- package/dist/adapter/express/coercion.d.ts +22 -0
- package/dist/adapter/express/coercion.d.ts.map +1 -0
- package/dist/adapter/express/index.d.ts +3 -50
- package/dist/adapter/express/index.d.ts.map +1 -1
- package/dist/adapter/express/merge.d.ts +0 -3
- package/dist/adapter/express/merge.d.ts.map +1 -1
- package/dist/adapter/express/openapi.d.ts +11 -0
- package/dist/adapter/express/openapi.d.ts.map +1 -0
- package/dist/adapter/express/router.d.ts +4 -0
- package/dist/adapter/express/router.d.ts.map +1 -0
- package/dist/adapter/express/swagger.d.ts +4 -0
- package/dist/adapter/express/swagger.d.ts.map +1 -0
- package/dist/adapter/express/types.d.ts +64 -0
- package/dist/adapter/express/types.d.ts.map +1 -0
- package/dist/adapter/express/validation.d.ts +10 -0
- package/dist/adapter/express/validation.d.ts.map +1 -0
- package/dist/cli.cjs +1003 -434
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1003 -434
- package/dist/cli.js.map +1 -1
- package/dist/compiler/analyze/scanControllers.d.ts +0 -1
- package/dist/compiler/analyze/scanControllers.d.ts.map +1 -1
- package/dist/compiler/cache/isStale.d.ts.map +1 -1
- package/dist/compiler/cache/writeCache.d.ts.map +1 -1
- package/dist/compiler/manifest/emit.d.ts.map +1 -1
- package/dist/compiler/manifest/format.d.ts +1 -1
- package/dist/compiler/manifest/format.d.ts.map +1 -1
- package/dist/compiler/schema/intersectionHandler.d.ts +7 -0
- package/dist/compiler/schema/intersectionHandler.d.ts.map +1 -0
- package/dist/compiler/schema/objectHandler.d.ts +20 -0
- package/dist/compiler/schema/objectHandler.d.ts.map +1 -0
- package/dist/compiler/schema/openapi.d.ts +1 -1
- package/dist/compiler/schema/openapi.d.ts.map +1 -1
- package/dist/compiler/schema/parameters.d.ts +18 -0
- package/dist/compiler/schema/parameters.d.ts.map +1 -0
- package/dist/compiler/schema/primitives.d.ts +10 -0
- package/dist/compiler/schema/primitives.d.ts.map +1 -0
- package/dist/compiler/schema/typeToJsonSchema.d.ts +3 -46
- package/dist/compiler/schema/typeToJsonSchema.d.ts.map +1 -1
- package/dist/compiler/schema/types.d.ts +54 -0
- package/dist/compiler/schema/types.d.ts.map +1 -0
- package/dist/compiler/schema/unionHandler.d.ts +10 -0
- package/dist/compiler/schema/unionHandler.d.ts.map +1 -0
- package/dist/decorators/index.d.ts +0 -1
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/express.cjs +522 -502
- package/dist/express.cjs.map +1 -1
- package/dist/express.js +522 -502
- package/dist/express.js.map +1 -1
- package/dist/http.d.ts +1 -10
- package/dist/http.d.ts.map +1 -1
- package/dist/index.cjs +3 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -34
- package/dist/index.js.map +1 -1
- package/dist/metal/applyListQuery.d.ts +27 -0
- package/dist/metal/applyListQuery.d.ts.map +1 -0
- package/dist/metal/index.cjs +61 -2
- package/dist/metal/index.cjs.map +1 -1
- package/dist/metal/index.d.ts +4 -0
- package/dist/metal/index.d.ts.map +1 -1
- package/dist/metal/index.js +57 -2
- package/dist/metal/index.js.map +1 -1
- package/dist/metal/listQuery.d.ts +7 -0
- package/dist/metal/listQuery.d.ts.map +1 -0
- package/dist/metal/queryOptions.d.ts +8 -0
- package/dist/metal/queryOptions.d.ts.map +1 -0
- package/dist/metal/registerMetalEntities.d.ts.map +1 -1
- package/dist/runtime/metadata/types.d.ts +0 -3
- package/dist/runtime/metadata/types.d.ts.map +1 -1
- package/package.json +4 -1
- package/dist/compiler/analyze/extractQueryStyle.d.ts +0 -8
- package/dist/compiler/analyze/extractQueryStyle.d.ts.map +0 -1
- package/dist/decorators/Paginated.d.ts +0 -5
- package/dist/decorators/Paginated.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
# Adorn-API
|
|
2
2
|
|
|
3
|
-
Stage-3 decorator-first OpenAPI + routing toolkit for TypeScript.
|
|
3
|
+
A Stage-3 decorator-first OpenAPI + routing toolkit for Express with full TypeScript support.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Features
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- **Decorators-First**: Use Stage-3 decorators to define controllers, routes, middleware, and auth
|
|
8
|
+
- **Auto-Generated OpenAPI**: OpenAPI 3.1 specs generated from your TypeScript types
|
|
9
|
+
- **Swagger UI**: Interactive API documentation at `/docs` with zero configuration
|
|
10
|
+
- **Type-Safe**: Full TypeScript inference throughout your API
|
|
11
|
+
- **Authentication**: Built-in auth support with scope-based authorization
|
|
12
|
+
- **Middleware**: Apply middleware globally, per-controller, or per-route
|
|
13
|
+
- **Metal-ORM Integration**: Seamless database integration with type-safe queries
|
|
14
|
+
- **Validation**: AJV runtime validation with optional precompiled validators
|
|
15
|
+
- **Hot Reload**: Development mode with automatic rebuilds
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
8
18
|
|
|
9
19
|
```bash
|
|
10
20
|
npm install adorn-api
|
|
21
|
+
npm install -D @types/express
|
|
11
22
|
```
|
|
12
23
|
|
|
13
|
-
|
|
24
|
+
## Quick Start
|
|
14
25
|
|
|
15
26
|
```typescript
|
|
27
|
+
// controller.ts
|
|
16
28
|
import { Controller, Get, Post } from "adorn-api";
|
|
17
29
|
|
|
18
30
|
interface User {
|
|
@@ -21,812 +33,498 @@ interface User {
|
|
|
21
33
|
email: string;
|
|
22
34
|
}
|
|
23
35
|
|
|
24
|
-
const users: User[] = [
|
|
25
|
-
{ id: 1, name: "Alice", email: "alice@example.com" },
|
|
26
|
-
];
|
|
27
|
-
|
|
28
36
|
@Controller("/users")
|
|
29
37
|
export class UserController {
|
|
30
38
|
@Get("/")
|
|
31
39
|
async getUsers(): Promise<User[]> {
|
|
32
|
-
return
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
@Get("/:id")
|
|
36
|
-
async getUser(id: number): Promise<User | null> {
|
|
37
|
-
return users.find(u => u.id === id) || null;
|
|
40
|
+
return [{ id: 1, name: "Alice", email: "alice@example.com" }];
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
@Post("/")
|
|
41
44
|
async createUser(body: { name: string; email: string }): Promise<User> {
|
|
42
|
-
|
|
43
|
-
id: users.length + 1,
|
|
44
|
-
name: body.name,
|
|
45
|
-
email: body.email,
|
|
46
|
-
};
|
|
47
|
-
users.push(user);
|
|
48
|
-
return user;
|
|
45
|
+
return { id: 2, name: body.name, email: body.email };
|
|
49
46
|
}
|
|
50
47
|
}
|
|
51
48
|
```
|
|
52
49
|
|
|
53
|
-
Start your server:
|
|
54
|
-
|
|
55
50
|
```typescript
|
|
51
|
+
// server.ts
|
|
56
52
|
import { bootstrap } from "adorn-api/express";
|
|
57
53
|
import { UserController } from "./controller.js";
|
|
58
54
|
|
|
59
|
-
await bootstrap({
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
Visit http://localhost:3000/docs to see your auto-generated Swagger UI.
|
|
63
|
-
|
|
64
|
-
## Features
|
|
65
|
-
|
|
66
|
-
- **Decorator-based routing** - `@Controller`, `@Get`, `@Post`, `@Put`, `@Patch`, `@Delete`
|
|
67
|
-
- **Automatic OpenAPI 3.1** - Generate specs from TypeScript decorators
|
|
68
|
-
- **Runtime validation** - AJV-powered request validation
|
|
69
|
-
- **Authentication** - `@Auth()` and `@Public()` decorators with custom schemes
|
|
70
|
-
- **Middleware** - Global, controller-level, and route-level middleware
|
|
71
|
-
- **Type-safe** - Full TypeScript inference throughout
|
|
72
|
-
- **Incremental builds** - Smart caching with `--if-stale` flag
|
|
73
|
-
- **Precompiled validators** - Optimized validation for production
|
|
74
|
-
- **Swagger UI** - Built-in documentation at `/docs`
|
|
75
|
-
- **Metal ORM integration** - Auto-generate schemas from entities
|
|
76
|
-
|
|
77
|
-
## Installation
|
|
78
|
-
|
|
79
|
-
```bash
|
|
80
|
-
npm install adorn-api
|
|
55
|
+
await bootstrap({
|
|
56
|
+
controllers: [UserController],
|
|
57
|
+
});
|
|
81
58
|
```
|
|
82
59
|
|
|
83
|
-
|
|
60
|
+
Run with:
|
|
84
61
|
|
|
85
62
|
```bash
|
|
86
|
-
|
|
63
|
+
npx adorn-api dev
|
|
87
64
|
```
|
|
88
65
|
|
|
89
|
-
|
|
90
|
-
- Node.js 18+
|
|
91
|
-
- TypeScript 5.0+
|
|
66
|
+
Open http://localhost:3000/docs to see your Swagger UI documentation.
|
|
92
67
|
|
|
93
68
|
## Core Concepts
|
|
94
69
|
|
|
95
70
|
### Controllers
|
|
96
71
|
|
|
97
|
-
|
|
72
|
+
Define a controller with a base path:
|
|
98
73
|
|
|
99
74
|
```typescript
|
|
100
75
|
@Controller("/api/users")
|
|
101
|
-
export class
|
|
102
|
-
@Get("/")
|
|
103
|
-
async getUsers() { /* ... */ }
|
|
104
|
-
|
|
105
|
-
@Get("/:id")
|
|
106
|
-
async getUser(id: number) { /* ... */ }
|
|
107
|
-
}
|
|
76
|
+
export class UserController {}
|
|
108
77
|
```
|
|
109
78
|
|
|
110
|
-
###
|
|
79
|
+
### Route Handlers
|
|
111
80
|
|
|
112
|
-
|
|
81
|
+
Use HTTP method decorators to define routes:
|
|
113
82
|
|
|
114
83
|
```typescript
|
|
115
|
-
@
|
|
116
|
-
|
|
117
|
-
@
|
|
118
|
-
|
|
119
|
-
@Delete("/:id") // DELETE
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
### Parameters
|
|
123
|
-
|
|
124
|
-
Adorn-API automatically extracts parameters from your handler signatures:
|
|
84
|
+
@Controller("/users")
|
|
85
|
+
export class UserController {
|
|
86
|
+
@Get("/")
|
|
87
|
+
async list(): Promise<User[]> {}
|
|
125
88
|
|
|
126
|
-
```typescript
|
|
127
|
-
@Controller("/products")
|
|
128
|
-
export class ProductsController {
|
|
129
89
|
@Get("/:id")
|
|
130
|
-
async
|
|
131
|
-
id: number, // Path parameter
|
|
132
|
-
query?: { category?: string } // Query parameter
|
|
133
|
-
) { /* ... */ }
|
|
90
|
+
async get(id: number): Promise<User> {}
|
|
134
91
|
|
|
135
92
|
@Post("/")
|
|
136
|
-
async
|
|
137
|
-
body: { name: string }, // Request body
|
|
138
|
-
headers: { "X-Request-Id": string } // Headers
|
|
139
|
-
) { /* ... */ }
|
|
140
|
-
}
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
### Query Objects
|
|
144
|
-
|
|
145
|
-
Use an object-typed parameter to bind flat query keys:
|
|
93
|
+
async create(body: CreateUserDto): Promise<User> {}
|
|
146
94
|
|
|
147
|
-
|
|
148
|
-
|
|
95
|
+
@Put("/:id")
|
|
96
|
+
async update(id: number, body: UpdateUserDto): Promise<User> {}
|
|
149
97
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
responsavelId?: number;
|
|
153
|
-
};
|
|
98
|
+
@Patch("/:id")
|
|
99
|
+
async patch(id: number, body: Partial<User>): Promise<User> {}
|
|
154
100
|
|
|
155
|
-
@
|
|
156
|
-
async
|
|
157
|
-
return query;
|
|
101
|
+
@Delete("/:id")
|
|
102
|
+
async delete(id: number): Promise<void> {}
|
|
158
103
|
}
|
|
159
104
|
```
|
|
160
105
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
### Deep Object Query (opt-in)
|
|
106
|
+
### Parameters
|
|
164
107
|
|
|
165
|
-
|
|
108
|
+
Parameters are automatically extracted from your handler signature:
|
|
166
109
|
|
|
167
110
|
```typescript
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
};
|
|
176
|
-
tags?: string[];
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
@Get("/")
|
|
180
|
-
@QueryStyle({ style: "deepObject" })
|
|
181
|
-
async list(where?: WhereFilter) {
|
|
182
|
-
return where;
|
|
183
|
-
}
|
|
111
|
+
async handler(
|
|
112
|
+
id: number, // Path parameter
|
|
113
|
+
query: { limit?: number; sort?: string }, // Query parameters
|
|
114
|
+
body: CreateUserDto, // Request body
|
|
115
|
+
headers: { authorization?: string }, // Headers
|
|
116
|
+
cookies: { sessionId?: string } // Cookies
|
|
117
|
+
) {}
|
|
184
118
|
```
|
|
185
119
|
|
|
186
|
-
|
|
187
|
-
- `GET /posts?where[tags]=a&where[tags]=b`
|
|
188
|
-
- `GET /posts?where[comments][author][name]=Ali` (matches `Alice`)
|
|
189
|
-
|
|
190
|
-
Notes:
|
|
191
|
-
- Deep object is explicit and only applies to the query object parameter on that method.
|
|
192
|
-
- Repeated keys become arrays (for example, `where[tags]=a&where[tags]=b`).
|
|
193
|
-
- The `[]` shorthand is not supported; use repeated keys instead.
|
|
194
|
-
|
|
195
|
-
## Examples
|
|
196
|
-
|
|
197
|
-
### Basic Example
|
|
120
|
+
### Middleware
|
|
198
121
|
|
|
199
|
-
|
|
122
|
+
Apply middleware at any level:
|
|
200
123
|
|
|
201
124
|
```typescript
|
|
202
|
-
|
|
125
|
+
// Global middleware
|
|
126
|
+
const app = await bootstrap({
|
|
127
|
+
controllers: [UserController],
|
|
128
|
+
middleware: {
|
|
129
|
+
global: [loggingMiddleware, corsMiddleware],
|
|
130
|
+
named: { auth: authMiddleware },
|
|
131
|
+
},
|
|
132
|
+
});
|
|
203
133
|
|
|
134
|
+
// Controller-level middleware
|
|
204
135
|
@Controller("/users")
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
async getUsers() { return [{ id: 1, name: "Alice" }]; }
|
|
136
|
+
@Use(authMiddleware)
|
|
137
|
+
export class UserController {}
|
|
208
138
|
|
|
209
|
-
|
|
210
|
-
|
|
139
|
+
// Route-level middleware
|
|
140
|
+
@Get("/admin")
|
|
141
|
+
@Use(adminMiddleware)
|
|
142
|
+
async adminOnly() {}
|
|
211
143
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
async updateUser(id: number, body: { name?: string }) { /* ... */ }
|
|
217
|
-
|
|
218
|
-
@Delete("/:id")
|
|
219
|
-
async deleteUser(id: number) { return { success: true }; }
|
|
220
|
-
}
|
|
144
|
+
// Named middleware
|
|
145
|
+
@Get("/protected")
|
|
146
|
+
@Use("auth")
|
|
147
|
+
async protected() {}
|
|
221
148
|
```
|
|
222
149
|
|
|
223
|
-
|
|
150
|
+
Middleware executes in order: global → controller → route → handler.
|
|
224
151
|
|
|
225
|
-
|
|
152
|
+
### Authentication
|
|
226
153
|
|
|
227
|
-
|
|
228
|
-
import { Controller, Get, Post } from "adorn-api";
|
|
229
|
-
import { Auth, Public } from "adorn-api/decorators";
|
|
230
|
-
|
|
231
|
-
@Controller("/api")
|
|
232
|
-
export class ApiController {
|
|
233
|
-
@Get("/public")
|
|
234
|
-
@Public()
|
|
235
|
-
async publicEndpoint() {
|
|
236
|
-
return { message: "No auth required" };
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
@Get("/profile")
|
|
240
|
-
@Auth("BearerAuth")
|
|
241
|
-
async getProfile(req: any) {
|
|
242
|
-
return { user: req.auth };
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
@Get("/data")
|
|
246
|
-
@Auth("BearerAuth", { scopes: ["read"] })
|
|
247
|
-
async getData() {
|
|
248
|
-
return { data: ["item1", "item2"] };
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
@Post("/items")
|
|
252
|
-
@Auth("BearerAuth", { scopes: ["write"] })
|
|
253
|
-
async createItem(req: any) {
|
|
254
|
-
return { id: 1, name: req.body.name };
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
Configure the auth scheme when creating your router:
|
|
154
|
+
Define auth schemes and protect routes:
|
|
260
155
|
|
|
261
156
|
```typescript
|
|
157
|
+
import { Auth, Public } from "adorn-api";
|
|
158
|
+
|
|
159
|
+
// Define auth scheme
|
|
262
160
|
const bearerRuntime = {
|
|
263
161
|
name: "BearerAuth",
|
|
264
|
-
async authenticate(req) {
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
return { principal: { userId: 1 }, scopes: ["read", "write"] };
|
|
162
|
+
async authenticate(req: any) {
|
|
163
|
+
const token = req.headers.authorization?.replace("Bearer ", "");
|
|
164
|
+
const user = await verifyToken(token);
|
|
165
|
+
return user ? { principal: user, scopes: user.scopes } : null;
|
|
269
166
|
},
|
|
270
|
-
challenge(res) {
|
|
167
|
+
challenge(res: any) {
|
|
271
168
|
res.status(401).json({ error: "Unauthorized" });
|
|
272
169
|
},
|
|
273
|
-
authorize(auth, requiredScopes) {
|
|
170
|
+
authorize(auth: any, requiredScopes: string[]) {
|
|
274
171
|
return requiredScopes.every(s => auth.scopes?.includes(s));
|
|
275
172
|
},
|
|
276
173
|
};
|
|
277
174
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
175
|
+
// Bootstrap with auth
|
|
176
|
+
await bootstrap({
|
|
177
|
+
controllers: [UserController],
|
|
178
|
+
auth: {
|
|
179
|
+
schemes: { BearerAuth: bearerRuntime },
|
|
180
|
+
},
|
|
281
181
|
});
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
### E-commerce Example
|
|
285
182
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
@
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
async getProducts() {
|
|
293
|
-
return products.filter(p => p.status === "published");
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
@Get("/:id")
|
|
297
|
-
async getProduct(id: number) { /* ... */ }
|
|
298
|
-
|
|
299
|
-
@Post("/")
|
|
300
|
-
async createProduct(body: { name: string; price: number }) { /* ... */ }
|
|
183
|
+
// Protect routes
|
|
184
|
+
@Controller("/api")
|
|
185
|
+
export class ApiController {
|
|
186
|
+
@Get("/public")
|
|
187
|
+
@Public()
|
|
188
|
+
async publicEndpoint() {}
|
|
301
189
|
|
|
302
|
-
@
|
|
303
|
-
|
|
190
|
+
@Get("/profile")
|
|
191
|
+
@Auth("BearerAuth")
|
|
192
|
+
async getProfile() {}
|
|
304
193
|
|
|
305
|
-
@
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
@Post("/:id/publish")
|
|
309
|
-
async publishProduct(id: number) { /* ... */ }
|
|
310
|
-
|
|
311
|
-
@Post("/search/advanced")
|
|
312
|
-
async advancedSearch(body: {
|
|
313
|
-
query?: string;
|
|
314
|
-
minPrice?: number;
|
|
315
|
-
maxPrice?: number;
|
|
316
|
-
inStockOnly?: boolean;
|
|
317
|
-
}) { /* ... */ }
|
|
194
|
+
@Post("/admin")
|
|
195
|
+
@Auth("BearerAuth", { scopes: ["admin"] })
|
|
196
|
+
async adminOnly() {}
|
|
318
197
|
}
|
|
319
198
|
```
|
|
320
199
|
|
|
321
|
-
###
|
|
322
|
-
|
|
323
|
-
Database-driven API using Metal ORM entities. See: `examples/blog-platform-metal-orm/`
|
|
200
|
+
### Optional Authentication
|
|
324
201
|
|
|
325
202
|
```typescript
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
export class BlogPostsController {
|
|
332
|
-
@Get("/")
|
|
333
|
-
async getPosts(query?: { authorId?: number; status?: string }) {
|
|
334
|
-
const session = getSession();
|
|
335
|
-
let qb = selectFromEntity(BlogPost);
|
|
336
|
-
if (query?.authorId) qb = qb.where(eq(BlogPost.authorId, query.authorId));
|
|
337
|
-
if (query?.status) qb = qb.where(eq(BlogPost.status, query.status));
|
|
338
|
-
return qb.execute(session);
|
|
203
|
+
@Get("/resource")
|
|
204
|
+
@Auth("BearerAuth", { optional: true })
|
|
205
|
+
async getResource(req: any) {
|
|
206
|
+
if (req.auth) {
|
|
207
|
+
return { user: req.auth.principal };
|
|
339
208
|
}
|
|
340
|
-
|
|
341
|
-
@Get("/:id")
|
|
342
|
-
async getPost(id: number) { /* ... */ }
|
|
343
|
-
|
|
344
|
-
@Post("/")
|
|
345
|
-
async createPost(body: Pick<BlogPost, "title" | "content" | "authorId">) {
|
|
346
|
-
const session = getSession();
|
|
347
|
-
const post = new BlogPost();
|
|
348
|
-
Object.assign(post, body);
|
|
349
|
-
await session.persist(post);
|
|
350
|
-
await session.flush();
|
|
351
|
-
return post;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
@Put("/:id")
|
|
355
|
-
async updatePost(id: number, body: Partial<BlogPost>) { /* ... */ }
|
|
356
|
-
|
|
357
|
-
@Delete("/:id")
|
|
358
|
-
async deletePost(id: number) { return { success: true }; }
|
|
209
|
+
return { user: null };
|
|
359
210
|
}
|
|
360
211
|
```
|
|
361
212
|
|
|
362
|
-
|
|
213
|
+
## Metal-ORM Integration
|
|
363
214
|
|
|
364
|
-
|
|
215
|
+
Seamlessly integrate with Metal-ORM for database operations:
|
|
365
216
|
|
|
366
217
|
```typescript
|
|
218
|
+
import { Controller, Get } from "adorn-api";
|
|
219
|
+
import type { ListQuery } from "adorn-api/metal";
|
|
220
|
+
import { applyListQuery } from "adorn-api/metal";
|
|
221
|
+
import { selectFromEntity, entityRef } from "metal-orm";
|
|
222
|
+
|
|
367
223
|
@Controller("/tasks")
|
|
368
224
|
export class TasksController {
|
|
369
225
|
@Get("/")
|
|
370
|
-
async
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
return allQuery(sql, params);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
@Get("/:id")
|
|
377
|
-
async getTask(id: number) { /* returns task with tags */ }
|
|
378
|
-
|
|
379
|
-
@Post("/")
|
|
380
|
-
async createTask(body: { title: string; priority?: "low" | "medium" | "high" }) {
|
|
381
|
-
const now = new Date().toISOString();
|
|
382
|
-
return runQuery(sql, [...values, now, now]);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
@Put("/:id")
|
|
386
|
-
async updateTask(id: number, body: Partial<Task>) { /* ... */ }
|
|
387
|
-
|
|
388
|
-
@Delete("/:id")
|
|
389
|
-
async deleteTask(id: number) { return { success: result.changes > 0 }; }
|
|
390
|
-
|
|
391
|
-
@Post("/:id/tags")
|
|
392
|
-
async addTagToTask(id: number, body: { tag_id: number }) { /* ... */ }
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
@Controller("/tags")
|
|
396
|
-
export class TagsController {
|
|
397
|
-
@Get("/")
|
|
398
|
-
async getTags() { return allQuery("SELECT * FROM tags"); }
|
|
399
|
-
|
|
400
|
-
@Post("/")
|
|
401
|
-
async createTag(body: { name: string; color?: string }) { /* ... */ }
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
@Controller("/stats")
|
|
405
|
-
export class StatsController {
|
|
406
|
-
@Get("/")
|
|
407
|
-
async getStats() {
|
|
408
|
-
return {
|
|
409
|
-
total: count,
|
|
410
|
-
byStatus: statusMap,
|
|
411
|
-
byPriority: priorityMap,
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
## Authentication
|
|
418
|
-
|
|
419
|
-
### @Auth Decorator
|
|
420
|
-
|
|
421
|
-
Protect endpoints with authentication requirements:
|
|
422
|
-
|
|
423
|
-
```typescript
|
|
424
|
-
@Controller("/api")
|
|
425
|
-
export class ApiController {
|
|
426
|
-
@Get("/secure")
|
|
427
|
-
@Auth("BearerAuth")
|
|
428
|
-
async secureEndpoint(req: any) {
|
|
429
|
-
return { user: req.auth };
|
|
430
|
-
}
|
|
226
|
+
async list(query: ListQuery<Task>): Promise<PaginatedResult<Task>> {
|
|
227
|
+
const session = getSession();
|
|
228
|
+
const T = entityRef(Task);
|
|
431
229
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
return { data: "secret" };
|
|
436
|
-
}
|
|
230
|
+
const qb = selectFromEntity(Task)
|
|
231
|
+
.select("id", "title", "completed")
|
|
232
|
+
.where(eq(T.completed, false));
|
|
437
233
|
|
|
438
|
-
|
|
439
|
-
@Auth("BearerAuth", { optional: true })
|
|
440
|
-
async optionalAuth(req: any) {
|
|
441
|
-
return { user: req.auth }; // null if no token
|
|
234
|
+
return applyListQuery(qb, session, query);
|
|
442
235
|
}
|
|
443
236
|
}
|
|
444
237
|
```
|
|
445
238
|
|
|
446
|
-
|
|
239
|
+
`ListQuery` supports:
|
|
240
|
+
- Pagination: `page`, `perPage`
|
|
241
|
+
- Sorting: `sort` (string or array, prefix with `-` for DESC)
|
|
242
|
+
- Filtering: `where` (deep object filters)
|
|
447
243
|
|
|
448
|
-
|
|
244
|
+
### Register Metal Entities
|
|
449
245
|
|
|
450
|
-
|
|
451
|
-
@Controller("/api")
|
|
452
|
-
export class ApiController {
|
|
453
|
-
@Get("/public")
|
|
454
|
-
@Public()
|
|
455
|
-
async publicEndpoint() {
|
|
456
|
-
return { message: "Anyone can access this" };
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
@Get("/protected")
|
|
460
|
-
@Auth("BearerAuth")
|
|
461
|
-
async protectedEndpoint() {
|
|
462
|
-
return { message: "Auth required" };
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
### Auth Scheme Configuration
|
|
468
|
-
|
|
469
|
-
Define custom authentication schemes:
|
|
246
|
+
Auto-generate OpenAPI schemas from Metal-ORM entities:
|
|
470
247
|
|
|
471
248
|
```typescript
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
async authenticate(req) {
|
|
475
|
-
const token = req.headers.authorization?.split(" ")[1];
|
|
476
|
-
if (!token) return null;
|
|
477
|
-
try {
|
|
478
|
-
const payload = verify(token, process.env.JWT_SECRET!);
|
|
479
|
-
return { principal: payload, scopes: payload.scopes || [] };
|
|
480
|
-
} catch {
|
|
481
|
-
return null;
|
|
482
|
-
}
|
|
483
|
-
},
|
|
484
|
-
challenge(res) {
|
|
485
|
-
res.setHeader("WWW-Authenticate", 'Bearer realm="api"');
|
|
486
|
-
res.status(401).json({ error: "Invalid token" });
|
|
487
|
-
},
|
|
488
|
-
authorize(auth, requiredScopes) {
|
|
489
|
-
return requiredScopes.every(s => auth.scopes.includes(s));
|
|
490
|
-
},
|
|
491
|
-
};
|
|
249
|
+
import { registerMetalEntities } from "adorn-api/metal";
|
|
250
|
+
import { User, Post, Comment } from "./entities/index.js";
|
|
492
251
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
252
|
+
registerMetalEntities(openapi, [User, Post, Comment], {
|
|
253
|
+
mode: "read",
|
|
254
|
+
stripEntitySuffix: true,
|
|
255
|
+
includeRelations: "inline",
|
|
496
256
|
});
|
|
497
257
|
```
|
|
498
258
|
|
|
499
|
-
##
|
|
500
|
-
|
|
501
|
-
Adorn-API supports middleware at three levels: global, controller, and route.
|
|
259
|
+
## Examples
|
|
502
260
|
|
|
503
|
-
|
|
261
|
+
The repository includes several examples demonstrating different features:
|
|
504
262
|
|
|
505
|
-
|
|
263
|
+
### [Basic](examples/basic/)
|
|
264
|
+
Simple CRUD API with GET, POST endpoints and in-memory data.
|
|
506
265
|
|
|
507
|
-
```
|
|
508
|
-
|
|
509
|
-
controllers: [ControllerA, ControllerB],
|
|
510
|
-
middleware: {
|
|
511
|
-
global: [loggingMw, corsMw],
|
|
512
|
-
},
|
|
513
|
-
});
|
|
266
|
+
```bash
|
|
267
|
+
npm run example basic
|
|
514
268
|
```
|
|
515
269
|
|
|
516
|
-
###
|
|
270
|
+
### [Simple Auth](examples/simple-auth/)
|
|
271
|
+
Authentication with bearer tokens, scope-based authorization, public/protected endpoints.
|
|
517
272
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
```typescript
|
|
521
|
-
@Controller("/api")
|
|
522
|
-
@Use(controllerMw)
|
|
523
|
-
export class ApiController {
|
|
524
|
-
@Get("/")
|
|
525
|
-
async getData() { /* global, controller, then handler */ }
|
|
526
|
-
}
|
|
273
|
+
```bash
|
|
274
|
+
npm run example simple-auth
|
|
527
275
|
```
|
|
528
276
|
|
|
529
|
-
###
|
|
530
|
-
|
|
531
|
-
Use `@Use` on individual methods:
|
|
277
|
+
### [Task Manager](examples/task-manager/)
|
|
278
|
+
Complete task management API with SQLite3, filtering, tags, and statistics.
|
|
532
279
|
|
|
533
|
-
```
|
|
534
|
-
|
|
535
|
-
export class ApiController {
|
|
536
|
-
@Get("/")
|
|
537
|
-
@Use(routeMw)
|
|
538
|
-
async getData() { /* global, controller, route, then handler */ }
|
|
539
|
-
}
|
|
280
|
+
```bash
|
|
281
|
+
npm run example task-manager
|
|
540
282
|
```
|
|
541
283
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
### Named Middleware
|
|
284
|
+
### [Three Controllers](examples/three-controllers/)
|
|
285
|
+
Multiple controllers (Users, Posts, Comments) in a blog application.
|
|
545
286
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
```typescript
|
|
549
|
-
const rateLimiter = (req: any, res: any, next: any) => {
|
|
550
|
-
// rate limiting logic
|
|
551
|
-
next();
|
|
552
|
-
};
|
|
553
|
-
|
|
554
|
-
await createExpressRouter({
|
|
555
|
-
controllers: [ApiController],
|
|
556
|
-
middleware: {
|
|
557
|
-
named: { rateLimiter },
|
|
558
|
-
},
|
|
559
|
-
});
|
|
560
|
-
```
|
|
561
|
-
|
|
562
|
-
```typescript
|
|
563
|
-
@Controller("/api")
|
|
564
|
-
export class ApiController {
|
|
565
|
-
@Get("/")
|
|
566
|
-
@Use("rateLimiter")
|
|
567
|
-
async getData() { /* uses named middleware */ }
|
|
568
|
-
}
|
|
287
|
+
```bash
|
|
288
|
+
npm run example three-controllers
|
|
569
289
|
```
|
|
570
290
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
### Schema Decorators
|
|
574
|
-
|
|
575
|
-
Apply validation rules to object properties:
|
|
291
|
+
### [Blog Platform (Metal-ORM)](examples/blog-platform-metal-orm/)
|
|
292
|
+
Full-featured blog platform with Metal-ORM, relationships, and advanced queries.
|
|
576
293
|
|
|
577
|
-
```
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
class CreateUserRequest {
|
|
581
|
-
@MinLength(2)
|
|
582
|
-
@MaxLength(50)
|
|
583
|
-
name: string;
|
|
584
|
-
|
|
585
|
-
@Pattern("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")
|
|
586
|
-
email: string;
|
|
587
|
-
|
|
588
|
-
@Min(18)
|
|
589
|
-
@Max(120)
|
|
590
|
-
age: number;
|
|
591
|
-
|
|
592
|
-
@Enum(["admin", "user", "guest"])
|
|
593
|
-
role: "admin" | "user" | "guest";
|
|
594
|
-
}
|
|
294
|
+
```bash
|
|
295
|
+
npm run example blog-platform-metal-orm
|
|
595
296
|
```
|
|
596
297
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
| Decorator | Purpose |
|
|
600
|
-
|-----------|---------|
|
|
601
|
-
| `@Min(n)` | Minimum numeric value |
|
|
602
|
-
| `@Max(n)` | Maximum numeric value |
|
|
603
|
-
| `@ExclusiveMin(n)` | Exclusive minimum |
|
|
604
|
-
| `@ExclusiveMax(n)` | Exclusive maximum |
|
|
605
|
-
| `@MinLength(n)` | Minimum string/array length |
|
|
606
|
-
| `@MaxLength(n)` | Maximum string/array length |
|
|
607
|
-
| `@Pattern(regex)` | Regex pattern match |
|
|
608
|
-
| `@Format(fmt)` | Format validation (email, uuid, etc.) |
|
|
609
|
-
| `@MinItems(n)` | Minimum array items |
|
|
610
|
-
| `@MaxItems(n)` | Maximum array items |
|
|
611
|
-
| `@MinProperties(n)` | Minimum object properties |
|
|
612
|
-
| `@MaxProperties(n)` | Maximum object properties |
|
|
613
|
-
| `@MultipleOf(n)` | Numeric multiple |
|
|
614
|
-
| `@Enum([...])` | Enumeration values |
|
|
615
|
-
| `@Const(value)` | Constant value |
|
|
616
|
-
| `@Default(value)` | Default value |
|
|
617
|
-
| `@Example(value)` | Example value for docs |
|
|
618
|
-
| `@Description(text)` | Property description |
|
|
619
|
-
| `@Closed()` | No additional properties |
|
|
620
|
-
| `@ClosedUnevaluated()` | Unevaluated properties not allowed |
|
|
621
|
-
|
|
622
|
-
### Validation Modes
|
|
623
|
-
|
|
624
|
-
Control how validation runs:
|
|
625
|
-
|
|
626
|
-
**Runtime (default):**
|
|
298
|
+
### [E-commerce](examples/ecommerce/)
|
|
299
|
+
E-commerce API with RESTful and non-RESTful endpoints, carts, orders, and coupons.
|
|
627
300
|
|
|
628
301
|
```bash
|
|
629
|
-
|
|
302
|
+
npm run example ecommerce
|
|
630
303
|
```
|
|
631
304
|
|
|
632
|
-
|
|
305
|
+
### [Simple Pagination (Metal-ORM)](examples/simple-pagination-metal-orm/)
|
|
306
|
+
Pagination and sorting with Metal-ORM integration.
|
|
633
307
|
|
|
634
308
|
```bash
|
|
635
|
-
|
|
309
|
+
npm run example simple-pagination-metal-orm
|
|
636
310
|
```
|
|
637
311
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
**Disabled:**
|
|
312
|
+
### [Query JSON (Metal-ORM)](examples/query-json-metal-orm/)
|
|
313
|
+
Advanced filtering with deep object query parameters.
|
|
641
314
|
|
|
642
315
|
```bash
|
|
643
|
-
|
|
316
|
+
npm run example query-json-metal-orm
|
|
644
317
|
```
|
|
645
318
|
|
|
646
|
-
## CLI
|
|
647
|
-
|
|
648
|
-
### Build Command
|
|
319
|
+
## CLI
|
|
649
320
|
|
|
650
|
-
|
|
321
|
+
### Development
|
|
651
322
|
|
|
652
323
|
```bash
|
|
653
|
-
adorn-api
|
|
324
|
+
npx adorn-api dev
|
|
654
325
|
```
|
|
655
326
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
| Option | Description | Default |
|
|
659
|
-
|--------|-------------|---------|
|
|
660
|
-
| `-p <path>` | Path to tsconfig.json | `./tsconfig.json` |
|
|
661
|
-
| `--output <dir>` | Output directory | `.adorn` |
|
|
662
|
-
| `--if-stale` | Only rebuild if stale | `false` |
|
|
663
|
-
| `--validation-mode <mode>` | Validation mode | `ajv-runtime` |
|
|
664
|
-
|
|
665
|
-
**Validation modes:** `none`, `ajv-runtime`, `precompiled`
|
|
327
|
+
Builds artifacts and starts server with hot-reload.
|
|
666
328
|
|
|
667
|
-
###
|
|
668
|
-
|
|
669
|
-
Remove generated artifacts:
|
|
329
|
+
### Build
|
|
670
330
|
|
|
671
331
|
```bash
|
|
672
|
-
adorn-api
|
|
332
|
+
npx adorn-api build
|
|
673
333
|
```
|
|
674
334
|
|
|
675
|
-
|
|
335
|
+
Generates `.adorn/` directory with:
|
|
336
|
+
- `openapi.json` - OpenAPI 3.1 specification
|
|
337
|
+
- `manifest.json` - Runtime binding metadata
|
|
338
|
+
- `cache.json` - Build cache for incremental rebuilds
|
|
339
|
+
- `validator.js` - Precompiled validators (if enabled)
|
|
676
340
|
|
|
677
|
-
|
|
341
|
+
### Run Examples
|
|
678
342
|
|
|
679
343
|
```bash
|
|
680
|
-
|
|
681
|
-
|
|
344
|
+
# List all examples
|
|
345
|
+
npm run example:list
|
|
682
346
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
347
|
+
# Run specific example
|
|
348
|
+
npm run example basic
|
|
349
|
+
npm run example blog-platform-metal-orm
|
|
350
|
+
```
|
|
687
351
|
|
|
688
352
|
## API Reference
|
|
689
353
|
|
|
690
|
-
###
|
|
354
|
+
### Decorators
|
|
355
|
+
|
|
356
|
+
- `@Controller(path)` - Define a controller with base path
|
|
357
|
+
- `@Get(path)` - GET route handler
|
|
358
|
+
- `@Post(path)` - POST route handler
|
|
359
|
+
- `@Put(path)` - PUT route handler
|
|
360
|
+
- `@Patch(path)` - PATCH route handler
|
|
361
|
+
- `@Delete(path)` - DELETE route handler
|
|
362
|
+
- `@Use(...middleware)` - Apply middleware
|
|
363
|
+
- `@Auth(scheme, options)` - Require authentication
|
|
364
|
+
- `@Public()` - Mark route as public (bypasses auth)
|
|
691
365
|
|
|
692
|
-
|
|
366
|
+
### Exports
|
|
693
367
|
|
|
694
368
|
```typescript
|
|
695
|
-
import {
|
|
369
|
+
import {
|
|
370
|
+
Controller,
|
|
371
|
+
Get,
|
|
372
|
+
Post,
|
|
373
|
+
Put,
|
|
374
|
+
Patch,
|
|
375
|
+
Delete,
|
|
376
|
+
Use,
|
|
377
|
+
Auth,
|
|
378
|
+
Public,
|
|
379
|
+
} from "adorn-api";
|
|
380
|
+
|
|
381
|
+
import {
|
|
382
|
+
bootstrap,
|
|
383
|
+
createExpressRouter,
|
|
384
|
+
setupSwagger,
|
|
385
|
+
} from "adorn-api/express";
|
|
386
|
+
|
|
387
|
+
import {
|
|
388
|
+
ListQuery,
|
|
389
|
+
applyListQuery,
|
|
390
|
+
registerMetalEntities,
|
|
391
|
+
} from "adorn-api/metal";
|
|
392
|
+
|
|
393
|
+
import { readAdornBucket } from "adorn-api";
|
|
394
|
+
import type { AdornBucket, AuthSchemeRuntime, AuthResult } from "adorn-api";
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### Bootstrap Options
|
|
696
398
|
|
|
399
|
+
```typescript
|
|
697
400
|
await bootstrap({
|
|
698
|
-
controllers: [UserController],
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
middleware
|
|
706
|
-
|
|
707
|
-
|
|
401
|
+
controllers: [UserController, PostController],
|
|
402
|
+
auth: {
|
|
403
|
+
schemes: {
|
|
404
|
+
BearerAuth: bearerRuntime,
|
|
405
|
+
ApiKey: apiKeyRuntime,
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
middleware: {
|
|
409
|
+
global: [logger, cors],
|
|
410
|
+
named: { auth: authMiddleware },
|
|
411
|
+
},
|
|
412
|
+
port: 3000,
|
|
413
|
+
host: "0.0.0.0",
|
|
708
414
|
});
|
|
709
415
|
```
|
|
710
416
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
### createExpressRouter()
|
|
714
|
-
|
|
715
|
-
Full control over router creation:
|
|
417
|
+
### Auth Scheme
|
|
716
418
|
|
|
717
419
|
```typescript
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
artifactsDir?: string, // Default: ".adorn"
|
|
723
|
-
manifest?: ManifestV1, // Auto-loaded if not provided
|
|
724
|
-
openapi?: OpenAPI31, // Auto-loaded if not provided
|
|
725
|
-
auth?: {
|
|
726
|
-
schemes: Record<string, AuthSchemeRuntime>,
|
|
420
|
+
const authScheme: AuthSchemeRuntime = {
|
|
421
|
+
name: "MyAuth",
|
|
422
|
+
async authenticate(req: any) {
|
|
423
|
+
return { principal: user, scopes: ["read", "write"] };
|
|
727
424
|
},
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
query?: boolean,
|
|
731
|
-
path?: boolean,
|
|
732
|
-
header?: boolean,
|
|
733
|
-
cookie?: boolean,
|
|
734
|
-
dateTime?: boolean,
|
|
735
|
-
date?: boolean,
|
|
425
|
+
challenge(res: any) {
|
|
426
|
+
res.status(401).json({ error: "Unauthorized" });
|
|
736
427
|
},
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
named?: Record<string, Middleware>,
|
|
428
|
+
authorize(auth: any, requiredScopes: string[]) {
|
|
429
|
+
return requiredScopes.every(s => auth.scopes?.includes(s));
|
|
740
430
|
},
|
|
741
|
-
}
|
|
431
|
+
};
|
|
742
432
|
```
|
|
743
433
|
|
|
744
|
-
|
|
434
|
+
## Validation
|
|
745
435
|
|
|
746
|
-
|
|
436
|
+
Adorn-API supports two validation modes:
|
|
747
437
|
|
|
748
|
-
|
|
438
|
+
### Runtime Validation (AJV)
|
|
749
439
|
|
|
750
440
|
```typescript
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
jsonPath?: string, // Default: "/docs/openapi.json"
|
|
756
|
-
uiPath?: string, // Default: "/docs"
|
|
757
|
-
swaggerOptions?: {
|
|
758
|
-
servers?: [{ url: string }],
|
|
759
|
-
// Other Swagger UI options
|
|
441
|
+
await bootstrap({
|
|
442
|
+
controllers: [UserController],
|
|
443
|
+
validation: {
|
|
444
|
+
mode: "ajv-runtime",
|
|
760
445
|
},
|
|
761
|
-
})
|
|
446
|
+
});
|
|
762
447
|
```
|
|
763
448
|
|
|
764
|
-
|
|
449
|
+
### Precompiled Validators
|
|
765
450
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
| Learning curve | Low | Medium | High |
|
|
775
|
-
| Framework agnostic | Yes | Partial | No |
|
|
776
|
-
| Metal ORM integration | Yes | No | No |
|
|
451
|
+
```typescript
|
|
452
|
+
await bootstrap({
|
|
453
|
+
controllers: [UserController],
|
|
454
|
+
validation: {
|
|
455
|
+
mode: "precompiled",
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
```
|
|
777
459
|
|
|
778
|
-
|
|
460
|
+
Precompiled validators are generated at build time in `.adorn/validator.js` for better performance.
|
|
779
461
|
|
|
780
|
-
|
|
462
|
+
## Testing
|
|
781
463
|
|
|
782
|
-
|
|
464
|
+
Tests are written with Vitest and cover:
|
|
783
465
|
|
|
784
|
-
|
|
466
|
+
- Compiler schema generation
|
|
467
|
+
- Decorator metadata
|
|
468
|
+
- Express integration
|
|
469
|
+
- Middleware execution order
|
|
470
|
+
- Authentication and authorization
|
|
471
|
+
- Metal-ORM integration
|
|
785
472
|
|
|
786
|
-
|
|
473
|
+
```bash
|
|
474
|
+
npm test
|
|
475
|
+
```
|
|
787
476
|
|
|
788
|
-
|
|
477
|
+
### Test Structure
|
|
789
478
|
|
|
790
|
-
|
|
479
|
+
```
|
|
480
|
+
test/
|
|
481
|
+
├── integration/ # Express integration tests
|
|
482
|
+
├── compiler/ # Schema and manifest generation
|
|
483
|
+
├── runtime/ # Decorator metadata
|
|
484
|
+
├── middleware/ # Middleware ordering and auth
|
|
485
|
+
├── metal/ # Metal-ORM integration
|
|
486
|
+
└── fixtures/ # Test fixtures
|
|
487
|
+
```
|
|
791
488
|
|
|
792
|
-
|
|
489
|
+
## Configuration
|
|
793
490
|
|
|
794
|
-
|
|
795
|
-
import { Controller, Get } from "adorn-api";
|
|
796
|
-
import { User } from "./entities/index.js";
|
|
797
|
-
import { registerMetalEntities } from "adorn-api/metal";
|
|
491
|
+
### TypeScript Config
|
|
798
492
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
493
|
+
```json
|
|
494
|
+
{
|
|
495
|
+
"compilerOptions": {
|
|
496
|
+
"target": "ES2022",
|
|
497
|
+
"module": "ES2022",
|
|
498
|
+
"moduleResolution": "bundler",
|
|
499
|
+
"experimentalDecorators": true,
|
|
500
|
+
"emitDecoratorMetadata": true
|
|
804
501
|
}
|
|
805
502
|
}
|
|
806
|
-
|
|
807
|
-
// Auto-generate schema from entity
|
|
808
|
-
const openapi = { /* base openapi */ };
|
|
809
|
-
registerMetalEntities(openapi, [User], { stripEntitySuffix: true });
|
|
810
503
|
```
|
|
811
504
|
|
|
812
|
-
|
|
813
|
-
- `mode`: `"read"` or `"create"` (excludes auto-generated columns)
|
|
814
|
-
- `stripEntitySuffix`: Remove `_Entity` suffix from schema names
|
|
815
|
-
|
|
816
|
-
## Project Structure
|
|
505
|
+
### Vitest Config
|
|
817
506
|
|
|
507
|
+
```typescript
|
|
508
|
+
import { defineConfig } from "vitest/config";
|
|
509
|
+
|
|
510
|
+
export default defineConfig({
|
|
511
|
+
test: {
|
|
512
|
+
include: ["test/**/*.test.ts"],
|
|
513
|
+
typecheck: {
|
|
514
|
+
enabled: true,
|
|
515
|
+
tsconfig: "./tsconfig.json",
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
});
|
|
818
519
|
```
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
├── tsconfig.json
|
|
828
|
-
└── package.json
|
|
829
|
-
```
|
|
520
|
+
|
|
521
|
+
## How It Works
|
|
522
|
+
|
|
523
|
+
1. **Compile**: The CLI analyzes your TypeScript controllers and extracts metadata using the compiler API
|
|
524
|
+
2. **Generate**: OpenAPI schemas and runtime manifests are generated from type information
|
|
525
|
+
3. **Bind**: At runtime, metadata is merged with controller instances to bind routes to Express
|
|
526
|
+
4. **Validate**: Optional validation ensures requests match your TypeScript types
|
|
527
|
+
5. **Document**: Swagger UI serves interactive documentation based on generated OpenAPI spec
|
|
830
528
|
|
|
831
529
|
## License
|
|
832
530
|
|