@koalarx/nest 4.0.6 → 4.1.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/cli/constants/cli-project-checklist.js +5 -0
- package/cli/constants/core-packages.js +4 -6
- package/cli/utils/install-module.js +21 -21
- package/cli/utils/parse-new-args.js +0 -1
- package/cli/utils/patch-app-test-module.js +2 -1
- package/cli/utils/patch-env.js +36 -0
- package/cli/utils/patch-main.js +3 -4
- package/cli/utils/remove-sample-parts.js +1 -2
- package/koala-nest/.env.example +12 -0
- package/koala-nest/src/application/common/pagination.request.ts +0 -5
- package/koala-nest/src/application/mapping/person.mapper.ts +0 -9
- package/koala-nest/src/application/person/jobs/cron/delete-inactive.job.ts +34 -17
- package/koala-nest/src/application/person/read-many/read-many-person.response.ts +2 -13
- package/koala-nest/src/core/background-services/cron-service/cron-job.handler.base.ts +36 -15
- package/koala-nest/src/core/background-services/event-service/event-queue.ts +2 -3
- package/koala-nest/src/core/common/list-response.base.ts +10 -0
- package/koala-nest/src/core/database/db-context.ts +3 -0
- package/koala-nest/src/core/database/entity.ts +9 -0
- package/koala-nest/src/core/env.ts +6 -0
- package/koala-nest/src/core/http/rate-limit.middleware.ts +36 -0
- package/koala-nest/src/core/tools/mapping/auto-map.ts +27 -3
- package/koala-nest/src/core/tools/mapping/auto-mapper.ts +25 -11
- package/koala-nest/src/core/tools/mapping/mapping-store.ts +13 -15
- package/koala-nest/src/core/utils/cron-expression-to-boolean.ts +32 -17
- package/koala-nest/src/core/utils/hash-password.ts +7 -2
- package/koala-nest/src/core/utils/initialize-undefined-array-props.ts +20 -0
- package/koala-nest/src/core/utils/resolve-cors-origins.ts +24 -0
- package/koala-nest/src/domain/dtos/pagination.dto.ts +87 -2
- package/koala-nest/src/domain/dtos/person-query.dto.ts +8 -0
- package/koala-nest/src/domain/entities/person/person-address.ts +2 -1
- package/koala-nest/src/domain/entities/person/person-contact.ts +2 -2
- package/koala-nest/src/domain/entities/person/person.ts +2 -4
- package/koala-nest/src/domain/entities/user/user.ts +1 -1
- package/koala-nest/src/host/bootstrap/apply-http-middleware.ts +22 -0
- package/koala-nest/src/host/main.ts +14 -14
- package/koala-nest/src/infra/common/redis-cache.service.ts +15 -4
- package/koala-nest/src/infra/database/data-source-factory.ts +4 -6
- package/koala-nest/src/infra/database/migrations/migration-datasource.ts +7 -0
- package/koala-nest/src/infra/repositories/person.repository.ts +3 -3
- package/koala-nest/src/infra/repositories/repository.base.ts +70 -1
- package/koala-nest/src/test/app-auth-test.module.ts +2 -1
- package/koala-nest/src/test/app-test.module.ts +2 -1
- package/koala-nest/src/test/application/delete-inactive.job.spec.ts +23 -10
- package/koala-nest/src/test/application/update-person.handler.spec.ts +42 -0
- package/koala-nest/src/test/core/cron-expression-to-boolean.spec.ts +20 -1
- package/koala-nest/src/test/core/env.spec.ts +19 -0
- package/koala-nest/src/test/core/http/rate-limit.middleware.spec.ts +62 -0
- package/koala-nest/src/test/core/initialize-undefined-array-props.spec.ts +63 -0
- package/koala-nest/src/test/core/mapping.spec.ts +15 -0
- package/koala-nest/src/test/core/pagination-typeorm.spec.ts +74 -0
- package/koala-nest/src/test/core/resolve-cors-origins.spec.ts +22 -0
- package/koala-nest/src/test/core/sync-improvements.spec.ts +39 -0
- package/koala-nest/src/test/e2e-context.ts +4 -1
- package/koala-nest/src/test/host/controllers/person/lazy-loading.e2e.spec.ts +16 -20
- package/koala-nest/src/test/infra/redis-cache.service.spec.ts +8 -0
- package/koala-nest/src/test/setup-e2e.ts +6 -43
- package/koala-nest/src/test/utils/configure-test-app.ts +2 -8
- package/koala-nest/src/test/utils/create-e2e-database.ts +24 -16
- package/koala-nest/src/test/utils/e2e-database-client.ts +2 -2
- package/koala-nest/src/test/utils/in-memory-base.repository.ts +80 -0
- package/package.json +1 -1
- 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) {
|
|
@@ -1,29 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
cpSync,
|
|
3
|
-
existsSync,
|
|
4
|
-
mkdirSync,
|
|
5
|
-
readFileSync,
|
|
6
|
-
rmSync,
|
|
7
|
-
writeFileSync
|
|
8
|
-
} from "node:fs";
|
|
9
|
-
import path from "node:path";
|
|
10
1
|
import {
|
|
11
2
|
AUTH_DEV_PACKAGES,
|
|
12
3
|
AUTH_PACKAGES,
|
|
13
4
|
CACHE_PACKAGES,
|
|
5
|
+
CORE_DEV_PACKAGES,
|
|
14
6
|
CORE_PACKAGES,
|
|
15
7
|
CRON_PACKAGES,
|
|
16
8
|
devAddFlag,
|
|
17
9
|
HEALTH_PACKAGES
|
|
18
10
|
} from "../constants/core-packages.js";
|
|
19
11
|
import {
|
|
20
|
-
AuthChoice,
|
|
21
12
|
AuthStrategy,
|
|
22
13
|
ExtraFeature,
|
|
23
14
|
InstallModule as Modules,
|
|
24
15
|
Template
|
|
25
16
|
} from "../constants/domain.js";
|
|
17
|
+
import {
|
|
18
|
+
cpSync,
|
|
19
|
+
existsSync,
|
|
20
|
+
mkdirSync,
|
|
21
|
+
readFileSync,
|
|
22
|
+
rmSync,
|
|
23
|
+
writeFileSync
|
|
24
|
+
} from "node:fs";
|
|
25
|
+
import path from "node:path";
|
|
26
|
+
import { getPackageManager } from "./get-package-manager.js";
|
|
26
27
|
import { getSourceCodePath } from "./get-source-code-path.js";
|
|
28
|
+
import { patchAuthInstall } from "./patch-auth-install.js";
|
|
29
|
+
import { restoreDefineDocumentationWithAuth } from "./patch-define-documentation.js";
|
|
27
30
|
import {
|
|
28
31
|
patchAppModuleForHealth,
|
|
29
32
|
patchHealthCheckWithoutRedis
|
|
@@ -33,22 +36,19 @@ import {
|
|
|
33
36
|
patchInfraModuleForCache,
|
|
34
37
|
stripInfraModuleCache
|
|
35
38
|
} from "./patch-infra-module.js";
|
|
36
|
-
import {
|
|
37
|
-
import { patchMainForAuth, stripMainOptionalFeatures } from "./patch-main.js";
|
|
38
|
-
import { restoreDefineDocumentationWithAuth } from "./patch-define-documentation.js";
|
|
39
|
+
import { patchMainForAuth } from "./patch-main.js";
|
|
39
40
|
import { pruneCoreAuthForSlimTemplate } from "./prune-core-auth.js";
|
|
40
41
|
import { removeSampleParts } from "./remove-sample-parts.js";
|
|
41
42
|
import { resolveProjectPath } from "./resolve-project-path.js";
|
|
42
43
|
import { runCommand } from "./run-command.js";
|
|
43
|
-
import { getPackageManager } from "./get-package-manager.js";
|
|
44
44
|
export {
|
|
45
45
|
AuthChoice,
|
|
46
46
|
AuthStrategy,
|
|
47
47
|
CRUD_BUNDLED_FEATURES,
|
|
48
48
|
ExtraFeature,
|
|
49
|
-
InstallModule as Modules,
|
|
50
49
|
mapExtraFeatureToModule,
|
|
51
50
|
mergeCrudSampleFeatures,
|
|
51
|
+
InstallModule as Modules,
|
|
52
52
|
resolveNewProjectOptions,
|
|
53
53
|
Template
|
|
54
54
|
} from "../constants/domain.js";
|
|
@@ -127,15 +127,16 @@ export async function installModule(module, template, projectName = "", options
|
|
|
127
127
|
install("src/infra/repositories/repository.base.ts", projectName);
|
|
128
128
|
install("src/infra/repositories/repository.module.ts", projectName);
|
|
129
129
|
install("src/infra/infra.module.ts", projectName);
|
|
130
|
+
install("src/core/http/rate-limit.middleware.ts", projectName);
|
|
131
|
+
install("src/core/utils/resolve-cors-origins.ts", projectName);
|
|
132
|
+
install("src/host/bootstrap/apply-http-middleware.ts", projectName);
|
|
133
|
+
install("src/test/core/http/rate-limit.middleware.spec.ts", projectName);
|
|
134
|
+
install("src/test/core/resolve-cors-origins.spec.ts", projectName);
|
|
130
135
|
install("src/test", projectName);
|
|
131
136
|
rmSync(path.join(resolveProjectPath(projectName), "src/core/background-services"), { recursive: true, force: true });
|
|
132
137
|
rmSync(path.join(resolveProjectPath(projectName), "src/core/utils/cron-expression-to-boolean.ts"), { force: true });
|
|
133
138
|
rmSync(path.join(resolveProjectPath(projectName), "src/core/utils/person-list-cache.ts"), { force: true });
|
|
134
139
|
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
140
|
rmSync(path.join(resolveProjectPath(projectName), "src/host/controllers/health-check"), { recursive: true, force: true });
|
|
140
141
|
rmSync(path.join(resolveProjectPath(projectName), "src/infra/services/database.indicator.service.ts"), { force: true });
|
|
141
142
|
rmSync(path.join(resolveProjectPath(projectName), "src/infra/services/redis.indicator.service.ts"), { force: true });
|
|
@@ -159,9 +160,8 @@ export async function installModule(module, template, projectName = "", options
|
|
|
159
160
|
}
|
|
160
161
|
}
|
|
161
162
|
patchInfraModuleFile(projectName, false);
|
|
162
|
-
patchMainFile(projectName, stripMainOptionalFeatures);
|
|
163
163
|
if (!options.skipPackages) {
|
|
164
|
-
await installPackages(projectName, CORE_PACKAGES);
|
|
164
|
+
await installPackages(projectName, CORE_PACKAGES, CORE_DEV_PACKAGES);
|
|
165
165
|
}
|
|
166
166
|
if (template === Template.DEFAULT) {
|
|
167
167
|
await removeSampleParts(projectName);
|
|
@@ -5,7 +5,7 @@ const defaultAppTestModule = `import { envSchema } from '@/core/env';
|
|
|
5
5
|
import { Module } from '@nestjs/common';
|
|
6
6
|
import { ConfigModule } from '@nestjs/config';
|
|
7
7
|
import { InfraModule } from '@/infra/infra.module';
|
|
8
|
-
import { e2eDatabaseUrl } from '@/test/e2e-context';
|
|
8
|
+
import { e2eDatabaseUrl, e2eSchemaName } from '@/test/e2e-context';
|
|
9
9
|
|
|
10
10
|
@Module({
|
|
11
11
|
imports: [
|
|
@@ -17,6 +17,7 @@ import { e2eDatabaseUrl } from '@/test/e2e-context';
|
|
|
17
17
|
PORT: 3000,
|
|
18
18
|
NODE_ENV: 'test',
|
|
19
19
|
DATABASE_URL: e2eDatabaseUrl,
|
|
20
|
+
DATABASE_SCHEMA: e2eSchemaName,
|
|
20
21
|
}),
|
|
21
22
|
}),
|
|
22
23
|
InfraModule,
|
package/cli/utils/patch-env.js
CHANGED
|
@@ -8,12 +8,18 @@ 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(),
|
|
14
|
+
DATABASE_SCHEMA: z.string().optional(),
|
|
13
15
|
REDIS_CONNECTION_STRING: z.string().optional(),
|
|
14
16
|
CACHE_KEY_PREFIX: z.string().optional(),
|
|
15
17
|
CRON_JOBS_ENABLED: envBooleanSchema(false),
|
|
16
18
|
BOOTSTRAP_DELAY_MS: z.coerce.number().default(0),
|
|
19
|
+
RATE_LIMIT_MAX: z.coerce.number().default(0),
|
|
20
|
+
RATE_LIMIT_WINDOW_MS: z.coerce.number().default(60_000),
|
|
21
|
+
CORS_ORIGINS: z.string().optional(),
|
|
22
|
+
BCRYPT_ROUNDS: z.coerce.number().min(4).max(15).default(10),
|
|
17
23
|
});
|
|
18
24
|
|
|
19
25
|
export type Env = z.infer<typeof envSchema>;
|
|
@@ -23,6 +29,8 @@ export function validateEnvConfig(config: Record<string, unknown>): Env {
|
|
|
23
29
|
}
|
|
24
30
|
`;
|
|
25
31
|
const envExampleWithoutAuth = `PORT=3000
|
|
32
|
+
# Endereço de bind do servidor (Docker/K8s). URLs públicas usam API_HOST.
|
|
33
|
+
HOST=0.0.0.0
|
|
26
34
|
NODE_ENV=develop
|
|
27
35
|
DATABASE_URL=postgresql://postgres:root@localhost:5432/koala_nest
|
|
28
36
|
|
|
@@ -35,6 +43,16 @@ DATABASE_URL=postgresql://postgres:root@localhost:5432/koala_nest
|
|
|
35
43
|
# Cron jobs internos. Ative com \`kl-nest add cron\`.
|
|
36
44
|
CRON_JOBS_ENABLED=false
|
|
37
45
|
BOOTSTRAP_DELAY_MS=0
|
|
46
|
+
|
|
47
|
+
# Rate limit (0 = desabilitado)
|
|
48
|
+
# RATE_LIMIT_MAX=300
|
|
49
|
+
# RATE_LIMIT_WINDOW_MS=60000
|
|
50
|
+
|
|
51
|
+
# CORS — aberto por padrão; restrinja com origens separadas por vírgula se necessário
|
|
52
|
+
# CORS_ORIGINS=http://localhost:4200,https://app.example.com
|
|
53
|
+
|
|
54
|
+
# Custo do bcrypt (padrão 10)
|
|
55
|
+
# BCRYPT_ROUNDS=10
|
|
38
56
|
`;
|
|
39
57
|
export function stripEnvAuth(projectName) {
|
|
40
58
|
const projectRoot = resolveProjectPath(projectName);
|
|
@@ -53,12 +71,18 @@ import { z } from 'zod';
|
|
|
53
71
|
|
|
54
72
|
export const envSchema = z.object({
|
|
55
73
|
PORT: z.coerce.number().default(3000),
|
|
74
|
+
HOST: z.string().default('0.0.0.0'),
|
|
56
75
|
NODE_ENV: z.enum(['test', 'develop', 'staging', 'production']),
|
|
57
76
|
DATABASE_URL: z.string(),
|
|
77
|
+
DATABASE_SCHEMA: z.string().optional(),
|
|
58
78
|
REDIS_CONNECTION_STRING: z.string().optional(),
|
|
59
79
|
CACHE_KEY_PREFIX: z.string().optional(),
|
|
60
80
|
CRON_JOBS_ENABLED: envBooleanSchema(false),
|
|
61
81
|
BOOTSTRAP_DELAY_MS: z.coerce.number().default(0),
|
|
82
|
+
RATE_LIMIT_MAX: z.coerce.number().default(0),
|
|
83
|
+
RATE_LIMIT_WINDOW_MS: z.coerce.number().default(60_000),
|
|
84
|
+
CORS_ORIGINS: z.string().optional(),
|
|
85
|
+
BCRYPT_ROUNDS: z.coerce.number().min(4).max(15).default(10),
|
|
62
86
|
JWT_PRIVATE_KEY: z.string().optional(),
|
|
63
87
|
JWT_PUBLIC_KEY: z.string().optional(),
|
|
64
88
|
JWT_ACCESS_TOKEN_EXPIRES_IN: z.string().default('15m'),
|
|
@@ -73,6 +97,8 @@ export function validateEnvConfig(config: Record<string, unknown>): Env {
|
|
|
73
97
|
}
|
|
74
98
|
`;
|
|
75
99
|
const envExampleJwtOnly = `PORT=3000
|
|
100
|
+
# Endereço de bind do servidor (Docker/K8s). URLs públicas usam API_HOST.
|
|
101
|
+
HOST=0.0.0.0
|
|
76
102
|
NODE_ENV=develop
|
|
77
103
|
DATABASE_URL=postgresql://postgres:root@localhost:5432/koala_nest
|
|
78
104
|
|
|
@@ -83,6 +109,16 @@ DATABASE_URL=postgresql://postgres:root@localhost:5432/koala_nest
|
|
|
83
109
|
CRON_JOBS_ENABLED=false
|
|
84
110
|
BOOTSTRAP_DELAY_MS=0
|
|
85
111
|
|
|
112
|
+
# Rate limit (0 = desabilitado)
|
|
113
|
+
# RATE_LIMIT_MAX=300
|
|
114
|
+
# RATE_LIMIT_WINDOW_MS=60000
|
|
115
|
+
|
|
116
|
+
# CORS — aberto por padrão; restrinja com origens separadas por vírgula se necessário
|
|
117
|
+
# CORS_ORIGINS=http://localhost:4200,https://app.example.com
|
|
118
|
+
|
|
119
|
+
# Custo do bcrypt (padrão 10)
|
|
120
|
+
# BCRYPT_ROUNDS=10
|
|
121
|
+
|
|
86
122
|
# JWT (RS256 — chaves em base64)
|
|
87
123
|
JWT_PRIVATE_KEY=
|
|
88
124
|
JWT_PUBLIC_KEY=
|
package/cli/utils/patch-main.js
CHANGED
|
@@ -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,
|
|
207
|
+
writeFileSync(mainPath, main, "utf8");
|
|
209
208
|
}
|
|
210
209
|
}
|
package/koala-nest/.env.example
CHANGED
|
@@ -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
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
57
|
+
totalDeleted += items.length;
|
|
58
|
+
page += 1;
|
|
38
59
|
|
|
39
|
-
|
|
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.
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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:
|
|
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:
|
|
98
|
+
id: string,
|
|
100
99
|
): EventJob<unknown> | undefined {
|
|
101
100
|
return this.markedAggregates.find((aggregate) => aggregate._id === id);
|
|
102
101
|
}
|
|
@@ -8,12 +8,18 @@ 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(),
|
|
14
|
+
DATABASE_SCHEMA: z.string().optional(),
|
|
13
15
|
REDIS_CONNECTION_STRING: z.string().optional(),
|
|
14
16
|
CACHE_KEY_PREFIX: z.string().optional(),
|
|
15
17
|
CRON_JOBS_ENABLED: envBooleanSchema(false),
|
|
16
18
|
BOOTSTRAP_DELAY_MS: z.coerce.number().default(0),
|
|
19
|
+
RATE_LIMIT_MAX: z.coerce.number().default(0),
|
|
20
|
+
RATE_LIMIT_WINDOW_MS: z.coerce.number().default(60_000),
|
|
21
|
+
CORS_ORIGINS: z.string().optional(),
|
|
22
|
+
BCRYPT_ROUNDS: z.coerce.number().min(4).max(15).default(10),
|
|
17
23
|
JWT_PRIVATE_KEY: z.string().optional(),
|
|
18
24
|
JWT_PUBLIC_KEY: z.string().optional(),
|
|
19
25
|
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
|
+
}
|