adorn-api 1.0.44 → 1.1.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 +800 -758
- package/dist/adapter/express/controllers.js +22 -0
- package/dist/core/errors.d.ts +36 -0
- package/dist/core/errors.js +63 -0
- package/dist/core/response.d.ts +45 -0
- package/dist/core/response.js +71 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +2 -2
- package/src/adapter/express/controllers.ts +61 -9
- package/src/core/decorators.ts +798 -766
- package/src/core/errors.ts +63 -0
- package/src/core/metadata.ts +128 -126
- package/src/core/response.ts +64 -0
- package/src/index.ts +1 -0
- package/tests/unit/error-helpers.test.ts +143 -0
- package/tests/unit/response-wrapper.test.ts +140 -0
package/README.md
CHANGED
|
@@ -1,380 +1,422 @@
|
|
|
1
|
-
# Adorn API
|
|
2
|
-
|
|
3
|
-
A modern, decorator-first web framework built on Express with built-in OpenAPI 3.1 schema generation, designed for rapid API development with excellent Type safety and developer experience.
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
- ✨ **Decorator-First API Definition**: Define controllers and DTOs with intuitive decorators
|
|
8
|
-
- 📚 **Automatic OpenAPI 3.1 Generation**: API documentation is generated from your code
|
|
9
|
-
- 🔌 **Express Integration**: Built on top of Express for familiarity and extensibility
|
|
10
|
-
- 🎯 **Type-Safe Data Transfer Objects**: Define schemas with TypeScript for compile-time checks
|
|
11
|
-
- 🔄 **DTO Composition**: Reuse and compose DTOs with PickDto, OmitDto, PartialDto, and MergeDto
|
|
1
|
+
# Adorn API
|
|
2
|
+
|
|
3
|
+
A modern, decorator-first web framework built on Express with built-in OpenAPI 3.1 schema generation, designed for rapid API development with excellent Type safety and developer experience.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✨ **Decorator-First API Definition**: Define controllers and DTOs with intuitive decorators
|
|
8
|
+
- 📚 **Automatic OpenAPI 3.1 Generation**: API documentation is generated from your code
|
|
9
|
+
- 🔌 **Express Integration**: Built on top of Express for familiarity and extensibility
|
|
10
|
+
- 🎯 **Type-Safe Data Transfer Objects**: Define schemas with TypeScript for compile-time checks
|
|
11
|
+
- 🔄 **DTO Composition**: Reuse and compose DTOs with PickDto, OmitDto, PartialDto, and MergeDto
|
|
12
12
|
- 📦 **Metal ORM Integration**: First-class support for Metal ORM with auto-generated CRUD DTOs, transformer-aware schema generation, and tree DTOs for nested set (MPTT) models
|
|
13
|
-
- 🚀 **Streaming Support**: Server-Sent Events (SSE) and streaming responses
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
@
|
|
75
|
-
@
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
@
|
|
86
|
-
@
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
import {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
console.log(`
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
@
|
|
156
|
-
@
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
- `ctx.
|
|
186
|
-
- `ctx.
|
|
187
|
-
- `ctx.
|
|
188
|
-
- `ctx.
|
|
189
|
-
- `ctx.
|
|
190
|
-
- `ctx.
|
|
191
|
-
- `ctx.
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
@
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
@
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
13
|
+
- 🚀 **Streaming Support**: Server-Sent Events (SSE) and streaming responses
|
|
14
|
+
- 🔧 **Raw Responses**: Return binary data, files, and non-JSON content with the `@Raw` decorator
|
|
15
|
+
- 📝 **Request Validation**: Automatic validation of request bodies, params, query, and headers
|
|
16
|
+
- 🔧 **Transformers**: Custom field transformations with @Transform decorator and built-in transform functions
|
|
17
|
+
- **Error Handling**: Structured error responses with error DTO support
|
|
18
|
+
- 💾 **File Uploads**: Easy handling of file uploads with multipart form data
|
|
19
|
+
- 🌐 **CORS Support**: Built-in CORS configuration
|
|
20
|
+
- 🏗️ **Lifecycle Hooks**: Application bootstrap and shutdown lifecycle events
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install adorn-api
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Note: Adorn uses Stage 3 decorator metadata (`Symbol.metadata`). If the runtime does not provide it, Adorn polyfills `Symbol.metadata` on import to keep decorator metadata consistent.
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
### 1. Define DTOs
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
// user.dtos.ts
|
|
36
|
+
import { Dto, Field, OmitDto, PickDto, t } from "adorn-api";
|
|
37
|
+
|
|
38
|
+
@Dto({ description: "User record returned by the API." })
|
|
39
|
+
export class UserDto {
|
|
40
|
+
@Field(t.uuid({ description: "User identifier." }))
|
|
41
|
+
id!: string;
|
|
42
|
+
|
|
43
|
+
@Field(t.string({ minLength: 1 }))
|
|
44
|
+
name!: string;
|
|
45
|
+
|
|
46
|
+
@Field(t.optional(t.string()))
|
|
47
|
+
nickname?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@OmitDto(UserDto, ["id"])
|
|
51
|
+
export class CreateUserDto {}
|
|
52
|
+
|
|
53
|
+
@PickDto(UserDto, ["id"])
|
|
54
|
+
export class UserParamsDto {}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 2. Create a Controller
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// user.controller.ts
|
|
61
|
+
import {
|
|
62
|
+
Body,
|
|
63
|
+
Controller,
|
|
64
|
+
Get,
|
|
65
|
+
Params,
|
|
66
|
+
Post,
|
|
67
|
+
Returns,
|
|
68
|
+
type RequestContext
|
|
69
|
+
} from "adorn-api";
|
|
70
|
+
import { CreateUserDto, UserDto, UserParamsDto } from "./user.dtos";
|
|
71
|
+
|
|
72
|
+
@Controller("/users")
|
|
73
|
+
export class UserController {
|
|
74
|
+
@Get("/:id")
|
|
75
|
+
@Params(UserParamsDto)
|
|
76
|
+
@Returns(UserDto)
|
|
77
|
+
async getOne(ctx: RequestContext<unknown, undefined, { id: string }>) {
|
|
78
|
+
return {
|
|
79
|
+
id: ctx.params.id,
|
|
80
|
+
name: "Ada Lovelace",
|
|
81
|
+
nickname: "Ada"
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@Post("/")
|
|
86
|
+
@Body(CreateUserDto)
|
|
87
|
+
@Returns({ status: 201, schema: UserDto, description: "Created" })
|
|
88
|
+
async create(ctx: RequestContext<CreateUserDto>) {
|
|
89
|
+
return {
|
|
90
|
+
id: "3f0f4d0f-1cb1-4cf1-9c32-3d4bce1b3f36",
|
|
91
|
+
name: ctx.body.name,
|
|
92
|
+
nickname: ctx.body.nickname
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 3. Bootstrap the Application
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// app.ts
|
|
102
|
+
import { createExpressApp } from "adorn-api";
|
|
103
|
+
import { UserController } from "./user.controller";
|
|
104
|
+
|
|
105
|
+
export async function createApp() {
|
|
106
|
+
return createExpressApp({
|
|
107
|
+
controllers: [UserController],
|
|
108
|
+
openApi: {
|
|
109
|
+
info: {
|
|
110
|
+
title: "Adorn API",
|
|
111
|
+
version: "1.0.0"
|
|
112
|
+
},
|
|
113
|
+
docs: true
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// index.ts
|
|
119
|
+
import { createApp } from "./app";
|
|
120
|
+
|
|
121
|
+
async function start() {
|
|
122
|
+
const app = await createApp();
|
|
123
|
+
const PORT = 3000;
|
|
124
|
+
|
|
125
|
+
app.listen(PORT, () => {
|
|
126
|
+
console.log(`Server running at http://localhost:${PORT}`);
|
|
127
|
+
console.log(`OpenAPI documentation: http://localhost:${PORT}/openapi.json`);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
start().catch(error => {
|
|
132
|
+
console.error("Failed to start server:", error);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Core Concepts
|
|
138
|
+
|
|
139
|
+
### Controllers
|
|
140
|
+
|
|
141
|
+
Controllers are classes decorated with `@Controller()` that group related API endpoints. Each controller has a base path and contains route handlers.
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
@Controller("/api/v1/users")
|
|
145
|
+
export class UserController {
|
|
146
|
+
// Routes go here
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Routes
|
|
151
|
+
|
|
152
|
+
Routes are methods decorated with HTTP verb decorators like `@Get()`, `@Post()`, `@Put()`, `@Patch()`, or `@Delete()`.
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
@Get("/:id")
|
|
156
|
+
@Params(UserParamsDto)
|
|
157
|
+
@Returns(UserDto)
|
|
158
|
+
async getOne(ctx: RequestContext) {
|
|
159
|
+
// Route handler logic
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### DTOs (Data Transfer Objects)
|
|
164
|
+
|
|
165
|
+
DTOs define the shape of data sent to and from your API. They provide validation, documentation, and type safety.
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
@Dto({ description: "User data" })
|
|
169
|
+
export class UserDto {
|
|
170
|
+
@Field(t.uuid({ description: "Unique identifier" }))
|
|
171
|
+
id!: string;
|
|
172
|
+
|
|
173
|
+
@Field(t.string({ minLength: 2, maxLength: 100 }))
|
|
174
|
+
name!: string;
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Stage 3 Decorator Metadata
|
|
179
|
+
|
|
180
|
+
Adorn relies on Stage 3 decorator metadata (`Symbol.metadata`) to connect information across decorators (DTO fields, routes, params, etc.). If the runtime does not provide it, Adorn polyfills `Symbol.metadata` on import so decorators share a consistent metadata object.
|
|
181
|
+
|
|
182
|
+
### Request Context
|
|
183
|
+
|
|
184
|
+
Each route handler receives a `RequestContext` object that provides access to:
|
|
185
|
+
- `ctx.body` - The request body (validated and typed)
|
|
186
|
+
- `ctx.params` - Route parameters
|
|
187
|
+
- `ctx.query` - Query parameters
|
|
188
|
+
- `ctx.headers` - Request headers
|
|
189
|
+
- `ctx.req` - The raw Express request
|
|
190
|
+
- `ctx.res` - The raw Express response
|
|
191
|
+
- `ctx.sse` - SSE emitter (for SSE routes)
|
|
192
|
+
- `ctx.stream` - Streaming writer (for streaming routes)
|
|
193
|
+
|
|
194
|
+
## Advanced Features
|
|
195
|
+
|
|
196
|
+
### Server-Sent Events (SSE)
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
import { Controller, Get, Sse } from "adorn-api";
|
|
200
|
+
|
|
201
|
+
@Controller("/events")
|
|
202
|
+
class EventsController {
|
|
203
|
+
@Get("/")
|
|
204
|
+
@Sse({ description: "Real-time events stream" })
|
|
205
|
+
async streamEvents(ctx: any) {
|
|
206
|
+
const emitter = ctx.sse;
|
|
207
|
+
|
|
208
|
+
let count = 0;
|
|
209
|
+
const interval = setInterval(() => {
|
|
210
|
+
count++;
|
|
211
|
+
emitter.emit("message", {
|
|
212
|
+
id: count,
|
|
213
|
+
timestamp: new Date().toISOString(),
|
|
214
|
+
message: `Event ${count}`
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
if (count >= 5) {
|
|
218
|
+
clearInterval(interval);
|
|
219
|
+
emitter.close();
|
|
220
|
+
}
|
|
221
|
+
}, 1000);
|
|
222
|
+
|
|
223
|
+
ctx.req.on("close", () => {
|
|
224
|
+
clearInterval(interval);
|
|
225
|
+
emitter.close();
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Streaming Responses
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
import { Controller, Get, Streaming } from "adorn-api";
|
|
235
|
+
|
|
236
|
+
@Controller("/streaming")
|
|
237
|
+
class StreamingController {
|
|
238
|
+
@Get("/")
|
|
239
|
+
@Streaming({ contentType: "text/plain" })
|
|
240
|
+
async streamText(ctx: any) {
|
|
241
|
+
const writer = ctx.stream;
|
|
242
|
+
const data = ["First line", "Second line", "Third line"];
|
|
243
|
+
|
|
244
|
+
for (let i = 0; i < data.length; i++) {
|
|
245
|
+
writer.writeLine(data[i]);
|
|
246
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
writer.close();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Raw Responses
|
|
255
|
+
|
|
256
|
+
Use the `@Raw()` decorator to return binary data (files, images, PDFs, etc.) without JSON serialization. The response body is sent with `res.send()` instead of `res.json()`.
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
import { Controller, Get, Raw, Params, ok, type RequestContext } from "adorn-api";
|
|
260
|
+
import fs from "fs/promises";
|
|
261
|
+
|
|
262
|
+
@Controller("/files")
|
|
263
|
+
class FileController {
|
|
264
|
+
@Get("/report.pdf")
|
|
265
|
+
@Raw({ contentType: "application/pdf", description: "Download PDF report" })
|
|
266
|
+
async downloadPdf(ctx: RequestContext) {
|
|
267
|
+
const buffer = await fs.readFile("report.pdf");
|
|
268
|
+
return ok(buffer);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
@Get("/avatar/:id")
|
|
272
|
+
@Raw({ contentType: "image/png" })
|
|
273
|
+
async getAvatar(ctx: RequestContext) {
|
|
274
|
+
const image = await fs.readFile(`avatars/${ctx.params.id}.png`);
|
|
275
|
+
return image;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
You can also set custom headers (e.g. `Content-Disposition`) via `HttpResponse`:
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
import { HttpResponse } from "adorn-api";
|
|
284
|
+
|
|
285
|
+
@Get("/download/:filename")
|
|
286
|
+
@Raw({ contentType: "application/octet-stream" })
|
|
287
|
+
async download(ctx: RequestContext) {
|
|
288
|
+
const buffer = await fs.readFile(`uploads/${ctx.params.filename}`);
|
|
289
|
+
return new HttpResponse(200, buffer, {
|
|
290
|
+
"Content-Disposition": `attachment; filename="${ctx.params.filename}"`
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### File Uploads
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
import { Controller, Post, UploadedFile, Returns, t } from "adorn-api";
|
|
299
|
+
|
|
300
|
+
@Controller("/uploads")
|
|
301
|
+
class UploadController {
|
|
302
|
+
@Post("/")
|
|
303
|
+
@UploadedFile("file", t.file({ accept: ["image/*"], maxSize: 5 * 1024 * 1024 }))
|
|
304
|
+
@Returns({ status: 200, schema: t.string() })
|
|
305
|
+
async uploadFile(ctx: any) {
|
|
306
|
+
const file = ctx.files?.file[0];
|
|
307
|
+
return `File uploaded: ${file.originalname}`;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
270
312
|
## Metal ORM Integration
|
|
271
|
-
|
|
272
|
-
Adorn API has first-class support for Metal ORM, providing automatic CRUD DTO generation.
|
|
273
|
-
Transformer decorators such as `@Email`, `@Length`, `@Pattern`, and `@Alphanumeric` are reflected in the generated DTO schemas (validation + OpenAPI).
|
|
274
|
-
|
|
275
|
-
### 1. Define Entities
|
|
276
|
-
|
|
277
|
-
```typescript
|
|
278
|
-
// user.entity.ts
|
|
279
|
-
import { Entity, PrimaryKey, Property } from "metal-orm";
|
|
280
|
-
|
|
281
|
-
@Entity("users")
|
|
282
|
-
export class User {
|
|
283
|
-
@PrimaryKey()
|
|
284
|
-
id!: number;
|
|
285
|
-
|
|
286
|
-
@Property()
|
|
287
|
-
name!: string;
|
|
288
|
-
|
|
289
|
-
@Property({ nullable: true })
|
|
290
|
-
nickname?: string;
|
|
291
|
-
}
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
### 2. Generate CRUD DTOs
|
|
295
|
-
|
|
296
|
-
```typescript
|
|
297
|
-
// user.dtos.ts
|
|
298
|
-
import { createMetalCrudDtoClasses } from "adorn-api";
|
|
299
|
-
import { User } from "./user.entity";
|
|
300
|
-
|
|
301
|
-
export const {
|
|
302
|
-
GetUserDto,
|
|
303
|
-
CreateUserDto,
|
|
304
|
-
UpdateUserDto,
|
|
305
|
-
ReplaceUserDto,
|
|
306
|
-
UserQueryDto,
|
|
307
|
-
UserPagedResponseDto
|
|
308
|
-
} = createMetalCrudDtoClasses(User);
|
|
309
|
-
```
|
|
310
|
-
|
|
313
|
+
|
|
314
|
+
Adorn API has first-class support for Metal ORM, providing automatic CRUD DTO generation.
|
|
315
|
+
Transformer decorators such as `@Email`, `@Length`, `@Pattern`, and `@Alphanumeric` are reflected in the generated DTO schemas (validation + OpenAPI).
|
|
316
|
+
|
|
317
|
+
### 1. Define Entities
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
// user.entity.ts
|
|
321
|
+
import { Entity, PrimaryKey, Property } from "metal-orm";
|
|
322
|
+
|
|
323
|
+
@Entity("users")
|
|
324
|
+
export class User {
|
|
325
|
+
@PrimaryKey()
|
|
326
|
+
id!: number;
|
|
327
|
+
|
|
328
|
+
@Property()
|
|
329
|
+
name!: string;
|
|
330
|
+
|
|
331
|
+
@Property({ nullable: true })
|
|
332
|
+
nickname?: string;
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### 2. Generate CRUD DTOs
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
// user.dtos.ts
|
|
340
|
+
import { createMetalCrudDtoClasses } from "adorn-api";
|
|
341
|
+
import { User } from "./user.entity";
|
|
342
|
+
|
|
343
|
+
export const {
|
|
344
|
+
GetUserDto,
|
|
345
|
+
CreateUserDto,
|
|
346
|
+
UpdateUserDto,
|
|
347
|
+
ReplaceUserDto,
|
|
348
|
+
UserQueryDto,
|
|
349
|
+
UserPagedResponseDto
|
|
350
|
+
} = createMetalCrudDtoClasses(User);
|
|
351
|
+
```
|
|
352
|
+
|
|
311
353
|
### 3. Create a CRUD Controller
|
|
312
354
|
|
|
313
355
|
```typescript
|
|
314
356
|
// user.controller.ts
|
|
315
|
-
import {
|
|
316
|
-
Controller,
|
|
317
|
-
Get,
|
|
318
|
-
Post,
|
|
319
|
-
Put,
|
|
320
|
-
Patch,
|
|
321
|
-
Delete,
|
|
322
|
-
Params,
|
|
323
|
-
Body,
|
|
324
|
-
Query,
|
|
325
|
-
Returns,
|
|
326
|
-
parsePagination,
|
|
327
|
-
type RequestContext
|
|
328
|
-
} from "adorn-api";
|
|
329
|
-
import { applyFilter, toPagedResponse } from "metal-orm";
|
|
330
|
-
import { createSession } from "./db";
|
|
331
|
-
import { User } from "./user.entity";
|
|
332
|
-
import {
|
|
333
|
-
GetUserDto,
|
|
334
|
-
CreateUserDto,
|
|
335
|
-
UpdateUserDto,
|
|
336
|
-
ReplaceUserDto,
|
|
337
|
-
UserQueryDto,
|
|
338
|
-
UserPagedResponseDto
|
|
339
|
-
} from "./user.dtos";
|
|
340
|
-
|
|
341
|
-
@Controller("/users")
|
|
342
|
-
export class UserController {
|
|
343
|
-
@Get("/")
|
|
344
|
-
@Query(UserQueryDto)
|
|
345
|
-
@Returns(UserPagedResponseDto)
|
|
346
|
-
async list(ctx: RequestContext<unknown, UserQueryDto>) {
|
|
347
|
-
const { page, pageSize } = parsePagination(ctx.query);
|
|
348
|
-
const session = createSession();
|
|
349
|
-
|
|
350
|
-
try {
|
|
351
|
-
const query = applyFilter(
|
|
352
|
-
User.select().orderBy(User.id, "ASC"),
|
|
353
|
-
User,
|
|
354
|
-
ctx.query
|
|
355
|
-
);
|
|
356
|
-
|
|
357
|
-
const paged = await query.executePaged(session, { page, pageSize });
|
|
358
|
-
return toPagedResponse(paged);
|
|
359
|
-
} finally {
|
|
360
|
-
await session.dispose();
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
@Get("/:id")
|
|
365
|
-
@Params({ id: t.integer() })
|
|
366
|
-
@Returns(GetUserDto)
|
|
367
|
-
async getOne(ctx: RequestContext<unknown, undefined, { id: string }>) {
|
|
368
|
-
const session = createSession();
|
|
369
|
-
|
|
370
|
-
try {
|
|
371
|
-
const user = await session.find(User, parseInt(ctx.params.id));
|
|
372
|
-
return user;
|
|
373
|
-
} finally {
|
|
374
|
-
await session.dispose();
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
357
|
+
import {
|
|
358
|
+
Controller,
|
|
359
|
+
Get,
|
|
360
|
+
Post,
|
|
361
|
+
Put,
|
|
362
|
+
Patch,
|
|
363
|
+
Delete,
|
|
364
|
+
Params,
|
|
365
|
+
Body,
|
|
366
|
+
Query,
|
|
367
|
+
Returns,
|
|
368
|
+
parsePagination,
|
|
369
|
+
type RequestContext
|
|
370
|
+
} from "adorn-api";
|
|
371
|
+
import { applyFilter, toPagedResponse } from "metal-orm";
|
|
372
|
+
import { createSession } from "./db";
|
|
373
|
+
import { User } from "./user.entity";
|
|
374
|
+
import {
|
|
375
|
+
GetUserDto,
|
|
376
|
+
CreateUserDto,
|
|
377
|
+
UpdateUserDto,
|
|
378
|
+
ReplaceUserDto,
|
|
379
|
+
UserQueryDto,
|
|
380
|
+
UserPagedResponseDto
|
|
381
|
+
} from "./user.dtos";
|
|
382
|
+
|
|
383
|
+
@Controller("/users")
|
|
384
|
+
export class UserController {
|
|
385
|
+
@Get("/")
|
|
386
|
+
@Query(UserQueryDto)
|
|
387
|
+
@Returns(UserPagedResponseDto)
|
|
388
|
+
async list(ctx: RequestContext<unknown, UserQueryDto>) {
|
|
389
|
+
const { page, pageSize } = parsePagination(ctx.query);
|
|
390
|
+
const session = createSession();
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
const query = applyFilter(
|
|
394
|
+
User.select().orderBy(User.id, "ASC"),
|
|
395
|
+
User,
|
|
396
|
+
ctx.query
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
const paged = await query.executePaged(session, { page, pageSize });
|
|
400
|
+
return toPagedResponse(paged);
|
|
401
|
+
} finally {
|
|
402
|
+
await session.dispose();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
@Get("/:id")
|
|
407
|
+
@Params({ id: t.integer() })
|
|
408
|
+
@Returns(GetUserDto)
|
|
409
|
+
async getOne(ctx: RequestContext<unknown, undefined, { id: string }>) {
|
|
410
|
+
const session = createSession();
|
|
411
|
+
|
|
412
|
+
try {
|
|
413
|
+
const user = await session.find(User, parseInt(ctx.params.id));
|
|
414
|
+
return user;
|
|
415
|
+
} finally {
|
|
416
|
+
await session.dispose();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
378
420
|
// Other CRUD operations...
|
|
379
421
|
}
|
|
380
422
|
```
|
|
@@ -523,395 +565,395 @@ class CategoryController {
|
|
|
523
565
|
}
|
|
524
566
|
}
|
|
525
567
|
```
|
|
526
|
-
|
|
527
|
-
## Configuration
|
|
528
|
-
|
|
529
|
-
### Express App Options
|
|
530
|
-
|
|
531
|
-
```typescript
|
|
532
|
-
createExpressApp({
|
|
533
|
-
// Required
|
|
534
|
-
controllers: [UserController],
|
|
535
|
-
|
|
536
|
-
// Optional
|
|
537
|
-
cors: true, // Enable CORS with default options or configure
|
|
538
|
-
jsonBody: true, // Parse JSON bodies (default: true)
|
|
539
|
-
inputCoercion: "safe", // Input coercion mode ("safe" or "strict")
|
|
540
|
-
validation: { // Validation configuration
|
|
541
|
-
enabled: true, // Enable validation (default: true)
|
|
542
|
-
mode: "strict" // Validation mode: "strict" or "safe"
|
|
543
|
-
},
|
|
544
|
-
multipart: { // File upload configuration
|
|
545
|
-
dest: "./uploads",
|
|
546
|
-
limits: { fileSize: 50 * 1024 * 1024 }
|
|
547
|
-
},
|
|
548
|
-
openApi: {
|
|
549
|
-
info: {
|
|
550
|
-
title: "My API",
|
|
551
|
-
version: "1.0.0",
|
|
552
|
-
description: "API documentation"
|
|
553
|
-
},
|
|
554
|
-
path: "/openapi.json", // OpenAPI schema endpoint
|
|
555
|
-
docs: true // Serve Swagger UI
|
|
556
|
-
}
|
|
557
|
-
});
|
|
558
|
-
```
|
|
559
|
-
|
|
560
|
-
## Schema Types
|
|
561
|
-
|
|
562
|
-
The `t` object provides a rich set of schema types:
|
|
563
|
-
|
|
564
|
-
- Primitives: `t.string()`, `t.number()`, `t.integer()`, `t.boolean()`
|
|
565
|
-
- Formats: `t.uuid()`, `t.dateTime()`
|
|
566
|
-
- Complex: `t.array()`, `t.object()`, `t.record()`
|
|
567
|
-
- Combinators: `t.union()`, `t.enum()`, `t.literal()`
|
|
568
|
-
- Special: `t.ref()`, `t.any()`, `t.null()`, `t.file()`
|
|
569
|
-
- Modifiers: `t.optional()`, `t.nullable()`
|
|
570
|
-
|
|
571
|
-
## DTO Composition
|
|
572
|
-
|
|
573
|
-
Reuse and compose DTOs with these decorators:
|
|
574
|
-
|
|
575
|
-
```typescript
|
|
576
|
-
// Pick specific fields from an existing DTO
|
|
577
|
-
@PickDto(UserDto, ["id", "name"])
|
|
578
|
-
export class UserSummaryDto {}
|
|
579
|
-
|
|
580
|
-
// Omit specific fields from an existing DTO
|
|
581
|
-
@OmitDto(UserDto, ["password"])
|
|
582
|
-
export class PublicUserDto {}
|
|
583
|
-
|
|
584
|
-
// Make all fields optional
|
|
585
|
-
@PartialDto(UserDto)
|
|
586
|
-
export class UpdateUserDto {}
|
|
587
|
-
|
|
588
|
-
// Merge multiple DTOs
|
|
589
|
-
@MergeDto([UserDto, AddressDto])
|
|
590
|
-
export class UserWithAddressDto {}
|
|
591
|
-
```
|
|
592
|
-
|
|
593
|
-
## Error Handling
|
|
594
|
-
|
|
595
|
-
Define structured error responses:
|
|
596
|
-
|
|
597
|
-
```typescript
|
|
598
|
-
import { Controller, Get, ReturnsError, t } from "adorn-api";
|
|
599
|
-
|
|
600
|
-
@Controller("/")
|
|
601
|
-
class ErrorController {
|
|
602
|
-
@Get("/error")
|
|
603
|
-
@ReturnsError({
|
|
604
|
-
status: 404,
|
|
605
|
-
schema: t.object({
|
|
606
|
-
code: t.string(),
|
|
607
|
-
message: t.string(),
|
|
608
|
-
details: t.optional(t.record(t.any()))
|
|
609
|
-
}),
|
|
610
|
-
description: "Resource not found"
|
|
611
|
-
})
|
|
612
|
-
async notFound() {
|
|
613
|
-
throw new HttpError(404, "Resource not found", { code: "NOT_FOUND" });
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
```
|
|
617
|
-
|
|
618
|
-
## Lifecycle Hooks
|
|
619
|
-
|
|
620
|
-
```typescript
|
|
621
|
-
import {
|
|
622
|
-
OnApplicationBootstrap,
|
|
623
|
-
OnShutdown
|
|
624
|
-
} from "adorn-api";
|
|
625
|
-
|
|
626
|
-
class DatabaseService implements OnApplicationBootstrap, OnShutdown {
|
|
627
|
-
async onApplicationBootstrap() {
|
|
628
|
-
console.log("Connecting to database...");
|
|
629
|
-
// Initialize database connection
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
async onShutdown(signal?: string) {
|
|
633
|
-
console.log(`Shutting down (${signal})...`);
|
|
634
|
-
// Cleanup resources
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
// Register the service
|
|
639
|
-
import { lifecycleRegistry } from "adorn-api";
|
|
640
|
-
lifecycleRegistry.register(new DatabaseService());
|
|
641
|
-
```
|
|
642
|
-
|
|
643
|
-
## Validation
|
|
644
|
-
|
|
645
|
-
Adorn API provides automatic request validation and a comprehensive validation system for your DTOs and schemas.
|
|
646
|
-
|
|
647
|
-
### Validation Configuration
|
|
648
|
-
|
|
649
|
-
```typescript
|
|
650
|
-
createExpressApp({
|
|
651
|
-
controllers: [UserController],
|
|
652
|
-
validation: {
|
|
653
|
-
enabled: true, // Enable validation (default: true)
|
|
654
|
-
mode: "strict" // Validation mode: "strict" or "safe"
|
|
655
|
-
}
|
|
656
|
-
});
|
|
657
|
-
```
|
|
658
|
-
|
|
659
|
-
### Validation Errors
|
|
660
|
-
|
|
661
|
-
Invalid requests automatically return structured validation errors:
|
|
662
|
-
|
|
663
|
-
```typescript
|
|
664
|
-
// Example error response
|
|
665
|
-
{
|
|
666
|
-
"statusCode": 400,
|
|
667
|
-
"message": "Validation failed",
|
|
668
|
-
"errors": [
|
|
669
|
-
{
|
|
670
|
-
"field": "name",
|
|
671
|
-
"message": "must be at least 1 character long",
|
|
672
|
-
"value": "",
|
|
673
|
-
"code": "STRING_MIN_LENGTH"
|
|
674
|
-
},
|
|
675
|
-
{
|
|
676
|
-
"field": "email",
|
|
677
|
-
"message": "must be a valid email",
|
|
678
|
-
"value": "invalid-email",
|
|
679
|
-
"code": "FORMAT_EMAIL"
|
|
680
|
-
}
|
|
681
|
-
]
|
|
682
|
-
}
|
|
683
|
-
```
|
|
684
|
-
|
|
685
|
-
### Validation Error Codes
|
|
686
|
-
|
|
687
|
-
Adorn API provides machine-readable error codes for programmatic error handling:
|
|
688
|
-
|
|
689
|
-
```typescript
|
|
690
|
-
import { ValidationErrorCode } from "adorn-api";
|
|
691
|
-
|
|
692
|
-
console.log(ValidationErrorCode.FORMAT_EMAIL); // "FORMAT_EMAIL"
|
|
693
|
-
console.log(ValidationErrorCode.STRING_MIN_LENGTH); // "STRING_MIN_LENGTH"
|
|
694
|
-
```
|
|
695
|
-
|
|
696
|
-
### Manual Validation
|
|
697
|
-
|
|
698
|
-
You can also manually validate data using the `validate` function:
|
|
699
|
-
|
|
700
|
-
```typescript
|
|
701
|
-
import { validate, ValidationErrors, t } from "adorn-api";
|
|
702
|
-
|
|
703
|
-
const data = { name: "", email: "invalid" };
|
|
704
|
-
const errors = validate(data, t.object({
|
|
705
|
-
name: t.string({ minLength: 1 }),
|
|
706
|
-
email: t.string({ format: "email" })
|
|
707
|
-
}));
|
|
708
|
-
|
|
709
|
-
if (errors.length > 0) {
|
|
710
|
-
throw new ValidationErrors(errors);
|
|
711
|
-
}
|
|
712
|
-
```
|
|
713
|
-
|
|
714
|
-
## Transformers
|
|
715
|
-
|
|
716
|
-
Transform fields during serialization with custom transform functions or built-in transform utilities.
|
|
717
|
-
|
|
718
|
-
### Basic Transform
|
|
719
|
-
|
|
720
|
-
```typescript
|
|
721
|
-
import { Dto, Field, Transform, t } from "adorn-api";
|
|
722
|
-
|
|
723
|
-
@Dto()
|
|
724
|
-
export class UserDto {
|
|
725
|
-
@Field(t.string())
|
|
726
|
-
@Transform((value) => value.toUpperCase())
|
|
727
|
-
name!: string;
|
|
728
|
-
|
|
729
|
-
@Field(t.dateTime())
|
|
730
|
-
@Transform((value) => value.toISOString())
|
|
731
|
-
createdAt!: Date;
|
|
732
|
-
}
|
|
733
|
-
```
|
|
734
|
-
|
|
735
|
-
### Built-in Transforms
|
|
736
|
-
|
|
737
|
-
Adorn API includes common transform functions:
|
|
738
|
-
|
|
739
|
-
```typescript
|
|
740
|
-
import { Dto, Field, Transform, Transforms, t } from "adorn-api";
|
|
741
|
-
|
|
742
|
-
@Dto()
|
|
743
|
-
export class UserDto {
|
|
744
|
-
@Field(t.string())
|
|
745
|
-
@Transform(Transforms.toLowerCase)
|
|
746
|
-
email!: string;
|
|
747
|
-
|
|
748
|
-
@Field(t.number())
|
|
749
|
-
@Transform(Transforms.round(2))
|
|
750
|
-
price!: number;
|
|
751
|
-
|
|
752
|
-
@Field(t.string())
|
|
753
|
-
@Transform(Transforms.mask(4)) // Mask all but last 4 characters
|
|
754
|
-
creditCard!: string;
|
|
755
|
-
|
|
756
|
-
@Field(t.dateTime())
|
|
757
|
-
@Transform(Transforms.toISOString)
|
|
758
|
-
birthDate!: Date;
|
|
759
|
-
}
|
|
760
|
-
```
|
|
761
|
-
|
|
762
|
-
### Conditional Transforms with Groups
|
|
763
|
-
|
|
764
|
-
Apply transforms only to specific serialization groups:
|
|
765
|
-
|
|
766
|
-
```typescript
|
|
767
|
-
import { Dto, Field, Transform, Expose, t } from "adorn-api";
|
|
768
|
-
|
|
769
|
-
@Dto()
|
|
770
|
-
export class UserDto {
|
|
771
|
-
@Field(t.string())
|
|
772
|
-
name!: string;
|
|
773
|
-
|
|
774
|
-
@Field(t.string())
|
|
775
|
-
@Expose({ groups: ["admin"] })
|
|
776
|
-
@Transform((value) => Transforms.mask(2), { groups: ["admin"] })
|
|
777
|
-
phoneNumber!: string;
|
|
778
|
-
|
|
779
|
-
@Field(t.string())
|
|
780
|
-
@Expose({ groups: ["internal"] })
|
|
781
|
-
@Transform((value) => "[REDACTED]", { groups: ["external"] })
|
|
782
|
-
internalNote!: string;
|
|
783
|
-
}
|
|
784
|
-
```
|
|
785
|
-
|
|
786
|
-
### Custom Transform Functions
|
|
787
|
-
|
|
788
|
-
Create custom transform functions:
|
|
789
|
-
|
|
790
|
-
```typescript
|
|
791
|
-
import { Dto, Field, Transform, t } from "adorn-api";
|
|
792
|
-
|
|
793
|
-
const toCurrency = (value: number, currency: string = "USD") => {
|
|
794
|
-
return new Intl.NumberFormat("en-US", {
|
|
795
|
-
style: "currency",
|
|
796
|
-
currency
|
|
797
|
-
}).format(value);
|
|
798
|
-
};
|
|
799
|
-
|
|
800
|
-
@Dto()
|
|
801
|
-
export class ProductDto {
|
|
802
|
-
@Field(t.string())
|
|
803
|
-
name!: string;
|
|
804
|
-
|
|
805
|
-
@Field(t.number())
|
|
806
|
-
@Transform(toCurrency)
|
|
807
|
-
price!: number;
|
|
808
|
-
|
|
809
|
-
@Field(t.number())
|
|
810
|
-
@Transform((value) => toCurrency(value, "EUR"))
|
|
811
|
-
priceEUR!: number;
|
|
812
|
-
}
|
|
813
|
-
```
|
|
814
|
-
|
|
815
|
-
### Serialization with Options
|
|
816
|
-
|
|
817
|
-
Control serialization with custom options:
|
|
818
|
-
|
|
819
|
-
```typescript
|
|
820
|
-
import { serialize, createSerializer } from "adorn-api";
|
|
821
|
-
import { UserDto } from "./user.dtos";
|
|
822
|
-
|
|
823
|
-
const user = new UserDto();
|
|
824
|
-
user.name = "John Doe";
|
|
825
|
-
user.phoneNumber = "123-456-7890";
|
|
826
|
-
user.internalNote = "This is an internal note";
|
|
827
|
-
|
|
828
|
-
// Basic serialization
|
|
829
|
-
const basic = serialize(user);
|
|
830
|
-
// Output: { name: "John Doe" }
|
|
831
|
-
|
|
832
|
-
// Admin group serialization
|
|
833
|
-
const admin = serialize(user, { groups: ["admin"] });
|
|
834
|
-
// Output: { name: "John Doe", phoneNumber: "********90" }
|
|
835
|
-
|
|
836
|
-
// External group serialization
|
|
837
|
-
const external = serialize(user, { groups: ["external"] });
|
|
838
|
-
// Output: { name: "John Doe", internalNote: "[REDACTED]" }
|
|
839
|
-
|
|
840
|
-
// Create a preset serializer
|
|
841
|
-
const adminSerializer = createSerializer({ groups: ["admin"] });
|
|
842
|
-
const adminData = adminSerializer(user);
|
|
843
|
-
```
|
|
844
|
-
|
|
845
|
-
### Exclude Fields
|
|
846
|
-
|
|
847
|
-
Control which fields are excluded or exposed:
|
|
848
|
-
|
|
849
|
-
```typescript
|
|
850
|
-
import { Dto, Field, Exclude, Expose, t } from "adorn-api";
|
|
851
|
-
|
|
852
|
-
@Dto()
|
|
853
|
-
export class UserDto {
|
|
854
|
-
@Field(t.string())
|
|
855
|
-
name!: string;
|
|
856
|
-
|
|
857
|
-
@Field(t.string())
|
|
858
|
-
@Exclude() // Always exclude from serialization
|
|
859
|
-
password!: string;
|
|
860
|
-
|
|
861
|
-
@Field(t.string())
|
|
862
|
-
@Expose({ name: "email_address" }) // Rename field in output
|
|
863
|
-
email!: string;
|
|
864
|
-
|
|
865
|
-
@Field(t.string())
|
|
866
|
-
@Exclude({ groups: ["public"] }) // Exclude from public group
|
|
867
|
-
internalComment!: string;
|
|
868
|
-
}
|
|
869
|
-
```
|
|
870
|
-
|
|
568
|
+
|
|
569
|
+
## Configuration
|
|
570
|
+
|
|
571
|
+
### Express App Options
|
|
572
|
+
|
|
573
|
+
```typescript
|
|
574
|
+
createExpressApp({
|
|
575
|
+
// Required
|
|
576
|
+
controllers: [UserController],
|
|
577
|
+
|
|
578
|
+
// Optional
|
|
579
|
+
cors: true, // Enable CORS with default options or configure
|
|
580
|
+
jsonBody: true, // Parse JSON bodies (default: true)
|
|
581
|
+
inputCoercion: "safe", // Input coercion mode ("safe" or "strict")
|
|
582
|
+
validation: { // Validation configuration
|
|
583
|
+
enabled: true, // Enable validation (default: true)
|
|
584
|
+
mode: "strict" // Validation mode: "strict" or "safe"
|
|
585
|
+
},
|
|
586
|
+
multipart: { // File upload configuration
|
|
587
|
+
dest: "./uploads",
|
|
588
|
+
limits: { fileSize: 50 * 1024 * 1024 }
|
|
589
|
+
},
|
|
590
|
+
openApi: {
|
|
591
|
+
info: {
|
|
592
|
+
title: "My API",
|
|
593
|
+
version: "1.0.0",
|
|
594
|
+
description: "API documentation"
|
|
595
|
+
},
|
|
596
|
+
path: "/openapi.json", // OpenAPI schema endpoint
|
|
597
|
+
docs: true // Serve Swagger UI
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
## Schema Types
|
|
603
|
+
|
|
604
|
+
The `t` object provides a rich set of schema types:
|
|
605
|
+
|
|
606
|
+
- Primitives: `t.string()`, `t.number()`, `t.integer()`, `t.boolean()`
|
|
607
|
+
- Formats: `t.uuid()`, `t.dateTime()`
|
|
608
|
+
- Complex: `t.array()`, `t.object()`, `t.record()`
|
|
609
|
+
- Combinators: `t.union()`, `t.enum()`, `t.literal()`
|
|
610
|
+
- Special: `t.ref()`, `t.any()`, `t.null()`, `t.file()`
|
|
611
|
+
- Modifiers: `t.optional()`, `t.nullable()`
|
|
612
|
+
|
|
613
|
+
## DTO Composition
|
|
614
|
+
|
|
615
|
+
Reuse and compose DTOs with these decorators:
|
|
616
|
+
|
|
617
|
+
```typescript
|
|
618
|
+
// Pick specific fields from an existing DTO
|
|
619
|
+
@PickDto(UserDto, ["id", "name"])
|
|
620
|
+
export class UserSummaryDto {}
|
|
621
|
+
|
|
622
|
+
// Omit specific fields from an existing DTO
|
|
623
|
+
@OmitDto(UserDto, ["password"])
|
|
624
|
+
export class PublicUserDto {}
|
|
625
|
+
|
|
626
|
+
// Make all fields optional
|
|
627
|
+
@PartialDto(UserDto)
|
|
628
|
+
export class UpdateUserDto {}
|
|
629
|
+
|
|
630
|
+
// Merge multiple DTOs
|
|
631
|
+
@MergeDto([UserDto, AddressDto])
|
|
632
|
+
export class UserWithAddressDto {}
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
## Error Handling
|
|
636
|
+
|
|
637
|
+
Define structured error responses:
|
|
638
|
+
|
|
639
|
+
```typescript
|
|
640
|
+
import { Controller, Get, ReturnsError, t } from "adorn-api";
|
|
641
|
+
|
|
642
|
+
@Controller("/")
|
|
643
|
+
class ErrorController {
|
|
644
|
+
@Get("/error")
|
|
645
|
+
@ReturnsError({
|
|
646
|
+
status: 404,
|
|
647
|
+
schema: t.object({
|
|
648
|
+
code: t.string(),
|
|
649
|
+
message: t.string(),
|
|
650
|
+
details: t.optional(t.record(t.any()))
|
|
651
|
+
}),
|
|
652
|
+
description: "Resource not found"
|
|
653
|
+
})
|
|
654
|
+
async notFound() {
|
|
655
|
+
throw new HttpError(404, "Resource not found", { code: "NOT_FOUND" });
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
## Lifecycle Hooks
|
|
661
|
+
|
|
662
|
+
```typescript
|
|
663
|
+
import {
|
|
664
|
+
OnApplicationBootstrap,
|
|
665
|
+
OnShutdown
|
|
666
|
+
} from "adorn-api";
|
|
667
|
+
|
|
668
|
+
class DatabaseService implements OnApplicationBootstrap, OnShutdown {
|
|
669
|
+
async onApplicationBootstrap() {
|
|
670
|
+
console.log("Connecting to database...");
|
|
671
|
+
// Initialize database connection
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
async onShutdown(signal?: string) {
|
|
675
|
+
console.log(`Shutting down (${signal})...`);
|
|
676
|
+
// Cleanup resources
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Register the service
|
|
681
|
+
import { lifecycleRegistry } from "adorn-api";
|
|
682
|
+
lifecycleRegistry.register(new DatabaseService());
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
## Validation
|
|
686
|
+
|
|
687
|
+
Adorn API provides automatic request validation and a comprehensive validation system for your DTOs and schemas.
|
|
688
|
+
|
|
689
|
+
### Validation Configuration
|
|
690
|
+
|
|
691
|
+
```typescript
|
|
692
|
+
createExpressApp({
|
|
693
|
+
controllers: [UserController],
|
|
694
|
+
validation: {
|
|
695
|
+
enabled: true, // Enable validation (default: true)
|
|
696
|
+
mode: "strict" // Validation mode: "strict" or "safe"
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
### Validation Errors
|
|
702
|
+
|
|
703
|
+
Invalid requests automatically return structured validation errors:
|
|
704
|
+
|
|
705
|
+
```typescript
|
|
706
|
+
// Example error response
|
|
707
|
+
{
|
|
708
|
+
"statusCode": 400,
|
|
709
|
+
"message": "Validation failed",
|
|
710
|
+
"errors": [
|
|
711
|
+
{
|
|
712
|
+
"field": "name",
|
|
713
|
+
"message": "must be at least 1 character long",
|
|
714
|
+
"value": "",
|
|
715
|
+
"code": "STRING_MIN_LENGTH"
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
"field": "email",
|
|
719
|
+
"message": "must be a valid email",
|
|
720
|
+
"value": "invalid-email",
|
|
721
|
+
"code": "FORMAT_EMAIL"
|
|
722
|
+
}
|
|
723
|
+
]
|
|
724
|
+
}
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
### Validation Error Codes
|
|
728
|
+
|
|
729
|
+
Adorn API provides machine-readable error codes for programmatic error handling:
|
|
730
|
+
|
|
731
|
+
```typescript
|
|
732
|
+
import { ValidationErrorCode } from "adorn-api";
|
|
733
|
+
|
|
734
|
+
console.log(ValidationErrorCode.FORMAT_EMAIL); // "FORMAT_EMAIL"
|
|
735
|
+
console.log(ValidationErrorCode.STRING_MIN_LENGTH); // "STRING_MIN_LENGTH"
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
### Manual Validation
|
|
739
|
+
|
|
740
|
+
You can also manually validate data using the `validate` function:
|
|
741
|
+
|
|
742
|
+
```typescript
|
|
743
|
+
import { validate, ValidationErrors, t } from "adorn-api";
|
|
744
|
+
|
|
745
|
+
const data = { name: "", email: "invalid" };
|
|
746
|
+
const errors = validate(data, t.object({
|
|
747
|
+
name: t.string({ minLength: 1 }),
|
|
748
|
+
email: t.string({ format: "email" })
|
|
749
|
+
}));
|
|
750
|
+
|
|
751
|
+
if (errors.length > 0) {
|
|
752
|
+
throw new ValidationErrors(errors);
|
|
753
|
+
}
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
## Transformers
|
|
757
|
+
|
|
758
|
+
Transform fields during serialization with custom transform functions or built-in transform utilities.
|
|
759
|
+
|
|
760
|
+
### Basic Transform
|
|
761
|
+
|
|
762
|
+
```typescript
|
|
763
|
+
import { Dto, Field, Transform, t } from "adorn-api";
|
|
764
|
+
|
|
765
|
+
@Dto()
|
|
766
|
+
export class UserDto {
|
|
767
|
+
@Field(t.string())
|
|
768
|
+
@Transform((value) => value.toUpperCase())
|
|
769
|
+
name!: string;
|
|
770
|
+
|
|
771
|
+
@Field(t.dateTime())
|
|
772
|
+
@Transform((value) => value.toISOString())
|
|
773
|
+
createdAt!: Date;
|
|
774
|
+
}
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
### Built-in Transforms
|
|
778
|
+
|
|
779
|
+
Adorn API includes common transform functions:
|
|
780
|
+
|
|
781
|
+
```typescript
|
|
782
|
+
import { Dto, Field, Transform, Transforms, t } from "adorn-api";
|
|
783
|
+
|
|
784
|
+
@Dto()
|
|
785
|
+
export class UserDto {
|
|
786
|
+
@Field(t.string())
|
|
787
|
+
@Transform(Transforms.toLowerCase)
|
|
788
|
+
email!: string;
|
|
789
|
+
|
|
790
|
+
@Field(t.number())
|
|
791
|
+
@Transform(Transforms.round(2))
|
|
792
|
+
price!: number;
|
|
793
|
+
|
|
794
|
+
@Field(t.string())
|
|
795
|
+
@Transform(Transforms.mask(4)) // Mask all but last 4 characters
|
|
796
|
+
creditCard!: string;
|
|
797
|
+
|
|
798
|
+
@Field(t.dateTime())
|
|
799
|
+
@Transform(Transforms.toISOString)
|
|
800
|
+
birthDate!: Date;
|
|
801
|
+
}
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
### Conditional Transforms with Groups
|
|
805
|
+
|
|
806
|
+
Apply transforms only to specific serialization groups:
|
|
807
|
+
|
|
808
|
+
```typescript
|
|
809
|
+
import { Dto, Field, Transform, Expose, t } from "adorn-api";
|
|
810
|
+
|
|
811
|
+
@Dto()
|
|
812
|
+
export class UserDto {
|
|
813
|
+
@Field(t.string())
|
|
814
|
+
name!: string;
|
|
815
|
+
|
|
816
|
+
@Field(t.string())
|
|
817
|
+
@Expose({ groups: ["admin"] })
|
|
818
|
+
@Transform((value) => Transforms.mask(2), { groups: ["admin"] })
|
|
819
|
+
phoneNumber!: string;
|
|
820
|
+
|
|
821
|
+
@Field(t.string())
|
|
822
|
+
@Expose({ groups: ["internal"] })
|
|
823
|
+
@Transform((value) => "[REDACTED]", { groups: ["external"] })
|
|
824
|
+
internalNote!: string;
|
|
825
|
+
}
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
### Custom Transform Functions
|
|
829
|
+
|
|
830
|
+
Create custom transform functions:
|
|
831
|
+
|
|
832
|
+
```typescript
|
|
833
|
+
import { Dto, Field, Transform, t } from "adorn-api";
|
|
834
|
+
|
|
835
|
+
const toCurrency = (value: number, currency: string = "USD") => {
|
|
836
|
+
return new Intl.NumberFormat("en-US", {
|
|
837
|
+
style: "currency",
|
|
838
|
+
currency
|
|
839
|
+
}).format(value);
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
@Dto()
|
|
843
|
+
export class ProductDto {
|
|
844
|
+
@Field(t.string())
|
|
845
|
+
name!: string;
|
|
846
|
+
|
|
847
|
+
@Field(t.number())
|
|
848
|
+
@Transform(toCurrency)
|
|
849
|
+
price!: number;
|
|
850
|
+
|
|
851
|
+
@Field(t.number())
|
|
852
|
+
@Transform((value) => toCurrency(value, "EUR"))
|
|
853
|
+
priceEUR!: number;
|
|
854
|
+
}
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
### Serialization with Options
|
|
858
|
+
|
|
859
|
+
Control serialization with custom options:
|
|
860
|
+
|
|
861
|
+
```typescript
|
|
862
|
+
import { serialize, createSerializer } from "adorn-api";
|
|
863
|
+
import { UserDto } from "./user.dtos";
|
|
864
|
+
|
|
865
|
+
const user = new UserDto();
|
|
866
|
+
user.name = "John Doe";
|
|
867
|
+
user.phoneNumber = "123-456-7890";
|
|
868
|
+
user.internalNote = "This is an internal note";
|
|
869
|
+
|
|
870
|
+
// Basic serialization
|
|
871
|
+
const basic = serialize(user);
|
|
872
|
+
// Output: { name: "John Doe" }
|
|
873
|
+
|
|
874
|
+
// Admin group serialization
|
|
875
|
+
const admin = serialize(user, { groups: ["admin"] });
|
|
876
|
+
// Output: { name: "John Doe", phoneNumber: "********90" }
|
|
877
|
+
|
|
878
|
+
// External group serialization
|
|
879
|
+
const external = serialize(user, { groups: ["external"] });
|
|
880
|
+
// Output: { name: "John Doe", internalNote: "[REDACTED]" }
|
|
881
|
+
|
|
882
|
+
// Create a preset serializer
|
|
883
|
+
const adminSerializer = createSerializer({ groups: ["admin"] });
|
|
884
|
+
const adminData = adminSerializer(user);
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
### Exclude Fields
|
|
888
|
+
|
|
889
|
+
Control which fields are excluded or exposed:
|
|
890
|
+
|
|
891
|
+
```typescript
|
|
892
|
+
import { Dto, Field, Exclude, Expose, t } from "adorn-api";
|
|
893
|
+
|
|
894
|
+
@Dto()
|
|
895
|
+
export class UserDto {
|
|
896
|
+
@Field(t.string())
|
|
897
|
+
name!: string;
|
|
898
|
+
|
|
899
|
+
@Field(t.string())
|
|
900
|
+
@Exclude() // Always exclude from serialization
|
|
901
|
+
password!: string;
|
|
902
|
+
|
|
903
|
+
@Field(t.string())
|
|
904
|
+
@Expose({ name: "email_address" }) // Rename field in output
|
|
905
|
+
email!: string;
|
|
906
|
+
|
|
907
|
+
@Field(t.string())
|
|
908
|
+
@Exclude({ groups: ["public"] }) // Exclude from public group
|
|
909
|
+
internalComment!: string;
|
|
910
|
+
}
|
|
911
|
+
```
|
|
912
|
+
|
|
871
913
|
## Examples
|
|
872
|
-
|
|
873
|
-
Check out the `examples/` directory for more comprehensive examples:
|
|
874
|
-
|
|
875
|
-
- `basic/` - Simple API with controllers and DTOs
|
|
876
|
-
- `restful/` - RESTful API with complete CRUD operations
|
|
914
|
+
|
|
915
|
+
Check out the `examples/` directory for more comprehensive examples:
|
|
916
|
+
|
|
917
|
+
- `basic/` - Simple API with controllers and DTOs
|
|
918
|
+
- `restful/` - RESTful API with complete CRUD operations
|
|
877
919
|
- `metal-orm-sqlite/` - Metal ORM integration with SQLite
|
|
878
920
|
- `metal-orm-tree/` - Metal ORM tree (nested set) DTO + OpenAPI integration
|
|
879
921
|
- `metal-orm-deep-filters/` - Deep relation filtering example (Alpha → Bravo → Charlie → Delta)
|
|
880
922
|
- `metal-orm-sqlite-music/` - Complex relations with Metal ORM
|
|
881
|
-
- `streaming/` - SSE and streaming responses
|
|
882
|
-
- `openapi/` - OpenAPI documentation customization
|
|
883
|
-
- `validation/` - Comprehensive validation examples with various schema types
|
|
884
|
-
|
|
885
|
-
## Testing
|
|
886
|
-
|
|
887
|
-
Adorn API works great with testing frameworks like Vitest and SuperTest. Here's an example:
|
|
888
|
-
|
|
889
|
-
```typescript
|
|
890
|
-
import { describe, it, expect } from "vitest";
|
|
891
|
-
import request from "supertest";
|
|
892
|
-
import { createApp } from "./app";
|
|
893
|
-
|
|
894
|
-
describe("User API", () => {
|
|
895
|
-
it("should get user by id", async () => {
|
|
896
|
-
const app = await createApp();
|
|
897
|
-
|
|
898
|
-
const response = await request(app)
|
|
899
|
-
.get("/users/1")
|
|
900
|
-
.expect(200);
|
|
901
|
-
|
|
902
|
-
expect(response.body).toEqual({
|
|
903
|
-
id: "1",
|
|
904
|
-
name: "Ada Lovelace",
|
|
905
|
-
nickname: "Ada"
|
|
906
|
-
});
|
|
907
|
-
});
|
|
908
|
-
});
|
|
909
|
-
```
|
|
910
|
-
|
|
911
|
-
## License
|
|
912
|
-
|
|
913
|
-
MIT
|
|
914
|
-
|
|
915
|
-
## Contributing
|
|
916
|
-
|
|
917
|
-
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
923
|
+
- `streaming/` - SSE and streaming responses
|
|
924
|
+
- `openapi/` - OpenAPI documentation customization
|
|
925
|
+
- `validation/` - Comprehensive validation examples with various schema types
|
|
926
|
+
|
|
927
|
+
## Testing
|
|
928
|
+
|
|
929
|
+
Adorn API works great with testing frameworks like Vitest and SuperTest. Here's an example:
|
|
930
|
+
|
|
931
|
+
```typescript
|
|
932
|
+
import { describe, it, expect } from "vitest";
|
|
933
|
+
import request from "supertest";
|
|
934
|
+
import { createApp } from "./app";
|
|
935
|
+
|
|
936
|
+
describe("User API", () => {
|
|
937
|
+
it("should get user by id", async () => {
|
|
938
|
+
const app = await createApp();
|
|
939
|
+
|
|
940
|
+
const response = await request(app)
|
|
941
|
+
.get("/users/1")
|
|
942
|
+
.expect(200);
|
|
943
|
+
|
|
944
|
+
expect(response.body).toEqual({
|
|
945
|
+
id: "1",
|
|
946
|
+
name: "Ada Lovelace",
|
|
947
|
+
nickname: "Ada"
|
|
948
|
+
});
|
|
949
|
+
});
|
|
950
|
+
});
|
|
951
|
+
```
|
|
952
|
+
|
|
953
|
+
## License
|
|
954
|
+
|
|
955
|
+
MIT
|
|
956
|
+
|
|
957
|
+
## Contributing
|
|
958
|
+
|
|
959
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|