@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.
- package/cli/commands/new/configure-test-runner.js +13 -2
- package/cli/commands/new/create-ddd-structure.js +2 -4
- package/cli/constants/cli-project-checklist.js +27 -3
- package/cli/constants/core-packages.js +4 -6
- package/cli/utils/install-module.js +15 -7
- package/cli/utils/install-workspace-config.js +2 -0
- package/cli/utils/parse-new-args.js +0 -1
- package/cli/utils/patch-app-test-module.js +29 -0
- package/cli/utils/patch-env.js +34 -0
- package/cli/utils/patch-main.js +3 -4
- package/cli/utils/remove-sample-parts.js +11 -5
- package/koala-nest/.env.example +12 -0
- package/koala-nest/.vscode/tasks.json +9 -0
- package/koala-nest/src/application/common/pagination.request.ts +0 -5
- package/koala-nest/src/application/common/request-validator.base.ts +36 -1
- 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/env.ts +5 -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 +30 -12
- package/koala-nest/src/core/tools/mapping/mapping-store.ts +35 -29
- 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.ts +0 -2
- 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/repositories/person.repository.ts +3 -3
- package/koala-nest/src/infra/repositories/repository.base.ts +70 -1
- package/koala-nest/src/test/application/delete-inactive.job.spec.ts +23 -10
- package/koala-nest/src/test/application/read-many-person.handler.spec.ts +42 -2
- 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 +76 -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/host/controllers/app/app.e2e.spec.ts +24 -0
- package/koala-nest/src/test/host/controllers/person/lazy-loading.e2e.spec.ts +20 -60
- package/koala-nest/src/test/infra/redis-cache.service.spec.ts +8 -0
- package/koala-nest/src/test/utils/configure-test-app.ts +2 -8
- package/koala-nest/src/test/utils/create-e2e-database.ts +17 -1
- package/koala-nest/src/test/utils/in-memory-base.repository.ts +80 -0
- package/koala-nest/vitest.config.e2e.ts +13 -0
- package/koala-nest/vitest.config.ts +12 -0
- package/package.json +1 -1
- package/koala-nest/src/core/utils/icomparable.ts +0 -1
|
@@ -6,8 +6,19 @@ export function configureTestRunner(packageJson, packageManager) {
|
|
|
6
6
|
scripts["test:watch"] = "bun test --watch";
|
|
7
7
|
return;
|
|
8
8
|
}
|
|
9
|
-
scripts.test = "vitest run";
|
|
10
|
-
scripts["test:watch"] = "vitest";
|
|
9
|
+
scripts.test = "vitest run --config vitest.config.ts";
|
|
10
|
+
scripts["test:watch"] = "vitest --config vitest.config.ts";
|
|
11
11
|
devDependencies.vitest = "^4.1.8";
|
|
12
12
|
devDependencies["vite-tsconfig-paths"] = "^5.1.4";
|
|
13
13
|
}
|
|
14
|
+
export function configureE2ETestRunner(packageJson, packageManager) {
|
|
15
|
+
const scripts = packageJson.scripts;
|
|
16
|
+
const devDependencies = packageJson.devDependencies;
|
|
17
|
+
devDependencies.supertest ??= "^7.1.0";
|
|
18
|
+
devDependencies["@types/supertest"] ??= "^6.0.2";
|
|
19
|
+
if (packageManager === "bun") {
|
|
20
|
+
scripts["test:e2e"] = "bun test --preload ./src/test/setup-e2e.ts src/test/host/controllers/";
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
scripts["test:e2e"] = "vitest run --config vitest.config.e2e.ts";
|
|
24
|
+
}
|
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { DDD_LAYER_FOLDERS } from "../../constants/domain.js";
|
|
4
4
|
import { patchGeneratedProjectConfig } from "../../utils/patch-generated-project.js";
|
|
5
5
|
import { runCommand } from "../../utils/run-command.js";
|
|
6
|
-
import { configureTestRunner } from "./configure-test-runner.js";
|
|
6
|
+
import { configureE2ETestRunner, configureTestRunner } from "./configure-test-runner.js";
|
|
7
7
|
export async function createDDDStructure(projectName, packageManager) {
|
|
8
8
|
const folders = [...DDD_LAYER_FOLDERS];
|
|
9
9
|
for (const folder of folders) {
|
|
@@ -38,14 +38,12 @@ export async function createDDDStructure(projectName, packageManager) {
|
|
|
38
38
|
packageJson.scripts["migration:revert"] = `${migrationRunner} ${typeormCli} migration:revert ${migrationDatasource}`;
|
|
39
39
|
packageJson.devDependencies ??= {};
|
|
40
40
|
configureTestRunner(packageJson, packageManager);
|
|
41
|
+
configureE2ETestRunner(packageJson, packageManager);
|
|
41
42
|
delete packageJson.scripts["test:cov"];
|
|
42
43
|
delete packageJson.scripts["test:debug"];
|
|
43
|
-
delete packageJson.scripts["test:e2e"];
|
|
44
44
|
delete packageJson.jest;
|
|
45
45
|
delete packageJson.devDependencies["@types/jest"];
|
|
46
|
-
delete packageJson.devDependencies["@types/supertest"];
|
|
47
46
|
delete packageJson.devDependencies["ts-jest"];
|
|
48
|
-
delete packageJson.devDependencies["supertest"];
|
|
49
47
|
writeFileSync(path.join(process.cwd(), projectName, "package.json"), JSON.stringify(packageJson, null, 2));
|
|
50
48
|
patchGeneratedProjectConfig(path.join(process.cwd(), projectName));
|
|
51
49
|
await runCommand([packageManager, "install"], path.join(process.cwd(), projectName));
|
|
@@ -5,6 +5,21 @@ import {
|
|
|
5
5
|
resolveNewProjectOptions
|
|
6
6
|
} from "./domain.js";
|
|
7
7
|
import { resolveProjectFeatures } from "../utils/install-module.js";
|
|
8
|
+
export const E2E_INFRA_PATHS = [
|
|
9
|
+
"src/test/setup-e2e.ts",
|
|
10
|
+
"src/test/create-e2e-test-app.ts",
|
|
11
|
+
"src/test/e2e-context.ts",
|
|
12
|
+
"src/test/utils/create-e2e-database.ts",
|
|
13
|
+
"src/test/utils/e2e-database-client.ts",
|
|
14
|
+
"src/test/host/controllers/app/app.e2e.spec.ts"
|
|
15
|
+
];
|
|
16
|
+
export const CRUD_E2E_EXAMPLE_PATHS = [
|
|
17
|
+
"src/test/host/controllers/person/person.controller.e2e.spec.ts",
|
|
18
|
+
"src/test/host/controllers/person/lazy-loading.e2e.spec.ts",
|
|
19
|
+
"src/test/host/controllers/auth/auth.controller.e2e.spec.ts",
|
|
20
|
+
"src/test/app-auth-test.module.ts",
|
|
21
|
+
"src/test/create-auth-e2e-test-app.ts"
|
|
22
|
+
];
|
|
8
23
|
export const WORKSPACE_SETUP_PATHS = [
|
|
9
24
|
".vscode/launch.json",
|
|
10
25
|
".vscode/settings.json",
|
|
@@ -14,6 +29,11 @@ export const WORKSPACE_SETUP_PATHS = [
|
|
|
14
29
|
];
|
|
15
30
|
export const CORE_REQUIRED_PATHS = [
|
|
16
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",
|
|
17
37
|
"src/host/main.ts",
|
|
18
38
|
"src/host/app.module.ts",
|
|
19
39
|
"src/host/jobs/jobs.module.ts",
|
|
@@ -172,9 +192,13 @@ export function buildProjectExpectation(template, auth, features) {
|
|
|
172
192
|
};
|
|
173
193
|
}
|
|
174
194
|
export function requiredPathsForExpectation(expectation) {
|
|
175
|
-
const paths = [
|
|
195
|
+
const paths = [
|
|
196
|
+
...CORE_REQUIRED_PATHS,
|
|
197
|
+
...WORKSPACE_SETUP_PATHS,
|
|
198
|
+
...E2E_INFRA_PATHS
|
|
199
|
+
];
|
|
176
200
|
if (expectation.template === Template.CRUD_SAMPLE) {
|
|
177
|
-
paths.push(...CRUD_TEMPLATE_REQUIRED_PATHS);
|
|
201
|
+
paths.push(...CRUD_TEMPLATE_REQUIRED_PATHS, ...CRUD_E2E_EXAMPLE_PATHS);
|
|
178
202
|
}
|
|
179
203
|
if (expectation.cache === "redis") {
|
|
180
204
|
paths.push(...CACHE_REDIS_REQUIRED_PATHS);
|
|
@@ -196,7 +220,7 @@ export function requiredPathsForExpectation(expectation) {
|
|
|
196
220
|
export function forbiddenPathsForExpectation(expectation) {
|
|
197
221
|
const paths = [];
|
|
198
222
|
if (expectation.template === Template.DEFAULT) {
|
|
199
|
-
paths.push(...DEFAULT_TEMPLATE_FORBIDDEN_PATHS);
|
|
223
|
+
paths.push(...DEFAULT_TEMPLATE_FORBIDDEN_PATHS, ...CRUD_E2E_EXAMPLE_PATHS);
|
|
200
224
|
} else {
|
|
201
225
|
paths.push(...CRUD_TEMPLATE_FORBIDDEN_PATHS);
|
|
202
226
|
}
|
|
@@ -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 {
|
|
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 });
|
|
@@ -153,10 +157,14 @@ export async function installModule(module, template, projectName = "", options
|
|
|
153
157
|
for (const configFile of ["tsconfig.build.json", ".env.example"]) {
|
|
154
158
|
cpSync(path.join(getSourceCodePath(), configFile), path.join(projectPath, configFile));
|
|
155
159
|
}
|
|
160
|
+
if (packageManager !== "bun") {
|
|
161
|
+
for (const configFile of ["vitest.config.ts", "vitest.config.e2e.ts"]) {
|
|
162
|
+
cpSync(path.join(getSourceCodePath(), configFile), path.join(projectPath, configFile));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
156
165
|
patchInfraModuleFile(projectName, false);
|
|
157
|
-
patchMainFile(projectName, stripMainOptionalFeatures);
|
|
158
166
|
if (!options.skipPackages) {
|
|
159
|
-
await installPackages(projectName, CORE_PACKAGES);
|
|
167
|
+
await installPackages(projectName, CORE_PACKAGES, CORE_DEV_PACKAGES);
|
|
160
168
|
}
|
|
161
169
|
if (template === Template.DEFAULT) {
|
|
162
170
|
await removeSampleParts(projectName);
|
|
@@ -45,6 +45,8 @@ export function installWorkspaceConfig(projectName, packageManager) {
|
|
|
45
45
|
task.command = runScriptCommand(packageManager, "start:dev");
|
|
46
46
|
} else if (task.command.includes("test")) {
|
|
47
47
|
task.command = runScriptCommand(packageManager, "test");
|
|
48
|
+
} else if (task.command.includes("test:e2e")) {
|
|
49
|
+
task.command = runScriptCommand(packageManager, "test:e2e");
|
|
48
50
|
} else if (task.command.includes("migration:run")) {
|
|
49
51
|
task.command = runScriptCommand(packageManager, "migration:run");
|
|
50
52
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveProjectPath } from "./resolve-project-path.js";
|
|
4
|
+
const defaultAppTestModule = `import { envSchema } from '@/core/env';
|
|
5
|
+
import { Module } from '@nestjs/common';
|
|
6
|
+
import { ConfigModule } from '@nestjs/config';
|
|
7
|
+
import { InfraModule } from '@/infra/infra.module';
|
|
8
|
+
import { e2eDatabaseUrl } from '@/test/e2e-context';
|
|
9
|
+
|
|
10
|
+
@Module({
|
|
11
|
+
imports: [
|
|
12
|
+
ConfigModule.forRoot({
|
|
13
|
+
isGlobal: true,
|
|
14
|
+
ignoreEnvFile: true,
|
|
15
|
+
validate: () =>
|
|
16
|
+
envSchema.parse({
|
|
17
|
+
PORT: 3000,
|
|
18
|
+
NODE_ENV: 'test',
|
|
19
|
+
DATABASE_URL: e2eDatabaseUrl,
|
|
20
|
+
}),
|
|
21
|
+
}),
|
|
22
|
+
InfraModule,
|
|
23
|
+
],
|
|
24
|
+
})
|
|
25
|
+
export class AppTestModule {}
|
|
26
|
+
`;
|
|
27
|
+
export function patchAppTestModuleForDefault(projectName) {
|
|
28
|
+
writeFileSync(path.join(resolveProjectPath(projectName), "src/test/app-test.module.ts"), defaultAppTestModule);
|
|
29
|
+
}
|
package/cli/utils/patch-env.js
CHANGED
|
@@ -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=
|
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
|
}
|
|
@@ -2,8 +2,8 @@ import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { removeImportLines } from "./project-files.js";
|
|
4
4
|
import { patchAppModuleJobs } from "./patch-jobs-module.js";
|
|
5
|
+
import { patchAppTestModuleForDefault } from "./patch-app-test-module.js";
|
|
5
6
|
import { resolveProjectPath } from "./resolve-project-path.js";
|
|
6
|
-
import { stripMainOptionalFeatures } from "./patch-main.js";
|
|
7
7
|
import { stripDefineDocumentationAuth } from "./patch-define-documentation.js";
|
|
8
8
|
import { stripEnvAuth } from "./patch-env.js";
|
|
9
9
|
import { pruneCoreAuthForSlimTemplate } from "./prune-core-auth.js";
|
|
@@ -85,11 +85,13 @@ const partsToRemove = [
|
|
|
85
85
|
replace: [{ from: "PersonMapper.createMap();", to: "" }]
|
|
86
86
|
}
|
|
87
87
|
];
|
|
88
|
-
const
|
|
88
|
+
const defaultSampleTestPathsToRemove = [
|
|
89
89
|
"src/test/application",
|
|
90
90
|
"src/test/mockup/person",
|
|
91
91
|
"src/test/host/controllers/person/person.controller.e2e.spec.ts",
|
|
92
|
-
"src/test/host/controllers/person/lazy-loading.e2e.spec.ts"
|
|
92
|
+
"src/test/host/controllers/person/lazy-loading.e2e.spec.ts"
|
|
93
|
+
];
|
|
94
|
+
const defaultAuthE2ePathsToRemove = [
|
|
93
95
|
"src/test/host/controllers/auth/auth.controller.e2e.spec.ts",
|
|
94
96
|
"src/test/app-auth-test.module.ts",
|
|
95
97
|
"src/test/create-auth-e2e-test-app.ts"
|
|
@@ -154,7 +156,11 @@ export async function removeSampleParts(projectName) {
|
|
|
154
156
|
writeFileSync(partPath, content, "utf8");
|
|
155
157
|
}
|
|
156
158
|
patchAppModuleJobs(projectName, { eventHandlers: [], cronJobs: [] });
|
|
157
|
-
|
|
159
|
+
patchAppTestModuleForDefault(projectName);
|
|
160
|
+
removePaths(projectName, [
|
|
161
|
+
...defaultSampleTestPathsToRemove,
|
|
162
|
+
...defaultAuthE2ePathsToRemove
|
|
163
|
+
]);
|
|
158
164
|
}
|
|
159
165
|
export async function cleanDefaultTemplateWithoutAuth(projectName) {
|
|
160
166
|
stripDefaultProjectAuth(projectName);
|
|
@@ -198,6 +204,6 @@ function stripDefaultProjectAuth(projectName) {
|
|
|
198
204
|
]);
|
|
199
205
|
main = main.replace(/\n {2}app\.useGlobalGuards\([\s\S]*?\);\n/, `
|
|
200
206
|
`);
|
|
201
|
-
writeFileSync(mainPath,
|
|
207
|
+
writeFileSync(mainPath, main, "utf8");
|
|
202
208
|
}
|
|
203
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=
|
|
@@ -25,6 +25,15 @@
|
|
|
25
25
|
"group": "test",
|
|
26
26
|
"label": "Run tests"
|
|
27
27
|
},
|
|
28
|
+
{
|
|
29
|
+
"type": "shell",
|
|
30
|
+
"command": "bun run test:e2e",
|
|
31
|
+
"options": {
|
|
32
|
+
"cwd": "${workspaceFolder}"
|
|
33
|
+
},
|
|
34
|
+
"group": "test",
|
|
35
|
+
"label": "Run E2E tests"
|
|
36
|
+
},
|
|
28
37
|
{
|
|
29
38
|
"type": "shell",
|
|
30
39
|
"command": "bun run migration:run",
|
|
@@ -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,
|
|
@@ -6,7 +6,7 @@ export abstract class RequestValidatorBase<
|
|
|
6
6
|
protected _request: Record<string, any>;
|
|
7
7
|
|
|
8
8
|
constructor(request: TRequest) {
|
|
9
|
-
this._request =
|
|
9
|
+
this._request = RequestValidatorBase.toValidationInput(request);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
validate(): TRequest {
|
|
@@ -30,4 +30,39 @@ export abstract class RequestValidatorBase<
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
protected abstract get schema(): ZodType;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Objetos plain (query HTTP) passam direto. Instâncias de classe carregam
|
|
36
|
+
* defaults nos fields — removemos os que não foram alterados para o Zod
|
|
37
|
+
* aplicar defaults/transforms sem sobrescrever query params explícitos.
|
|
38
|
+
*/
|
|
39
|
+
private static toValidationInput<T extends Record<string, unknown>>(
|
|
40
|
+
request: T,
|
|
41
|
+
): Record<string, unknown> {
|
|
42
|
+
if (request === null || typeof request !== 'object') {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const prototype = Object.getPrototypeOf(request);
|
|
47
|
+
const isClassInstance =
|
|
48
|
+
prototype !== null &&
|
|
49
|
+
prototype !== Object.prototype &&
|
|
50
|
+
typeof prototype.constructor === 'function';
|
|
51
|
+
|
|
52
|
+
if (!isClassInstance) {
|
|
53
|
+
return { ...request };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const Constructor = prototype.constructor as new () => T;
|
|
57
|
+
const defaultValues = new Constructor() as Record<string, unknown>;
|
|
58
|
+
const input: Record<string, unknown> = { ...request };
|
|
59
|
+
|
|
60
|
+
for (const key of Object.keys(defaultValues)) {
|
|
61
|
+
if (Object.is(input[key], defaultValues[key])) {
|
|
62
|
+
delete input[key];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return input;
|
|
67
|
+
}
|
|
33
68
|
}
|
|
@@ -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> {}
|