@koalarx/nest 4.0.3 → 4.0.6

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.
@@ -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));
@@ -26,6 +26,7 @@ import {
26
26
  resolveProjectFeatures
27
27
  } from "../../utils/install-module.js";
28
28
  import { fixLintConfig } from "./fix-lint-config.js";
29
+ import { finalizeNewProjectSetup } from "../../utils/install-workspace-config.js";
29
30
  async function promptAuthStrategies(template) {
30
31
  const isCrud = template === Template.CRUD_SAMPLE;
31
32
  return assertNotCancel(await p.multiselect({
@@ -180,6 +181,8 @@ export async function runNew(args = []) {
180
181
  auth: authStrategies,
181
182
  features
182
183
  });
184
+ spinner.message("Configurando workspace (.vscode e .env)...");
185
+ finalizeNewProjectSetup(project.name, project.packageManager);
183
186
  spinner.stop("Projeto criado com sucesso!");
184
187
  const projectFeatures = resolveProjectFeatures(features, authStrategies);
185
188
  const extrasSummary = [
@@ -196,7 +199,8 @@ export async function runNew(args = []) {
196
199
  `${color.bold("Gerenciador:")} ${project.packageManager}`,
197
200
  `${color.bold("Autenticação:")} ${formatAuthStrategies(authStrategies)}`,
198
201
  `${color.bold("Extras:")} ${extrasSummary || color.dim("nenhum")}`,
199
- `${color.dim("Depois:")} cd ${project.name} && kl-nest add <feature>`
202
+ `${color.dim("Depois:")} cd ${project.name} && ${project.packageManager} start`,
203
+ `${color.dim("Extras:")} kl-nest add <feature>`
200
204
  ].join(`
201
205
  `), "Resumo");
202
206
  }
@@ -5,6 +5,28 @@ 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
+ ];
23
+ export const WORKSPACE_SETUP_PATHS = [
24
+ ".vscode/launch.json",
25
+ ".vscode/settings.json",
26
+ ".vscode/tasks.json",
27
+ ".vscode/extensions.json",
28
+ ".env"
29
+ ];
8
30
  export const CORE_REQUIRED_PATHS = [
9
31
  "src/core/env.ts",
10
32
  "src/host/main.ts",
@@ -165,9 +187,13 @@ export function buildProjectExpectation(template, auth, features) {
165
187
  };
166
188
  }
167
189
  export function requiredPathsForExpectation(expectation) {
168
- const paths = [...CORE_REQUIRED_PATHS];
190
+ const paths = [
191
+ ...CORE_REQUIRED_PATHS,
192
+ ...WORKSPACE_SETUP_PATHS,
193
+ ...E2E_INFRA_PATHS
194
+ ];
169
195
  if (expectation.template === Template.CRUD_SAMPLE) {
170
- paths.push(...CRUD_TEMPLATE_REQUIRED_PATHS);
196
+ paths.push(...CRUD_TEMPLATE_REQUIRED_PATHS, ...CRUD_E2E_EXAMPLE_PATHS);
171
197
  }
172
198
  if (expectation.cache === "redis") {
173
199
  paths.push(...CACHE_REDIS_REQUIRED_PATHS);
@@ -189,7 +215,7 @@ export function requiredPathsForExpectation(expectation) {
189
215
  export function forbiddenPathsForExpectation(expectation) {
190
216
  const paths = [];
191
217
  if (expectation.template === Template.DEFAULT) {
192
- paths.push(...DEFAULT_TEMPLATE_FORBIDDEN_PATHS);
218
+ paths.push(...DEFAULT_TEMPLATE_FORBIDDEN_PATHS, ...CRUD_E2E_EXAMPLE_PATHS);
193
219
  } else {
194
220
  paths.push(...CRUD_TEMPLATE_FORBIDDEN_PATHS);
195
221
  }
@@ -153,6 +153,11 @@ export async function installModule(module, template, projectName = "", options
153
153
  for (const configFile of ["tsconfig.build.json", ".env.example"]) {
154
154
  cpSync(path.join(getSourceCodePath(), configFile), path.join(projectPath, configFile));
155
155
  }
156
+ if (packageManager !== "bun") {
157
+ for (const configFile of ["vitest.config.ts", "vitest.config.e2e.ts"]) {
158
+ cpSync(path.join(getSourceCodePath(), configFile), path.join(projectPath, configFile));
159
+ }
160
+ }
156
161
  patchInfraModuleFile(projectName, false);
157
162
  patchMainFile(projectName, stripMainOptionalFeatures);
158
163
  if (!options.skipPackages) {
@@ -0,0 +1,81 @@
1
+ import { cpSync, existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { getSourceCodePath } from "./get-source-code-path.js";
4
+ import { resolveProjectPath } from "./resolve-project-path.js";
5
+ const PACKAGE_MANAGER_COMMAND = {
6
+ bun: "bun",
7
+ npm: "npm",
8
+ pnpm: "pnpm"
9
+ };
10
+ export function projectNameToSnakeCase(projectName) {
11
+ const baseName = path.isAbsolute(projectName) ? path.basename(projectName) : projectName;
12
+ return baseName.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[\s-]+/g, "_").replace(/__+/g, "_").toLowerCase().replace(/^_+|_+$/g, "");
13
+ }
14
+ function patchDatabaseUrl(content, databaseName) {
15
+ if (!/^DATABASE_URL=/m.test(content)) {
16
+ return content;
17
+ }
18
+ return content.replace(/^(DATABASE_URL=.+\/)([^/\r\n]+)(\s*)$/m, `$1${databaseName}$3`);
19
+ }
20
+ function patchJsonFile(filePath, patch) {
21
+ const content = readFileSync(filePath, "utf8");
22
+ const json = JSON.parse(content);
23
+ patch(json);
24
+ writeFileSync(filePath, `${JSON.stringify(json, null, 2)}
25
+ `);
26
+ }
27
+ function runScriptCommand(packageManager, script) {
28
+ return `${PACKAGE_MANAGER_COMMAND[packageManager]} run ${script}`;
29
+ }
30
+ export function installWorkspaceConfig(projectName, packageManager) {
31
+ const projectRoot = resolveProjectPath(projectName);
32
+ const sourceVscode = path.join(getSourceCodePath(), ".vscode");
33
+ const targetVscode = path.join(projectRoot, ".vscode");
34
+ cpSync(sourceVscode, targetVscode, { recursive: true });
35
+ const pmCommand = PACKAGE_MANAGER_COMMAND[packageManager];
36
+ patchJsonFile(path.join(targetVscode, "launch.json"), (launch) => {
37
+ const config = launch.configurations[0];
38
+ config.runtimeExecutable = pmCommand;
39
+ config.runtimeArgs = ["run", "start:debug"];
40
+ config.cwd = "${workspaceFolder}";
41
+ });
42
+ patchJsonFile(path.join(targetVscode, "tasks.json"), (tasks) => {
43
+ for (const task of tasks.tasks) {
44
+ if (task.command.includes("start:dev")) {
45
+ task.command = runScriptCommand(packageManager, "start:dev");
46
+ } else if (task.command.includes("test")) {
47
+ task.command = runScriptCommand(packageManager, "test");
48
+ } else if (task.command.includes("test:e2e")) {
49
+ task.command = runScriptCommand(packageManager, "test:e2e");
50
+ } else if (task.command.includes("migration:run")) {
51
+ task.command = runScriptCommand(packageManager, "migration:run");
52
+ }
53
+ task.options = { cwd: "${workspaceFolder}" };
54
+ }
55
+ });
56
+ patchJsonFile(path.join(targetVscode, "settings.json"), (settings) => {
57
+ settings["npm.packageManager"] = packageManager;
58
+ });
59
+ const gitignoreSource = path.join(getSourceCodePath(), ".gitignore");
60
+ if (existsSync(gitignoreSource)) {
61
+ cpSync(gitignoreSource, path.join(projectRoot, ".gitignore"), {
62
+ force: true
63
+ });
64
+ }
65
+ }
66
+ export function createEnvFromExample(projectName) {
67
+ const projectRoot = resolveProjectPath(projectName);
68
+ const examplePath = path.join(projectRoot, ".env.example");
69
+ const envPath = path.join(projectRoot, ".env");
70
+ if (!existsSync(examplePath)) {
71
+ return;
72
+ }
73
+ const databaseName = projectNameToSnakeCase(projectName);
74
+ const envContent = patchDatabaseUrl(readFileSync(examplePath, "utf8"), databaseName);
75
+ writeFileSync(examplePath, envContent);
76
+ writeFileSync(envPath, envContent);
77
+ }
78
+ export function finalizeNewProjectSetup(projectName, packageManager) {
79
+ installWorkspaceConfig(projectName, packageManager);
80
+ createEnvFromExample(projectName);
81
+ }
@@ -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
+ }
@@ -2,6 +2,7 @@ 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
7
  import { stripMainOptionalFeatures } from "./patch-main.js";
7
8
  import { stripDefineDocumentationAuth } from "./patch-define-documentation.js";
@@ -85,11 +86,13 @@ const partsToRemove = [
85
86
  replace: [{ from: "PersonMapper.createMap();", to: "" }]
86
87
  }
87
88
  ];
88
- const defaultTemplatePathsToRemove = [
89
+ const defaultSampleTestPathsToRemove = [
89
90
  "src/test/application",
90
91
  "src/test/mockup/person",
91
92
  "src/test/host/controllers/person/person.controller.e2e.spec.ts",
92
- "src/test/host/controllers/person/lazy-loading.e2e.spec.ts",
93
+ "src/test/host/controllers/person/lazy-loading.e2e.spec.ts"
94
+ ];
95
+ const defaultAuthE2ePathsToRemove = [
93
96
  "src/test/host/controllers/auth/auth.controller.e2e.spec.ts",
94
97
  "src/test/app-auth-test.module.ts",
95
98
  "src/test/create-auth-e2e-test-app.ts"
@@ -154,7 +157,11 @@ export async function removeSampleParts(projectName) {
154
157
  writeFileSync(partPath, content, "utf8");
155
158
  }
156
159
  patchAppModuleJobs(projectName, { eventHandlers: [], cronJobs: [] });
157
- removePaths(projectName, defaultTemplatePathsToRemove);
160
+ patchAppTestModuleForDefault(projectName);
161
+ removePaths(projectName, [
162
+ ...defaultSampleTestPathsToRemove,
163
+ ...defaultAuthE2ePathsToRemove
164
+ ]);
158
165
  }
159
166
  export async function cleanDefaultTemplateWithoutAuth(projectName) {
160
167
  stripDefaultProjectAuth(projectName);
@@ -0,0 +1,7 @@
1
+ {
2
+ "recommendations": [
3
+ "esbenp.prettier-vscode",
4
+ "dbaeumer.vscode-eslint",
5
+ "PKief.material-icon-theme"
6
+ ]
7
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "version": "0.2.0",
3
+ "configurations": [
4
+ {
5
+ "type": "node",
6
+ "request": "launch",
7
+ "name": "NestJS Debug",
8
+ "runtimeExecutable": "bun",
9
+ "runtimeArgs": ["run", "start:debug"],
10
+ "cwd": "${workspaceFolder}",
11
+ "autoAttachChildProcesses": true,
12
+ "restart": true,
13
+ "console": "integratedTerminal"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,77 @@
1
+ {
2
+ "material-icon-theme.activeIconPack": "nest",
3
+ "editor.fontFamily": "Fira Code",
4
+ "editor.fontSize": 15,
5
+ "editor.fontLigatures": true,
6
+ "workbench.startupEditor": "newUntitledFile",
7
+ "js/ts.suggest.autoImports": true,
8
+ "js/ts.updateImportsOnFileMove.enabled": "always",
9
+ "editor.rulers": [80, 120],
10
+ "extensions.ignoreRecommendations": true,
11
+ "files.associations": {
12
+ ".env.*": "dotenv",
13
+ ".prettierrc": "json"
14
+ },
15
+ "editor.parameterHints.enabled": false,
16
+ "editor.renderLineHighlight": "gutter",
17
+ "editor.lineHeight": 26,
18
+ "editor.suggestSelection": "first",
19
+ "explorer.confirmDelete": false,
20
+ "workbench.editor.labelFormat": "short",
21
+ "editor.acceptSuggestionOnCommitCharacter": false,
22
+ "explorer.compactFolders": false,
23
+ "git.enableSmartCommit": true,
24
+ "explorer.confirmDragAndDrop": false,
25
+ "terminal.integrated.fontSize": 14,
26
+ "breadcrumbs.enabled": true,
27
+ "editor.tabSize": 2,
28
+ "workbench.iconTheme": "material-icon-theme",
29
+ "material-icon-theme.languages.associations": {
30
+ "dotenv": "tune"
31
+ },
32
+ "material-icon-theme.files.associations": {
33
+ "*.e2e-spec.ts": "test-js",
34
+ "*.dto.ts": "diff",
35
+ "*.handler.ts": "esbuild",
36
+ "*.request.ts": "log",
37
+ "*.schema.ts": "scheme",
38
+ "*.response.ts": "conduct",
39
+ "*.validator.ts": "commitlint"
40
+ },
41
+ "material-icon-theme.folders.associations": {
42
+ "entities": "class",
43
+ "migrations": "tools",
44
+ "schemas": "class",
45
+ "domain": "class",
46
+ "infra": "app",
47
+ "dtos": "typescript",
48
+ "repositories": "mappings"
49
+ },
50
+ "[javascript]": {
51
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
52
+ "editor.formatOnSave": true,
53
+ "editor.codeActionsOnSave": {
54
+ "source.fixAll.eslint": "explicit"
55
+ }
56
+ },
57
+ "[typescript]": {
58
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
59
+ "editor.formatOnSave": true,
60
+ "editor.codeActionsOnSave": {
61
+ "source.fixAll.eslint": "explicit"
62
+ }
63
+ },
64
+ "prettier.documentSelectors": ["**/*.ts", "**/*.tsx", "**/*.js"],
65
+ "npm.scriptExplorerAction": "run",
66
+ "npm.packageManager": "bun",
67
+ "editor.wordWrap": "on",
68
+ "editor.wrappingIndent": "indent",
69
+ "eslint.workingDirectories": [{ "mode": "auto" }],
70
+ "eslint.validate": [
71
+ "javascript",
72
+ "javascriptreact",
73
+ "typescript",
74
+ "typescriptreact"
75
+ ],
76
+ "files.eol": "\n"
77
+ }
@@ -0,0 +1,46 @@
1
+ {
2
+ "version": "2.0.0",
3
+ "tasks": [
4
+ {
5
+ "type": "shell",
6
+ "command": "bun run start:dev",
7
+ "options": {
8
+ "cwd": "${workspaceFolder}"
9
+ },
10
+ "group": {
11
+ "kind": "build",
12
+ "isDefault": true
13
+ },
14
+ "isBackground": true,
15
+ "problemMatcher": [],
16
+ "label": "Start API (dev)",
17
+ "detail": "nest start --watch"
18
+ },
19
+ {
20
+ "type": "shell",
21
+ "command": "bun run test",
22
+ "options": {
23
+ "cwd": "${workspaceFolder}"
24
+ },
25
+ "group": "test",
26
+ "label": "Run tests"
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
+ },
37
+ {
38
+ "type": "shell",
39
+ "command": "bun run migration:run",
40
+ "options": {
41
+ "cwd": "${workspaceFolder}"
42
+ },
43
+ "label": "Run migrations"
44
+ }
45
+ ]
46
+ }
@@ -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 = { ...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
  }
@@ -94,7 +94,11 @@ export class AutoMapper {
94
94
  continue;
95
95
  }
96
96
 
97
- targetInstance[targetPropName] = data[sourceProp.name];
97
+ const sourceValue = data[sourceProp.name];
98
+
99
+ if (sourceValue !== undefined) {
100
+ targetInstance[targetPropName] = sourceValue;
101
+ }
98
102
  }
99
103
 
100
104
  return targetInstance;
@@ -57,32 +57,40 @@ export class MappingStore {
57
57
  }
58
58
  }
59
59
 
60
- current = Object.getPrototypeOf(current) as Type<any> | null;
60
+ const parentPrototype = Object.getPrototypeOf(current.prototype);
61
+ current = (parentPrototype?.constructor as Type<any> | undefined) ?? null;
61
62
  }
62
63
 
63
64
  return props;
64
65
  }
65
66
 
66
67
  static getPropType(entity: Type<any>, propName: string) {
67
- const prop = this._entities.get(entity)?.find((p) => p.name === propName);
68
+ let current: Type<any> | null = entity;
68
69
 
69
- if (!prop) {
70
- throw new Error(
71
- `Property ${propName} not found in entity ${entity.name}`,
72
- );
73
- }
70
+ while (current?.prototype) {
71
+ const prop = this._entities.get(current)?.find((p) => p.name === propName);
74
72
 
75
- const { type, isArray } = prop;
73
+ if (prop) {
74
+ const { type, isArray } = prop;
76
75
 
77
- if (!type) {
78
- return null;
79
- }
76
+ if (!type) {
77
+ return null;
78
+ }
80
79
 
81
- if (isArray && type instanceof Function) {
82
- return (type as () => Type<any>)();
80
+ if (isArray && type instanceof Function) {
81
+ return (type as () => Type<any>)();
82
+ }
83
+
84
+ return type;
85
+ }
86
+
87
+ const parentPrototype = Object.getPrototypeOf(current.prototype);
88
+ current = (parentPrototype?.constructor as Type<any> | undefined) ?? null;
83
89
  }
84
90
 
85
- return type;
91
+ throw new Error(
92
+ `Property ${propName} not found in entity ${entity.name}`,
93
+ );
86
94
  }
87
95
 
88
96
  static add(
@@ -1,17 +1,34 @@
1
1
  import { DATA_SOURCE_PROVIDER_TOKEN } from '@/infra/database/data-source-factory';
2
2
  import { Inject, Injectable } from '@nestjs/common';
3
- import { TypeOrmHealthIndicator } from '@nestjs/terminus';
3
+ import {
4
+ HealthIndicatorResult,
5
+ HealthIndicatorService,
6
+ } from '@nestjs/terminus';
4
7
  import { DataSource } from 'typeorm';
5
8
 
6
9
  @Injectable()
7
10
  export class DatabaseIndicator {
8
11
  constructor(
9
- private readonly typeOrm: TypeOrmHealthIndicator,
12
+ private readonly healthIndicatorService: HealthIndicatorService,
10
13
  @Inject(DATA_SOURCE_PROVIDER_TOKEN)
11
14
  private readonly dataSource: DataSource,
12
15
  ) {}
13
16
 
14
- isHealthy() {
15
- return this.typeOrm.pingCheck('db', { connection: this.dataSource });
17
+ async isHealthy(): Promise<HealthIndicatorResult> {
18
+ const indicator = this.healthIndicatorService.check('db');
19
+
20
+ try {
21
+ if (!this.dataSource.isInitialized) {
22
+ return indicator.down({ message: 'DataSource not initialized' });
23
+ }
24
+
25
+ await this.dataSource.query('SELECT 1');
26
+
27
+ return indicator.up();
28
+ } catch (error) {
29
+ const message = error instanceof Error ? error.message : String(error);
30
+
31
+ return indicator.down({ message });
32
+ }
16
33
  }
17
34
  }
@@ -21,18 +21,58 @@ describe('ReadManyPersonHandler', () => {
21
21
  contacts: [],
22
22
  });
23
23
 
24
+ let capturedPage: number | undefined;
24
25
  const repository = {
25
- findMany: async () => ({ items: [person], count: 1 }),
26
+ findMany: async (query: { page?: number; skip: () => number }) => {
27
+ capturedPage = query.page;
28
+ return { items: [person], count: 1 };
29
+ },
26
30
  } as unknown as IPersonRepository;
27
31
 
28
32
  const handler = new ReadManyPersonHandler(repository, new CacheStub());
29
- const result = await handler.handle(new ReadManyPersonRequest());
33
+ const result = await handler.handle({ page: '2', limit: '5' } as never);
30
34
 
35
+ expect(capturedPage).toBe(1);
31
36
  expect(result.count).toBe(1);
32
37
  expect(result.items).toHaveLength(1);
33
38
  expect(result.items[0].name).toBe('Jane');
34
39
  });
35
40
 
41
+ it('preserva paginação ao receber instância de ReadManyPersonRequest', async () => {
42
+ let capturedPage: number | undefined;
43
+ const repository = {
44
+ findMany: async (query: { page?: number }) => {
45
+ capturedPage = query.page;
46
+ return { items: [], count: 0 };
47
+ },
48
+ } as unknown as IPersonRepository;
49
+
50
+ const handler = new ReadManyPersonHandler(repository, new CacheStub());
51
+ const request = Object.assign(new ReadManyPersonRequest(), {
52
+ page: '2',
53
+ limit: '5',
54
+ });
55
+
56
+ await handler.handle(request);
57
+
58
+ expect(capturedPage).toBe(1);
59
+ });
60
+
61
+ it('aplica defaults de paginação quando a request não informa page', async () => {
62
+ let capturedPage: number | undefined;
63
+ const repository = {
64
+ findMany: async (query: { page?: number }) => {
65
+ capturedPage = query.page;
66
+ return { items: [], count: 0 };
67
+ },
68
+ } as unknown as IPersonRepository;
69
+
70
+ const handler = new ReadManyPersonHandler(repository, new CacheStub());
71
+ await handler.handle(new ReadManyPersonRequest());
72
+
73
+ expect(capturedPage).toBe(0);
74
+ });
75
+
36
76
  it('reutiliza cache para a mesma consulta de listagem', async () => {
37
77
  const person = Person.from({
38
78
  id: 1,
@@ -137,6 +137,67 @@ describe('AutoMapper', () => {
137
137
  expect(target.fullName).toBe('John Doe');
138
138
  });
139
139
 
140
+ it('should map inherited properties from parent classes', () => {
141
+ class BaseRequest {
142
+ @AutoMap()
143
+ page?: number;
144
+
145
+ @AutoMap()
146
+ limit?: number;
147
+ }
148
+
149
+ class ChildRequest extends BaseRequest {
150
+ @AutoMap()
151
+ name?: string;
152
+ }
153
+
154
+ class BaseDto {
155
+ @AutoMap()
156
+ page?: number = 0;
157
+
158
+ @AutoMap()
159
+ limit?: number = 10;
160
+ }
161
+
162
+ class ChildDto extends BaseDto {
163
+ @AutoMap()
164
+ name?: string;
165
+ }
166
+
167
+ createMap(ChildRequest, ChildDto);
168
+
169
+ const source = Object.assign(new ChildRequest(), {
170
+ page: 2,
171
+ limit: 25,
172
+ name: 'John',
173
+ });
174
+
175
+ const target = AutoMapper.map(source, ChildRequest, ChildDto);
176
+
177
+ expect(target.page).toBe(2);
178
+ expect(target.limit).toBe(25);
179
+ expect(target.name).toBe('John');
180
+ });
181
+
182
+ it('should resolve inherited property types via getProps and getPropType', () => {
183
+ class BaseRequest {
184
+ @AutoMap()
185
+ page?: number;
186
+ }
187
+
188
+ class ChildRequest extends BaseRequest {
189
+ @AutoMap()
190
+ name?: string;
191
+ }
192
+
193
+ const props = MappingStore.getProps(ChildRequest).map((p) => p.name);
194
+
195
+ expect(props).toContain('page');
196
+ expect(props).toContain('name');
197
+ expect(MappingStore.getPropType(ChildRequest, 'page')).toBe(Number);
198
+ expect(MappingStore.getPropType(ChildRequest, 'name')).toBe(String);
199
+ });
200
+
140
201
  it('should map nested objects using the target property name', () => {
141
202
  class SourceChild {
142
203
  @AutoMap()
@@ -0,0 +1,24 @@
1
+ /// <reference types="bun-types/test-globals" />
2
+
3
+ import { createE2ETestApp } from '@/test/create-e2e-test-app';
4
+ import type { INestApplication } from '@nestjs/common';
5
+
6
+ /**
7
+ * Estrutura mínima de E2E — use como ponto de partida no template Padrão.
8
+ * No exemplo CRUD, veja `person.controller.e2e.spec.ts` e `auth.controller.e2e.spec.ts`.
9
+ */
10
+ describe('App (E2E)', () => {
11
+ let app: INestApplication;
12
+
13
+ beforeAll(async () => {
14
+ app = await createE2ETestApp();
15
+ });
16
+
17
+ afterAll(async () => {
18
+ await app.close();
19
+ });
20
+
21
+ it('should bootstrap the application', () => {
22
+ expect(app.getHttpServer()).toBeDefined();
23
+ });
24
+ });
@@ -1,9 +1,24 @@
1
1
  import { Type } from '@nestjs/common';
2
2
  import { execSync } from 'node:child_process';
3
3
  import { randomUUID } from 'node:crypto';
4
+ import { readFileSync } from 'node:fs';
4
5
  import { setE2EDatabaseUrl } from '../e2e-context';
5
6
  import { E2EDatabaseClient } from './e2e-database-client';
6
7
 
8
+ function resolveMigrationRunner(): string {
9
+ const packageJson = JSON.parse(readFileSync('package.json', 'utf8')) as {
10
+ packageManager?: string;
11
+ };
12
+
13
+ const packageManager = packageJson.packageManager ?? 'bun';
14
+
15
+ if (packageManager === 'bun') {
16
+ return 'bun';
17
+ }
18
+
19
+ return 'node --import ts-node/register/transpile-only';
20
+ }
21
+
7
22
  function generateUniqueDatabaseURL() {
8
23
  const schemaId = randomUUID();
9
24
 
@@ -33,8 +48,9 @@ export async function createE2EDatabase<T extends E2EDatabaseClient>(
33
48
  await client.createDatabase(schemaId);
34
49
 
35
50
  const env = { ...process.env, DATABASE_URL: url, NODE_ENV: 'test' };
51
+ const migrationRunner = resolveMigrationRunner();
36
52
  execSync(
37
- 'bun ./node_modules/typeorm/cli.js migration:run -d ./src/infra/database/migrations/migration-datasource.ts',
53
+ `${migrationRunner} ./node_modules/typeorm/cli.js migration:run -d ./src/infra/database/migrations/migration-datasource.ts`,
38
54
  {
39
55
  cwd: process.cwd(),
40
56
  env,
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from 'vitest/config';
2
+ import tsconfigPaths from 'vite-tsconfig-paths';
3
+
4
+ export default defineConfig({
5
+ plugins: [tsconfigPaths()],
6
+ test: {
7
+ include: ['src/test/**/*.e2e.spec.ts'],
8
+ setupFiles: ['src/test/setup-e2e.ts'],
9
+ environment: 'node',
10
+ testTimeout: 60_000,
11
+ hookTimeout: 60_000,
12
+ },
13
+ });
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from 'vitest/config';
2
+ import tsconfigPaths from 'vite-tsconfig-paths';
3
+
4
+ export default defineConfig({
5
+ plugins: [tsconfigPaths()],
6
+ test: {
7
+ include: ['src/test/**/*.spec.ts'],
8
+ exclude: ['src/test/**/*.e2e.spec.ts'],
9
+ setupFiles: ['src/test/setup.ts'],
10
+ environment: 'node',
11
+ },
12
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koalarx/nest",
3
- "version": "4.0.3",
3
+ "version": "4.0.6",
4
4
  "description": "CLI para criar APIs NestJS com arquitetura DDD — copia módulos prontos para o seu repositório.",
5
5
  "license": "MIT",
6
6
  "type": "module",