nestjs-ddd-cli 2.2.1 → 3.2.1
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/README.md +247 -408
- package/ddd.schema.json +111 -0
- package/dist/commands/aggregate-validator.d.ts +9 -0
- package/dist/commands/aggregate-validator.js +953 -0
- package/dist/commands/aggregate-validator.js.map +1 -0
- package/dist/commands/ai-assist.d.ts +8 -0
- package/dist/commands/ai-assist.js +337 -0
- package/dist/commands/ai-assist.js.map +1 -0
- package/dist/commands/api-contracts.d.ts +9 -0
- package/dist/commands/api-contracts.js +1368 -0
- package/dist/commands/api-contracts.js.map +1 -0
- package/dist/commands/api-docs.d.ts +8 -0
- package/dist/commands/api-docs.js +408 -0
- package/dist/commands/api-docs.js.map +1 -0
- package/dist/commands/api-versioning.d.ts +11 -0
- package/dist/commands/api-versioning.js +643 -0
- package/dist/commands/api-versioning.js.map +1 -0
- package/dist/commands/audit-logging.d.ts +9 -0
- package/dist/commands/audit-logging.js +1129 -0
- package/dist/commands/audit-logging.js.map +1 -0
- package/dist/commands/batch-generate.d.ts +10 -0
- package/dist/commands/batch-generate.js +405 -0
- package/dist/commands/batch-generate.js.map +1 -0
- package/dist/commands/caching-strategies.d.ts +9 -0
- package/dist/commands/caching-strategies.js +874 -0
- package/dist/commands/caching-strategies.js.map +1 -0
- package/dist/commands/code-analyzer.d.ts +42 -0
- package/dist/commands/code-analyzer.js +474 -0
- package/dist/commands/code-analyzer.js.map +1 -0
- package/dist/commands/database-seeding.d.ts +6 -0
- package/dist/commands/database-seeding.js +621 -0
- package/dist/commands/database-seeding.js.map +1 -0
- package/dist/commands/db-optimization.d.ts +7 -0
- package/dist/commands/db-optimization.js +687 -0
- package/dist/commands/db-optimization.js.map +1 -0
- package/dist/commands/dependency-graph.d.ts +6 -0
- package/dist/commands/dependency-graph.js +329 -0
- package/dist/commands/dependency-graph.js.map +1 -0
- package/dist/commands/doctor-enhanced.d.ts +22 -0
- package/dist/commands/doctor-enhanced.js +543 -0
- package/dist/commands/doctor-enhanced.js.map +1 -0
- package/dist/commands/doctor.d.ts +4 -0
- package/dist/commands/doctor.js +151 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/env-manager.d.ts +6 -0
- package/dist/commands/env-manager.js +419 -0
- package/dist/commands/env-manager.js.map +1 -0
- package/dist/commands/event-sourcing-full.d.ts +10 -0
- package/dist/commands/event-sourcing-full.js +1107 -0
- package/dist/commands/event-sourcing-full.js.map +1 -0
- package/dist/commands/feature-flags.d.ts +9 -0
- package/dist/commands/feature-flags.js +824 -0
- package/dist/commands/feature-flags.js.map +1 -0
- package/dist/commands/filter-dsl.d.ts +10 -0
- package/dist/commands/filter-dsl.js +1407 -0
- package/dist/commands/filter-dsl.js.map +1 -0
- package/dist/commands/generate-all.js +485 -32
- package/dist/commands/generate-all.js.map +1 -1
- package/dist/commands/generate-deployment.d.ts +8 -0
- package/dist/commands/generate-deployment.js +746 -0
- package/dist/commands/generate-deployment.js.map +1 -0
- package/dist/commands/generate-domain-service.d.ts +14 -0
- package/dist/commands/generate-domain-service.js +796 -0
- package/dist/commands/generate-domain-service.js.map +1 -0
- package/dist/commands/generate-entity.js +82 -24
- package/dist/commands/generate-entity.js.map +1 -1
- package/dist/commands/generate-from-schema.d.ts +56 -0
- package/dist/commands/generate-from-schema.js +222 -0
- package/dist/commands/generate-from-schema.js.map +1 -0
- package/dist/commands/generate-orchestrator.d.ts +14 -0
- package/dist/commands/generate-orchestrator.js +887 -0
- package/dist/commands/generate-orchestrator.js.map +1 -0
- package/dist/commands/generate-repository.d.ts +14 -0
- package/dist/commands/generate-repository.js +1019 -0
- package/dist/commands/generate-repository.js.map +1 -0
- package/dist/commands/generate-shared.d.ts +4 -0
- package/dist/commands/generate-shared.js +388 -0
- package/dist/commands/generate-shared.js.map +1 -0
- package/dist/commands/generate-value-object.d.ts +32 -0
- package/dist/commands/generate-value-object.js +700 -0
- package/dist/commands/generate-value-object.js.map +1 -0
- package/dist/commands/graphql-subscriptions.d.ts +6 -0
- package/dist/commands/graphql-subscriptions.js +607 -0
- package/dist/commands/graphql-subscriptions.js.map +1 -0
- package/dist/commands/graphql-types.d.ts +5 -0
- package/dist/commands/graphql-types.js +423 -0
- package/dist/commands/graphql-types.js.map +1 -0
- package/dist/commands/health-probes-advanced.d.ts +6 -0
- package/dist/commands/health-probes-advanced.js +655 -0
- package/dist/commands/health-probes-advanced.js.map +1 -0
- package/dist/commands/i18n-setup.d.ts +10 -0
- package/dist/commands/i18n-setup.js +677 -0
- package/dist/commands/i18n-setup.js.map +1 -0
- package/dist/commands/init-config.d.ts +6 -0
- package/dist/commands/init-config.js +370 -0
- package/dist/commands/init-config.js.map +1 -0
- package/dist/commands/init-project.js +56 -6
- package/dist/commands/init-project.js.map +1 -1
- package/dist/commands/interactive-scaffold.d.ts +5 -0
- package/dist/commands/interactive-scaffold.js +271 -0
- package/dist/commands/interactive-scaffold.js.map +1 -0
- package/dist/commands/metrics-prometheus.d.ts +6 -0
- package/dist/commands/metrics-prometheus.js +681 -0
- package/dist/commands/metrics-prometheus.js.map +1 -0
- package/dist/commands/migration-engine.d.ts +6 -0
- package/dist/commands/migration-engine.js +446 -0
- package/dist/commands/migration-engine.js.map +1 -0
- package/dist/commands/migration.d.ts +12 -0
- package/dist/commands/migration.js +484 -0
- package/dist/commands/migration.js.map +1 -0
- package/dist/commands/monorepo.d.ts +8 -0
- package/dist/commands/monorepo.js +483 -0
- package/dist/commands/monorepo.js.map +1 -0
- package/dist/commands/multi-database.d.ts +5 -0
- package/dist/commands/multi-database.js +439 -0
- package/dist/commands/multi-database.js.map +1 -0
- package/dist/commands/observability-tracing.d.ts +10 -0
- package/dist/commands/observability-tracing.js +740 -0
- package/dist/commands/observability-tracing.js.map +1 -0
- package/dist/commands/openapi-export.d.ts +8 -0
- package/dist/commands/openapi-export.js +359 -0
- package/dist/commands/openapi-export.js.map +1 -0
- package/dist/commands/perf-analyzer.d.ts +8 -0
- package/dist/commands/perf-analyzer.js +423 -0
- package/dist/commands/perf-analyzer.js.map +1 -0
- package/dist/commands/rate-limiting.d.ts +10 -0
- package/dist/commands/rate-limiting.js +953 -0
- package/dist/commands/rate-limiting.js.map +1 -0
- package/dist/commands/recipe-plugin.d.ts +56 -0
- package/dist/commands/recipe-plugin.js +315 -0
- package/dist/commands/recipe-plugin.js.map +1 -0
- package/dist/commands/recipe.d.ts +6 -0
- package/dist/commands/recipe.js +3941 -0
- package/dist/commands/recipe.js.map +1 -0
- package/dist/commands/recipes/elasticsearch.recipe.d.ts +1 -0
- package/dist/commands/recipes/elasticsearch.recipe.js +761 -0
- package/dist/commands/recipes/elasticsearch.recipe.js.map +1 -0
- package/dist/commands/recipes/event-sourcing.recipe.d.ts +1 -0
- package/dist/commands/recipes/event-sourcing.recipe.js +889 -0
- package/dist/commands/recipes/event-sourcing.recipe.js.map +1 -0
- package/dist/commands/recipes/index.d.ts +7 -0
- package/dist/commands/recipes/index.js +24 -0
- package/dist/commands/recipes/index.js.map +1 -0
- package/dist/commands/recipes/message-queue.recipe.d.ts +1 -0
- package/dist/commands/recipes/message-queue.recipe.js +706 -0
- package/dist/commands/recipes/message-queue.recipe.js.map +1 -0
- package/dist/commands/recipes/middleware.recipe.d.ts +1 -0
- package/dist/commands/recipes/middleware.recipe.js +383 -0
- package/dist/commands/recipes/middleware.recipe.js.map +1 -0
- package/dist/commands/recipes/multi-tenancy.recipe.d.ts +1 -0
- package/dist/commands/recipes/multi-tenancy.recipe.js +520 -0
- package/dist/commands/recipes/multi-tenancy.recipe.js.map +1 -0
- package/dist/commands/recipes/oauth2.recipe.d.ts +1 -0
- package/dist/commands/recipes/oauth2.recipe.js +472 -0
- package/dist/commands/recipes/oauth2.recipe.js.map +1 -0
- package/dist/commands/recipes/websocket.recipe.d.ts +1 -0
- package/dist/commands/recipes/websocket.recipe.js +453 -0
- package/dist/commands/recipes/websocket.recipe.js.map +1 -0
- package/dist/commands/resilience-patterns.d.ts +13 -0
- package/dist/commands/resilience-patterns.js +1029 -0
- package/dist/commands/resilience-patterns.js.map +1 -0
- package/dist/commands/security-patterns.d.ts +11 -0
- package/dist/commands/security-patterns.js +2233 -0
- package/dist/commands/security-patterns.js.map +1 -0
- package/dist/commands/template-debug.d.ts +27 -0
- package/dist/commands/template-debug.js +388 -0
- package/dist/commands/template-debug.js.map +1 -0
- package/dist/commands/test-factory-full.d.ts +9 -0
- package/dist/commands/test-factory-full.js +1570 -0
- package/dist/commands/test-factory-full.js.map +1 -0
- package/dist/commands/test-scaffold.d.ts +7 -0
- package/dist/commands/test-scaffold.js +621 -0
- package/dist/commands/test-scaffold.js.map +1 -0
- package/dist/index.js +1088 -0
- package/dist/index.js.map +1 -1
- package/dist/templates/ai-context/CLAUDE.md.hbs +158 -0
- package/dist/templates/ai-context/conventions.md.hbs +154 -0
- package/dist/templates/command/create-command.hbs +6 -14
- package/dist/templates/command/delete-command.hbs +19 -0
- package/dist/templates/command/update-command.hbs +24 -0
- package/dist/templates/controller/controller.hbs +64 -17
- package/dist/templates/dto/create-dto.hbs +29 -5
- package/dist/templates/dto/filter-dto.hbs +52 -0
- package/dist/templates/dto/filter-query.dto.hbs +148 -0
- package/dist/templates/dto/paginated-response.dto.hbs +29 -0
- package/dist/templates/dto/pagination-query.dto.hbs +30 -0
- package/dist/templates/dto/response-dto.hbs +38 -0
- package/dist/templates/dto/update-dto.hbs +11 -0
- package/dist/templates/entity/entity.hbs +32 -1
- package/dist/templates/event/domain-event.hbs +33 -7
- package/dist/templates/event/event-handler.hbs +40 -0
- package/dist/templates/exception/base-exceptions.hbs +69 -0
- package/dist/templates/exception/entity-not-found.exception.hbs +7 -0
- package/dist/templates/mapper/mapper.hbs +49 -24
- package/dist/templates/module/module.hbs +34 -10
- package/dist/templates/orm-entity/orm-entity.hbs +63 -12
- package/dist/templates/prisma/prisma-mapper.hbs +71 -0
- package/dist/templates/prisma/prisma-repository.hbs +114 -0
- package/dist/templates/prisma/prisma-schema.hbs +20 -0
- package/dist/templates/prisma/prisma-service.hbs +51 -0
- package/dist/templates/query/get-all.query.hbs +50 -0
- package/dist/templates/query/get-by-id.query.hbs +31 -0
- package/dist/templates/repository/repository.hbs +55 -13
- package/dist/templates/resolver/graphql-input.hbs +54 -0
- package/dist/templates/resolver/graphql-type.hbs +58 -0
- package/dist/templates/resolver/pagination-args.hbs +33 -0
- package/dist/templates/resolver/resolver.hbs +62 -0
- package/dist/templates/shared/prisma-query-builder.util.hbs +189 -0
- package/dist/templates/shared/query-builder.util.hbs +218 -0
- package/dist/templates/test/controller.spec.hbs +124 -0
- package/dist/templates/test/repository.spec.hbs +158 -0
- package/dist/templates/test/usecase.spec.hbs +116 -0
- package/dist/templates/usecase/create-usecase.hbs +19 -7
- package/dist/templates/usecase/delete-usecase.hbs +17 -0
- package/dist/templates/usecase/update-usecase.hbs +31 -0
- package/dist/utils/config.utils.d.ts +45 -0
- package/dist/utils/config.utils.js +211 -0
- package/dist/utils/config.utils.js.map +1 -0
- package/dist/utils/error.utils.d.ts +145 -0
- package/dist/utils/error.utils.js +422 -0
- package/dist/utils/error.utils.js.map +1 -0
- package/dist/utils/field.utils.d.ts +54 -0
- package/dist/utils/field.utils.js +389 -0
- package/dist/utils/field.utils.js.map +1 -0
- package/dist/utils/file.utils.d.ts +19 -8
- package/dist/utils/file.utils.js +135 -4
- package/dist/utils/file.utils.js.map +1 -1
- package/dist/utils/idempotency.utils.d.ts +123 -0
- package/dist/utils/idempotency.utils.js +444 -0
- package/dist/utils/idempotency.utils.js.map +1 -0
- package/dist/utils/naming.utils.js +24 -5
- package/dist/utils/naming.utils.js.map +1 -1
- package/dist/utils/performance.utils.d.ts +37 -0
- package/dist/utils/performance.utils.js +158 -0
- package/dist/utils/performance.utils.js.map +1 -0
- package/dist/utils/relation.utils.d.ts +92 -0
- package/dist/utils/relation.utils.js +388 -0
- package/dist/utils/relation.utils.js.map +1 -0
- package/dist/utils/rollback.utils.d.ts +49 -0
- package/dist/utils/rollback.utils.js +306 -0
- package/dist/utils/rollback.utils.js.map +1 -0
- package/dist/utils/schema.utils.d.ts +123 -0
- package/dist/utils/schema.utils.js +419 -0
- package/dist/utils/schema.utils.js.map +1 -0
- package/dist/utils/security.utils.d.ts +57 -0
- package/dist/utils/security.utils.js +315 -0
- package/dist/utils/security.utils.js.map +1 -0
- package/dist/utils/template-engine.utils.d.ts +80 -0
- package/dist/utils/template-engine.utils.js +463 -0
- package/dist/utils/template-engine.utils.js.map +1 -0
- package/dist/utils/validation-registry.utils.d.ts +160 -0
- package/dist/utils/validation-registry.utils.js +526 -0
- package/dist/utils/validation-registry.utils.js.map +1 -0
- package/package.json +3 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Args, ID, Mutation, Query, Resolver } from "@nestjs/graphql";
|
|
2
|
+
import { CommandBus, QueryBus } from "@nestjs/cqrs";
|
|
3
|
+
import { Create{{entityNamePascal}}Command } from "@modules/{{moduleNameKebab}}/application/commands/create-{{entityNameKebab}}.command";
|
|
4
|
+
import { Update{{entityNamePascal}}Command } from "@modules/{{moduleNameKebab}}/application/commands/update-{{entityNameKebab}}.command";
|
|
5
|
+
import { Delete{{entityNamePascal}}Command } from "@modules/{{moduleNameKebab}}/application/commands/delete-{{entityNameKebab}}.command";
|
|
6
|
+
import { Get{{entityNamePascal}}ByIdQuery } from "@modules/{{moduleNameKebab}}/application/queries/get-{{entityNameKebab}}-by-id.query";
|
|
7
|
+
import { GetAll{{entityNamePluralPascal}}Query } from "@modules/{{moduleNameKebab}}/application/queries/get-all-{{entityNamePluralKebab}}.query";
|
|
8
|
+
import { Create{{entityNamePascal}}Input } from "./inputs/create-{{entityNameKebab}}.input";
|
|
9
|
+
import { Update{{entityNamePascal}}Input } from "./inputs/update-{{entityNameKebab}}.input";
|
|
10
|
+
import { {{entityNamePascal}}Type } from "./types/{{entityNameKebab}}.type";
|
|
11
|
+
import { Paginated{{entityNamePluralPascal}}Type } from "./types/paginated-{{entityNamePluralKebab}}.type";
|
|
12
|
+
import { PaginationArgs } from "./args/pagination.args";
|
|
13
|
+
|
|
14
|
+
@Resolver(() => {{entityNamePascal}}Type)
|
|
15
|
+
export class {{entityNamePascal}}Resolver {
|
|
16
|
+
constructor(
|
|
17
|
+
private readonly commandBus: CommandBus,
|
|
18
|
+
private readonly queryBus: QueryBus,
|
|
19
|
+
) {}
|
|
20
|
+
|
|
21
|
+
@Query(() => {{entityNamePascal}}Type, { name: "{{entityNameCamel}}" })
|
|
22
|
+
async get{{entityNamePascal}}(
|
|
23
|
+
@Args("id", { type: () => ID }) id: string,
|
|
24
|
+
): Promise<{{entityNamePascal}}Type> {
|
|
25
|
+
const query = new Get{{entityNamePascal}}ByIdQuery(id);
|
|
26
|
+
return this.queryBus.execute(query);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Query(() => Paginated{{entityNamePluralPascal}}Type, { name: "{{entityNamePluralCamel}}" })
|
|
30
|
+
async get{{entityNamePluralPascal}}(
|
|
31
|
+
@Args() pagination: PaginationArgs,
|
|
32
|
+
): Promise<Paginated{{entityNamePluralPascal}}Type> {
|
|
33
|
+
const query = new GetAll{{entityNamePluralPascal}}Query(pagination);
|
|
34
|
+
return this.queryBus.execute(query);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Mutation(() => {{entityNamePascal}}Type)
|
|
38
|
+
async create{{entityNamePascal}}(
|
|
39
|
+
@Args("input") input: Create{{entityNamePascal}}Input,
|
|
40
|
+
): Promise<{{entityNamePascal}}Type> {
|
|
41
|
+
const command = new Create{{entityNamePascal}}Command(input);
|
|
42
|
+
return this.commandBus.execute(command);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@Mutation(() => {{entityNamePascal}}Type)
|
|
46
|
+
async update{{entityNamePascal}}(
|
|
47
|
+
@Args("id", { type: () => ID }) id: string,
|
|
48
|
+
@Args("input") input: Update{{entityNamePascal}}Input,
|
|
49
|
+
): Promise<{{entityNamePascal}}Type> {
|
|
50
|
+
const command = new Update{{entityNamePascal}}Command(id, input);
|
|
51
|
+
return this.commandBus.execute(command);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@Mutation(() => Boolean)
|
|
55
|
+
async delete{{entityNamePascal}}(
|
|
56
|
+
@Args("id", { type: () => ID }) id: string,
|
|
57
|
+
): Promise<boolean> {
|
|
58
|
+
const command = new Delete{{entityNamePascal}}Command(id);
|
|
59
|
+
await this.commandBus.execute(command);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query builder utilities for Prisma
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type FilterOperator =
|
|
6
|
+
| "eq"
|
|
7
|
+
| "ne"
|
|
8
|
+
| "gt"
|
|
9
|
+
| "gte"
|
|
10
|
+
| "lt"
|
|
11
|
+
| "lte"
|
|
12
|
+
| "in"
|
|
13
|
+
| "nin"
|
|
14
|
+
| "contains"
|
|
15
|
+
| "startsWith"
|
|
16
|
+
| "endsWith"
|
|
17
|
+
| "isNull";
|
|
18
|
+
|
|
19
|
+
export interface FilterCondition {
|
|
20
|
+
field: string;
|
|
21
|
+
operator: FilterOperator;
|
|
22
|
+
value?: any;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface QueryOptions {
|
|
26
|
+
page?: number;
|
|
27
|
+
limit?: number;
|
|
28
|
+
sortBy?: string;
|
|
29
|
+
sortOrder?: "asc" | "desc";
|
|
30
|
+
search?: string;
|
|
31
|
+
searchFields?: string[];
|
|
32
|
+
filters?: Record<string, any>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse filter parameters from query string to Prisma where conditions
|
|
37
|
+
*/
|
|
38
|
+
export function parseFiltersToPrismaWhere(query: Record<string, any>): Record<string, any> {
|
|
39
|
+
const where: Record<string, any> = {};
|
|
40
|
+
|
|
41
|
+
for (const [key, value] of Object.entries(query)) {
|
|
42
|
+
if (value === undefined || value === null || value === "") continue;
|
|
43
|
+
|
|
44
|
+
// Skip pagination and sort params
|
|
45
|
+
if (["page", "limit", "sortBy", "sortOrder", "search", "searchFields"].includes(key)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check for operator suffix (field__operator)
|
|
50
|
+
const parts = key.split("__");
|
|
51
|
+
const field = parts[0];
|
|
52
|
+
const operator = parts[1] || "eq";
|
|
53
|
+
|
|
54
|
+
// Convert to Prisma where condition
|
|
55
|
+
where[field] = convertToPrismaCondition(operator, value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return where;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Convert operator and value to Prisma condition
|
|
63
|
+
*/
|
|
64
|
+
function convertToPrismaCondition(operator: string, value: any): any {
|
|
65
|
+
switch (operator) {
|
|
66
|
+
case "eq":
|
|
67
|
+
return value;
|
|
68
|
+
case "ne":
|
|
69
|
+
return { not: value };
|
|
70
|
+
case "gt":
|
|
71
|
+
return { gt: value };
|
|
72
|
+
case "gte":
|
|
73
|
+
return { gte: value };
|
|
74
|
+
case "lt":
|
|
75
|
+
return { lt: value };
|
|
76
|
+
case "lte":
|
|
77
|
+
return { lte: value };
|
|
78
|
+
case "in":
|
|
79
|
+
return { in: Array.isArray(value) ? value : value.split(",") };
|
|
80
|
+
case "nin":
|
|
81
|
+
return { notIn: Array.isArray(value) ? value : value.split(",") };
|
|
82
|
+
case "contains":
|
|
83
|
+
case "like":
|
|
84
|
+
return { contains: value, mode: "insensitive" };
|
|
85
|
+
case "startsWith":
|
|
86
|
+
return { startsWith: value, mode: "insensitive" };
|
|
87
|
+
case "endsWith":
|
|
88
|
+
return { endsWith: value, mode: "insensitive" };
|
|
89
|
+
case "isNull":
|
|
90
|
+
return value === "true" || value === true ? null : { not: null };
|
|
91
|
+
case "between":
|
|
92
|
+
const [min, max] = Array.isArray(value) ? value : value.split(",");
|
|
93
|
+
return { gte: min, lte: max };
|
|
94
|
+
default:
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Build search conditions for multiple fields
|
|
101
|
+
*/
|
|
102
|
+
export function buildPrismaSearchCondition(
|
|
103
|
+
search: string,
|
|
104
|
+
searchFields: string[]
|
|
105
|
+
): Record<string, any> {
|
|
106
|
+
if (!search || searchFields.length === 0) return {};
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
OR: searchFields.map((field) => ({
|
|
110
|
+
[field]: { contains: search, mode: "insensitive" },
|
|
111
|
+
})),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Build complete Prisma query options
|
|
117
|
+
*/
|
|
118
|
+
export function buildPrismaQueryOptions(options: QueryOptions): {
|
|
119
|
+
where: Record<string, any>;
|
|
120
|
+
skip: number;
|
|
121
|
+
take: number;
|
|
122
|
+
orderBy: Record<string, any>;
|
|
123
|
+
} {
|
|
124
|
+
const {
|
|
125
|
+
page = 1,
|
|
126
|
+
limit = 10,
|
|
127
|
+
sortBy = "created_at",
|
|
128
|
+
sortOrder = "desc",
|
|
129
|
+
filters = {},
|
|
130
|
+
search,
|
|
131
|
+
searchFields = [],
|
|
132
|
+
} = options;
|
|
133
|
+
|
|
134
|
+
// Build where conditions
|
|
135
|
+
const filterWhere = parseFiltersToPrismaWhere(filters);
|
|
136
|
+
const searchWhere = search ? buildPrismaSearchCondition(search, searchFields) : {};
|
|
137
|
+
|
|
138
|
+
// Combine where conditions
|
|
139
|
+
const where: Record<string, any> = {
|
|
140
|
+
...filterWhere,
|
|
141
|
+
deleted_at: null, // Default soft delete filter
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
if (Object.keys(searchWhere).length > 0) {
|
|
145
|
+
where.AND = [...(where.AND || []), searchWhere];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
where,
|
|
150
|
+
skip: (page - 1) * limit,
|
|
151
|
+
take: limit,
|
|
152
|
+
orderBy: { [sortBy]: sortOrder },
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Execute filtered query with Prisma
|
|
158
|
+
*/
|
|
159
|
+
export async function executeFilteredPrismaQuery<T>(
|
|
160
|
+
model: any,
|
|
161
|
+
options: QueryOptions,
|
|
162
|
+
defaultSearchFields: string[] = []
|
|
163
|
+
): Promise<[T[], number]> {
|
|
164
|
+
const queryOptions = buildPrismaQueryOptions({
|
|
165
|
+
...options,
|
|
166
|
+
searchFields: options.searchFields || defaultSearchFields,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const [items, total] = await Promise.all([
|
|
170
|
+
model.findMany(queryOptions),
|
|
171
|
+
model.count({ where: queryOptions.where }),
|
|
172
|
+
]);
|
|
173
|
+
|
|
174
|
+
return [items, total];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Helper to build dynamic includes for relations
|
|
179
|
+
*/
|
|
180
|
+
export function buildPrismaIncludes(
|
|
181
|
+
includeFields?: string[]
|
|
182
|
+
): Record<string, boolean> | undefined {
|
|
183
|
+
if (!includeFields || includeFields.length === 0) return undefined;
|
|
184
|
+
|
|
185
|
+
return includeFields.reduce((acc, field) => {
|
|
186
|
+
acc[field] = true;
|
|
187
|
+
return acc;
|
|
188
|
+
}, {} as Record<string, boolean>);
|
|
189
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { SelectQueryBuilder, Brackets } from "typeorm";
|
|
2
|
+
|
|
3
|
+
export type FilterOperator =
|
|
4
|
+
| "eq"
|
|
5
|
+
| "ne"
|
|
6
|
+
| "gt"
|
|
7
|
+
| "gte"
|
|
8
|
+
| "lt"
|
|
9
|
+
| "lte"
|
|
10
|
+
| "in"
|
|
11
|
+
| "nin"
|
|
12
|
+
| "like"
|
|
13
|
+
| "ilike"
|
|
14
|
+
| "between"
|
|
15
|
+
| "isNull"
|
|
16
|
+
| "isNotNull";
|
|
17
|
+
|
|
18
|
+
export interface FilterCondition {
|
|
19
|
+
field: string;
|
|
20
|
+
operator: FilterOperator;
|
|
21
|
+
value?: any;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface QueryOptions {
|
|
25
|
+
page?: number;
|
|
26
|
+
limit?: number;
|
|
27
|
+
sortBy?: string;
|
|
28
|
+
sortOrder?: "ASC" | "DESC";
|
|
29
|
+
search?: string;
|
|
30
|
+
searchFields?: string[];
|
|
31
|
+
filters?: Record<string, any>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse filter parameters from query string
|
|
36
|
+
* Supports format: field__operator=value (e.g., age__gte=18, name__like=john)
|
|
37
|
+
*/
|
|
38
|
+
export function parseFilters(query: Record<string, any>): FilterCondition[] {
|
|
39
|
+
const conditions: FilterCondition[] = [];
|
|
40
|
+
const operatorMap: Record<string, FilterOperator> = {
|
|
41
|
+
eq: "eq",
|
|
42
|
+
ne: "ne",
|
|
43
|
+
gt: "gt",
|
|
44
|
+
gte: "gte",
|
|
45
|
+
lt: "lt",
|
|
46
|
+
lte: "lte",
|
|
47
|
+
in: "in",
|
|
48
|
+
nin: "nin",
|
|
49
|
+
like: "like",
|
|
50
|
+
ilike: "ilike",
|
|
51
|
+
between: "between",
|
|
52
|
+
isNull: "isNull",
|
|
53
|
+
isNotNull: "isNotNull",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
for (const [key, value] of Object.entries(query)) {
|
|
57
|
+
if (value === undefined || value === null || value === "") continue;
|
|
58
|
+
|
|
59
|
+
// Skip pagination and sort params
|
|
60
|
+
if (["page", "limit", "sortBy", "sortOrder", "search", "searchFields"].includes(key)) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check for operator suffix (field__operator)
|
|
65
|
+
const parts = key.split("__");
|
|
66
|
+
const field = parts[0];
|
|
67
|
+
const operatorKey = parts[1] || "eq";
|
|
68
|
+
const operator = operatorMap[operatorKey] || "eq";
|
|
69
|
+
|
|
70
|
+
conditions.push({ field, operator, value });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return conditions;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Apply filters to TypeORM QueryBuilder
|
|
78
|
+
*/
|
|
79
|
+
export function applyFiltersToQueryBuilder<T>(
|
|
80
|
+
queryBuilder: SelectQueryBuilder<T>,
|
|
81
|
+
conditions: FilterCondition[],
|
|
82
|
+
alias: string
|
|
83
|
+
): SelectQueryBuilder<T> {
|
|
84
|
+
for (const condition of conditions) {
|
|
85
|
+
const { field, operator, value } = condition;
|
|
86
|
+
const paramKey = `${field}_${operator}_${Date.now()}`;
|
|
87
|
+
const columnPath = `${alias}.${field}`;
|
|
88
|
+
|
|
89
|
+
switch (operator) {
|
|
90
|
+
case "eq":
|
|
91
|
+
queryBuilder.andWhere(`${columnPath} = :${paramKey}`, { [paramKey]: value });
|
|
92
|
+
break;
|
|
93
|
+
case "ne":
|
|
94
|
+
queryBuilder.andWhere(`${columnPath} != :${paramKey}`, { [paramKey]: value });
|
|
95
|
+
break;
|
|
96
|
+
case "gt":
|
|
97
|
+
queryBuilder.andWhere(`${columnPath} > :${paramKey}`, { [paramKey]: value });
|
|
98
|
+
break;
|
|
99
|
+
case "gte":
|
|
100
|
+
queryBuilder.andWhere(`${columnPath} >= :${paramKey}`, { [paramKey]: value });
|
|
101
|
+
break;
|
|
102
|
+
case "lt":
|
|
103
|
+
queryBuilder.andWhere(`${columnPath} < :${paramKey}`, { [paramKey]: value });
|
|
104
|
+
break;
|
|
105
|
+
case "lte":
|
|
106
|
+
queryBuilder.andWhere(`${columnPath} <= :${paramKey}`, { [paramKey]: value });
|
|
107
|
+
break;
|
|
108
|
+
case "in":
|
|
109
|
+
const inValues = Array.isArray(value) ? value : value.split(",");
|
|
110
|
+
queryBuilder.andWhere(`${columnPath} IN (:...${paramKey})`, { [paramKey]: inValues });
|
|
111
|
+
break;
|
|
112
|
+
case "nin":
|
|
113
|
+
const ninValues = Array.isArray(value) ? value : value.split(",");
|
|
114
|
+
queryBuilder.andWhere(`${columnPath} NOT IN (:...${paramKey})`, { [paramKey]: ninValues });
|
|
115
|
+
break;
|
|
116
|
+
case "like":
|
|
117
|
+
queryBuilder.andWhere(`LOWER(${columnPath}) LIKE LOWER(:${paramKey})`, {
|
|
118
|
+
[paramKey]: `%${value}%`,
|
|
119
|
+
});
|
|
120
|
+
break;
|
|
121
|
+
case "ilike":
|
|
122
|
+
queryBuilder.andWhere(`${columnPath} ILIKE :${paramKey}`, {
|
|
123
|
+
[paramKey]: `%${value}%`,
|
|
124
|
+
});
|
|
125
|
+
break;
|
|
126
|
+
case "between":
|
|
127
|
+
const [min, max] = Array.isArray(value) ? value : value.split(",");
|
|
128
|
+
queryBuilder.andWhere(`${columnPath} BETWEEN :${paramKey}_min AND :${paramKey}_max`, {
|
|
129
|
+
[`${paramKey}_min`]: min,
|
|
130
|
+
[`${paramKey}_max`]: max,
|
|
131
|
+
});
|
|
132
|
+
break;
|
|
133
|
+
case "isNull":
|
|
134
|
+
queryBuilder.andWhere(`${columnPath} IS NULL`);
|
|
135
|
+
break;
|
|
136
|
+
case "isNotNull":
|
|
137
|
+
queryBuilder.andWhere(`${columnPath} IS NOT NULL`);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return queryBuilder;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Apply search across multiple fields
|
|
147
|
+
*/
|
|
148
|
+
export function applySearchToQueryBuilder<T>(
|
|
149
|
+
queryBuilder: SelectQueryBuilder<T>,
|
|
150
|
+
search: string,
|
|
151
|
+
searchFields: string[],
|
|
152
|
+
alias: string
|
|
153
|
+
): SelectQueryBuilder<T> {
|
|
154
|
+
if (!search || searchFields.length === 0) return queryBuilder;
|
|
155
|
+
|
|
156
|
+
queryBuilder.andWhere(
|
|
157
|
+
new Brackets((qb) => {
|
|
158
|
+
for (const field of searchFields) {
|
|
159
|
+
qb.orWhere(`LOWER(${alias}.${field}) LIKE LOWER(:search)`, {
|
|
160
|
+
search: `%${search}%`,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
return queryBuilder;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Apply pagination and sorting to QueryBuilder
|
|
171
|
+
*/
|
|
172
|
+
export function applyPaginationToQueryBuilder<T>(
|
|
173
|
+
queryBuilder: SelectQueryBuilder<T>,
|
|
174
|
+
options: QueryOptions,
|
|
175
|
+
alias: string
|
|
176
|
+
): SelectQueryBuilder<T> {
|
|
177
|
+
const { page = 1, limit = 10, sortBy = "createdAt", sortOrder = "DESC" } = options;
|
|
178
|
+
|
|
179
|
+
queryBuilder
|
|
180
|
+
.skip((page - 1) * limit)
|
|
181
|
+
.take(limit)
|
|
182
|
+
.orderBy(`${alias}.${sortBy}`, sortOrder);
|
|
183
|
+
|
|
184
|
+
return queryBuilder;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Build complete query with filters, search, and pagination
|
|
189
|
+
*/
|
|
190
|
+
export async function executeFilteredQuery<T>(
|
|
191
|
+
queryBuilder: SelectQueryBuilder<T>,
|
|
192
|
+
options: QueryOptions,
|
|
193
|
+
alias: string,
|
|
194
|
+
defaultSearchFields: string[] = []
|
|
195
|
+
): Promise<[T[], number]> {
|
|
196
|
+
// Parse and apply filters
|
|
197
|
+
if (options.filters) {
|
|
198
|
+
const conditions = parseFilters(options.filters);
|
|
199
|
+
applyFiltersToQueryBuilder(queryBuilder, conditions, alias);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Apply search
|
|
203
|
+
if (options.search) {
|
|
204
|
+
const searchFields = options.searchFields || defaultSearchFields;
|
|
205
|
+
applySearchToQueryBuilder(queryBuilder, options.search, searchFields, alias);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Get total count before pagination
|
|
209
|
+
const total = await queryBuilder.getCount();
|
|
210
|
+
|
|
211
|
+
// Apply pagination and sorting
|
|
212
|
+
applyPaginationToQueryBuilder(queryBuilder, options, alias);
|
|
213
|
+
|
|
214
|
+
// Execute query
|
|
215
|
+
const items = await queryBuilder.getMany();
|
|
216
|
+
|
|
217
|
+
return [items, total];
|
|
218
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Test, TestingModule } from "@nestjs/testing";
|
|
2
|
+
import { CommandBus, QueryBus } from "@nestjs/cqrs";
|
|
3
|
+
import { {{entityNamePascal}}Controller } from "../{{entityNameKebab}}.controller";
|
|
4
|
+
import { Create{{entityNamePascal}}Command } from "@modules/{{moduleNameKebab}}/application/commands/create-{{entityNameKebab}}.command";
|
|
5
|
+
import { Update{{entityNamePascal}}Command } from "@modules/{{moduleNameKebab}}/application/commands/update-{{entityNameKebab}}.command";
|
|
6
|
+
import { Delete{{entityNamePascal}}Command } from "@modules/{{moduleNameKebab}}/application/commands/delete-{{entityNameKebab}}.command";
|
|
7
|
+
import { Get{{entityNamePascal}}ByIdQuery } from "@modules/{{moduleNameKebab}}/application/queries/get-{{entityNameKebab}}-by-id.query";
|
|
8
|
+
import { GetAll{{entityNamePluralPascal}}Query } from "@modules/{{moduleNameKebab}}/application/queries/get-all-{{entityNamePluralKebab}}.query";
|
|
9
|
+
|
|
10
|
+
describe("{{entityNamePascal}}Controller", () => {
|
|
11
|
+
let controller: {{entityNamePascal}}Controller;
|
|
12
|
+
let commandBus: jest.Mocked<CommandBus>;
|
|
13
|
+
let queryBus: jest.Mocked<QueryBus>;
|
|
14
|
+
|
|
15
|
+
const mockResponseDto = {
|
|
16
|
+
id: "test-uuid",
|
|
17
|
+
isActive: true,
|
|
18
|
+
createdAt: new Date(),
|
|
19
|
+
updatedAt: new Date(),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const mockPaginatedResponse = {
|
|
23
|
+
items: [mockResponseDto],
|
|
24
|
+
meta: {
|
|
25
|
+
total: 1,
|
|
26
|
+
page: 1,
|
|
27
|
+
limit: 10,
|
|
28
|
+
totalPages: 1,
|
|
29
|
+
hasNextPage: false,
|
|
30
|
+
hasPreviousPage: false,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
beforeEach(async () => {
|
|
35
|
+
const module: TestingModule = await Test.createTestingModule({
|
|
36
|
+
controllers: [{{entityNamePascal}}Controller],
|
|
37
|
+
providers: [
|
|
38
|
+
{
|
|
39
|
+
provide: CommandBus,
|
|
40
|
+
useValue: {
|
|
41
|
+
execute: jest.fn(),
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
provide: QueryBus,
|
|
46
|
+
useValue: {
|
|
47
|
+
execute: jest.fn(),
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
}).compile();
|
|
52
|
+
|
|
53
|
+
controller = module.get<{{entityNamePascal}}Controller>({{entityNamePascal}}Controller);
|
|
54
|
+
commandBus = module.get(CommandBus);
|
|
55
|
+
queryBus = module.get(QueryBus);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("create", () => {
|
|
59
|
+
it("should create a new {{entityNameCamel}}", async () => {
|
|
60
|
+
const createDto = {};
|
|
61
|
+
commandBus.execute.mockResolvedValue(mockResponseDto);
|
|
62
|
+
|
|
63
|
+
const result = await controller.create(createDto as any);
|
|
64
|
+
|
|
65
|
+
expect(commandBus.execute).toHaveBeenCalledWith(
|
|
66
|
+
expect.any(Create{{entityNamePascal}}Command)
|
|
67
|
+
);
|
|
68
|
+
expect(result).toEqual(mockResponseDto);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("findAll", () => {
|
|
73
|
+
it("should return paginated {{entityNamePluralCamel}}", async () => {
|
|
74
|
+
const paginationQuery = { page: 1, limit: 10 };
|
|
75
|
+
queryBus.execute.mockResolvedValue(mockPaginatedResponse);
|
|
76
|
+
|
|
77
|
+
const result = await controller.findAll(paginationQuery as any);
|
|
78
|
+
|
|
79
|
+
expect(queryBus.execute).toHaveBeenCalledWith(
|
|
80
|
+
expect.any(GetAll{{entityNamePluralPascal}}Query)
|
|
81
|
+
);
|
|
82
|
+
expect(result).toEqual(mockPaginatedResponse);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("findOne", () => {
|
|
87
|
+
it("should return a single {{entityNameCamel}}", async () => {
|
|
88
|
+
queryBus.execute.mockResolvedValue(mockResponseDto);
|
|
89
|
+
|
|
90
|
+
const result = await controller.findOne("test-uuid");
|
|
91
|
+
|
|
92
|
+
expect(queryBus.execute).toHaveBeenCalledWith(
|
|
93
|
+
expect.any(Get{{entityNamePascal}}ByIdQuery)
|
|
94
|
+
);
|
|
95
|
+
expect(result).toEqual(mockResponseDto);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("update", () => {
|
|
100
|
+
it("should update and return the {{entityNameCamel}}", async () => {
|
|
101
|
+
const updateDto = {};
|
|
102
|
+
commandBus.execute.mockResolvedValue(mockResponseDto);
|
|
103
|
+
|
|
104
|
+
const result = await controller.update("test-uuid", updateDto as any);
|
|
105
|
+
|
|
106
|
+
expect(commandBus.execute).toHaveBeenCalledWith(
|
|
107
|
+
expect.any(Update{{entityNamePascal}}Command)
|
|
108
|
+
);
|
|
109
|
+
expect(result).toEqual(mockResponseDto);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("delete", () => {
|
|
114
|
+
it("should delete the {{entityNameCamel}}", async () => {
|
|
115
|
+
commandBus.execute.mockResolvedValue(undefined);
|
|
116
|
+
|
|
117
|
+
await controller.delete("test-uuid");
|
|
118
|
+
|
|
119
|
+
expect(commandBus.execute).toHaveBeenCalledWith(
|
|
120
|
+
expect.any(Delete{{entityNamePascal}}Command)
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|