adorn-api 1.0.10 → 1.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +318 -620
  2. package/dist/adapter/express/auth.d.ts +5 -0
  3. package/dist/adapter/express/auth.d.ts.map +1 -0
  4. package/dist/adapter/express/coercion.d.ts +22 -0
  5. package/dist/adapter/express/coercion.d.ts.map +1 -0
  6. package/dist/adapter/express/index.d.ts +3 -50
  7. package/dist/adapter/express/index.d.ts.map +1 -1
  8. package/dist/adapter/express/openapi.d.ts +11 -0
  9. package/dist/adapter/express/openapi.d.ts.map +1 -0
  10. package/dist/adapter/express/router.d.ts +4 -0
  11. package/dist/adapter/express/router.d.ts.map +1 -0
  12. package/dist/adapter/express/swagger.d.ts +4 -0
  13. package/dist/adapter/express/swagger.d.ts.map +1 -0
  14. package/dist/adapter/express/types.d.ts +64 -0
  15. package/dist/adapter/express/types.d.ts.map +1 -0
  16. package/dist/adapter/express/validation.d.ts +10 -0
  17. package/dist/adapter/express/validation.d.ts.map +1 -0
  18. package/dist/cli.cjs +330 -142
  19. package/dist/cli.cjs.map +1 -1
  20. package/dist/cli.js +329 -141
  21. package/dist/cli.js.map +1 -1
  22. package/dist/compiler/manifest/emit.d.ts.map +1 -1
  23. package/dist/compiler/manifest/format.d.ts +1 -0
  24. package/dist/compiler/manifest/format.d.ts.map +1 -1
  25. package/dist/compiler/schema/openapi.d.ts.map +1 -1
  26. package/dist/compiler/schema/typeToJsonSchema.d.ts +7 -1
  27. package/dist/compiler/schema/typeToJsonSchema.d.ts.map +1 -1
  28. package/dist/express.cjs +618 -586
  29. package/dist/express.cjs.map +1 -1
  30. package/dist/express.js +615 -583
  31. package/dist/express.js.map +1 -1
  32. package/dist/http.d.ts +11 -9
  33. package/dist/http.d.ts.map +1 -1
  34. package/dist/index.cjs +2 -10
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.ts +2 -3
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +1 -9
  39. package/dist/index.js.map +1 -1
  40. package/dist/metal/applyListQuery.d.ts +27 -0
  41. package/dist/metal/applyListQuery.d.ts.map +1 -0
  42. package/dist/metal/index.cjs +59 -0
  43. package/dist/metal/index.cjs.map +1 -1
  44. package/dist/metal/index.d.ts +4 -0
  45. package/dist/metal/index.d.ts.map +1 -1
  46. package/dist/metal/index.js +55 -0
  47. package/dist/metal/index.js.map +1 -1
  48. package/dist/metal/listQuery.d.ts +7 -0
  49. package/dist/metal/listQuery.d.ts.map +1 -0
  50. package/dist/metal/queryOptions.d.ts +8 -0
  51. package/dist/metal/queryOptions.d.ts.map +1 -0
  52. package/package.json +2 -2
  53. package/dist/compiler/analyze/extractQueryStyle.d.ts +0 -8
  54. package/dist/compiler/analyze/extractQueryStyle.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
- Build type-safe REST APIs with decorators. Get automatic OpenAPI 3.1 documentation, runtime validation, and authentication out of the box.
5
+ ## Features
6
6
 
7
- ## Quick Start
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
- Create a controller:
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 users;
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
- const user: User = {
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({ controllers: [UserController] });
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
- **Peer dependency:**
60
+ Run with:
84
61
 
85
62
  ```bash
86
- npm install express
63
+ npx adorn-api dev
87
64
  ```
88
65
 
89
- **Requirements:**
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
- A controller is a class decorated with `@Controller(path)` that groups related endpoints:
72
+ Define a controller with a base path:
98
73
 
99
74
  ```typescript
100
75
  @Controller("/api/users")
101
- export class UsersController {
102
- @Get("/")
103
- async getUsers() { /* ... */ }
104
-
105
- @Get("/:id")
106
- async getUser(id: number) { /* ... */ }
107
- }
76
+ export class UserController {}
108
77
  ```
109
78
 
110
- ### HTTP Methods
79
+ ### Route Handlers
111
80
 
112
- All standard HTTP methods are supported:
81
+ Use HTTP method decorators to define routes:
113
82
 
114
83
  ```typescript
115
- @Get("/") // GET
116
- @Post("/") // POST
117
- @Put("/:id") // PUT
118
- @Patch("/:id") // PATCH
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 getProduct(
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 createProduct(
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
- ```typescript
148
- import type { Query } from "adorn-api";
95
+ @Put("/:id")
96
+ async update(id: number, body: UpdateUserDto): Promise<User> {}
149
97
 
150
- type Filters = {
151
- status?: string;
152
- responsavelId?: number;
153
- };
98
+ @Patch("/:id")
99
+ async patch(id: number, body: Partial<User>): Promise<User> {}
154
100
 
155
- @Get("/")
156
- async list(query?: Query<Filters>) {
157
- return query;
101
+ @Delete("/:id")
102
+ async delete(id: number): Promise<void> {}
158
103
  }
159
104
  ```
160
105
 
161
- `GET /posts?status=published&responsavelId=1`
162
-
163
- ### Deep Object Query (opt-in)
106
+ ### Parameters
164
107
 
165
- For bracketed query serialization, opt in with `@QueryStyle({ style: "deepObject" })`:
108
+ Parameters are automatically extracted from your handler signature:
166
109
 
167
110
  ```typescript
168
- import { QueryStyle } from "adorn-api";
169
-
170
- type WhereFilter = {
171
- responsavel?: {
172
- perfil?: {
173
- nome?: string;
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
- - `GET /posts?where[responsavel][perfil][nome]=Admin`
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
- Minimal API with CRUD operations. See: `examples/basic/`
122
+ Apply middleware at any level:
200
123
 
201
124
  ```typescript
202
- import { Controller, Get, Post, Put, Delete } from "adorn-api";
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
- export class UserController {
206
- @Get("/")
207
- async getUsers() { return [{ id: 1, name: "Alice" }]; }
136
+ @Use(authMiddleware)
137
+ export class UserController {}
208
138
 
209
- @Get("/:id")
210
- async getUser(id: number) { /* ... */ }
139
+ // Route-level middleware
140
+ @Get("/admin")
141
+ @Use(adminMiddleware)
142
+ async adminOnly() {}
211
143
 
212
- @Post("/")
213
- async createUser(body: { name: string }) { /* ... */ }
214
-
215
- @Put("/:id")
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
- ### Authentication Example
150
+ Middleware executes in order: global → controller → route → handler.
224
151
 
225
- Bearer token auth with scopes and public endpoints. See: `examples/simple-auth/`
152
+ ### Authentication
226
153
 
227
- ```typescript
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 auth = req.headers.authorization;
266
- if (!auth?.startsWith("Bearer ")) return null;
267
- if (auth !== "Bearer valid-token") return null;
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
- await createExpressRouter({
279
- controllers: [ApiController],
280
- auth: { schemes: { BearerAuth: bearerRuntime } },
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
- Full CRUD with search and status management. See: `examples/ecommerce/`
287
-
288
- ```typescript
289
- @Controller("/products")
290
- export class ProductsController {
291
- @Get("/")
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
- @Put("/:id")
303
- async updateProduct(id: number, body: Partial<{ name: string; price: number }>) { /* ... */ }
190
+ @Get("/profile")
191
+ @Auth("BearerAuth")
192
+ async getProfile() {}
304
193
 
305
- @Delete("/:id")
306
- async deleteProduct(id: number) { return { success: true }; }
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
- ### Blog Platform with Metal ORM
322
-
323
- Database-driven API using Metal ORM entities. See: `examples/blog-platform-metal-orm/`
200
+ ### Optional Authentication
324
201
 
325
202
  ```typescript
326
- import { Controller, Get, Post, Put, Delete } from "adorn-api";
327
- import { BlogPost } from "../entities/index.js";
328
- import { getSession, selectFromEntity, eq } from "metal-orm";
329
-
330
- @Controller("/blog-posts")
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
- ### Task Manager Example
213
+ ## Metal-ORM Integration
363
214
 
364
- Multi-controller architecture with SQL database. See: `examples/task-manager/`
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 getTasks(query?: { status?: string; priority?: string; search?: string }) {
371
- let sql = "SELECT * FROM tasks WHERE 1=1";
372
- if (query?.status) sql += " AND status = ?";
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
- @Get("/with-scopes")
433
- @Auth("BearerAuth", { scopes: ["read", "write"] })
434
- async scopedEndpoint() {
435
- return { data: "secret" };
436
- }
230
+ const qb = selectFromEntity(Task)
231
+ .select("id", "title", "completed")
232
+ .where(eq(T.completed, false));
437
233
 
438
- @Get("/optional")
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
- ### @Public Decorator
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
- Mark endpoints as publicly accessible (bypasses auth):
244
+ ### Register Metal Entities
449
245
 
450
- ```typescript
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
- const jwtRuntime = {
473
- name: "JwtAuth",
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
- await createExpressRouter({
494
- controllers: [ApiController],
495
- auth: { schemes: { JwtAuth: jwtRuntime } },
252
+ registerMetalEntities(openapi, [User, Post, Comment], {
253
+ mode: "read",
254
+ stripEntitySuffix: true,
255
+ includeRelations: "inline",
496
256
  });
497
257
  ```
498
258
 
499
- ## Middleware
500
-
501
- Adorn-API supports middleware at three levels: global, controller, and route.
259
+ ## Examples
502
260
 
503
- ### Global Middleware
261
+ The repository includes several examples demonstrating different features:
504
262
 
505
- Apply middleware to all routes:
263
+ ### [Basic](examples/basic/)
264
+ Simple CRUD API with GET, POST endpoints and in-memory data.
506
265
 
507
- ```typescript
508
- await createExpressRouter({
509
- controllers: [ControllerA, ControllerB],
510
- middleware: {
511
- global: [loggingMw, corsMw],
512
- },
513
- });
266
+ ```bash
267
+ npm run example basic
514
268
  ```
515
269
 
516
- ### Controller-Level Middleware
270
+ ### [Simple Auth](examples/simple-auth/)
271
+ Authentication with bearer tokens, scope-based authorization, public/protected endpoints.
517
272
 
518
- Use `@Use` on a controller class:
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
- ### Route-Level Middleware
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
- ```typescript
534
- @Controller("/api")
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
- Middleware execution order: `global` → `controller` → `route` → `handler`
543
-
544
- ### Named Middleware
284
+ ### [Three Controllers](examples/three-controllers/)
285
+ Multiple controllers (Users, Posts, Comments) in a blog application.
545
286
 
546
- Register middleware by name for reuse:
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
- ## Schema & Validation
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
- ```typescript
578
- import { Schema, Min, Max, MinLength, MaxLength, Pattern, Enum } from "adorn-api/schema";
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
- Available decorators:
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
- adorn-api build --validation-mode ajv-runtime
302
+ npm run example ecommerce
630
303
  ```
631
304
 
632
- **Precompiled (production-optimized):**
305
+ ### [Simple Pagination (Metal-ORM)](examples/simple-pagination-metal-orm/)
306
+ Pagination and sorting with Metal-ORM integration.
633
307
 
634
308
  ```bash
635
- adorn-api build --validation-mode precompiled
309
+ npm run example simple-pagination-metal-orm
636
310
  ```
637
311
 
638
- Generates optimized validator modules for faster startup.
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
- adorn-api build --validation-mode none
316
+ npm run example query-json-metal-orm
644
317
  ```
645
318
 
646
- ## CLI Reference
647
-
648
- ### Build Command
319
+ ## CLI
649
320
 
650
- Generate OpenAPI spec and manifest from TypeScript source:
321
+ ### Development
651
322
 
652
323
  ```bash
653
- adorn-api build -p ./tsconfig.json --output .adorn
324
+ npx adorn-api dev
654
325
  ```
655
326
 
656
- **Options:**
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
- ### Clean Command
668
-
669
- Remove generated artifacts:
329
+ ### Build
670
330
 
671
331
  ```bash
672
- adorn-api clean --output .adorn
332
+ npx adorn-api build
673
333
  ```
674
334
 
675
- ### Incremental Builds
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
- Use `--if-stale` for efficient rebuilds:
341
+ ### Run Examples
678
342
 
679
343
  ```bash
680
- adorn-api build --if-stale
681
- ```
344
+ # List all examples
345
+ npm run example:list
682
346
 
683
- Only rebuilds when:
684
- - Controller source files changed
685
- - TypeScript version changed
686
- - Adorn-API version changed
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
- ### bootstrap()
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
- Quick server setup with sensible defaults:
366
+ ### Exports
693
367
 
694
368
  ```typescript
695
- import { bootstrap } from "adorn-api/express";
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
- port?: number, // Default: 3000 or PORT env
700
- host?: string, // Default: "0.0.0.0" or HOST env
701
- artifactsDir?: string, // Default: ".adorn"
702
- enableSwagger?: boolean, // Default: true
703
- swaggerPath?: string, // Default: "/docs"
704
- swaggerJsonPath?: string, // Default: "/docs/openapi.json"
705
- middleware?: CreateRouterOptions["middleware"],
706
- auth?: CreateRouterOptions["auth"],
707
- coerce?: CreateRouterOptions["coerce"],
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
- **Returns:** `{ server, app, url, port, host, close }`
712
-
713
- ### createExpressRouter()
714
-
715
- Full control over router creation:
417
+ ### Auth Scheme
716
418
 
717
419
  ```typescript
718
- import { createExpressRouter } from "adorn-api/express";
719
-
720
- const router = await createExpressRouter({
721
- controllers: [UserController],
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
- coerce?: {
729
- body?: boolean,
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
- middleware?: {
738
- global?: Middleware[],
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
- Date properties in TypeScript map to OpenAPI `type: "string"` with `format: "date-time"`. Enable `coerce` to convert ISO 8601 date-time strings into `Date` instances before your handler runs. For date-only strings, use `@Format("date")` and keep `date` coercion off to avoid timezone shifts. You can disable per-field coercion with `@Schema({ "x-adorn-coerce": false })`.
434
+ ## Validation
745
435
 
746
- ### setupSwagger()
436
+ Adorn-API supports two validation modes:
747
437
 
748
- Add Swagger UI to any Express app:
438
+ ### Runtime Validation (AJV)
749
439
 
750
440
  ```typescript
751
- import { setupSwagger } from "adorn-api/express";
752
-
753
- app.use(setupSwagger({
754
- artifactsDir?: string, // Default: ".adorn"
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
- ## Why Adorn-API?
449
+ ### Precompiled Validators
765
450
 
766
- | Feature | Adorn-API | tsoa | NestJS |
767
- |---------|-----------|------|--------|
768
- | Decorator-first | Yes | Yes | Yes |
769
- | OpenAPI 3.1 | Yes | Yes | Yes |
770
- | No transpilation | Yes | No | No |
771
- | Incremental builds | Yes | No | No |
772
- | Precompiled validators | Yes | No | No |
773
- | Stage-3 decorators | Yes | Stage 2 | Custom |
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
- ### What Makes Adorn-API Different
460
+ Precompiled validators are generated at build time in `.adorn/validator.js` for better performance.
779
461
 
780
- **Stage-3 Decorators:** Uses the official TC39 decorators proposal (ES2024), not legacy TypeScript experimental decorators. This means no build-time transformer is required—your decorators compile directly to metadata that Adorn-API reads at runtime.
462
+ ## Testing
781
463
 
782
- **No Transpilation Needed:** Unlike tsoa, Adorn-API doesn't require a custom TypeScript transformer. Controllers are regular TypeScript classes that work with native decorators.
464
+ Tests are written with Vitest and cover:
783
465
 
784
- **Incremental Builds:** The `--if-stale` flag intelligently determines when rebuilds are needed, caching TypeScript program states for fast iteration.
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
- **Precompiled Validators:** Generate optimized validation modules at build time for production deployments where startup time matters.
473
+ ```bash
474
+ npm test
475
+ ```
787
476
 
788
- **Framework Agnostic:** While Express is the primary adapter, the core is adapter-based and could support other frameworks in the future.
477
+ ### Test Structure
789
478
 
790
- ## Metal ORM Integration
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
- Adorn-API can auto-generate OpenAPI schemas from Metal ORM entities:
489
+ ## Configuration
793
490
 
794
- ```typescript
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
- @Controller("/users")
800
- export class UsersController {
801
- @Get("/")
802
- async getUsers(): Promise<User[]> {
803
- return session.find(User);
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
- Options:
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
- my-api/
820
- ├── src/
821
- │ ├── controller.ts # Your controllers
822
- │ └── server.ts # Entry point
823
- ├── .adorn/ # Generated artifacts (gitignored)
824
- │ ├── openapi.json # OpenAPI spec
825
- │ ├── manifest.json # Route manifest
826
- │ └── cache.json # Build cache
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