@koalarx/nest 4.0.6 → 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 (48) hide show
  1. package/cli/constants/cli-project-checklist.js +5 -0
  2. package/cli/constants/core-packages.js +4 -6
  3. package/cli/utils/install-module.js +10 -7
  4. package/cli/utils/parse-new-args.js +0 -1
  5. package/cli/utils/patch-env.js +34 -0
  6. package/cli/utils/patch-main.js +3 -4
  7. package/cli/utils/remove-sample-parts.js +1 -2
  8. package/koala-nest/.env.example +12 -0
  9. package/koala-nest/src/application/common/pagination.request.ts +0 -5
  10. package/koala-nest/src/application/mapping/person.mapper.ts +0 -9
  11. package/koala-nest/src/application/person/jobs/cron/delete-inactive.job.ts +34 -17
  12. package/koala-nest/src/application/person/read-many/read-many-person.response.ts +2 -13
  13. package/koala-nest/src/core/background-services/cron-service/cron-job.handler.base.ts +36 -15
  14. package/koala-nest/src/core/background-services/event-service/event-queue.ts +2 -3
  15. package/koala-nest/src/core/common/list-response.base.ts +10 -0
  16. package/koala-nest/src/core/env.ts +5 -0
  17. package/koala-nest/src/core/http/rate-limit.middleware.ts +36 -0
  18. package/koala-nest/src/core/tools/mapping/auto-map.ts +27 -3
  19. package/koala-nest/src/core/tools/mapping/auto-mapper.ts +25 -11
  20. package/koala-nest/src/core/tools/mapping/mapping-store.ts +13 -15
  21. package/koala-nest/src/core/utils/cron-expression-to-boolean.ts +32 -17
  22. package/koala-nest/src/core/utils/hash-password.ts +7 -2
  23. package/koala-nest/src/core/utils/initialize-undefined-array-props.ts +20 -0
  24. package/koala-nest/src/core/utils/resolve-cors-origins.ts +24 -0
  25. package/koala-nest/src/domain/dtos/pagination.dto.ts +87 -2
  26. package/koala-nest/src/domain/dtos/person-query.dto.ts +8 -0
  27. package/koala-nest/src/domain/entities/person/person.ts +0 -2
  28. package/koala-nest/src/host/bootstrap/apply-http-middleware.ts +22 -0
  29. package/koala-nest/src/host/main.ts +14 -14
  30. package/koala-nest/src/infra/common/redis-cache.service.ts +15 -4
  31. package/koala-nest/src/infra/repositories/person.repository.ts +3 -3
  32. package/koala-nest/src/infra/repositories/repository.base.ts +70 -1
  33. package/koala-nest/src/test/application/delete-inactive.job.spec.ts +23 -10
  34. package/koala-nest/src/test/application/update-person.handler.spec.ts +42 -0
  35. package/koala-nest/src/test/core/cron-expression-to-boolean.spec.ts +20 -1
  36. package/koala-nest/src/test/core/env.spec.ts +19 -0
  37. package/koala-nest/src/test/core/http/rate-limit.middleware.spec.ts +62 -0
  38. package/koala-nest/src/test/core/initialize-undefined-array-props.spec.ts +63 -0
  39. package/koala-nest/src/test/core/mapping.spec.ts +15 -0
  40. package/koala-nest/src/test/core/pagination-typeorm.spec.ts +74 -0
  41. package/koala-nest/src/test/core/resolve-cors-origins.spec.ts +22 -0
  42. package/koala-nest/src/test/core/sync-improvements.spec.ts +39 -0
  43. package/koala-nest/src/test/host/controllers/person/lazy-loading.e2e.spec.ts +20 -60
  44. package/koala-nest/src/test/infra/redis-cache.service.spec.ts +8 -0
  45. package/koala-nest/src/test/utils/configure-test-app.ts +2 -8
  46. package/koala-nest/src/test/utils/in-memory-base.repository.ts +80 -0
  47. package/package.json +1 -1
  48. package/koala-nest/src/core/utils/icomparable.ts +0 -1
@@ -29,6 +29,11 @@ export const WORKSPACE_SETUP_PATHS = [
29
29
  ];
30
30
  export const CORE_REQUIRED_PATHS = [
31
31
  "src/core/env.ts",
32
+ "src/core/common/list-response.base.ts",
33
+ "src/core/utils/initialize-undefined-array-props.ts",
34
+ "src/core/http/rate-limit.middleware.ts",
35
+ "src/core/utils/resolve-cors-origins.ts",
36
+ "src/host/bootstrap/apply-http-middleware.ts",
32
37
  "src/host/main.ts",
33
38
  "src/host/app.module.ts",
34
39
  "src/host/jobs/jobs.module.ts",
@@ -5,21 +5,19 @@ export const CORE_PACKAGES = [
5
5
  "typeorm",
6
6
  "pg",
7
7
  "zod",
8
- "@scalar/nestjs-api-reference"
8
+ "@scalar/nestjs-api-reference",
9
+ "cookie-parser"
9
10
  ];
11
+ export const CORE_DEV_PACKAGES = ["@types/cookie-parser"];
10
12
  export const CACHE_PACKAGES = ["ioredis"];
11
13
  export const AUTH_PACKAGES = [
12
14
  "@nestjs/jwt",
13
15
  "@nestjs/passport",
14
16
  "passport",
15
17
  "passport-jwt",
16
- "cookie-parser",
17
18
  "bcrypt"
18
19
  ];
19
- export const AUTH_DEV_PACKAGES = [
20
- "@types/cookie-parser",
21
- "@types/bcrypt"
22
- ];
20
+ export const AUTH_DEV_PACKAGES = ["@types/bcrypt"];
23
21
  export const CRON_PACKAGES = ["cron-parser"];
24
22
  export const HEALTH_PACKAGES = ["@nestjs/terminus", "@nestjs/axios"];
25
23
  export function devAddFlag(packageManager) {
@@ -11,6 +11,7 @@ import {
11
11
  AUTH_DEV_PACKAGES,
12
12
  AUTH_PACKAGES,
13
13
  CACHE_PACKAGES,
14
+ CORE_DEV_PACKAGES,
14
15
  CORE_PACKAGES,
15
16
  CRON_PACKAGES,
16
17
  devAddFlag,
@@ -34,7 +35,9 @@ import {
34
35
  stripInfraModuleCache
35
36
  } from "./patch-infra-module.js";
36
37
  import { patchAuthInstall } from "./patch-auth-install.js";
37
- import { patchMainForAuth, stripMainOptionalFeatures } from "./patch-main.js";
38
+ import {
39
+ patchMainForAuth
40
+ } from "./patch-main.js";
38
41
  import { restoreDefineDocumentationWithAuth } from "./patch-define-documentation.js";
39
42
  import { pruneCoreAuthForSlimTemplate } from "./prune-core-auth.js";
40
43
  import { removeSampleParts } from "./remove-sample-parts.js";
@@ -127,15 +130,16 @@ export async function installModule(module, template, projectName = "", options
127
130
  install("src/infra/repositories/repository.base.ts", projectName);
128
131
  install("src/infra/repositories/repository.module.ts", projectName);
129
132
  install("src/infra/infra.module.ts", projectName);
133
+ install("src/core/http/rate-limit.middleware.ts", projectName);
134
+ install("src/core/utils/resolve-cors-origins.ts", projectName);
135
+ install("src/host/bootstrap/apply-http-middleware.ts", projectName);
136
+ install("src/test/core/http/rate-limit.middleware.spec.ts", projectName);
137
+ install("src/test/core/resolve-cors-origins.spec.ts", projectName);
130
138
  install("src/test", projectName);
131
139
  rmSync(path.join(resolveProjectPath(projectName), "src/core/background-services"), { recursive: true, force: true });
132
140
  rmSync(path.join(resolveProjectPath(projectName), "src/core/utils/cron-expression-to-boolean.ts"), { force: true });
133
141
  rmSync(path.join(resolveProjectPath(projectName), "src/core/utils/person-list-cache.ts"), { force: true });
134
142
  rmSync(path.join(resolveProjectPath(projectName), "src/core/utils/build-list-cache-key.ts"), { force: true });
135
- rmSync(path.join(resolveProjectPath(projectName), "src/host/bootstrap"), {
136
- recursive: true,
137
- force: true
138
- });
139
143
  rmSync(path.join(resolveProjectPath(projectName), "src/host/controllers/health-check"), { recursive: true, force: true });
140
144
  rmSync(path.join(resolveProjectPath(projectName), "src/infra/services/database.indicator.service.ts"), { force: true });
141
145
  rmSync(path.join(resolveProjectPath(projectName), "src/infra/services/redis.indicator.service.ts"), { force: true });
@@ -159,9 +163,8 @@ export async function installModule(module, template, projectName = "", options
159
163
  }
160
164
  }
161
165
  patchInfraModuleFile(projectName, false);
162
- patchMainFile(projectName, stripMainOptionalFeatures);
163
166
  if (!options.skipPackages) {
164
- await installPackages(projectName, CORE_PACKAGES);
167
+ await installPackages(projectName, CORE_PACKAGES, CORE_DEV_PACKAGES);
165
168
  }
166
169
  if (template === Template.DEFAULT) {
167
170
  await removeSampleParts(projectName);
@@ -1,5 +1,4 @@
1
1
  import {
2
- AuthStrategy,
3
2
  FEATURE_ALIASES,
4
3
  parseAuthStrategies,
5
4
  Template,
@@ -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
  });
18
23
 
19
24
  export type Env = z.infer<typeof envSchema>;
@@ -23,6 +28,8 @@ export function validateEnvConfig(config: Record<string, unknown>): Env {
23
28
  }
24
29
  `;
25
30
  const envExampleWithoutAuth = `PORT=3000
31
+ # Endereço de bind do servidor (Docker/K8s). URLs públicas usam API_HOST.
32
+ HOST=0.0.0.0
26
33
  NODE_ENV=develop
27
34
  DATABASE_URL=postgresql://postgres:root@localhost:5432/koala_nest
28
35
 
@@ -35,6 +42,16 @@ DATABASE_URL=postgresql://postgres:root@localhost:5432/koala_nest
35
42
  # Cron jobs internos. Ative com \`kl-nest add cron\`.
36
43
  CRON_JOBS_ENABLED=false
37
44
  BOOTSTRAP_DELAY_MS=0
45
+
46
+ # Rate limit (0 = desabilitado)
47
+ # RATE_LIMIT_MAX=300
48
+ # RATE_LIMIT_WINDOW_MS=60000
49
+
50
+ # CORS — aberto por padrão; restrinja com origens separadas por vírgula se necessário
51
+ # CORS_ORIGINS=http://localhost:4200,https://app.example.com
52
+
53
+ # Custo do bcrypt (padrão 10)
54
+ # BCRYPT_ROUNDS=10
38
55
  `;
39
56
  export function stripEnvAuth(projectName) {
40
57
  const projectRoot = resolveProjectPath(projectName);
@@ -53,12 +70,17 @@ import { z } from 'zod';
53
70
 
54
71
  export const envSchema = z.object({
55
72
  PORT: z.coerce.number().default(3000),
73
+ HOST: z.string().default('0.0.0.0'),
56
74
  NODE_ENV: z.enum(['test', 'develop', 'staging', 'production']),
57
75
  DATABASE_URL: z.string(),
58
76
  REDIS_CONNECTION_STRING: z.string().optional(),
59
77
  CACHE_KEY_PREFIX: z.string().optional(),
60
78
  CRON_JOBS_ENABLED: envBooleanSchema(false),
61
79
  BOOTSTRAP_DELAY_MS: z.coerce.number().default(0),
80
+ RATE_LIMIT_MAX: z.coerce.number().default(0),
81
+ RATE_LIMIT_WINDOW_MS: z.coerce.number().default(60_000),
82
+ CORS_ORIGINS: z.string().optional(),
83
+ BCRYPT_ROUNDS: z.coerce.number().min(4).max(15).default(10),
62
84
  JWT_PRIVATE_KEY: z.string().optional(),
63
85
  JWT_PUBLIC_KEY: z.string().optional(),
64
86
  JWT_ACCESS_TOKEN_EXPIRES_IN: z.string().default('15m'),
@@ -73,6 +95,8 @@ export function validateEnvConfig(config: Record<string, unknown>): Env {
73
95
  }
74
96
  `;
75
97
  const envExampleJwtOnly = `PORT=3000
98
+ # Endereço de bind do servidor (Docker/K8s). URLs públicas usam API_HOST.
99
+ HOST=0.0.0.0
76
100
  NODE_ENV=develop
77
101
  DATABASE_URL=postgresql://postgres:root@localhost:5432/koala_nest
78
102
 
@@ -83,6 +107,16 @@ DATABASE_URL=postgresql://postgres:root@localhost:5432/koala_nest
83
107
  CRON_JOBS_ENABLED=false
84
108
  BOOTSTRAP_DELAY_MS=0
85
109
 
110
+ # Rate limit (0 = desabilitado)
111
+ # RATE_LIMIT_MAX=300
112
+ # RATE_LIMIT_WINDOW_MS=60000
113
+
114
+ # CORS — aberto por padrão; restrinja com origens separadas por vírgula se necessário
115
+ # CORS_ORIGINS=http://localhost:4200,https://app.example.com
116
+
117
+ # Custo do bcrypt (padrão 10)
118
+ # BCRYPT_ROUNDS=10
119
+
86
120
  # JWT (RS256 — chaves em base64)
87
121
  JWT_PRIVATE_KEY=
88
122
  JWT_PUBLIC_KEY=
@@ -1,8 +1,7 @@
1
- export function stripMainOptionalFeatures(content) {
2
- return content.replace(/import cookieParser from 'cookie-parser';\n/, "").replace(/\n {2}app\.use\(cookieParser\(\)\);\n/, `
3
- `);
4
- }
5
1
  export function patchMainForAuth(content) {
2
+ if (content.includes("applyHttpMiddleware")) {
3
+ return content;
4
+ }
6
5
  if (content.includes("cookieParser()")) {
7
6
  return content;
8
7
  }
@@ -4,7 +4,6 @@ import { removeImportLines } from "./project-files.js";
4
4
  import { patchAppModuleJobs } from "./patch-jobs-module.js";
5
5
  import { patchAppTestModuleForDefault } from "./patch-app-test-module.js";
6
6
  import { resolveProjectPath } from "./resolve-project-path.js";
7
- import { stripMainOptionalFeatures } from "./patch-main.js";
8
7
  import { stripDefineDocumentationAuth } from "./patch-define-documentation.js";
9
8
  import { stripEnvAuth } from "./patch-env.js";
10
9
  import { pruneCoreAuthForSlimTemplate } from "./prune-core-auth.js";
@@ -205,6 +204,6 @@ function stripDefaultProjectAuth(projectName) {
205
204
  ]);
206
205
  main = main.replace(/\n {2}app\.useGlobalGuards\([\s\S]*?\);\n/, `
207
206
  `);
208
- writeFileSync(mainPath, stripMainOptionalFeatures(main), "utf8");
207
+ writeFileSync(mainPath, main, "utf8");
209
208
  }
210
209
  }
@@ -1,4 +1,6 @@
1
1
  PORT=3000
2
+ # Endereço de bind do servidor (Docker/K8s). URLs públicas usam API_HOST.
3
+ HOST=0.0.0.0
2
4
  NODE_ENV=develop
3
5
  DATABASE_URL=postgresql://postgres:root@localhost:5432/koala_nest
4
6
 
@@ -12,6 +14,16 @@ DATABASE_URL=postgresql://postgres:root@localhost:5432/koala_nest
12
14
  CRON_JOBS_ENABLED=true
13
15
  BOOTSTRAP_DELAY_MS=0
14
16
 
17
+ # Rate limit (0 = desabilitado)
18
+ # RATE_LIMIT_MAX=300
19
+ # RATE_LIMIT_WINDOW_MS=60000
20
+
21
+ # CORS — aberto por padrão; restrinja com origens separadas por vírgula se necessário
22
+ # CORS_ORIGINS=http://localhost:4200,https://app.example.com
23
+
24
+ # Custo do bcrypt (padrão 10)
25
+ # BCRYPT_ROUNDS=10
26
+
15
27
  # JWT (RS256 — chaves em base64) — ative com `kl-nest new --auth jwt` ou `kl-nest add auth jwt`
16
28
  JWT_PRIVATE_KEY=
17
29
  JWT_PUBLIC_KEY=
@@ -3,11 +3,6 @@ import { AutoMap } from '@/core/tools/mapping';
3
3
  import type { QueryDirectionType } from '@/core/types';
4
4
  import { ApiProperty } from '@nestjs/swagger';
5
5
 
6
- export type PaginatedRequestProps<T extends PaginationRequest> = Omit<
7
- { [K in keyof T as T[K] extends Function ? never : K]: T[K] },
8
- '_id'
9
- >;
10
-
11
6
  export class PaginationRequest {
12
7
  @ApiProperty({
13
8
  required: false,
@@ -15,11 +15,6 @@ import {
15
15
  ReadPersonContactResponse,
16
16
  ReadPersonResponse,
17
17
  } from '@/application/person/read/read-person.response';
18
- import {
19
- UpdatePersonAddressRequest,
20
- UpdatePersonContactRequest,
21
- UpdatePersonRequest,
22
- } from '@/application/person/update/update-person.request';
23
18
  import { ReadManyPersonResponseItem } from '@/application/person/read-many/read-many-person.response';
24
19
 
25
20
  export class PersonMapper {
@@ -34,10 +29,6 @@ export class PersonMapper {
34
29
  createMap(CreatePersonAddressRequest, PersonAddress);
35
30
  createMap(CreatePersonContactRequest, PersonContact);
36
31
 
37
- createMap(UpdatePersonRequest, Person);
38
- createMap(UpdatePersonAddressRequest, PersonAddress);
39
- createMap(UpdatePersonContactRequest, PersonContact);
40
-
41
32
  createMap(ReadManyPersonRequest, PersonQueryDto);
42
33
  createMap(Person, ReadManyPersonResponseItem);
43
34
  }
@@ -1,16 +1,18 @@
1
1
  import { CronExpression } from '@/core/constants/cron.constants';
2
2
  import { DeletePersonHandler } from '@/application/person/delete/delete-person.handler';
3
- import { ReadManyPersonHandler } from '@/application/person/read-many/read-many-person.handler';
4
- import { ReadManyPersonRequest } from '@/application/person/read-many/read-many-person.request';
5
3
  import {
6
4
  CronJobHandlerBase,
7
5
  CronJobSettings,
8
6
  cronJobSettings,
9
7
  } from '@/core/background-services/cron-service/cron-job.handler.base';
8
+ import { PersonQueryDto } from '@/domain/dtos/person-query.dto';
10
9
  import { ILoggingService } from '@/domain/common/ilogging.service';
11
10
  import { IRedLockService } from '@/domain/common/ired-lock.service';
11
+ import { IPersonRepository } from '@/domain/repositories/iperson.repository';
12
12
  import { Injectable, Logger } from '@nestjs/common';
13
13
 
14
+ const DELETE_INACTIVE_BATCH_SIZE = 100;
15
+
14
16
  @Injectable()
15
17
  export class DeleteInactiveJob extends CronJobHandlerBase {
16
18
  private readonly logger = new Logger(DeleteInactiveJob.name);
@@ -18,7 +20,7 @@ export class DeleteInactiveJob extends CronJobHandlerBase {
18
20
  constructor(
19
21
  redlockService: IRedLockService,
20
22
  loggingService: ILoggingService,
21
- private readonly readManyPerson: ReadManyPersonHandler,
23
+ private readonly repository: IPersonRepository,
22
24
  private readonly deletePerson: DeletePersonHandler,
23
25
  ) {
24
26
  super(redlockService, loggingService);
@@ -31,27 +33,42 @@ export class DeleteInactiveJob extends CronJobHandlerBase {
31
33
  protected async run(): Promise<void> {
32
34
  this.logger.debug('Iniciando remoção de pessoas inativas...');
33
35
 
34
- const request = new ReadManyPersonRequest();
35
- request.active = false;
36
+ let page = 0;
37
+ let totalDeleted = 0;
38
+
39
+ while (true) {
40
+ const query = PersonQueryDto.from({
41
+ active: false,
42
+ page,
43
+ limit: DELETE_INACTIVE_BATCH_SIZE,
44
+ });
45
+ const { items, count } = await this.repository.findMany(query);
46
+
47
+ if (items.length === 0) {
48
+ break;
49
+ }
50
+
51
+ this.logger.debug('Removendo pessoas inativas...', items.length);
52
+
53
+ for (const person of items) {
54
+ await this.deletePerson.handle(person.id);
55
+ }
36
56
 
37
- const result = await this.readManyPerson.handle(request);
57
+ totalDeleted += items.length;
58
+ page += 1;
38
59
 
39
- if (result.items.length === 0) {
60
+ if (page * DELETE_INACTIVE_BATCH_SIZE >= count) {
61
+ break;
62
+ }
63
+ }
64
+
65
+ if (totalDeleted === 0) {
40
66
  this.logger.log('Nenhuma pessoa inativa encontrada.');
41
67
  this.logger.log('Job concluído sem ação.');
42
68
  return;
43
69
  }
44
70
 
45
- this.logger.debug('Removendo pessoas inativas...', result.items.length);
46
-
47
- for (const person of result.items) {
48
- await this.deletePerson.handle(person.id);
49
- }
50
-
51
- this.logger.log(
52
- 'Pessoas inativas removidas com sucesso.',
53
- result.items.length,
54
- );
71
+ this.logger.log('Pessoas inativas removidas com sucesso.', totalDeleted);
55
72
  this.logger.log('Job concluído.');
56
73
  }
57
74
  }
@@ -1,6 +1,5 @@
1
- import { ObjectClass } from '@/core/base/object-class';
1
+ import { ListResponseBase } from '@/core/common/list-response.base';
2
2
  import { AutoMap } from '@/core/tools/mapping';
3
- import { ListResponse } from '@/core/types';
4
3
  import { ApiProperty } from '@nestjs/swagger';
5
4
 
6
5
  export class ReadManyPersonResponseItem {
@@ -17,14 +16,4 @@ export class ReadManyPersonResponseItem {
17
16
  active: boolean;
18
17
  }
19
18
 
20
- export class ReadManyPersonResponse extends ObjectClass<
21
- ListResponse<ReadManyPersonResponseItem>
22
- > {
23
- @ApiProperty({ type: [ReadManyPersonResponseItem] })
24
- @AutoMap({ type: () => ReadManyPersonResponseItem })
25
- items: ReadManyPersonResponseItem[];
26
-
27
- @ApiProperty()
28
- @AutoMap()
29
- count: number;
30
- }
19
+ export class ReadManyPersonResponse extends ListResponseBase<ReadManyPersonResponseItem> {}
@@ -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,17 +85,21 @@ 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
 
@@ -101,6 +110,11 @@ export class AutoMapper {
101
110
  }
102
111
  }
103
112
 
113
+ initializeUndefinedArrayProps(
114
+ targetInstance as Record<string, unknown>,
115
+ target,
116
+ );
117
+
104
118
  return targetInstance;
105
119
  }
106
120
  }