framework-do-dede 3.3.1 → 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
- To install dependencies:
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
- To run:
33
+ Para executar o exemplo (Express e Elysia):
10
34
 
11
35
  ```bash
12
- bun run index.ts
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
- This project was created using `bun init` in bun v1.1.42. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
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, };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "framework-do-dede",
3
- "version": "3.3.1",
3
+ "version": "3.4.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",