nestjs-ddd-cli 2.2.0 → 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.
Files changed (254) hide show
  1. package/README.md +247 -408
  2. package/ddd.schema.json +111 -0
  3. package/dist/commands/aggregate-validator.d.ts +9 -0
  4. package/dist/commands/aggregate-validator.js +953 -0
  5. package/dist/commands/aggregate-validator.js.map +1 -0
  6. package/dist/commands/ai-assist.d.ts +8 -0
  7. package/dist/commands/ai-assist.js +337 -0
  8. package/dist/commands/ai-assist.js.map +1 -0
  9. package/dist/commands/api-contracts.d.ts +9 -0
  10. package/dist/commands/api-contracts.js +1368 -0
  11. package/dist/commands/api-contracts.js.map +1 -0
  12. package/dist/commands/api-docs.d.ts +8 -0
  13. package/dist/commands/api-docs.js +408 -0
  14. package/dist/commands/api-docs.js.map +1 -0
  15. package/dist/commands/api-versioning.d.ts +11 -0
  16. package/dist/commands/api-versioning.js +643 -0
  17. package/dist/commands/api-versioning.js.map +1 -0
  18. package/dist/commands/audit-logging.d.ts +9 -0
  19. package/dist/commands/audit-logging.js +1129 -0
  20. package/dist/commands/audit-logging.js.map +1 -0
  21. package/dist/commands/batch-generate.d.ts +10 -0
  22. package/dist/commands/batch-generate.js +405 -0
  23. package/dist/commands/batch-generate.js.map +1 -0
  24. package/dist/commands/caching-strategies.d.ts +9 -0
  25. package/dist/commands/caching-strategies.js +874 -0
  26. package/dist/commands/caching-strategies.js.map +1 -0
  27. package/dist/commands/code-analyzer.d.ts +42 -0
  28. package/dist/commands/code-analyzer.js +474 -0
  29. package/dist/commands/code-analyzer.js.map +1 -0
  30. package/dist/commands/database-seeding.d.ts +6 -0
  31. package/dist/commands/database-seeding.js +621 -0
  32. package/dist/commands/database-seeding.js.map +1 -0
  33. package/dist/commands/db-optimization.d.ts +7 -0
  34. package/dist/commands/db-optimization.js +687 -0
  35. package/dist/commands/db-optimization.js.map +1 -0
  36. package/dist/commands/dependency-graph.d.ts +6 -0
  37. package/dist/commands/dependency-graph.js +329 -0
  38. package/dist/commands/dependency-graph.js.map +1 -0
  39. package/dist/commands/doctor-enhanced.d.ts +22 -0
  40. package/dist/commands/doctor-enhanced.js +543 -0
  41. package/dist/commands/doctor-enhanced.js.map +1 -0
  42. package/dist/commands/doctor.d.ts +4 -0
  43. package/dist/commands/doctor.js +151 -0
  44. package/dist/commands/doctor.js.map +1 -0
  45. package/dist/commands/env-manager.d.ts +6 -0
  46. package/dist/commands/env-manager.js +419 -0
  47. package/dist/commands/env-manager.js.map +1 -0
  48. package/dist/commands/event-sourcing-full.d.ts +10 -0
  49. package/dist/commands/event-sourcing-full.js +1107 -0
  50. package/dist/commands/event-sourcing-full.js.map +1 -0
  51. package/dist/commands/feature-flags.d.ts +9 -0
  52. package/dist/commands/feature-flags.js +824 -0
  53. package/dist/commands/feature-flags.js.map +1 -0
  54. package/dist/commands/filter-dsl.d.ts +10 -0
  55. package/dist/commands/filter-dsl.js +1407 -0
  56. package/dist/commands/filter-dsl.js.map +1 -0
  57. package/dist/commands/generate-all.js +485 -32
  58. package/dist/commands/generate-all.js.map +1 -1
  59. package/dist/commands/generate-deployment.d.ts +8 -0
  60. package/dist/commands/generate-deployment.js +746 -0
  61. package/dist/commands/generate-deployment.js.map +1 -0
  62. package/dist/commands/generate-domain-service.d.ts +14 -0
  63. package/dist/commands/generate-domain-service.js +796 -0
  64. package/dist/commands/generate-domain-service.js.map +1 -0
  65. package/dist/commands/generate-entity.js +82 -24
  66. package/dist/commands/generate-entity.js.map +1 -1
  67. package/dist/commands/generate-from-schema.d.ts +56 -0
  68. package/dist/commands/generate-from-schema.js +222 -0
  69. package/dist/commands/generate-from-schema.js.map +1 -0
  70. package/dist/commands/generate-orchestrator.d.ts +14 -0
  71. package/dist/commands/generate-orchestrator.js +887 -0
  72. package/dist/commands/generate-orchestrator.js.map +1 -0
  73. package/dist/commands/generate-repository.d.ts +14 -0
  74. package/dist/commands/generate-repository.js +1019 -0
  75. package/dist/commands/generate-repository.js.map +1 -0
  76. package/dist/commands/generate-shared.d.ts +4 -0
  77. package/dist/commands/generate-shared.js +388 -0
  78. package/dist/commands/generate-shared.js.map +1 -0
  79. package/dist/commands/generate-value-object.d.ts +32 -0
  80. package/dist/commands/generate-value-object.js +700 -0
  81. package/dist/commands/generate-value-object.js.map +1 -0
  82. package/dist/commands/graphql-subscriptions.d.ts +6 -0
  83. package/dist/commands/graphql-subscriptions.js +607 -0
  84. package/dist/commands/graphql-subscriptions.js.map +1 -0
  85. package/dist/commands/graphql-types.d.ts +5 -0
  86. package/dist/commands/graphql-types.js +423 -0
  87. package/dist/commands/graphql-types.js.map +1 -0
  88. package/dist/commands/health-probes-advanced.d.ts +6 -0
  89. package/dist/commands/health-probes-advanced.js +655 -0
  90. package/dist/commands/health-probes-advanced.js.map +1 -0
  91. package/dist/commands/i18n-setup.d.ts +10 -0
  92. package/dist/commands/i18n-setup.js +677 -0
  93. package/dist/commands/i18n-setup.js.map +1 -0
  94. package/dist/commands/init-config.d.ts +6 -0
  95. package/dist/commands/init-config.js +370 -0
  96. package/dist/commands/init-config.js.map +1 -0
  97. package/dist/commands/init-project.js +56 -6
  98. package/dist/commands/init-project.js.map +1 -1
  99. package/dist/commands/interactive-scaffold.d.ts +5 -0
  100. package/dist/commands/interactive-scaffold.js +271 -0
  101. package/dist/commands/interactive-scaffold.js.map +1 -0
  102. package/dist/commands/metrics-prometheus.d.ts +6 -0
  103. package/dist/commands/metrics-prometheus.js +681 -0
  104. package/dist/commands/metrics-prometheus.js.map +1 -0
  105. package/dist/commands/migration-engine.d.ts +6 -0
  106. package/dist/commands/migration-engine.js +446 -0
  107. package/dist/commands/migration-engine.js.map +1 -0
  108. package/dist/commands/migration.d.ts +12 -0
  109. package/dist/commands/migration.js +484 -0
  110. package/dist/commands/migration.js.map +1 -0
  111. package/dist/commands/monorepo.d.ts +8 -0
  112. package/dist/commands/monorepo.js +483 -0
  113. package/dist/commands/monorepo.js.map +1 -0
  114. package/dist/commands/multi-database.d.ts +5 -0
  115. package/dist/commands/multi-database.js +439 -0
  116. package/dist/commands/multi-database.js.map +1 -0
  117. package/dist/commands/observability-tracing.d.ts +10 -0
  118. package/dist/commands/observability-tracing.js +740 -0
  119. package/dist/commands/observability-tracing.js.map +1 -0
  120. package/dist/commands/openapi-export.d.ts +8 -0
  121. package/dist/commands/openapi-export.js +359 -0
  122. package/dist/commands/openapi-export.js.map +1 -0
  123. package/dist/commands/perf-analyzer.d.ts +8 -0
  124. package/dist/commands/perf-analyzer.js +423 -0
  125. package/dist/commands/perf-analyzer.js.map +1 -0
  126. package/dist/commands/rate-limiting.d.ts +10 -0
  127. package/dist/commands/rate-limiting.js +953 -0
  128. package/dist/commands/rate-limiting.js.map +1 -0
  129. package/dist/commands/recipe-plugin.d.ts +56 -0
  130. package/dist/commands/recipe-plugin.js +315 -0
  131. package/dist/commands/recipe-plugin.js.map +1 -0
  132. package/dist/commands/recipe.d.ts +6 -0
  133. package/dist/commands/recipe.js +3941 -0
  134. package/dist/commands/recipe.js.map +1 -0
  135. package/dist/commands/recipes/elasticsearch.recipe.d.ts +1 -0
  136. package/dist/commands/recipes/elasticsearch.recipe.js +761 -0
  137. package/dist/commands/recipes/elasticsearch.recipe.js.map +1 -0
  138. package/dist/commands/recipes/event-sourcing.recipe.d.ts +1 -0
  139. package/dist/commands/recipes/event-sourcing.recipe.js +889 -0
  140. package/dist/commands/recipes/event-sourcing.recipe.js.map +1 -0
  141. package/dist/commands/recipes/index.d.ts +7 -0
  142. package/dist/commands/recipes/index.js +24 -0
  143. package/dist/commands/recipes/index.js.map +1 -0
  144. package/dist/commands/recipes/message-queue.recipe.d.ts +1 -0
  145. package/dist/commands/recipes/message-queue.recipe.js +706 -0
  146. package/dist/commands/recipes/message-queue.recipe.js.map +1 -0
  147. package/dist/commands/recipes/middleware.recipe.d.ts +1 -0
  148. package/dist/commands/recipes/middleware.recipe.js +383 -0
  149. package/dist/commands/recipes/middleware.recipe.js.map +1 -0
  150. package/dist/commands/recipes/multi-tenancy.recipe.d.ts +1 -0
  151. package/dist/commands/recipes/multi-tenancy.recipe.js +520 -0
  152. package/dist/commands/recipes/multi-tenancy.recipe.js.map +1 -0
  153. package/dist/commands/recipes/oauth2.recipe.d.ts +1 -0
  154. package/dist/commands/recipes/oauth2.recipe.js +472 -0
  155. package/dist/commands/recipes/oauth2.recipe.js.map +1 -0
  156. package/dist/commands/recipes/websocket.recipe.d.ts +1 -0
  157. package/dist/commands/recipes/websocket.recipe.js +453 -0
  158. package/dist/commands/recipes/websocket.recipe.js.map +1 -0
  159. package/dist/commands/resilience-patterns.d.ts +13 -0
  160. package/dist/commands/resilience-patterns.js +1029 -0
  161. package/dist/commands/resilience-patterns.js.map +1 -0
  162. package/dist/commands/security-patterns.d.ts +11 -0
  163. package/dist/commands/security-patterns.js +2233 -0
  164. package/dist/commands/security-patterns.js.map +1 -0
  165. package/dist/commands/template-debug.d.ts +27 -0
  166. package/dist/commands/template-debug.js +388 -0
  167. package/dist/commands/template-debug.js.map +1 -0
  168. package/dist/commands/test-factory-full.d.ts +9 -0
  169. package/dist/commands/test-factory-full.js +1570 -0
  170. package/dist/commands/test-factory-full.js.map +1 -0
  171. package/dist/commands/test-scaffold.d.ts +7 -0
  172. package/dist/commands/test-scaffold.js +621 -0
  173. package/dist/commands/test-scaffold.js.map +1 -0
  174. package/dist/index.js +1088 -0
  175. package/dist/index.js.map +1 -1
  176. package/dist/templates/ai-context/CLAUDE.md.hbs +158 -0
  177. package/dist/templates/ai-context/conventions.md.hbs +154 -0
  178. package/dist/templates/command/create-command.hbs +6 -14
  179. package/dist/templates/command/delete-command.hbs +19 -0
  180. package/dist/templates/command/update-command.hbs +24 -0
  181. package/dist/templates/controller/controller.hbs +64 -17
  182. package/dist/templates/dto/create-dto.hbs +29 -5
  183. package/dist/templates/dto/filter-dto.hbs +52 -0
  184. package/dist/templates/dto/filter-query.dto.hbs +148 -0
  185. package/dist/templates/dto/paginated-response.dto.hbs +29 -0
  186. package/dist/templates/dto/pagination-query.dto.hbs +30 -0
  187. package/dist/templates/dto/response-dto.hbs +38 -0
  188. package/dist/templates/dto/update-dto.hbs +11 -0
  189. package/dist/templates/entity/entity.hbs +32 -1
  190. package/dist/templates/event/domain-event.hbs +33 -7
  191. package/dist/templates/event/event-handler.hbs +40 -0
  192. package/dist/templates/exception/base-exceptions.hbs +69 -0
  193. package/dist/templates/exception/entity-not-found.exception.hbs +7 -0
  194. package/dist/templates/mapper/mapper.hbs +49 -24
  195. package/dist/templates/module/module.hbs +34 -10
  196. package/dist/templates/orm-entity/orm-entity.hbs +63 -12
  197. package/dist/templates/prisma/prisma-mapper.hbs +71 -0
  198. package/dist/templates/prisma/prisma-repository.hbs +114 -0
  199. package/dist/templates/prisma/prisma-schema.hbs +20 -0
  200. package/dist/templates/prisma/prisma-service.hbs +51 -0
  201. package/dist/templates/query/get-all.query.hbs +50 -0
  202. package/dist/templates/query/get-by-id.query.hbs +31 -0
  203. package/dist/templates/repository/repository.hbs +55 -13
  204. package/dist/templates/resolver/graphql-input.hbs +54 -0
  205. package/dist/templates/resolver/graphql-type.hbs +58 -0
  206. package/dist/templates/resolver/pagination-args.hbs +33 -0
  207. package/dist/templates/resolver/resolver.hbs +62 -0
  208. package/dist/templates/shared/prisma-query-builder.util.hbs +189 -0
  209. package/dist/templates/shared/query-builder.util.hbs +218 -0
  210. package/dist/templates/test/controller.spec.hbs +124 -0
  211. package/dist/templates/test/repository.spec.hbs +158 -0
  212. package/dist/templates/test/usecase.spec.hbs +116 -0
  213. package/dist/templates/usecase/create-usecase.hbs +19 -7
  214. package/dist/templates/usecase/delete-usecase.hbs +17 -0
  215. package/dist/templates/usecase/update-usecase.hbs +31 -0
  216. package/dist/utils/config.utils.d.ts +45 -0
  217. package/dist/utils/config.utils.js +211 -0
  218. package/dist/utils/config.utils.js.map +1 -0
  219. package/dist/utils/error.utils.d.ts +145 -0
  220. package/dist/utils/error.utils.js +422 -0
  221. package/dist/utils/error.utils.js.map +1 -0
  222. package/dist/utils/field.utils.d.ts +54 -0
  223. package/dist/utils/field.utils.js +389 -0
  224. package/dist/utils/field.utils.js.map +1 -0
  225. package/dist/utils/file.utils.d.ts +19 -8
  226. package/dist/utils/file.utils.js +135 -4
  227. package/dist/utils/file.utils.js.map +1 -1
  228. package/dist/utils/idempotency.utils.d.ts +123 -0
  229. package/dist/utils/idempotency.utils.js +444 -0
  230. package/dist/utils/idempotency.utils.js.map +1 -0
  231. package/dist/utils/naming.utils.js +26 -3
  232. package/dist/utils/naming.utils.js.map +1 -1
  233. package/dist/utils/performance.utils.d.ts +37 -0
  234. package/dist/utils/performance.utils.js +158 -0
  235. package/dist/utils/performance.utils.js.map +1 -0
  236. package/dist/utils/relation.utils.d.ts +92 -0
  237. package/dist/utils/relation.utils.js +388 -0
  238. package/dist/utils/relation.utils.js.map +1 -0
  239. package/dist/utils/rollback.utils.d.ts +49 -0
  240. package/dist/utils/rollback.utils.js +306 -0
  241. package/dist/utils/rollback.utils.js.map +1 -0
  242. package/dist/utils/schema.utils.d.ts +123 -0
  243. package/dist/utils/schema.utils.js +419 -0
  244. package/dist/utils/schema.utils.js.map +1 -0
  245. package/dist/utils/security.utils.d.ts +57 -0
  246. package/dist/utils/security.utils.js +315 -0
  247. package/dist/utils/security.utils.js.map +1 -0
  248. package/dist/utils/template-engine.utils.d.ts +80 -0
  249. package/dist/utils/template-engine.utils.js +463 -0
  250. package/dist/utils/template-engine.utils.js.map +1 -0
  251. package/dist/utils/validation-registry.utils.d.ts +160 -0
  252. package/dist/utils/validation-registry.utils.js +526 -0
  253. package/dist/utils/validation-registry.utils.js.map +1 -0
  254. 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
+ });