framework-do-dede 3.3.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +515 -4
- package/dist/application/controller.d.ts +6 -2
- package/dist/application/controller.js +10 -18
- package/dist/application/index.d.ts +2 -2
- package/dist/application/index.js +2 -2
- package/dist/application/services.d.ts +2 -1
- package/dist/application/services.js +3 -3
- package/dist/dede.d.ts +10 -1
- package/dist/dede.js +30 -9
- package/dist/domain/entity.d.ts +4 -0
- package/dist/domain/entity.js +25 -0
- package/dist/domain/errors/app-error.d.ts +12 -0
- package/dist/domain/errors/app-error.js +14 -0
- package/dist/domain/errors/http-errors.d.ts +42 -0
- package/dist/domain/errors/http-errors.js +40 -0
- package/dist/domain/index.d.ts +2 -0
- package/dist/domain/index.js +2 -0
- package/dist/http/controller.handler.d.ts +4 -5
- package/dist/http/controller.handler.js +27 -119
- package/dist/http/errors/server.d.ts +2 -28
- package/dist/http/errors/server.js +2 -49
- package/dist/http/http-server.d.ts +2 -0
- package/dist/http/http-server.js +1 -1
- package/dist/http/index.d.ts +2 -2
- package/dist/http/index.js +2 -2
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -3
- package/dist/infra/di/registry.d.ts +4 -6
- package/dist/infra/di/registry.js +7 -10
- package/dist/{application → infra/serialization}/entity.d.ts +8 -1
- package/dist/{application → infra/serialization}/entity.js +87 -23
- package/dist/interface/errors/http-error-mapper.d.ts +10 -0
- package/dist/interface/errors/http-error-mapper.js +31 -0
- package/dist/interface/http/middleware-executor.d.ts +10 -0
- package/dist/interface/http/middleware-executor.js +33 -0
- package/dist/interface/http/request-mapper.d.ts +21 -0
- package/dist/interface/http/request-mapper.js +55 -0
- package/dist/interface/validation/class-validator.d.ts +6 -0
- package/dist/interface/validation/class-validator.js +28 -0
- package/dist/interface/validation/validator.d.ts +5 -0
- package/dist/interface/validation/validator.js +1 -0
- package/dist/protocols/repository.d.ts +1 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,15 +1,526 @@
|
|
|
1
1
|
# Framework do Dedé
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Um framework TypeScript simples para construir APIs HTTP com controllers, use cases e entities, com suporte a Express ou Elysia, DI leve e serialização de entidades.
|
|
4
|
+
|
|
5
|
+
## Índice
|
|
6
|
+
|
|
7
|
+
- Instalação
|
|
8
|
+
- Quickstart
|
|
9
|
+
- Conceitos
|
|
10
|
+
- Controllers e Rotas
|
|
11
|
+
- Input, params e filtros
|
|
12
|
+
- Middlewares
|
|
13
|
+
- Tracing
|
|
14
|
+
- UseCase e Decorators
|
|
15
|
+
- Entity e Serialização
|
|
16
|
+
- Hooks Before/After ToEntity
|
|
17
|
+
- Storage Gateway
|
|
18
|
+
- DI (Container/Inject)
|
|
19
|
+
- Errors
|
|
20
|
+
- Protocolos de Repositório
|
|
21
|
+
- Exemplos
|
|
22
|
+
- Express
|
|
23
|
+
- Elysia
|
|
24
|
+
- Fila com AfterToEntity
|
|
25
|
+
- Testes
|
|
26
|
+
- Benchmark
|
|
27
|
+
|
|
28
|
+
## Instalação
|
|
4
29
|
|
|
5
30
|
```bash
|
|
6
31
|
bun install
|
|
7
32
|
```
|
|
8
33
|
|
|
9
|
-
|
|
34
|
+
Para executar o exemplo (Express e Elysia):
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
bun run example/express_app/server.ts
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quickstart
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { Controller, Get, Post, UseCase, Dede } from './src';
|
|
44
|
+
|
|
45
|
+
@Controller('/hello')
|
|
46
|
+
class HelloController {
|
|
47
|
+
@Get({ statusCode: 200 })
|
|
48
|
+
async get() {
|
|
49
|
+
const useCase = new HelloUseCase({ data: undefined });
|
|
50
|
+
return await useCase.execute();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@Post({ statusCode: 201, body: ['name|string'] })
|
|
54
|
+
async post(request: { data: { name: string } }) {
|
|
55
|
+
const useCase = new HelloUseCase({ data: request.data });
|
|
56
|
+
return await useCase.execute();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
class HelloUseCase extends UseCase<{ name?: string }, { message: string }> {
|
|
61
|
+
async execute() {
|
|
62
|
+
return { message: `Hello ${this.data?.name ?? 'world'}` };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const app = await Dede.create({
|
|
67
|
+
framework: { use: 'express', port: 3000 },
|
|
68
|
+
registries: []
|
|
69
|
+
});
|
|
70
|
+
app.registerControllers([HelloController]);
|
|
71
|
+
app.listen();
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Conceitos
|
|
75
|
+
|
|
76
|
+
### Controllers e Rotas
|
|
77
|
+
|
|
78
|
+
Use decorators para expor métodos como rotas HTTP. O Controller define metadados, e o ControllerHandler monta as rotas em runtime a partir da lista de controllers passada ao `app.registerControllers(...)`.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { Controller, Get, Post, Put, Delete, Patch } from './src';
|
|
82
|
+
|
|
83
|
+
@Controller('/users')
|
|
84
|
+
export class UsersController {
|
|
85
|
+
@Get({ statusCode: 200 })
|
|
86
|
+
async list() { /* ... */ }
|
|
87
|
+
|
|
88
|
+
@Post({ statusCode: 201, body: ['name|string', 'email|string'] })
|
|
89
|
+
async create(request: { data: any }) { /* ... */ }
|
|
90
|
+
|
|
91
|
+
@Put({ params: ['id|string'], body: ['name|string'] })
|
|
92
|
+
async update(request: { data: any }) { /* ... */ }
|
|
93
|
+
|
|
94
|
+
@Delete({ params: ['id|string'] })
|
|
95
|
+
async remove(request: { data: any }) { /* ... */ }
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Decorators disponíveis:
|
|
100
|
+
|
|
101
|
+
- `@Controller(basePath?: string)`
|
|
102
|
+
- `@Get`, `@Post`, `@Put`, `@Patch`, `@Delete`
|
|
103
|
+
|
|
104
|
+
Opções de rota (comuns):
|
|
105
|
+
|
|
106
|
+
- `path`: string
|
|
107
|
+
- `statusCode`: number
|
|
108
|
+
- `params`, `query`, `headers`, `body`: array de strings no formato `campo|tipo`
|
|
109
|
+
- `bodyFilter`: `"restrict" | "none"`
|
|
110
|
+
- `responseType`: `"json" | "text" | "html"`
|
|
111
|
+
- `validator`: pode ser uma classe com decorators do `class-validator` **ou** um objeto com `validate(data)` (sync/async)
|
|
112
|
+
|
|
113
|
+
### Input, params e filtros
|
|
114
|
+
|
|
115
|
+
O framework compõe um objeto `request.data` a partir de:
|
|
116
|
+
|
|
117
|
+
1) headers filtrados
|
|
118
|
+
2) params filtrados
|
|
119
|
+
3) query filtrada
|
|
120
|
+
4) body filtrado
|
|
121
|
+
|
|
122
|
+
Quando `bodyFilter: "restrict"`, apenas os campos definidos em `body` serão usados. Caso contrário, o corpo completo é mesclado.
|
|
123
|
+
|
|
124
|
+
Tipos suportados no filtro:
|
|
125
|
+
|
|
126
|
+
- `boolean`, `integer`, `string`, `number`
|
|
127
|
+
|
|
128
|
+
Exemplos:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
@Put({
|
|
132
|
+
params: ['id|string'],
|
|
133
|
+
query: ['active|boolean'],
|
|
134
|
+
headers: ['x-type|string'],
|
|
135
|
+
body: ['name|string'],
|
|
136
|
+
bodyFilter: 'restrict',
|
|
137
|
+
validator: CreateUserDto
|
|
138
|
+
})
|
|
139
|
+
async update(request: { data: any }) {
|
|
140
|
+
// request.data: { id, active, 'x-type', name }
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import 'reflect-metadata'
|
|
146
|
+
import { IsEmail, IsNotEmpty } from 'class-validator'
|
|
147
|
+
|
|
148
|
+
class CreateUserDto {
|
|
149
|
+
@IsNotEmpty({ message: 'O nome é obrigatório.' })
|
|
150
|
+
name!: string
|
|
151
|
+
|
|
152
|
+
@IsEmail({}, { message: 'Email inválido.' })
|
|
153
|
+
email?: string
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
@Put({
|
|
159
|
+
body: ['name|string', 'email|string'],
|
|
160
|
+
bodyFilter: 'restrict',
|
|
161
|
+
validator: CreateUserDto
|
|
162
|
+
})
|
|
163
|
+
async update(request: { data: any }) {}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Obs: o framework usa `class-validator` como `peerDependency`, então o projeto que consome deve ter a mesma versão instalada.
|
|
167
|
+
|
|
168
|
+
Suporte a notacao com colchetes:
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{ "user[name]": "Joao", "user[email]": "a@b.com" }
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
vira:
|
|
175
|
+
|
|
176
|
+
```json
|
|
177
|
+
{ "user": { "name": "Joao", "email": "a@b.com" } }
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Middlewares
|
|
181
|
+
|
|
182
|
+
Middlewares devem implementar `execute(input: Input<any>)`. Podem ser classe, factory ou instancia.
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
import { Middleware, UseMiddleware, UseMiddlewares, Input } from './src';
|
|
186
|
+
|
|
187
|
+
class AuthMiddleware implements Middleware {
|
|
188
|
+
async execute(input: Input<any>) {
|
|
189
|
+
input.context.auth = { userId: 123 };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
@Controller('/secure')
|
|
194
|
+
class SecureController {
|
|
195
|
+
@Get()
|
|
196
|
+
@UseMiddleware(AuthMiddleware)
|
|
197
|
+
async get(request: { data: any; context: any }) {
|
|
198
|
+
return { userId: request.context.auth.userId };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Tracing
|
|
204
|
+
|
|
205
|
+
Use `@Tracing` no controller ou em um metodo para capturar metadados de request.
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
import { Tracing, Tracer, TracerData } from './src';
|
|
209
|
+
|
|
210
|
+
class ConsoleTracer implements Tracer<void> {
|
|
211
|
+
trace(data: TracerData) {
|
|
212
|
+
console.log(data);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
@Tracing(new ConsoleTracer())
|
|
217
|
+
@Controller('/trace')
|
|
218
|
+
class TraceController {
|
|
219
|
+
@Get()
|
|
220
|
+
async get() { return { ok: true }; }
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### UseCase e Decorators
|
|
225
|
+
|
|
226
|
+
UseCase provê `data` e `context` do request.
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
import { UseCase } from './src';
|
|
230
|
+
|
|
231
|
+
class CreateUserUseCase extends UseCase<{ name: string }, { id: string }> {
|
|
232
|
+
async execute() {
|
|
233
|
+
return { id: 'new-id' };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Decorator `@DecorateUseCase` permite executar use cases antes do principal (chaining).
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
import { UseCase, DecorateUseCase } from './src';
|
|
242
|
+
|
|
243
|
+
class AuditUseCase extends UseCase<any, void> {
|
|
244
|
+
async execute() { /* audit */ }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
@DecorateUseCase({ useCase: AuditUseCase })
|
|
248
|
+
class CreateUserUseCase extends UseCase<{ name: string }, { id: string }> {
|
|
249
|
+
async execute() { return { id: 'new-id' }; }
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Entity e Serializacao
|
|
254
|
+
|
|
255
|
+
Entities suportam:
|
|
256
|
+
|
|
257
|
+
- `toEntity()` e `toAsyncEntity()`
|
|
258
|
+
- `toData()` e `toAsyncData()`
|
|
259
|
+
- `@Serialize`, `@Restrict`, `@VirtualProperty`, `@GetterPrefix`
|
|
260
|
+
|
|
261
|
+
Obs: a serializacao fica na camada de infraestrutura, mas a API continua sendo exposta pelo framework (importe direto de `./src`).
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
import { Entity, Serialize, Restrict, VirtualProperty, GetterPrefix } from './src';
|
|
265
|
+
|
|
266
|
+
class User extends Entity {
|
|
267
|
+
@Serialize((value: Email) => value.getValue())
|
|
268
|
+
private readonly email: Email;
|
|
269
|
+
|
|
270
|
+
@Restrict()
|
|
271
|
+
private readonly passwordHash: string;
|
|
272
|
+
|
|
273
|
+
@GetterPrefix('has')
|
|
274
|
+
private readonly profile?: Profile;
|
|
275
|
+
|
|
276
|
+
@VirtualProperty('displayName')
|
|
277
|
+
private display() {
|
|
278
|
+
return 'User ' + this.email.getValue();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
constructor(email: string, passwordHash: string) {
|
|
282
|
+
super();
|
|
283
|
+
this.email = new Email(email);
|
|
284
|
+
this.passwordHash = passwordHash;
|
|
285
|
+
this.generateGetters();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const user = new User('a@b.com', 'hash');
|
|
290
|
+
const serialized = user.toEntity();
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Regras principais:
|
|
294
|
+
|
|
295
|
+
- `@Serialize` pode retornar objeto: cada chave vira uma propriedade do resultado
|
|
296
|
+
- `@Restrict` remove campo em `toData`
|
|
297
|
+
- `@VirtualProperty` mapeia metodos para campos virtuais
|
|
298
|
+
- `generateGetters()` cria getters para campos (ex.: `getName`, `isActive`, `hasProfile`)
|
|
299
|
+
|
|
300
|
+
### Hooks Before/After ToEntity
|
|
301
|
+
|
|
302
|
+
Use `@BeforeToEntity()` e `@AfterToEntity()` em metodos de Entities.
|
|
303
|
+
|
|
304
|
+
- Before recebe objeto bruto (antes de serializacao)
|
|
305
|
+
- After recebe objeto tratado (resultado final)
|
|
306
|
+
- `toEntity()` executa hooks sem aguardar promessas
|
|
307
|
+
- `toAsyncEntity()` aguarda hooks async
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
import { Entity, AfterToEntity, BeforeToEntity } from './src';
|
|
311
|
+
|
|
312
|
+
class FileEntity extends Entity {
|
|
313
|
+
private readonly name: string;
|
|
314
|
+
private readonly s3Key: string;
|
|
315
|
+
|
|
316
|
+
constructor(name: string, s3Key: string) {
|
|
317
|
+
super();
|
|
318
|
+
this.name = name;
|
|
319
|
+
this.s3Key = s3Key;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
@BeforeToEntity()
|
|
323
|
+
private before(payload: Record<string, any>) {
|
|
324
|
+
payload.rawTouched = true;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
@AfterToEntity()
|
|
328
|
+
private async after(payload: Record<string, any>) {
|
|
329
|
+
await saveToS3(payload.s3Key);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Storage Gateway
|
|
335
|
+
|
|
336
|
+
Use `@Storage` para injetar gateways com interface `StorageGateway`.
|
|
337
|
+
|
|
338
|
+
```ts
|
|
339
|
+
import { Storage, StorageGateway } from './src';
|
|
340
|
+
|
|
341
|
+
class S3Gateway implements StorageGateway {
|
|
342
|
+
async save(file: File, path: string) { /* ... */ }
|
|
343
|
+
async get(key: string) { return 'url'; }
|
|
344
|
+
async delete(key: string) { return true; }
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
class FileService {
|
|
348
|
+
@Storage('S3Gateway')
|
|
349
|
+
private readonly storage!: StorageGateway;
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### DI (Container/Inject)
|
|
354
|
+
|
|
355
|
+
Registre dependencias ao iniciar o server (usando o container padrão):
|
|
356
|
+
|
|
357
|
+
```ts
|
|
358
|
+
import { Dede } from './src';
|
|
359
|
+
|
|
360
|
+
class UserRepository { /* ... */ }
|
|
361
|
+
|
|
362
|
+
const app = await Dede.create({
|
|
363
|
+
framework: { use: 'express', port: 3000 },
|
|
364
|
+
registries: [
|
|
365
|
+
{ name: 'UserRepository', classLoader: UserRepository }
|
|
366
|
+
]
|
|
367
|
+
});
|
|
368
|
+
app.listen();
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Use `@Inject('Name')` para injetar dependencias:
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
import { Inject, UseCase } from './src';
|
|
375
|
+
|
|
376
|
+
class ExampleUseCase extends UseCase<void, any> {
|
|
377
|
+
@Inject('UserRepository')
|
|
378
|
+
private readonly userRepository!: any;
|
|
379
|
+
|
|
380
|
+
async execute() {
|
|
381
|
+
return await this.userRepository.findById('1');
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### Errors
|
|
387
|
+
|
|
388
|
+
Erros de dominio disponiveis:
|
|
389
|
+
|
|
390
|
+
- `BadRequest` (400)
|
|
391
|
+
- `Unauthorized` (401)
|
|
392
|
+
- `Forbidden` (403)
|
|
393
|
+
- `NotFound` (404)
|
|
394
|
+
- `Conflict` (409)
|
|
395
|
+
- `UnprocessableEntity` (422)
|
|
396
|
+
- `InternalServerError` (500)
|
|
397
|
+
|
|
398
|
+
Quando um erro e lancado, o handler padroniza a resposta. Erros de dominio (`AppError`) sao mapeados para HTTP. Se o erro for `CustomServerError`, o payload customizado sera retornado diretamente.
|
|
399
|
+
|
|
400
|
+
### Protocolos de Repositorio
|
|
401
|
+
|
|
402
|
+
Interfaces tipadas para padrao de repositorio:
|
|
403
|
+
|
|
404
|
+
- `RepositoryCreate<T extends Entity>`
|
|
405
|
+
- `RepositoryUpdate<T extends Entity>`
|
|
406
|
+
- `RepositoryRemove`
|
|
407
|
+
- `RepositoryRestore<T extends Entity>`
|
|
408
|
+
- `RepositoryRemoveBy<T>`
|
|
409
|
+
- `RepositoryRestoreBy<T>`
|
|
410
|
+
- `RepositoryExistsBy<T>`
|
|
411
|
+
- `RepositoryNotExistsBy<T>`
|
|
412
|
+
- `RepositoryPagination<T>`
|
|
413
|
+
|
|
414
|
+
## Exemplos
|
|
415
|
+
|
|
416
|
+
### Express
|
|
417
|
+
|
|
418
|
+
```ts
|
|
419
|
+
import { Dede } from './src/dede';
|
|
420
|
+
import { ExampleController } from './example/express_app/example.controller';
|
|
421
|
+
|
|
422
|
+
class UserRepository {
|
|
423
|
+
async findById(id: string) {
|
|
424
|
+
return { id, name: 'John Doe' };
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const app = await Dede.create({
|
|
429
|
+
framework: { use: 'express', port: 3000 },
|
|
430
|
+
registries: [{ name: 'UserRepository', classLoader: UserRepository }]
|
|
431
|
+
});
|
|
432
|
+
app.registerControllers([ExampleController]);
|
|
433
|
+
app.listen();
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### Elysia
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
import { Dede } from './src/dede';
|
|
440
|
+
import { ExampleController } from './example/express_app/example.controller';
|
|
441
|
+
|
|
442
|
+
const app = await Dede.create({
|
|
443
|
+
framework: { use: 'elysia', port: 3001 },
|
|
444
|
+
registries: []
|
|
445
|
+
});
|
|
446
|
+
app.registerControllers([ExampleController]);
|
|
447
|
+
app.listen();
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
### Fila com AfterToEntity
|
|
451
|
+
|
|
452
|
+
```ts
|
|
453
|
+
import { Entity, AfterToEntity } from './src';
|
|
454
|
+
|
|
455
|
+
type QueueJob = { type: string; payload: Record<string, any> };
|
|
456
|
+
|
|
457
|
+
type Queue = { enqueue(job: QueueJob): Promise<void> };
|
|
458
|
+
|
|
459
|
+
const queue: Queue = {
|
|
460
|
+
async enqueue(job) {
|
|
461
|
+
console.log('queued job', job);
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
class FileEntity extends Entity {
|
|
466
|
+
private readonly name: string;
|
|
467
|
+
private readonly s3Key: string;
|
|
468
|
+
|
|
469
|
+
private constructor({ name, s3Key }: { name: string; s3Key: string }) {
|
|
470
|
+
super();
|
|
471
|
+
this.name = name;
|
|
472
|
+
this.s3Key = s3Key;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
@AfterToEntity()
|
|
476
|
+
private async enqueueFileSync(payload: Record<string, any>) {
|
|
477
|
+
await queue.enqueue({
|
|
478
|
+
type: 'files.create',
|
|
479
|
+
payload: {
|
|
480
|
+
name: payload.name,
|
|
481
|
+
s3Key: payload.s3Key
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
static create(input: { name: string; s3Key: string }) {
|
|
487
|
+
return new FileEntity(input);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const entity = FileEntity.create({ name: 'report', s3Key: 's3://bucket/report.pdf' });
|
|
492
|
+
const serialized = entity.toEntity();
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
## Testes
|
|
496
|
+
|
|
497
|
+
```bash
|
|
498
|
+
npm test -- tests/src/application/entity.spec.ts --runInBand
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
Testes de integração (exemplos):
|
|
10
502
|
|
|
11
503
|
```bash
|
|
12
|
-
|
|
504
|
+
RUN_EXAMPLE_TESTS=true npm test -- example/tests/main.test.ts
|
|
13
505
|
```
|
|
14
506
|
|
|
15
|
-
|
|
507
|
+
Obs: os testes de Elysia só rodam no runtime do Bun (em Node eles são ignorados).
|
|
508
|
+
|
|
509
|
+
## Benchmark
|
|
510
|
+
|
|
511
|
+
Resultados locais ficam em `bench/results.md`. Para rodar:
|
|
512
|
+
|
|
513
|
+
```bash
|
|
514
|
+
npm run bench:compare
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
Parâmetros (opcional):
|
|
518
|
+
|
|
519
|
+
```bash
|
|
520
|
+
BENCH_REQUESTS=5000 BENCH_CONCURRENCY=50 BENCH_WARMUP=200 npm run bench:compare
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
Resumo (média de 3 rodadas locais, 5000 req / conc 50 / warmup 200):
|
|
524
|
+
|
|
525
|
+
- Express: avg 3.34 ms, p50 2.45 ms, p95 8.54 ms, 8298.61 req/s
|
|
526
|
+
- Elysia: avg 3.16 ms, p50 2.63 ms, p95 6.98 ms, 8765.78 req/s
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
|
+
import type { ValidatorLike } from "../interface/validation/validator";
|
|
2
3
|
export interface Middleware {
|
|
3
4
|
execute(input: Input<any>): Promise<any>;
|
|
4
5
|
}
|
|
@@ -28,8 +29,6 @@ export interface Input<T, K = any> {
|
|
|
28
29
|
type BodyFilter = "restrict" | "none";
|
|
29
30
|
export declare function Controller(basePath?: string): (target: any) => void;
|
|
30
31
|
export declare function Tracing<R>(tracer: Tracer<R>): (target: any, propertyKey?: string) => void;
|
|
31
|
-
export declare function getControllers(): any[];
|
|
32
|
-
export declare function flushControllers(): void;
|
|
33
32
|
export declare function UseMiddleware(middlewareClass: MiddlewareDefinition): (target: any, propertyKey: string, descriptor: PropertyDescriptor) => void;
|
|
34
33
|
export declare function UseMiddlewares(middlewareClasses: MiddlewareDefinition[]): (target: any, propertyKey: string, descriptor: PropertyDescriptor) => void;
|
|
35
34
|
export declare function Post(config?: {
|
|
@@ -41,6 +40,7 @@ export declare function Post(config?: {
|
|
|
41
40
|
body?: string[];
|
|
42
41
|
bodyFilter?: BodyFilter;
|
|
43
42
|
responseType?: 'json' | 'text' | 'html';
|
|
43
|
+
validator?: ValidatorLike;
|
|
44
44
|
}): (target: any, propertyKey: string, descriptor: PropertyDescriptor) => void;
|
|
45
45
|
export declare function Get(config?: {
|
|
46
46
|
path?: string;
|
|
@@ -49,6 +49,7 @@ export declare function Get(config?: {
|
|
|
49
49
|
query?: string[];
|
|
50
50
|
headers?: string[];
|
|
51
51
|
responseType?: 'json' | 'text' | 'html';
|
|
52
|
+
validator?: ValidatorLike;
|
|
52
53
|
}): (target: any, propertyKey: string, descriptor: PropertyDescriptor) => void;
|
|
53
54
|
export declare function Put(config?: {
|
|
54
55
|
path?: string;
|
|
@@ -59,6 +60,7 @@ export declare function Put(config?: {
|
|
|
59
60
|
body?: string[];
|
|
60
61
|
bodyFilter?: BodyFilter;
|
|
61
62
|
responseType?: 'json' | 'text' | 'html';
|
|
63
|
+
validator?: ValidatorLike;
|
|
62
64
|
}): (target: any, propertyKey: string, descriptor: PropertyDescriptor) => void;
|
|
63
65
|
export declare function Patch(config?: {
|
|
64
66
|
path?: string;
|
|
@@ -69,6 +71,7 @@ export declare function Patch(config?: {
|
|
|
69
71
|
body?: string[];
|
|
70
72
|
bodyFilter?: BodyFilter;
|
|
71
73
|
responseType?: 'json' | 'text' | 'html';
|
|
74
|
+
validator?: ValidatorLike;
|
|
72
75
|
}): (target: any, propertyKey: string, descriptor: PropertyDescriptor) => void;
|
|
73
76
|
export declare function Delete(config?: {
|
|
74
77
|
path?: string;
|
|
@@ -79,5 +82,6 @@ export declare function Delete(config?: {
|
|
|
79
82
|
body?: string[];
|
|
80
83
|
bodyFilter?: BodyFilter;
|
|
81
84
|
responseType?: 'json' | 'text' | 'html';
|
|
85
|
+
validator?: ValidatorLike;
|
|
82
86
|
}): (target: any, propertyKey: string, descriptor: PropertyDescriptor) => void;
|
|
83
87
|
export {};
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
2
|
import { FrameworkError } from "../http/errors/framework";
|
|
3
|
-
import { Registry } from "../infra/di/registry";
|
|
4
|
-
let controllers = [];
|
|
5
3
|
export function Controller(basePath = '/') {
|
|
6
4
|
return function (target) {
|
|
7
5
|
if (!basePath)
|
|
8
6
|
throw new FrameworkError('basePath cannot be empty');
|
|
9
7
|
Reflect.defineMetadata('basePath', basePath, target);
|
|
10
|
-
controllers.push(target.name);
|
|
11
|
-
Registry.load(target.name, target);
|
|
12
8
|
};
|
|
13
9
|
}
|
|
14
10
|
export function Tracing(tracer) {
|
|
@@ -21,15 +17,6 @@ export function Tracing(tracer) {
|
|
|
21
17
|
}
|
|
22
18
|
};
|
|
23
19
|
}
|
|
24
|
-
export function getControllers() {
|
|
25
|
-
return controllers.map((controller) => Registry.inject(controller));
|
|
26
|
-
}
|
|
27
|
-
export function flushControllers() {
|
|
28
|
-
controllers.map((controller) => {
|
|
29
|
-
Registry.remove(controller);
|
|
30
|
-
});
|
|
31
|
-
controllers = [];
|
|
32
|
-
}
|
|
33
20
|
function isClass(fn) {
|
|
34
21
|
return /^\s*class\s/.test(Function.prototype.toString.call(fn));
|
|
35
22
|
}
|
|
@@ -78,7 +65,8 @@ export function Post(config = {}) {
|
|
|
78
65
|
body: config.body,
|
|
79
66
|
bodyFilter: config.bodyFilter || 'none',
|
|
80
67
|
statusCode: config.statusCode || 200,
|
|
81
|
-
responseType: config.responseType || 'json'
|
|
68
|
+
responseType: config.responseType || 'json',
|
|
69
|
+
validator: config.validator
|
|
82
70
|
}, target, propertyKey);
|
|
83
71
|
};
|
|
84
72
|
}
|
|
@@ -91,7 +79,8 @@ export function Get(config = {}) {
|
|
|
91
79
|
query: config.query,
|
|
92
80
|
headers: config.headers,
|
|
93
81
|
statusCode: config.statusCode || 200,
|
|
94
|
-
responseType: config.responseType || 'json'
|
|
82
|
+
responseType: config.responseType || 'json',
|
|
83
|
+
validator: config.validator
|
|
95
84
|
}, target, propertyKey);
|
|
96
85
|
};
|
|
97
86
|
}
|
|
@@ -106,7 +95,8 @@ export function Put(config = {}) {
|
|
|
106
95
|
body: config.body,
|
|
107
96
|
bodyFilter: config.bodyFilter || 'none',
|
|
108
97
|
statusCode: config.statusCode || 200,
|
|
109
|
-
responseType: config.responseType || 'json'
|
|
98
|
+
responseType: config.responseType || 'json',
|
|
99
|
+
validator: config.validator
|
|
110
100
|
}, target, propertyKey);
|
|
111
101
|
};
|
|
112
102
|
}
|
|
@@ -121,7 +111,8 @@ export function Patch(config = {}) {
|
|
|
121
111
|
body: config.body,
|
|
122
112
|
bodyFilter: config.bodyFilter || 'none',
|
|
123
113
|
statusCode: config.statusCode || 200,
|
|
124
|
-
responseType: config.responseType || 'json'
|
|
114
|
+
responseType: config.responseType || 'json',
|
|
115
|
+
validator: config.validator
|
|
125
116
|
}, target, propertyKey);
|
|
126
117
|
};
|
|
127
118
|
}
|
|
@@ -136,7 +127,8 @@ export function Delete(config = {}) {
|
|
|
136
127
|
body: config.body,
|
|
137
128
|
bodyFilter: config.bodyFilter || 'none',
|
|
138
129
|
statusCode: config.statusCode || 200,
|
|
139
|
-
responseType: config.responseType || 'json'
|
|
130
|
+
responseType: config.responseType || 'json',
|
|
131
|
+
validator: config.validator
|
|
140
132
|
}, target, propertyKey);
|
|
141
133
|
};
|
|
142
134
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Controller, Post, Get, Put, Delete, Patch, UseMiddleware, UseMiddlewares, Tracing, type Middleware, type Input, type Tracer, type TracerData } from './controller';
|
|
2
|
-
import { Entity, Restrict, VirtualProperty, Serialize, GetterPrefix } from '
|
|
2
|
+
import { Entity, Restrict, VirtualProperty, Serialize, GetterPrefix, BeforeToEntity, AfterToEntity } from '../infra/serialization/entity';
|
|
3
3
|
import { DecorateUseCase, UseCase } from './usecase';
|
|
4
4
|
import { Storage, type StorageGateway } from './services';
|
|
5
|
-
export { Controller, UseMiddleware, UseMiddlewares, Post, Get, Put, Delete, Patch, Tracing, DecorateUseCase, UseCase, Storage, Entity, Restrict, VirtualProperty, Serialize, GetterPrefix, };
|
|
5
|
+
export { Controller, UseMiddleware, UseMiddlewares, Post, Get, Put, Delete, Patch, Tracing, DecorateUseCase, UseCase, Storage, Entity, Restrict, VirtualProperty, Serialize, GetterPrefix, BeforeToEntity, AfterToEntity, };
|
|
6
6
|
export type { Middleware, Input, StorageGateway, Tracer, TracerData };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Controller, Post, Get, Put, Delete, Patch, UseMiddleware, UseMiddlewares, Tracing } from './controller';
|
|
2
|
-
import { Entity, Restrict, VirtualProperty, Serialize, GetterPrefix } from '
|
|
2
|
+
import { Entity, Restrict, VirtualProperty, Serialize, GetterPrefix, BeforeToEntity, AfterToEntity } from '../infra/serialization/entity';
|
|
3
3
|
import { DecorateUseCase, UseCase } from './usecase';
|
|
4
4
|
import { Storage } from './services';
|
|
5
|
-
export { Controller, UseMiddleware, UseMiddlewares, Post, Get, Put, Delete, Patch, Tracing, DecorateUseCase, UseCase, Storage, Entity, Restrict, VirtualProperty, Serialize, GetterPrefix, };
|
|
5
|
+
export { Controller, UseMiddleware, UseMiddlewares, Post, Get, Put, Delete, Patch, Tracing, DecorateUseCase, UseCase, Storage, Entity, Restrict, VirtualProperty, Serialize, GetterPrefix, BeforeToEntity, AfterToEntity, };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { Container } from "../infra/di/registry";
|
|
1
2
|
import 'reflect-metadata';
|
|
2
3
|
export interface StorageGateway {
|
|
3
4
|
save(file: File, path: string): Promise<void>;
|
|
4
5
|
get(key: string): Promise<string>;
|
|
5
6
|
delete(key: string): Promise<boolean>;
|
|
6
7
|
}
|
|
7
|
-
export declare function Storage(gatewayName: string): (target: any, propertyKey: string) => void;
|
|
8
|
+
export declare function Storage(gatewayName: string, container?: Container): (target: any, propertyKey: string) => void;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DefaultContainer } from "../infra/di/registry";
|
|
2
2
|
import 'reflect-metadata';
|
|
3
|
-
export function Storage(gatewayName) {
|
|
3
|
+
export function Storage(gatewayName, container = DefaultContainer) {
|
|
4
4
|
return function (target, propertyKey) {
|
|
5
5
|
let dependency;
|
|
6
6
|
Object.defineProperty(target, propertyKey, {
|
|
7
7
|
get: function () {
|
|
8
8
|
if (!dependency) {
|
|
9
|
-
dependency =
|
|
9
|
+
dependency = container.inject(gatewayName);
|
|
10
10
|
}
|
|
11
11
|
if (!dependency.save || !dependency.get || !dependency.delete) {
|
|
12
12
|
throw new Error(`${gatewayName} is not a valid StorageGateway`);
|