@koalarx/nest 4.0.5 → 4.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/cli/commands/new/configure-test-runner.js +13 -2
  2. package/cli/commands/new/create-ddd-structure.js +2 -4
  3. package/cli/constants/cli-project-checklist.js +27 -3
  4. package/cli/constants/core-packages.js +4 -6
  5. package/cli/utils/install-module.js +15 -7
  6. package/cli/utils/install-workspace-config.js +2 -0
  7. package/cli/utils/parse-new-args.js +0 -1
  8. package/cli/utils/patch-app-test-module.js +29 -0
  9. package/cli/utils/patch-env.js +34 -0
  10. package/cli/utils/patch-main.js +3 -4
  11. package/cli/utils/remove-sample-parts.js +11 -5
  12. package/koala-nest/.env.example +12 -0
  13. package/koala-nest/.vscode/tasks.json +9 -0
  14. package/koala-nest/src/application/common/pagination.request.ts +0 -5
  15. package/koala-nest/src/application/common/request-validator.base.ts +36 -1
  16. package/koala-nest/src/application/mapping/person.mapper.ts +0 -9
  17. package/koala-nest/src/application/person/jobs/cron/delete-inactive.job.ts +34 -17
  18. package/koala-nest/src/application/person/read-many/read-many-person.response.ts +2 -13
  19. package/koala-nest/src/core/background-services/cron-service/cron-job.handler.base.ts +36 -15
  20. package/koala-nest/src/core/background-services/event-service/event-queue.ts +2 -3
  21. package/koala-nest/src/core/common/list-response.base.ts +10 -0
  22. package/koala-nest/src/core/env.ts +5 -0
  23. package/koala-nest/src/core/http/rate-limit.middleware.ts +36 -0
  24. package/koala-nest/src/core/tools/mapping/auto-map.ts +27 -3
  25. package/koala-nest/src/core/tools/mapping/auto-mapper.ts +30 -12
  26. package/koala-nest/src/core/tools/mapping/mapping-store.ts +35 -29
  27. package/koala-nest/src/core/utils/cron-expression-to-boolean.ts +32 -17
  28. package/koala-nest/src/core/utils/hash-password.ts +7 -2
  29. package/koala-nest/src/core/utils/initialize-undefined-array-props.ts +20 -0
  30. package/koala-nest/src/core/utils/resolve-cors-origins.ts +24 -0
  31. package/koala-nest/src/domain/dtos/pagination.dto.ts +87 -2
  32. package/koala-nest/src/domain/dtos/person-query.dto.ts +8 -0
  33. package/koala-nest/src/domain/entities/person/person.ts +0 -2
  34. package/koala-nest/src/host/bootstrap/apply-http-middleware.ts +22 -0
  35. package/koala-nest/src/host/main.ts +14 -14
  36. package/koala-nest/src/infra/common/redis-cache.service.ts +15 -4
  37. package/koala-nest/src/infra/repositories/person.repository.ts +3 -3
  38. package/koala-nest/src/infra/repositories/repository.base.ts +70 -1
  39. package/koala-nest/src/test/application/delete-inactive.job.spec.ts +23 -10
  40. package/koala-nest/src/test/application/read-many-person.handler.spec.ts +42 -2
  41. package/koala-nest/src/test/application/update-person.handler.spec.ts +42 -0
  42. package/koala-nest/src/test/core/cron-expression-to-boolean.spec.ts +20 -1
  43. package/koala-nest/src/test/core/env.spec.ts +19 -0
  44. package/koala-nest/src/test/core/http/rate-limit.middleware.spec.ts +62 -0
  45. package/koala-nest/src/test/core/initialize-undefined-array-props.spec.ts +63 -0
  46. package/koala-nest/src/test/core/mapping.spec.ts +76 -0
  47. package/koala-nest/src/test/core/pagination-typeorm.spec.ts +74 -0
  48. package/koala-nest/src/test/core/resolve-cors-origins.spec.ts +22 -0
  49. package/koala-nest/src/test/core/sync-improvements.spec.ts +39 -0
  50. package/koala-nest/src/test/host/controllers/app/app.e2e.spec.ts +24 -0
  51. package/koala-nest/src/test/host/controllers/person/lazy-loading.e2e.spec.ts +20 -60
  52. package/koala-nest/src/test/infra/redis-cache.service.spec.ts +8 -0
  53. package/koala-nest/src/test/utils/configure-test-app.ts +2 -8
  54. package/koala-nest/src/test/utils/create-e2e-database.ts +17 -1
  55. package/koala-nest/src/test/utils/in-memory-base.repository.ts +80 -0
  56. package/koala-nest/vitest.config.e2e.ts +13 -0
  57. package/koala-nest/vitest.config.ts +12 -0
  58. package/package.json +1 -1
  59. package/koala-nest/src/core/utils/icomparable.ts +0 -1
@@ -1,6 +1,9 @@
1
1
  import { ILoggingService } from '@/domain/common/ilogging.service';
2
2
  import { IRedLockService } from '@/domain/common/ired-lock.service';
3
- import { cronExpressionToBoolean } from '@/core/utils/cron-expression-to-boolean';
3
+ import {
4
+ cronExpressionToBoolean,
5
+ getCronExecutionKey,
6
+ } from '@/core/utils/cron-expression-to-boolean';
4
7
  import { DEFAULT_CRON_POLL_MINUTES } from '@/core/constants/cron.constants';
5
8
  import { MINUTES_TO_MS, MS_TO_SECONDS } from '@/core/utils/time.constants';
6
9
  import { delay } from '@koalarx/utils/KlDelay';
@@ -9,6 +12,7 @@ import { reportErrorToLogging } from '@/core/utils/report-error';
9
12
  export interface CronJobSettings {
10
13
  isActive: boolean;
11
14
  timeInMinutes: number;
15
+ cronExpression?: string;
12
16
  }
13
17
 
14
18
  export function cronJobSettings(
@@ -18,10 +22,13 @@ export function cronJobSettings(
18
22
  return {
19
23
  isActive: cronExpressionToBoolean(cronExpression),
20
24
  timeInMinutes,
25
+ cronExpression,
21
26
  };
22
27
  }
23
28
 
24
29
  export abstract class CronJobHandlerBase {
30
+ private lastExecutedCronKey: string | null = null;
31
+
25
32
  constructor(
26
33
  private readonly redlockService: IRedLockService,
27
34
  private readonly loggingService: ILoggingService,
@@ -39,22 +46,36 @@ export abstract class CronJobHandlerBase {
39
46
  const timeout = settings.timeInMinutes * MINUTES_TO_MS;
40
47
 
41
48
  if (settings.isActive) {
42
- const ttlSecondsLock = timeout / MS_TO_SECONDS;
43
- const acquiredLock = await this.redlockService.acquiredLock(
44
- name,
45
- ttlSecondsLock,
46
- );
49
+ const executionKey = settings.cronExpression
50
+ ? getCronExecutionKey(settings.cronExpression)
51
+ : null;
52
+
53
+ const alreadyExecutedThisTick =
54
+ executionKey !== null &&
55
+ executionKey === this.lastExecutedCronKey;
56
+
57
+ if (!alreadyExecutedThisTick) {
58
+ const ttlSecondsLock = timeout / MS_TO_SECONDS;
59
+ const acquiredLock = await this.redlockService.acquiredLock(
60
+ name,
61
+ ttlSecondsLock,
62
+ );
63
+
64
+ if (acquiredLock) {
65
+ try {
66
+ await this.run();
67
+ } catch (error) {
68
+ const reportError =
69
+ error instanceof Error ? error : new Error(String(error));
47
70
 
48
- if (acquiredLock) {
49
- try {
50
- await this.run();
51
- } catch (error) {
52
- const reportError =
53
- error instanceof Error ? error : new Error(String(error));
71
+ await reportErrorToLogging(this.loggingService, reportError);
72
+ } finally {
73
+ if (executionKey !== null) {
74
+ this.lastExecutedCronKey = executionKey;
75
+ }
54
76
 
55
- await reportErrorToLogging(this.loggingService, reportError);
56
- } finally {
57
- await this.redlockService.releaseLock(name);
77
+ await this.redlockService.releaseLock(name);
78
+ }
58
79
  }
59
80
  }
60
81
  }
@@ -1,4 +1,3 @@
1
- import { IComparableId } from '@/core/utils/icomparable';
2
1
  import { EventClass } from './event-class';
3
2
  import { EventJob } from './event-job';
4
3
 
@@ -62,7 +61,7 @@ export class EventQueue {
62
61
  }
63
62
  }
64
63
 
65
- static dispatchEventsForAggregate(id: IComparableId) {
64
+ static dispatchEventsForAggregate(id: string) {
66
65
  const aggregate = this.findMarkedAggregateByID(id);
67
66
 
68
67
  if (aggregate) {
@@ -96,7 +95,7 @@ export class EventQueue {
96
95
  }
97
96
 
98
97
  static findMarkedAggregateByID(
99
- id: IComparableId,
98
+ id: string,
100
99
  ): EventJob<unknown> | undefined {
101
100
  return this.markedAggregates.find((aggregate) => aggregate._id === id);
102
101
  }
@@ -0,0 +1,10 @@
1
+ import { ObjectClass } from '@/core/base/object-class';
2
+ import { AutoMap } from '@/core/tools/mapping';
3
+
4
+ export class ListResponseBase<T> extends ObjectClass<ListResponseBase<T>> {
5
+ @AutoMap()
6
+ items!: T[];
7
+
8
+ @AutoMap()
9
+ count!: number;
10
+ }
@@ -8,12 +8,17 @@ import { z } from 'zod';
8
8
 
9
9
  export const envSchema = z.object({
10
10
  PORT: z.coerce.number().default(3000),
11
+ HOST: z.string().default('0.0.0.0'),
11
12
  NODE_ENV: z.enum(['test', 'develop', 'staging', 'production']),
12
13
  DATABASE_URL: z.string(),
13
14
  REDIS_CONNECTION_STRING: z.string().optional(),
14
15
  CACHE_KEY_PREFIX: z.string().optional(),
15
16
  CRON_JOBS_ENABLED: envBooleanSchema(false),
16
17
  BOOTSTRAP_DELAY_MS: z.coerce.number().default(0),
18
+ RATE_LIMIT_MAX: z.coerce.number().default(0),
19
+ RATE_LIMIT_WINDOW_MS: z.coerce.number().default(60_000),
20
+ CORS_ORIGINS: z.string().optional(),
21
+ BCRYPT_ROUNDS: z.coerce.number().min(4).max(15).default(10),
17
22
  JWT_PRIVATE_KEY: z.string().optional(),
18
23
  JWT_PUBLIC_KEY: z.string().optional(),
19
24
  JWT_ACCESS_TOKEN_EXPIRES_IN: z.string().default('15m'),
@@ -0,0 +1,36 @@
1
+ import type { NextFunction, Request, Response } from 'express';
2
+
3
+ export interface RateLimitOptions {
4
+ windowMs: number;
5
+ maxRequests: number;
6
+ }
7
+
8
+ export function createRateLimitMiddleware(options: RateLimitOptions) {
9
+ const hits = new Map<string, number[]>();
10
+
11
+ return (req: Request, res: Response, next: NextFunction) => {
12
+ if (options.maxRequests <= 0) {
13
+ next();
14
+ return;
15
+ }
16
+
17
+ const key = req.ip ?? req.socket.remoteAddress ?? 'unknown';
18
+ const now = Date.now();
19
+ const windowStart = now - options.windowMs;
20
+ const timestamps = (hits.get(key) ?? []).filter(
21
+ (timestamp) => timestamp > windowStart,
22
+ );
23
+
24
+ if (timestamps.length >= options.maxRequests) {
25
+ res.status(429).json({
26
+ statusCode: 429,
27
+ message: 'Muitas requisições. Tente novamente em instantes.',
28
+ });
29
+ return;
30
+ }
31
+
32
+ timestamps.push(now);
33
+ hits.set(key, timestamps);
34
+ next();
35
+ };
36
+ }
@@ -3,12 +3,36 @@ import { MappingStore } from './mapping-store';
3
3
 
4
4
  interface AutoMapConfig<T> {
5
5
  type?: () => Type<T>;
6
+ isArray?: boolean;
6
7
  }
7
8
 
8
9
  export function AutoMap<T>(config?: AutoMapConfig<T>) {
9
- return function (target: any, propertyKey: string) {
10
- const compositionType: (() => any) | undefined = config?.type;
10
+ return function (target: object, propertyKey: string) {
11
+ const designType = Reflect.getMetadata('design:type', target, propertyKey);
12
+ const isArray = config?.isArray ?? designType === Array;
11
13
 
12
- MappingStore.setProp(target.constructor, propertyKey, compositionType);
14
+ if (config?.type) {
15
+ if (isArray) {
16
+ Reflect.defineMetadata(
17
+ 'composition:type',
18
+ config.type,
19
+ target,
20
+ propertyKey,
21
+ );
22
+ } else if (!designType) {
23
+ Reflect.defineMetadata(
24
+ 'design:type',
25
+ config.type(),
26
+ target,
27
+ propertyKey,
28
+ );
29
+ }
30
+ }
31
+
32
+ MappingStore.setProp(
33
+ (target as { constructor: Type<unknown> }).constructor,
34
+ propertyKey,
35
+ config?.type,
36
+ );
13
37
  };
14
38
  }
@@ -1,3 +1,4 @@
1
+ import { initializeUndefinedArrayProps } from '@/core/utils/initialize-undefined-array-props';
1
2
  import { Type } from '@nestjs/common';
2
3
  import { MappingStore } from './mapping-store';
3
4
 
@@ -25,7 +26,6 @@ export class AutoMapper {
25
26
 
26
27
  static map<S, R>(data: any, source: Type<S>, target: Type<R>): R {
27
28
  const mapping = MappingStore.getMapping(source, target);
28
-
29
29
  const targetInstance = new target();
30
30
 
31
31
  if (!mapping) {
@@ -70,9 +70,14 @@ export class AutoMapper {
70
70
  }
71
71
 
72
72
  const targetClass: Type<any> = this.toClass(targetPropType);
73
+ const sourceValue = data[sourceProp.name];
74
+
75
+ if (sourceValue === undefined) {
76
+ continue;
77
+ }
73
78
 
74
79
  targetInstance[targetPropName] = this.map(
75
- data[sourceProp.name],
80
+ sourceValue,
76
81
  sourcePropTypeClass,
77
82
  targetClass,
78
83
  );
@@ -80,23 +85,36 @@ export class AutoMapper {
80
85
  }
81
86
 
82
87
  if (sourceProp.isArray) {
83
- targetInstance[targetPropName] = data[sourceProp.name].map(
84
- (item: unknown) => {
85
- const targetItemType = targetProps.find(
86
- (p) => p.name === targetPropName,
87
- )!.type;
88
+ const sourceArray = data[sourceProp.name];
88
89
 
89
- const targetClassType = this.toClass(targetItemType);
90
+ if (sourceArray === undefined) {
91
+ continue;
92
+ }
90
93
 
91
- return this.map(item, sourcePropTypeClass, targetClassType);
92
- },
93
- );
94
+ targetInstance[targetPropName] = sourceArray.map((item: unknown) => {
95
+ const targetItemType = targetProps.find(
96
+ (p) => p.name === targetPropName,
97
+ )!.type;
98
+
99
+ const targetClassType = this.toClass(targetItemType);
100
+
101
+ return this.map(item, sourcePropTypeClass, targetClassType);
102
+ });
94
103
  continue;
95
104
  }
96
105
 
97
- targetInstance[targetPropName] = data[sourceProp.name];
106
+ const sourceValue = data[sourceProp.name];
107
+
108
+ if (sourceValue !== undefined) {
109
+ targetInstance[targetPropName] = sourceValue;
110
+ }
98
111
  }
99
112
 
113
+ initializeUndefinedArrayProps(
114
+ targetInstance as Record<string, unknown>,
115
+ target,
116
+ );
117
+
100
118
  return targetInstance;
101
119
  }
102
120
  }
@@ -30,8 +30,15 @@ export class MappingStore {
30
30
  entity.prototype,
31
31
  propName,
32
32
  );
33
- const isArray = propType === Array;
34
- const type = isArray ? (compositionType ?? propType) : propType;
33
+ const explicitComposition = Reflect.getMetadata(
34
+ 'composition:type',
35
+ entity.prototype,
36
+ propName,
37
+ );
38
+ const isArray = propType === Array || explicitComposition != null;
39
+ const type = isArray
40
+ ? (compositionType ?? explicitComposition ?? propType)
41
+ : propType;
35
42
 
36
43
  const props = this._entities.get(entity) || [];
37
44
 
@@ -57,32 +64,44 @@ export class MappingStore {
57
64
  }
58
65
  }
59
66
 
60
- current = Object.getPrototypeOf(current) as Type<any> | null;
67
+ const parentPrototype = Object.getPrototypeOf(current.prototype);
68
+ current = (parentPrototype?.constructor as Type<any> | undefined) ?? null;
61
69
  }
62
70
 
63
71
  return props;
64
72
  }
65
73
 
74
+ static getAllProps(entity: Type<any>) {
75
+ return this.getProps(entity);
76
+ }
77
+
66
78
  static getPropType(entity: Type<any>, propName: string) {
67
- const prop = this._entities.get(entity)?.find((p) => p.name === propName);
79
+ let current: Type<any> | null = entity;
68
80
 
69
- if (!prop) {
70
- throw new Error(
71
- `Property ${propName} not found in entity ${entity.name}`,
72
- );
73
- }
81
+ while (current?.prototype) {
82
+ const prop = this._entities.get(current)?.find((p) => p.name === propName);
74
83
 
75
- const { type, isArray } = prop;
84
+ if (prop) {
85
+ const { type, isArray } = prop;
76
86
 
77
- if (!type) {
78
- return null;
79
- }
87
+ if (!type) {
88
+ return null;
89
+ }
80
90
 
81
- if (isArray && type instanceof Function) {
82
- return (type as () => Type<any>)();
91
+ if (isArray && type instanceof Function) {
92
+ return (type as () => Type<any>)();
93
+ }
94
+
95
+ return type;
96
+ }
97
+
98
+ const parentPrototype = Object.getPrototypeOf(current.prototype);
99
+ current = (parentPrototype?.constructor as Type<any> | undefined) ?? null;
83
100
  }
84
101
 
85
- return type;
102
+ throw new Error(
103
+ `Property ${propName} not found in entity ${entity.name}`,
104
+ );
86
105
  }
87
106
 
88
107
  static add(
@@ -105,17 +124,4 @@ export class MappingStore {
105
124
  static getMapping(source: Type<any>, target: Type<any>) {
106
125
  return this._mappings.get(source)?.get(target) ?? null;
107
126
  }
108
-
109
- /** Compatibilidade com chave legada usada em testes. */
110
- static getMappingByName(mapName: string) {
111
- for (const targetMappings of this._mappings.values()) {
112
- for (const mapping of targetMappings.values()) {
113
- if (`${mapping.source.name}To${mapping.target.name}` === mapName) {
114
- return mapping;
115
- }
116
- }
117
- }
118
-
119
- return null;
120
- }
121
127
  }
@@ -1,5 +1,36 @@
1
1
  import { CronExpressionParser } from 'cron-parser';
2
2
 
3
+ /**
4
+ * Retorna uma chave única do tick cron atual, ou null se o instante não coincide.
5
+ */
6
+ export function getCronExecutionKey(
7
+ cronExpression: string,
8
+ now: Date = new Date(),
9
+ ): string | null {
10
+ try {
11
+ const interval = CronExpressionParser.parse(cronExpression, {
12
+ currentDate: now,
13
+ });
14
+ const prev = interval.prev();
15
+
16
+ const matches =
17
+ prev.getFullYear() === now.getFullYear() &&
18
+ prev.getMonth() === now.getMonth() &&
19
+ prev.getDate() === now.getDate() &&
20
+ prev.getHours() === now.getHours() &&
21
+ prev.getMinutes() === now.getMinutes() &&
22
+ prev.getSeconds() === now.getSeconds();
23
+
24
+ if (!matches) {
25
+ return null;
26
+ }
27
+
28
+ return prev.toISOString();
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
3
34
  /**
4
35
  * Verifica se a expressão cron corresponde ao momento atual.
5
36
  *
@@ -45,21 +76,5 @@ export function cronExpressionToBoolean(
45
76
  cronExpression: string,
46
77
  now: Date = new Date(),
47
78
  ): boolean {
48
- try {
49
- const interval = CronExpressionParser.parse(cronExpression, {
50
- currentDate: now,
51
- });
52
- const prev = interval.prev();
53
-
54
- return (
55
- prev.getFullYear() === now.getFullYear() &&
56
- prev.getMonth() === now.getMonth() &&
57
- prev.getDate() === now.getDate() &&
58
- prev.getHours() === now.getHours() &&
59
- prev.getMinutes() === now.getMinutes() &&
60
- prev.getSeconds() === now.getSeconds()
61
- );
62
- } catch {
63
- return false;
64
- }
79
+ return getCronExecutionKey(cronExpression, now) !== null;
65
80
  }
@@ -1,5 +1,10 @@
1
1
  import { hash } from 'bcrypt';
2
2
 
3
- export function hashPassword(password: string): Promise<string> {
4
- return hash(password, 6);
3
+ const DEFAULT_BCRYPT_ROUNDS = 10;
4
+
5
+ export function hashPassword(
6
+ password: string,
7
+ rounds = Number(process.env.BCRYPT_ROUNDS) || DEFAULT_BCRYPT_ROUNDS,
8
+ ): Promise<string> {
9
+ return hash(password, rounds);
5
10
  }
@@ -0,0 +1,20 @@
1
+ import type { Type } from '@nestjs/common';
2
+ import { MappingStore } from '@/core/tools/mapping/mapping-store';
3
+
4
+ export function initializeUndefinedArrayProps(
5
+ target: Record<string, unknown>,
6
+ entity: Type<unknown>,
7
+ onlyProps?: string[],
8
+ ): void {
9
+ MappingStore.getAllProps(entity).forEach((prop) => {
10
+ if (onlyProps && !onlyProps.includes(prop.name)) {
11
+ return;
12
+ }
13
+
14
+ if (target[prop.name] !== undefined || !prop.isArray) {
15
+ return;
16
+ }
17
+
18
+ target[prop.name] = [];
19
+ });
20
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * CORS aberto por padrão (`true` = reflete a origin da requisição).
3
+ * Defina `CORS_ORIGINS` apenas quando quiser restringir a origens específicas.
4
+ */
5
+ export function resolveCorsOrigin(
6
+ origins: string | undefined,
7
+ ): boolean | string | string[] {
8
+ const normalized = origins?.trim();
9
+
10
+ if (!normalized) {
11
+ return true;
12
+ }
13
+
14
+ const list = normalized
15
+ .split(',')
16
+ .map((origin) => origin.trim())
17
+ .filter(Boolean);
18
+
19
+ if (list.length === 1) {
20
+ return list[0];
21
+ }
22
+
23
+ return list;
24
+ }
@@ -1,6 +1,61 @@
1
1
  import { QUERY_FILTER_PARAMS } from '@/core/constants/query-params';
2
2
  import { AutoMap } from '@/core/tools/mapping';
3
3
  import type { QueryDirectionType } from '@/core/types';
4
+ import type {
5
+ FindOptionsOrder,
6
+ ObjectLiteral,
7
+ SelectQueryBuilder,
8
+ } from 'typeorm';
9
+
10
+ type OrderDirection = 'ASC' | 'DESC';
11
+
12
+ function normalizeDirection(
13
+ direction?: QueryDirectionType | string,
14
+ ): OrderDirection {
15
+ return String(direction ?? 'asc').toLowerCase() === 'desc' ? 'DESC' : 'ASC';
16
+ }
17
+
18
+ function flattenOrderBy(
19
+ order: Record<string, unknown>,
20
+ prefix = '',
21
+ ): Array<{ path: string; direction: OrderDirection }> {
22
+ const entries: Array<{ path: string; direction: OrderDirection }> = [];
23
+
24
+ for (const [key, value] of Object.entries(order)) {
25
+ const path = prefix ? `${prefix}.${key}` : key;
26
+
27
+ if (typeof value === 'string') {
28
+ entries.push({ path, direction: normalizeDirection(value) });
29
+ continue;
30
+ }
31
+
32
+ if (value && typeof value === 'object') {
33
+ entries.push(...flattenOrderBy(value as Record<string, unknown>, path));
34
+ }
35
+ }
36
+
37
+ return entries;
38
+ }
39
+
40
+ function normalizeFindOptionsOrder(
41
+ order: Record<string, unknown>,
42
+ ): FindOptionsOrder<ObjectLiteral> {
43
+ const normalized: Record<string, unknown> = {};
44
+
45
+ for (const [key, value] of Object.entries(order)) {
46
+ if (typeof value === 'string') {
47
+ normalized[key] = normalizeDirection(value);
48
+ } else if (value && typeof value === 'object') {
49
+ normalized[key] = normalizeFindOptionsOrder(
50
+ value as Record<string, unknown>,
51
+ );
52
+ } else {
53
+ normalized[key] = value;
54
+ }
55
+ }
56
+
57
+ return normalized as FindOptionsOrder<ObjectLiteral>;
58
+ }
4
59
 
5
60
  export class PaginationDto {
6
61
  @AutoMap()
@@ -19,17 +74,47 @@ export class PaginationDto {
19
74
  return (this.limit ?? 0) * (this.page ?? QUERY_FILTER_PARAMS.page);
20
75
  }
21
76
 
22
- generateOrderBy() {
77
+ generateOrderBy(): Record<string, unknown> | undefined {
23
78
  if (this.orderBy) {
24
79
  const orderByField = this.orderBy.split('.');
25
80
  return orderByField.reduceRight(
26
81
  (acc, item, index) => ({
27
82
  [item]: index === orderByField.length - 1 ? this.direction : acc,
28
83
  }),
29
- {},
84
+ {} as Record<string, unknown>,
30
85
  );
31
86
  }
32
87
 
33
88
  return undefined;
34
89
  }
90
+
91
+ toFindOptionsOrder(): FindOptionsOrder<ObjectLiteral> | undefined {
92
+ const order = this.generateOrderBy();
93
+
94
+ if (!order) {
95
+ return undefined;
96
+ }
97
+
98
+ return normalizeFindOptionsOrder(order);
99
+ }
100
+
101
+ applyQueryBuilderPagination<T extends ObjectLiteral>(
102
+ qb: SelectQueryBuilder<T>,
103
+ alias: string,
104
+ ): SelectQueryBuilder<T> {
105
+ const order = this.generateOrderBy();
106
+
107
+ if (order) {
108
+ for (const { path, direction } of flattenOrderBy(order)) {
109
+ const sortPath = path.includes('.') ? path : `${alias}.${path}`;
110
+ qb.addOrderBy(sortPath, direction);
111
+ }
112
+ }
113
+
114
+ return qb.skip(this.skip()).take(this.limit);
115
+ }
116
+
117
+ static from<T extends PaginationDto>(this: new () => T, props: Partial<T>): T {
118
+ return Object.assign(new this(), props);
119
+ }
35
120
  }
@@ -7,4 +7,12 @@ export class PersonQueryDto extends PaginationDto {
7
7
 
8
8
  @AutoMap()
9
9
  active?: boolean;
10
+
11
+ override generateOrderBy() {
12
+ if (this.orderBy) {
13
+ return super.generateOrderBy();
14
+ }
15
+
16
+ return { id: 'asc' };
17
+ }
10
18
  }
@@ -27,7 +27,6 @@ export class Person extends EntityBase<Person> {
27
27
 
28
28
  @OneToOne(() => PersonAddress, {
29
29
  cascade: true,
30
- eager: true,
31
30
  onDelete: 'CASCADE',
32
31
  })
33
32
  @JoinColumn()
@@ -36,7 +35,6 @@ export class Person extends EntityBase<Person> {
36
35
 
37
36
  @OneToMany(() => PersonContact, (contact) => contact.person, {
38
37
  cascade: true,
39
- eager: true,
40
38
  onDelete: 'CASCADE',
41
39
  })
42
40
  @AutoMap({ type: () => PersonContact })
@@ -0,0 +1,22 @@
1
+ import { createRateLimitMiddleware } from '@/core/http/rate-limit.middleware';
2
+ import { resolveCorsOrigin } from '@/core/utils/resolve-cors-origins';
3
+ import { EnvService } from '@/infra/common/env.service';
4
+ import type { INestApplication } from '@nestjs/common';
5
+ import cookieParser from 'cookie-parser';
6
+
7
+ export function applyHttpMiddleware(app: INestApplication) {
8
+ const env = app.get(EnvService);
9
+
10
+ app.use(cookieParser());
11
+ app.use(
12
+ createRateLimitMiddleware({
13
+ windowMs: env.get('RATE_LIMIT_WINDOW_MS'),
14
+ maxRequests: env.get('RATE_LIMIT_MAX'),
15
+ }),
16
+ );
17
+ app.enableCors({
18
+ credentials: true,
19
+ origin: resolveCorsOrigin(env.get('CORS_ORIGINS')),
20
+ optionsSuccessStatus: 200,
21
+ });
22
+ }