framework-do-dede 3.3.0 → 3.4.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
CHANGED
|
@@ -1,15 +1,463 @@
|
|
|
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 (Registry/Inject)
|
|
19
|
+
- Errors
|
|
20
|
+
- Protocolos de Repositório
|
|
21
|
+
- Exemplos
|
|
22
|
+
- Express
|
|
23
|
+
- Elysia
|
|
24
|
+
- Fila com AfterToEntity
|
|
25
|
+
- Testes
|
|
26
|
+
|
|
27
|
+
## Instalação
|
|
4
28
|
|
|
5
29
|
```bash
|
|
6
30
|
bun install
|
|
7
31
|
```
|
|
8
32
|
|
|
9
|
-
|
|
33
|
+
Para executar o exemplo (Express e Elysia):
|
|
10
34
|
|
|
11
35
|
```bash
|
|
12
|
-
bun run
|
|
36
|
+
bun run example/express_app/server.ts
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quickstart
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { Controller, Get, Post, UseCase, Dede } from './src';
|
|
43
|
+
|
|
44
|
+
@Controller('/hello')
|
|
45
|
+
class HelloController {
|
|
46
|
+
@Get({ statusCode: 200 })
|
|
47
|
+
async get() {
|
|
48
|
+
const useCase = new HelloUseCase({ data: undefined });
|
|
49
|
+
return await useCase.execute();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@Post({ statusCode: 201, body: ['name|string'] })
|
|
53
|
+
async post(request: { data: { name: string } }) {
|
|
54
|
+
const useCase = new HelloUseCase({ data: request.data });
|
|
55
|
+
return await useCase.execute();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
class HelloUseCase extends UseCase<{ name?: string }, { message: string }> {
|
|
60
|
+
async execute() {
|
|
61
|
+
return { message: `Hello ${this.data?.name ?? 'world'}` };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await Dede.start({
|
|
66
|
+
framework: { use: 'express', port: 3000 },
|
|
67
|
+
registries: []
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Conceitos
|
|
72
|
+
|
|
73
|
+
### Controllers e Rotas
|
|
74
|
+
|
|
75
|
+
Use decorators para expor métodos como rotas HTTP. O Controller registra o class loader no Registry e o ControllerHandler monta as rotas em runtime.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { Controller, Get, Post, Put, Delete, Patch } from './src';
|
|
79
|
+
|
|
80
|
+
@Controller('/users')
|
|
81
|
+
export class UsersController {
|
|
82
|
+
@Get({ statusCode: 200 })
|
|
83
|
+
async list() { /* ... */ }
|
|
84
|
+
|
|
85
|
+
@Post({ statusCode: 201, body: ['name|string', 'email|string'] })
|
|
86
|
+
async create(request: { data: any }) { /* ... */ }
|
|
87
|
+
|
|
88
|
+
@Put({ params: ['id|string'], body: ['name|string'] })
|
|
89
|
+
async update(request: { data: any }) { /* ... */ }
|
|
90
|
+
|
|
91
|
+
@Delete({ params: ['id|string'] })
|
|
92
|
+
async remove(request: { data: any }) { /* ... */ }
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Decorators disponíveis:
|
|
97
|
+
|
|
98
|
+
- `@Controller(basePath?: string)`
|
|
99
|
+
- `@Get`, `@Post`, `@Put`, `@Patch`, `@Delete`
|
|
100
|
+
|
|
101
|
+
Opções de rota (comuns):
|
|
102
|
+
|
|
103
|
+
- `path`: string
|
|
104
|
+
- `statusCode`: number
|
|
105
|
+
- `params`, `query`, `headers`, `body`: array de strings no formato `campo|tipo`
|
|
106
|
+
- `bodyFilter`: `"restrict" | "none"`
|
|
107
|
+
- `responseType`: `"json" | "text" | "html"`
|
|
108
|
+
|
|
109
|
+
### Input, params e filtros
|
|
110
|
+
|
|
111
|
+
O framework compõe um objeto `request.data` a partir de:
|
|
112
|
+
|
|
113
|
+
1) headers filtrados
|
|
114
|
+
2) params filtrados
|
|
115
|
+
3) query filtrada
|
|
116
|
+
4) body filtrado
|
|
117
|
+
|
|
118
|
+
Quando `bodyFilter: "restrict"`, apenas os campos definidos em `body` serão usados. Caso contrário, o corpo completo é mesclado.
|
|
119
|
+
|
|
120
|
+
Tipos suportados no filtro:
|
|
121
|
+
|
|
122
|
+
- `boolean`, `integer`, `string`, `number`
|
|
123
|
+
|
|
124
|
+
Exemplo:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
@Put({
|
|
128
|
+
params: ['id|string'],
|
|
129
|
+
query: ['active|boolean'],
|
|
130
|
+
headers: ['x-type|string'],
|
|
131
|
+
body: ['name|string'],
|
|
132
|
+
bodyFilter: 'restrict'
|
|
133
|
+
})
|
|
134
|
+
async update(request: { data: any }) {
|
|
135
|
+
// request.data: { id, active, 'x-type', name }
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Suporte a notacao com colchetes:
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{ "user[name]": "Joao", "user[email]": "a@b.com" }
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
vira:
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{ "user": { "name": "Joao", "email": "a@b.com" } }
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Middlewares
|
|
152
|
+
|
|
153
|
+
Middlewares devem implementar `execute(input: Input<any>)`. Podem ser classe, factory ou instancia.
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
import { Middleware, UseMiddleware, UseMiddlewares, Input } from './src';
|
|
157
|
+
|
|
158
|
+
class AuthMiddleware implements Middleware {
|
|
159
|
+
async execute(input: Input<any>) {
|
|
160
|
+
input.context.auth = { userId: 123 };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@Controller('/secure')
|
|
165
|
+
class SecureController {
|
|
166
|
+
@Get()
|
|
167
|
+
@UseMiddleware(AuthMiddleware)
|
|
168
|
+
async get(request: { data: any; context: any }) {
|
|
169
|
+
return { userId: request.context.auth.userId };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Tracing
|
|
175
|
+
|
|
176
|
+
Use `@Tracing` no controller ou em um metodo para capturar metadados de request.
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
import { Tracing, Tracer, TracerData } from './src';
|
|
180
|
+
|
|
181
|
+
class ConsoleTracer implements Tracer<void> {
|
|
182
|
+
trace(data: TracerData) {
|
|
183
|
+
console.log(data);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
@Tracing(new ConsoleTracer())
|
|
188
|
+
@Controller('/trace')
|
|
189
|
+
class TraceController {
|
|
190
|
+
@Get()
|
|
191
|
+
async get() { return { ok: true }; }
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### UseCase e Decorators
|
|
196
|
+
|
|
197
|
+
UseCase provê `data` e `context` do request.
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
import { UseCase } from './src';
|
|
201
|
+
|
|
202
|
+
class CreateUserUseCase extends UseCase<{ name: string }, { id: string }> {
|
|
203
|
+
async execute() {
|
|
204
|
+
return { id: 'new-id' };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Decorator `@DecorateUseCase` permite executar use cases antes do principal (chaining).
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
import { UseCase, DecorateUseCase } from './src';
|
|
213
|
+
|
|
214
|
+
class AuditUseCase extends UseCase<any, void> {
|
|
215
|
+
async execute() { /* audit */ }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
@DecorateUseCase({ useCase: AuditUseCase })
|
|
219
|
+
class CreateUserUseCase extends UseCase<{ name: string }, { id: string }> {
|
|
220
|
+
async execute() { return { id: 'new-id' }; }
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Entity e Serializacao
|
|
225
|
+
|
|
226
|
+
Entities suportam:
|
|
227
|
+
|
|
228
|
+
- `toEntity()` e `toAsyncEntity()`
|
|
229
|
+
- `toData()` e `toAsyncData()`
|
|
230
|
+
- `@Serialize`, `@Restrict`, `@VirtualProperty`, `@GetterPrefix`
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import { Entity, Serialize, Restrict, VirtualProperty, GetterPrefix } from './src';
|
|
234
|
+
|
|
235
|
+
class User extends Entity {
|
|
236
|
+
@Serialize((value: Email) => value.getValue())
|
|
237
|
+
private readonly email: Email;
|
|
238
|
+
|
|
239
|
+
@Restrict()
|
|
240
|
+
private readonly passwordHash: string;
|
|
241
|
+
|
|
242
|
+
@GetterPrefix('has')
|
|
243
|
+
private readonly profile?: Profile;
|
|
244
|
+
|
|
245
|
+
@VirtualProperty('displayName')
|
|
246
|
+
private display() {
|
|
247
|
+
return 'User ' + this.email.getValue();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
constructor(email: string, passwordHash: string) {
|
|
251
|
+
super();
|
|
252
|
+
this.email = new Email(email);
|
|
253
|
+
this.passwordHash = passwordHash;
|
|
254
|
+
this.generateGetters();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const user = new User('a@b.com', 'hash');
|
|
259
|
+
const serialized = user.toEntity();
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Regras principais:
|
|
263
|
+
|
|
264
|
+
- `@Serialize` pode retornar objeto: cada chave vira uma propriedade do resultado
|
|
265
|
+
- `@Restrict` remove campo em `toData`
|
|
266
|
+
- `@VirtualProperty` mapeia metodos para campos virtuais
|
|
267
|
+
- `generateGetters()` cria getters para campos (ex.: `getName`, `isActive`, `hasProfile`)
|
|
268
|
+
|
|
269
|
+
### Hooks Before/After ToEntity
|
|
270
|
+
|
|
271
|
+
Use `@BeforeToEntity()` e `@AfterToEntity()` em metodos de Entities.
|
|
272
|
+
|
|
273
|
+
- Before recebe objeto bruto (antes de serializacao)
|
|
274
|
+
- After recebe objeto tratado (resultado final)
|
|
275
|
+
- `toEntity()` executa hooks sem aguardar promessas
|
|
276
|
+
- `toAsyncEntity()` aguarda hooks async
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
import { Entity, AfterToEntity, BeforeToEntity } from './src';
|
|
280
|
+
|
|
281
|
+
class FileEntity extends Entity {
|
|
282
|
+
private readonly name: string;
|
|
283
|
+
private readonly s3Key: string;
|
|
284
|
+
|
|
285
|
+
constructor(name: string, s3Key: string) {
|
|
286
|
+
super();
|
|
287
|
+
this.name = name;
|
|
288
|
+
this.s3Key = s3Key;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
@BeforeToEntity()
|
|
292
|
+
private before(payload: Record<string, any>) {
|
|
293
|
+
payload.rawTouched = true;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
@AfterToEntity()
|
|
297
|
+
private async after(payload: Record<string, any>) {
|
|
298
|
+
await saveToS3(payload.s3Key);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
13
301
|
```
|
|
14
302
|
|
|
15
|
-
|
|
303
|
+
### Storage Gateway
|
|
304
|
+
|
|
305
|
+
Use `@Storage` para injetar gateways com interface `StorageGateway`.
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
import { Storage, StorageGateway } from './src';
|
|
309
|
+
|
|
310
|
+
class S3Gateway implements StorageGateway {
|
|
311
|
+
async save(file: File, path: string) { /* ... */ }
|
|
312
|
+
async get(key: string) { return 'url'; }
|
|
313
|
+
async delete(key: string) { return true; }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
class FileService {
|
|
317
|
+
@Storage('S3Gateway')
|
|
318
|
+
private readonly storage!: StorageGateway;
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### DI (Registry/Inject)
|
|
323
|
+
|
|
324
|
+
Registre dependencias ao iniciar o server:
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
import { Dede } from './src';
|
|
328
|
+
|
|
329
|
+
class UserRepository { /* ... */ }
|
|
330
|
+
|
|
331
|
+
await Dede.start({
|
|
332
|
+
framework: { use: 'express', port: 3000 },
|
|
333
|
+
registries: [
|
|
334
|
+
{ name: 'UserRepository', classLoader: UserRepository }
|
|
335
|
+
]
|
|
336
|
+
});
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Use `@Inject('Name')` para injetar dependencias:
|
|
340
|
+
|
|
341
|
+
```ts
|
|
342
|
+
import { Inject, UseCase } from './src';
|
|
343
|
+
|
|
344
|
+
class ExampleUseCase extends UseCase<void, any> {
|
|
345
|
+
@Inject('UserRepository')
|
|
346
|
+
private readonly userRepository!: any;
|
|
347
|
+
|
|
348
|
+
async execute() {
|
|
349
|
+
return await this.userRepository.findById('1');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Errors
|
|
355
|
+
|
|
356
|
+
Erros de dominio disponiveis:
|
|
357
|
+
|
|
358
|
+
- `BadRequest` (400)
|
|
359
|
+
- `Unauthorized` (401)
|
|
360
|
+
- `Forbidden` (403)
|
|
361
|
+
- `NotFound` (404)
|
|
362
|
+
- `Conflict` (409)
|
|
363
|
+
- `UnprocessableEntity` (422)
|
|
364
|
+
- `InternalServerError` (500)
|
|
365
|
+
|
|
366
|
+
Quando um erro e lancado, o handler padroniza a resposta. Se o erro for `CustomServerError`, o payload customizado sera retornado diretamente.
|
|
367
|
+
|
|
368
|
+
### Protocolos de Repositorio
|
|
369
|
+
|
|
370
|
+
Interfaces tipadas para padrao de repositorio:
|
|
371
|
+
|
|
372
|
+
- `RepositoryCreate<T extends Entity>`
|
|
373
|
+
- `RepositoryUpdate<T extends Entity>`
|
|
374
|
+
- `RepositoryRemove`
|
|
375
|
+
- `RepositoryRestore<T extends Entity>`
|
|
376
|
+
- `RepositoryRemoveBy<T>`
|
|
377
|
+
- `RepositoryRestoreBy<T>`
|
|
378
|
+
- `RepositoryExistsBy<T>`
|
|
379
|
+
- `RepositoryNotExistsBy<T>`
|
|
380
|
+
- `RepositoryPagination<T>`
|
|
381
|
+
|
|
382
|
+
## Exemplos
|
|
383
|
+
|
|
384
|
+
### Express
|
|
385
|
+
|
|
386
|
+
```ts
|
|
387
|
+
import { Dede } from './src/dede';
|
|
388
|
+
import './example/express_app/example.controller';
|
|
389
|
+
|
|
390
|
+
class UserRepository {
|
|
391
|
+
async findById(id: string) {
|
|
392
|
+
return { id, name: 'John Doe' };
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
await Dede.start({
|
|
397
|
+
framework: { use: 'express', port: 3000 },
|
|
398
|
+
registries: [{ name: 'UserRepository', classLoader: UserRepository }]
|
|
399
|
+
});
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Elysia
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
import { Dede } from './src/dede';
|
|
406
|
+
import './example/express_app/example.controller';
|
|
407
|
+
|
|
408
|
+
await Dede.start({
|
|
409
|
+
framework: { use: 'elysia', port: 3001 },
|
|
410
|
+
registries: []
|
|
411
|
+
});
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### Fila com AfterToEntity
|
|
415
|
+
|
|
416
|
+
```ts
|
|
417
|
+
import { Entity, AfterToEntity } from './src/application/entity';
|
|
418
|
+
|
|
419
|
+
type QueueJob = { type: string; payload: Record<string, any> };
|
|
420
|
+
|
|
421
|
+
type Queue = { enqueue(job: QueueJob): Promise<void> };
|
|
422
|
+
|
|
423
|
+
const queue: Queue = {
|
|
424
|
+
async enqueue(job) {
|
|
425
|
+
console.log('queued job', job);
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
class FileEntity extends Entity {
|
|
430
|
+
private readonly name: string;
|
|
431
|
+
private readonly s3Key: string;
|
|
432
|
+
|
|
433
|
+
private constructor({ name, s3Key }: { name: string; s3Key: string }) {
|
|
434
|
+
super();
|
|
435
|
+
this.name = name;
|
|
436
|
+
this.s3Key = s3Key;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
@AfterToEntity()
|
|
440
|
+
private async enqueueFileSync(payload: Record<string, any>) {
|
|
441
|
+
await queue.enqueue({
|
|
442
|
+
type: 'files.create',
|
|
443
|
+
payload: {
|
|
444
|
+
name: payload.name,
|
|
445
|
+
s3Key: payload.s3Key
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
static create(input: { name: string; s3Key: string }) {
|
|
451
|
+
return new FileEntity(input);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const entity = FileEntity.create({ name: 'report', s3Key: 's3://bucket/report.pdf' });
|
|
456
|
+
const serialized = entity.toEntity();
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
## Testes
|
|
460
|
+
|
|
461
|
+
```bash
|
|
462
|
+
npm test -- tests/src/application/entity.spec.ts --runInBand
|
|
463
|
+
```
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export declare abstract class Entity {
|
|
2
2
|
[x: string]: any;
|
|
3
|
+
private buildRawEntityObject;
|
|
4
|
+
private getEntityHooks;
|
|
5
|
+
private runEntityHooks;
|
|
3
6
|
toEntity(): Record<string, any>;
|
|
4
7
|
toAsyncEntity(): Promise<Record<string, any>>;
|
|
5
8
|
toData({ serialize }?: {
|
|
@@ -14,3 +17,5 @@ export declare function Restrict(): (target: any, propertyKey: string) => void;
|
|
|
14
17
|
export declare function VirtualProperty(propertyName: string): (target: any, methodName: string) => void;
|
|
15
18
|
export declare function Serialize(callback: (value: any) => any): PropertyDecorator;
|
|
16
19
|
export declare function GetterPrefix(prefix: string): (target: any, propertyKey: string) => void;
|
|
20
|
+
export declare function BeforeToEntity(): MethodDecorator;
|
|
21
|
+
export declare function AfterToEntity(): MethodDecorator;
|
|
@@ -1,5 +1,60 @@
|
|
|
1
1
|
export class Entity {
|
|
2
|
+
buildRawEntityObject() {
|
|
3
|
+
const result = {};
|
|
4
|
+
for (const [propName] of Object.entries(this)) {
|
|
5
|
+
let value = this[propName];
|
|
6
|
+
if (typeof value === 'function')
|
|
7
|
+
continue;
|
|
8
|
+
if (value === undefined)
|
|
9
|
+
continue;
|
|
10
|
+
result[propName] = value;
|
|
11
|
+
}
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
getEntityHooks(hookKey) {
|
|
15
|
+
const hooks = [];
|
|
16
|
+
let current = this.constructor;
|
|
17
|
+
while (current && current !== Entity) {
|
|
18
|
+
const currentHooks = current[hookKey];
|
|
19
|
+
if (currentHooks && currentHooks.length) {
|
|
20
|
+
hooks.unshift(...currentHooks);
|
|
21
|
+
}
|
|
22
|
+
current = Object.getPrototypeOf(current);
|
|
23
|
+
}
|
|
24
|
+
return hooks;
|
|
25
|
+
}
|
|
26
|
+
runEntityHooks(hookKey, payload, awaitHooks) {
|
|
27
|
+
const hooks = this.getEntityHooks(hookKey);
|
|
28
|
+
if (!hooks.length)
|
|
29
|
+
return;
|
|
30
|
+
if (awaitHooks) {
|
|
31
|
+
return (async () => {
|
|
32
|
+
for (const hookName of hooks) {
|
|
33
|
+
const hook = this[hookName];
|
|
34
|
+
if (typeof hook !== 'function')
|
|
35
|
+
continue;
|
|
36
|
+
await hook.call(this, payload);
|
|
37
|
+
}
|
|
38
|
+
})();
|
|
39
|
+
}
|
|
40
|
+
for (const hookName of hooks) {
|
|
41
|
+
const hook = this[hookName];
|
|
42
|
+
if (typeof hook !== 'function')
|
|
43
|
+
continue;
|
|
44
|
+
try {
|
|
45
|
+
const result = hook.call(this, payload);
|
|
46
|
+
if (result && typeof result.then === 'function') {
|
|
47
|
+
void result.catch(() => undefined);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
2
55
|
toEntity() {
|
|
56
|
+
const raw = this.buildRawEntityObject();
|
|
57
|
+
this.runEntityHooks(BEFORE_TO_ENTITY, raw, false);
|
|
3
58
|
// @ts-ignore
|
|
4
59
|
const propertiesConfigs = this.constructor.propertiesConfigs;
|
|
5
60
|
const result = {};
|
|
@@ -31,9 +86,12 @@ export class Entity {
|
|
|
31
86
|
value = null;
|
|
32
87
|
result[propertyName] = value;
|
|
33
88
|
}
|
|
89
|
+
this.runEntityHooks(AFTER_TO_ENTITY, result, false);
|
|
34
90
|
return result;
|
|
35
91
|
}
|
|
36
92
|
async toAsyncEntity() {
|
|
93
|
+
const raw = this.buildRawEntityObject();
|
|
94
|
+
await this.runEntityHooks(BEFORE_TO_ENTITY, raw, true);
|
|
37
95
|
// @ts-ignore
|
|
38
96
|
const propertiesConfigs = this.constructor.propertiesConfigs;
|
|
39
97
|
const result = {};
|
|
@@ -65,6 +123,7 @@ export class Entity {
|
|
|
65
123
|
value = null;
|
|
66
124
|
result[propertyName] = value;
|
|
67
125
|
}
|
|
126
|
+
await this.runEntityHooks(AFTER_TO_ENTITY, result, true);
|
|
68
127
|
return result;
|
|
69
128
|
}
|
|
70
129
|
toData({ serialize = false } = {}) {
|
|
@@ -178,3 +237,26 @@ const loadPropertiesConfig = (target, propertyKey) => {
|
|
|
178
237
|
target.constructor.propertiesConfigs[propertyKey] = {};
|
|
179
238
|
}
|
|
180
239
|
};
|
|
240
|
+
const BEFORE_TO_ENTITY = Symbol('beforeToEntity');
|
|
241
|
+
const AFTER_TO_ENTITY = Symbol('afterToEntity');
|
|
242
|
+
const assertEntityDecoratorTarget = (target, decoratorName) => {
|
|
243
|
+
if (!Entity.prototype.isPrototypeOf(target)) {
|
|
244
|
+
throw new Error(`${decoratorName} can only be used on Entity classes`);
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
export function BeforeToEntity() {
|
|
248
|
+
return function (target, propertyKey) {
|
|
249
|
+
assertEntityDecoratorTarget(target, 'BeforeToEntity');
|
|
250
|
+
const cls = target.constructor;
|
|
251
|
+
cls[BEFORE_TO_ENTITY] = cls[BEFORE_TO_ENTITY] || [];
|
|
252
|
+
cls[BEFORE_TO_ENTITY].push(propertyKey);
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
export function AfterToEntity() {
|
|
256
|
+
return function (target, propertyKey) {
|
|
257
|
+
assertEntityDecoratorTarget(target, 'AfterToEntity');
|
|
258
|
+
const cls = target.constructor;
|
|
259
|
+
cls[AFTER_TO_ENTITY] = cls[AFTER_TO_ENTITY] || [];
|
|
260
|
+
cls[AFTER_TO_ENTITY].push(propertyKey);
|
|
261
|
+
};
|
|
262
|
+
}
|
|
@@ -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 './entity';
|
|
2
|
+
import { Entity, Restrict, VirtualProperty, Serialize, GetterPrefix, BeforeToEntity, AfterToEntity } from './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 './entity';
|
|
2
|
+
import { Entity, Restrict, VirtualProperty, Serialize, GetterPrefix, BeforeToEntity, AfterToEntity } from './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, };
|