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,1407 @@
1
+ "use strict";
2
+ /**
3
+ * Query & Filter DSL (Specification Pattern)
4
+ * Provides standardized filtering, sorting, and search capabilities
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ var __importDefault = (this && this.__importDefault) || function (mod) {
40
+ return (mod && mod.__esModule) ? mod : { "default": mod };
41
+ };
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.generateFilterDSL = generateFilterDSL;
44
+ const path = __importStar(require("path"));
45
+ const chalk_1 = __importDefault(require("chalk"));
46
+ const file_utils_1 = require("../utils/file.utils");
47
+ async function generateFilterDSL(basePath, options = {}) {
48
+ console.log(chalk_1.default.bold.blue('\n🔍 Generating Filter DSL System\n'));
49
+ const sharedPath = path.join(basePath, 'src/shared');
50
+ const filtersPath = path.join(sharedPath, 'filters');
51
+ await (0, file_utils_1.ensureDir)(filtersPath);
52
+ // Generate base filter types
53
+ await generateBaseFilterTypes(filtersPath);
54
+ // Generate filter builder
55
+ await generateFilterBuilder(filtersPath);
56
+ // Generate specification pattern
57
+ await generateSpecificationPattern(filtersPath);
58
+ // Generate TypeORM filter adapter
59
+ await generateTypeORMAdapter(filtersPath);
60
+ // Generate Prisma filter adapter
61
+ await generatePrismaAdapter(filtersPath);
62
+ // Generate filter DTO templates
63
+ await generateFilterDTOTemplates(filtersPath);
64
+ // Generate index
65
+ await (0, file_utils_1.writeFile)(path.join(filtersPath, 'index.ts'), `export * from './filter.types';
66
+ export * from './filter.builder';
67
+ export * from './specification';
68
+ export * from './typeorm.adapter';
69
+ export * from './prisma.adapter';
70
+ export * from './filter.dto';
71
+ `);
72
+ console.log(chalk_1.default.green('\n✅ Filter DSL system generated!'));
73
+ }
74
+ async function generateBaseFilterTypes(filtersPath) {
75
+ const content = `/**
76
+ * Base filter types for query DSL
77
+ */
78
+
79
+ export type FilterOperator =
80
+ | 'eq' // equals
81
+ | 'ne' // not equals
82
+ | 'gt' // greater than
83
+ | 'gte' // greater than or equal
84
+ | 'lt' // less than
85
+ | 'lte' // less than or equal
86
+ | 'in' // in array
87
+ | 'nin' // not in array
88
+ | 'like' // LIKE pattern
89
+ | 'ilike' // case-insensitive LIKE
90
+ | 'between' // between two values
91
+ | 'isNull' // is null
92
+ | 'isNotNull' // is not null
93
+ | 'contains' // contains substring
94
+ | 'startsWith'// starts with
95
+ | 'endsWith'; // ends with
96
+
97
+ export type SortDirection = 'ASC' | 'DESC';
98
+
99
+ export interface FilterCondition<T = any> {
100
+ field: string;
101
+ operator: FilterOperator;
102
+ value: T;
103
+ }
104
+
105
+ export interface SortCondition {
106
+ field: string;
107
+ direction: SortDirection;
108
+ nulls?: 'first' | 'last';
109
+ }
110
+
111
+ export interface PaginationOptions {
112
+ page: number;
113
+ limit: number;
114
+ offset?: number;
115
+ }
116
+
117
+ export interface FilterQuery {
118
+ conditions: FilterCondition[];
119
+ sort?: SortCondition[];
120
+ pagination?: PaginationOptions;
121
+ search?: SearchOptions;
122
+ include?: string[];
123
+ select?: string[];
124
+ }
125
+
126
+ export interface SearchOptions {
127
+ query: string;
128
+ fields: string[];
129
+ mode?: 'any' | 'all' | 'phrase';
130
+ fuzzy?: boolean;
131
+ }
132
+
133
+ export interface FilterResult<T> {
134
+ data: T[];
135
+ total: number;
136
+ page: number;
137
+ limit: number;
138
+ totalPages: number;
139
+ hasNext: boolean;
140
+ hasPrev: boolean;
141
+ }
142
+
143
+ export interface FilterGroup {
144
+ type: 'and' | 'or';
145
+ conditions: (FilterCondition | FilterGroup)[];
146
+ }
147
+
148
+ /**
149
+ * Type-safe filter definition for an entity
150
+ */
151
+ export type EntityFilter<T> = {
152
+ [K in keyof T]?: FilterValue<T[K]>;
153
+ };
154
+
155
+ export type FilterValue<T> =
156
+ | T
157
+ | { eq: T }
158
+ | { ne: T }
159
+ | { gt: T }
160
+ | { gte: T }
161
+ | { lt: T }
162
+ | { lte: T }
163
+ | { in: T[] }
164
+ | { nin: T[] }
165
+ | { like: string }
166
+ | { ilike: string }
167
+ | { between: [T, T] }
168
+ | { isNull: boolean }
169
+ | { contains: string }
170
+ | { startsWith: string }
171
+ | { endsWith: string };
172
+
173
+ /**
174
+ * Range filter for numeric/date fields
175
+ */
176
+ export interface RangeFilter<T = number | Date> {
177
+ from?: T;
178
+ to?: T;
179
+ inclusive?: boolean;
180
+ }
181
+
182
+ /**
183
+ * Date range presets
184
+ */
185
+ export type DateRangePreset =
186
+ | 'today'
187
+ | 'yesterday'
188
+ | 'thisWeek'
189
+ | 'lastWeek'
190
+ | 'thisMonth'
191
+ | 'lastMonth'
192
+ | 'thisYear'
193
+ | 'lastYear'
194
+ | 'last7Days'
195
+ | 'last30Days'
196
+ | 'last90Days';
197
+
198
+ export function getDateRange(preset: DateRangePreset): { from: Date; to: Date } {
199
+ const now = new Date();
200
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
201
+
202
+ switch (preset) {
203
+ case 'today':
204
+ return { from: today, to: now };
205
+ case 'yesterday':
206
+ const yesterday = new Date(today);
207
+ yesterday.setDate(yesterday.getDate() - 1);
208
+ return { from: yesterday, to: today };
209
+ case 'thisWeek':
210
+ const weekStart = new Date(today);
211
+ weekStart.setDate(weekStart.getDate() - weekStart.getDay());
212
+ return { from: weekStart, to: now };
213
+ case 'lastWeek':
214
+ const lastWeekEnd = new Date(today);
215
+ lastWeekEnd.setDate(lastWeekEnd.getDate() - lastWeekEnd.getDay());
216
+ const lastWeekStart = new Date(lastWeekEnd);
217
+ lastWeekStart.setDate(lastWeekStart.getDate() - 7);
218
+ return { from: lastWeekStart, to: lastWeekEnd };
219
+ case 'thisMonth':
220
+ return { from: new Date(now.getFullYear(), now.getMonth(), 1), to: now };
221
+ case 'lastMonth':
222
+ return {
223
+ from: new Date(now.getFullYear(), now.getMonth() - 1, 1),
224
+ to: new Date(now.getFullYear(), now.getMonth(), 0),
225
+ };
226
+ case 'thisYear':
227
+ return { from: new Date(now.getFullYear(), 0, 1), to: now };
228
+ case 'lastYear':
229
+ return {
230
+ from: new Date(now.getFullYear() - 1, 0, 1),
231
+ to: new Date(now.getFullYear() - 1, 11, 31),
232
+ };
233
+ case 'last7Days':
234
+ const week = new Date(today);
235
+ week.setDate(week.getDate() - 7);
236
+ return { from: week, to: now };
237
+ case 'last30Days':
238
+ const month = new Date(today);
239
+ month.setDate(month.getDate() - 30);
240
+ return { from: month, to: now };
241
+ case 'last90Days':
242
+ const quarter = new Date(today);
243
+ quarter.setDate(quarter.getDate() - 90);
244
+ return { from: quarter, to: now };
245
+ }
246
+ }
247
+ `;
248
+ await (0, file_utils_1.writeFile)(path.join(filtersPath, 'filter.types.ts'), content);
249
+ console.log(chalk_1.default.green(' ✓ Filter types'));
250
+ }
251
+ async function generateFilterBuilder(filtersPath) {
252
+ const content = `import {
253
+ FilterCondition,
254
+ FilterGroup,
255
+ FilterOperator,
256
+ FilterQuery,
257
+ PaginationOptions,
258
+ SearchOptions,
259
+ SortCondition,
260
+ SortDirection,
261
+ } from './filter.types';
262
+
263
+ /**
264
+ * Fluent filter builder for constructing queries
265
+ */
266
+ export class FilterBuilder {
267
+ private query: FilterQuery = {
268
+ conditions: [],
269
+ sort: [],
270
+ };
271
+
272
+ private currentGroup: FilterGroup | null = null;
273
+
274
+ /**
275
+ * Add a filter condition
276
+ */
277
+ where(field: string, operator: FilterOperator, value: any): this {
278
+ const condition: FilterCondition = { field, operator, value };
279
+
280
+ if (this.currentGroup) {
281
+ this.currentGroup.conditions.push(condition);
282
+ } else {
283
+ this.query.conditions.push(condition);
284
+ }
285
+
286
+ return this;
287
+ }
288
+
289
+ /**
290
+ * Shorthand for equals
291
+ */
292
+ eq(field: string, value: any): this {
293
+ return this.where(field, 'eq', value);
294
+ }
295
+
296
+ /**
297
+ * Shorthand for not equals
298
+ */
299
+ ne(field: string, value: any): this {
300
+ return this.where(field, 'ne', value);
301
+ }
302
+
303
+ /**
304
+ * Shorthand for greater than
305
+ */
306
+ gt(field: string, value: any): this {
307
+ return this.where(field, 'gt', value);
308
+ }
309
+
310
+ /**
311
+ * Shorthand for greater than or equal
312
+ */
313
+ gte(field: string, value: any): this {
314
+ return this.where(field, 'gte', value);
315
+ }
316
+
317
+ /**
318
+ * Shorthand for less than
319
+ */
320
+ lt(field: string, value: any): this {
321
+ return this.where(field, 'lt', value);
322
+ }
323
+
324
+ /**
325
+ * Shorthand for less than or equal
326
+ */
327
+ lte(field: string, value: any): this {
328
+ return this.where(field, 'lte', value);
329
+ }
330
+
331
+ /**
332
+ * Filter by value in array
333
+ */
334
+ in(field: string, values: any[]): this {
335
+ return this.where(field, 'in', values);
336
+ }
337
+
338
+ /**
339
+ * Filter by value not in array
340
+ */
341
+ notIn(field: string, values: any[]): this {
342
+ return this.where(field, 'nin', values);
343
+ }
344
+
345
+ /**
346
+ * Filter by LIKE pattern
347
+ */
348
+ like(field: string, pattern: string): this {
349
+ return this.where(field, 'like', pattern);
350
+ }
351
+
352
+ /**
353
+ * Filter by case-insensitive LIKE pattern
354
+ */
355
+ ilike(field: string, pattern: string): this {
356
+ return this.where(field, 'ilike', pattern);
357
+ }
358
+
359
+ /**
360
+ * Filter by value between two values
361
+ */
362
+ between(field: string, from: any, to: any): this {
363
+ return this.where(field, 'between', [from, to]);
364
+ }
365
+
366
+ /**
367
+ * Filter by null
368
+ */
369
+ isNull(field: string): this {
370
+ return this.where(field, 'isNull', true);
371
+ }
372
+
373
+ /**
374
+ * Filter by not null
375
+ */
376
+ isNotNull(field: string): this {
377
+ return this.where(field, 'isNotNull', true);
378
+ }
379
+
380
+ /**
381
+ * Filter by contains substring
382
+ */
383
+ contains(field: string, value: string): this {
384
+ return this.where(field, 'contains', value);
385
+ }
386
+
387
+ /**
388
+ * Filter by starts with
389
+ */
390
+ startsWith(field: string, value: string): this {
391
+ return this.where(field, 'startsWith', value);
392
+ }
393
+
394
+ /**
395
+ * Filter by ends with
396
+ */
397
+ endsWith(field: string, value: string): this {
398
+ return this.where(field, 'endsWith', value);
399
+ }
400
+
401
+ /**
402
+ * Start an AND group
403
+ */
404
+ andWhere(builder: (fb: FilterBuilder) => FilterBuilder): this {
405
+ const group: FilterGroup = { type: 'and', conditions: [] };
406
+ const nestedBuilder = new FilterBuilder();
407
+ nestedBuilder.currentGroup = group;
408
+ builder(nestedBuilder);
409
+ this.query.conditions.push(group as any);
410
+ return this;
411
+ }
412
+
413
+ /**
414
+ * Start an OR group
415
+ */
416
+ orWhere(builder: (fb: FilterBuilder) => FilterBuilder): this {
417
+ const group: FilterGroup = { type: 'or', conditions: [] };
418
+ const nestedBuilder = new FilterBuilder();
419
+ nestedBuilder.currentGroup = group;
420
+ builder(nestedBuilder);
421
+ this.query.conditions.push(group as any);
422
+ return this;
423
+ }
424
+
425
+ /**
426
+ * Add sort condition
427
+ */
428
+ orderBy(field: string, direction: SortDirection = 'ASC', nulls?: 'first' | 'last'): this {
429
+ this.query.sort = this.query.sort || [];
430
+ this.query.sort.push({ field, direction, nulls });
431
+ return this;
432
+ }
433
+
434
+ /**
435
+ * Sort ascending
436
+ */
437
+ asc(field: string): this {
438
+ return this.orderBy(field, 'ASC');
439
+ }
440
+
441
+ /**
442
+ * Sort descending
443
+ */
444
+ desc(field: string): this {
445
+ return this.orderBy(field, 'DESC');
446
+ }
447
+
448
+ /**
449
+ * Set pagination
450
+ */
451
+ paginate(page: number, limit: number): this {
452
+ this.query.pagination = {
453
+ page,
454
+ limit,
455
+ offset: (page - 1) * limit,
456
+ };
457
+ return this;
458
+ }
459
+
460
+ /**
461
+ * Set offset-based pagination
462
+ */
463
+ skip(offset: number): this {
464
+ this.query.pagination = this.query.pagination || { page: 1, limit: 10 };
465
+ this.query.pagination.offset = offset;
466
+ return this;
467
+ }
468
+
469
+ /**
470
+ * Set limit
471
+ */
472
+ take(limit: number): this {
473
+ this.query.pagination = this.query.pagination || { page: 1, limit };
474
+ this.query.pagination.limit = limit;
475
+ return this;
476
+ }
477
+
478
+ /**
479
+ * Add full-text search
480
+ */
481
+ search(query: string, fields: string[], options?: Partial<SearchOptions>): this {
482
+ this.query.search = {
483
+ query,
484
+ fields,
485
+ mode: options?.mode || 'any',
486
+ fuzzy: options?.fuzzy || false,
487
+ };
488
+ return this;
489
+ }
490
+
491
+ /**
492
+ * Include relations
493
+ */
494
+ include(...relations: string[]): this {
495
+ this.query.include = [...(this.query.include || []), ...relations];
496
+ return this;
497
+ }
498
+
499
+ /**
500
+ * Select specific fields
501
+ */
502
+ select(...fields: string[]): this {
503
+ this.query.select = [...(this.query.select || []), ...fields];
504
+ return this;
505
+ }
506
+
507
+ /**
508
+ * Build the filter query
509
+ */
510
+ build(): FilterQuery {
511
+ return { ...this.query };
512
+ }
513
+
514
+ /**
515
+ * Create a new builder
516
+ */
517
+ static create(): FilterBuilder {
518
+ return new FilterBuilder();
519
+ }
520
+ }
521
+
522
+ /**
523
+ * Helper function to create filter builder
524
+ */
525
+ export function filter(): FilterBuilder {
526
+ return FilterBuilder.create();
527
+ }
528
+ `;
529
+ await (0, file_utils_1.writeFile)(path.join(filtersPath, 'filter.builder.ts'), content);
530
+ console.log(chalk_1.default.green(' ✓ Filter builder'));
531
+ }
532
+ async function generateSpecificationPattern(filtersPath) {
533
+ const content = `/**
534
+ * Specification Pattern Implementation
535
+ * For composable business rules
536
+ */
537
+
538
+ export interface Specification<T> {
539
+ isSatisfiedBy(candidate: T): boolean;
540
+ and(other: Specification<T>): Specification<T>;
541
+ or(other: Specification<T>): Specification<T>;
542
+ not(): Specification<T>;
543
+ toSQL?(): { where: string; params: any[] };
544
+ }
545
+
546
+ /**
547
+ * Base specification class
548
+ */
549
+ export abstract class BaseSpecification<T> implements Specification<T> {
550
+ abstract isSatisfiedBy(candidate: T): boolean;
551
+
552
+ and(other: Specification<T>): Specification<T> {
553
+ return new AndSpecification(this, other);
554
+ }
555
+
556
+ or(other: Specification<T>): Specification<T> {
557
+ return new OrSpecification(this, other);
558
+ }
559
+
560
+ not(): Specification<T> {
561
+ return new NotSpecification(this);
562
+ }
563
+ }
564
+
565
+ /**
566
+ * AND composite specification
567
+ */
568
+ export class AndSpecification<T> extends BaseSpecification<T> {
569
+ constructor(
570
+ private readonly left: Specification<T>,
571
+ private readonly right: Specification<T>
572
+ ) {
573
+ super();
574
+ }
575
+
576
+ isSatisfiedBy(candidate: T): boolean {
577
+ return this.left.isSatisfiedBy(candidate) && this.right.isSatisfiedBy(candidate);
578
+ }
579
+
580
+ toSQL(): { where: string; params: any[] } {
581
+ const leftSql = this.left.toSQL?.() || { where: '1=1', params: [] };
582
+ const rightSql = this.right.toSQL?.() || { where: '1=1', params: [] };
583
+
584
+ return {
585
+ where: \`(\${leftSql.where}) AND (\${rightSql.where})\`,
586
+ params: [...leftSql.params, ...rightSql.params],
587
+ };
588
+ }
589
+ }
590
+
591
+ /**
592
+ * OR composite specification
593
+ */
594
+ export class OrSpecification<T> extends BaseSpecification<T> {
595
+ constructor(
596
+ private readonly left: Specification<T>,
597
+ private readonly right: Specification<T>
598
+ ) {
599
+ super();
600
+ }
601
+
602
+ isSatisfiedBy(candidate: T): boolean {
603
+ return this.left.isSatisfiedBy(candidate) || this.right.isSatisfiedBy(candidate);
604
+ }
605
+
606
+ toSQL(): { where: string; params: any[] } {
607
+ const leftSql = this.left.toSQL?.() || { where: '1=1', params: [] };
608
+ const rightSql = this.right.toSQL?.() || { where: '1=1', params: [] };
609
+
610
+ return {
611
+ where: \`(\${leftSql.where}) OR (\${rightSql.where})\`,
612
+ params: [...leftSql.params, ...rightSql.params],
613
+ };
614
+ }
615
+ }
616
+
617
+ /**
618
+ * NOT specification
619
+ */
620
+ export class NotSpecification<T> extends BaseSpecification<T> {
621
+ constructor(private readonly spec: Specification<T>) {
622
+ super();
623
+ }
624
+
625
+ isSatisfiedBy(candidate: T): boolean {
626
+ return !this.spec.isSatisfiedBy(candidate);
627
+ }
628
+
629
+ toSQL(): { where: string; params: any[] } {
630
+ const sql = this.spec.toSQL?.() || { where: '1=1', params: [] };
631
+ return {
632
+ where: \`NOT (\${sql.where})\`,
633
+ params: sql.params,
634
+ };
635
+ }
636
+ }
637
+
638
+ /**
639
+ * Field equals value specification
640
+ */
641
+ export class FieldEqualsSpecification<T, K extends keyof T> extends BaseSpecification<T> {
642
+ constructor(
643
+ private readonly field: K,
644
+ private readonly value: T[K]
645
+ ) {
646
+ super();
647
+ }
648
+
649
+ isSatisfiedBy(candidate: T): boolean {
650
+ return candidate[this.field] === this.value;
651
+ }
652
+
653
+ toSQL(): { where: string; params: any[] } {
654
+ return {
655
+ where: \`\${String(this.field)} = ?\`,
656
+ params: [this.value],
657
+ };
658
+ }
659
+ }
660
+
661
+ /**
662
+ * Field greater than specification
663
+ */
664
+ export class FieldGreaterThanSpecification<T, K extends keyof T> extends BaseSpecification<T> {
665
+ constructor(
666
+ private readonly field: K,
667
+ private readonly value: T[K]
668
+ ) {
669
+ super();
670
+ }
671
+
672
+ isSatisfiedBy(candidate: T): boolean {
673
+ return candidate[this.field] > this.value;
674
+ }
675
+
676
+ toSQL(): { where: string; params: any[] } {
677
+ return {
678
+ where: \`\${String(this.field)} > ?\`,
679
+ params: [this.value],
680
+ };
681
+ }
682
+ }
683
+
684
+ /**
685
+ * Field in array specification
686
+ */
687
+ export class FieldInSpecification<T, K extends keyof T> extends BaseSpecification<T> {
688
+ constructor(
689
+ private readonly field: K,
690
+ private readonly values: T[K][]
691
+ ) {
692
+ super();
693
+ }
694
+
695
+ isSatisfiedBy(candidate: T): boolean {
696
+ return this.values.includes(candidate[this.field]);
697
+ }
698
+
699
+ toSQL(): { where: string; params: any[] } {
700
+ const placeholders = this.values.map(() => '?').join(', ');
701
+ return {
702
+ where: \`\${String(this.field)} IN (\${placeholders})\`,
703
+ params: this.values,
704
+ };
705
+ }
706
+ }
707
+
708
+ /**
709
+ * Field contains substring specification
710
+ */
711
+ export class FieldContainsSpecification<T, K extends keyof T> extends BaseSpecification<T> {
712
+ constructor(
713
+ private readonly field: K,
714
+ private readonly substring: string
715
+ ) {
716
+ super();
717
+ }
718
+
719
+ isSatisfiedBy(candidate: T): boolean {
720
+ const value = candidate[this.field];
721
+ return typeof value === 'string' && value.includes(this.substring);
722
+ }
723
+
724
+ toSQL(): { where: string; params: any[] } {
725
+ return {
726
+ where: \`\${String(this.field)} LIKE ?\`,
727
+ params: [\`%\${this.substring}%\`],
728
+ };
729
+ }
730
+ }
731
+
732
+ /**
733
+ * Field is null specification
734
+ */
735
+ export class FieldIsNullSpecification<T, K extends keyof T> extends BaseSpecification<T> {
736
+ constructor(private readonly field: K) {
737
+ super();
738
+ }
739
+
740
+ isSatisfiedBy(candidate: T): boolean {
741
+ return candidate[this.field] === null || candidate[this.field] === undefined;
742
+ }
743
+
744
+ toSQL(): { where: string; params: any[] } {
745
+ return {
746
+ where: \`\${String(this.field)} IS NULL\`,
747
+ params: [],
748
+ };
749
+ }
750
+ }
751
+
752
+ /**
753
+ * Date range specification
754
+ */
755
+ export class DateRangeSpecification<T, K extends keyof T> extends BaseSpecification<T> {
756
+ constructor(
757
+ private readonly field: K,
758
+ private readonly from: Date,
759
+ private readonly to: Date
760
+ ) {
761
+ super();
762
+ }
763
+
764
+ isSatisfiedBy(candidate: T): boolean {
765
+ const value = candidate[this.field];
766
+ if (!(value instanceof Date)) return false;
767
+ return value >= this.from && value <= this.to;
768
+ }
769
+
770
+ toSQL(): { where: string; params: any[] } {
771
+ return {
772
+ where: \`\${String(this.field)} BETWEEN ? AND ?\`,
773
+ params: [this.from, this.to],
774
+ };
775
+ }
776
+ }
777
+
778
+ /**
779
+ * Helper functions to create specifications
780
+ */
781
+ export const spec = {
782
+ equals: <T, K extends keyof T>(field: K, value: T[K]) =>
783
+ new FieldEqualsSpecification<T, K>(field, value),
784
+
785
+ greaterThan: <T, K extends keyof T>(field: K, value: T[K]) =>
786
+ new FieldGreaterThanSpecification<T, K>(field, value),
787
+
788
+ in: <T, K extends keyof T>(field: K, values: T[K][]) =>
789
+ new FieldInSpecification<T, K>(field, values),
790
+
791
+ contains: <T, K extends keyof T>(field: K, substring: string) =>
792
+ new FieldContainsSpecification<T, K>(field, substring),
793
+
794
+ isNull: <T, K extends keyof T>(field: K) =>
795
+ new FieldIsNullSpecification<T, K>(field),
796
+
797
+ dateRange: <T, K extends keyof T>(field: K, from: Date, to: Date) =>
798
+ new DateRangeSpecification<T, K>(field, from, to),
799
+ };
800
+ `;
801
+ await (0, file_utils_1.writeFile)(path.join(filtersPath, 'specification.ts'), content);
802
+ console.log(chalk_1.default.green(' ✓ Specification pattern'));
803
+ }
804
+ async function generateTypeORMAdapter(filtersPath) {
805
+ const content = `import { SelectQueryBuilder, ObjectLiteral } from 'typeorm';
806
+ import { FilterCondition, FilterQuery, FilterResult, FilterGroup } from './filter.types';
807
+
808
+ /**
809
+ * TypeORM adapter for filter queries
810
+ */
811
+ export class TypeORMFilterAdapter {
812
+ /**
813
+ * Apply filter query to TypeORM query builder
814
+ */
815
+ static apply<T extends ObjectLiteral>(
816
+ qb: SelectQueryBuilder<T>,
817
+ query: FilterQuery,
818
+ alias?: string
819
+ ): SelectQueryBuilder<T> {
820
+ const tableAlias = alias || qb.alias;
821
+
822
+ // Apply conditions
823
+ for (const condition of query.conditions) {
824
+ if ('type' in condition) {
825
+ // It's a filter group
826
+ this.applyGroup(qb, condition as unknown as FilterGroup, tableAlias);
827
+ } else {
828
+ this.applyCondition(qb, condition, tableAlias);
829
+ }
830
+ }
831
+
832
+ // Apply search
833
+ if (query.search) {
834
+ const searchConditions = query.search.fields.map(field => {
835
+ const paramName = \`search_\${field.replace('.', '_')}\`;
836
+ qb.setParameter(paramName, \`%\${query.search!.query}%\`);
837
+ return \`\${tableAlias}.\${field} ILIKE :\${paramName}\`;
838
+ });
839
+
840
+ const operator = query.search.mode === 'all' ? ' AND ' : ' OR ';
841
+ qb.andWhere(\`(\${searchConditions.join(operator)})\`);
842
+ }
843
+
844
+ // Apply sorting
845
+ if (query.sort?.length) {
846
+ for (const sort of query.sort) {
847
+ qb.addOrderBy(
848
+ \`\${tableAlias}.\${sort.field}\`,
849
+ sort.direction,
850
+ sort.nulls === 'first' ? 'NULLS FIRST' : sort.nulls === 'last' ? 'NULLS LAST' : undefined
851
+ );
852
+ }
853
+ }
854
+
855
+ // Apply pagination
856
+ if (query.pagination) {
857
+ qb.skip(query.pagination.offset ?? (query.pagination.page - 1) * query.pagination.limit);
858
+ qb.take(query.pagination.limit);
859
+ }
860
+
861
+ // Apply relations
862
+ if (query.include?.length) {
863
+ for (const relation of query.include) {
864
+ qb.leftJoinAndSelect(\`\${tableAlias}.\${relation}\`, relation);
865
+ }
866
+ }
867
+
868
+ // Apply field selection
869
+ if (query.select?.length) {
870
+ qb.select(query.select.map(f => \`\${tableAlias}.\${f}\`));
871
+ }
872
+
873
+ return qb;
874
+ }
875
+
876
+ /**
877
+ * Apply a single condition
878
+ */
879
+ private static applyCondition<T extends ObjectLiteral>(
880
+ qb: SelectQueryBuilder<T>,
881
+ condition: FilterCondition,
882
+ alias: string
883
+ ): void {
884
+ const field = \`\${alias}.\${condition.field}\`;
885
+ const paramName = \`\${condition.field.replace('.', '_')}_\${Date.now()}\`;
886
+
887
+ switch (condition.operator) {
888
+ case 'eq':
889
+ qb.andWhere(\`\${field} = :\${paramName}\`, { [paramName]: condition.value });
890
+ break;
891
+ case 'ne':
892
+ qb.andWhere(\`\${field} != :\${paramName}\`, { [paramName]: condition.value });
893
+ break;
894
+ case 'gt':
895
+ qb.andWhere(\`\${field} > :\${paramName}\`, { [paramName]: condition.value });
896
+ break;
897
+ case 'gte':
898
+ qb.andWhere(\`\${field} >= :\${paramName}\`, { [paramName]: condition.value });
899
+ break;
900
+ case 'lt':
901
+ qb.andWhere(\`\${field} < :\${paramName}\`, { [paramName]: condition.value });
902
+ break;
903
+ case 'lte':
904
+ qb.andWhere(\`\${field} <= :\${paramName}\`, { [paramName]: condition.value });
905
+ break;
906
+ case 'in':
907
+ qb.andWhere(\`\${field} IN (:\${paramName})\`, { [paramName]: condition.value });
908
+ break;
909
+ case 'nin':
910
+ qb.andWhere(\`\${field} NOT IN (:\${paramName})\`, { [paramName]: condition.value });
911
+ break;
912
+ case 'like':
913
+ qb.andWhere(\`\${field} LIKE :\${paramName}\`, { [paramName]: condition.value });
914
+ break;
915
+ case 'ilike':
916
+ qb.andWhere(\`\${field} ILIKE :\${paramName}\`, { [paramName]: condition.value });
917
+ break;
918
+ case 'between':
919
+ const [from, to] = condition.value;
920
+ qb.andWhere(\`\${field} BETWEEN :\${paramName}_from AND :\${paramName}_to\`, {
921
+ [\`\${paramName}_from\`]: from,
922
+ [\`\${paramName}_to\`]: to,
923
+ });
924
+ break;
925
+ case 'isNull':
926
+ qb.andWhere(\`\${field} IS NULL\`);
927
+ break;
928
+ case 'isNotNull':
929
+ qb.andWhere(\`\${field} IS NOT NULL\`);
930
+ break;
931
+ case 'contains':
932
+ qb.andWhere(\`\${field} ILIKE :\${paramName}\`, { [paramName]: \`%\${condition.value}%\` });
933
+ break;
934
+ case 'startsWith':
935
+ qb.andWhere(\`\${field} ILIKE :\${paramName}\`, { [paramName]: \`\${condition.value}%\` });
936
+ break;
937
+ case 'endsWith':
938
+ qb.andWhere(\`\${field} ILIKE :\${paramName}\`, { [paramName]: \`%\${condition.value}\` });
939
+ break;
940
+ }
941
+ }
942
+
943
+ /**
944
+ * Apply a filter group (AND/OR)
945
+ */
946
+ private static applyGroup<T extends ObjectLiteral>(
947
+ qb: SelectQueryBuilder<T>,
948
+ group: FilterGroup,
949
+ alias: string
950
+ ): void {
951
+ // Create a subquery builder for the group
952
+ const conditions: string[] = [];
953
+ const params: Record<string, any> = {};
954
+
955
+ for (const condition of group.conditions) {
956
+ if ('type' in condition) {
957
+ // Nested group - recursive
958
+ // For simplicity, we'll handle one level of nesting
959
+ continue;
960
+ }
961
+
962
+ const cond = condition as FilterCondition;
963
+ const field = \`\${alias}.\${cond.field}\`;
964
+ const paramName = \`\${cond.field.replace('.', '_')}_\${Date.now()}_\${Math.random().toString(36).slice(2, 7)}\`;
965
+
966
+ switch (cond.operator) {
967
+ case 'eq':
968
+ conditions.push(\`\${field} = :\${paramName}\`);
969
+ params[paramName] = cond.value;
970
+ break;
971
+ case 'ne':
972
+ conditions.push(\`\${field} != :\${paramName}\`);
973
+ params[paramName] = cond.value;
974
+ break;
975
+ // Add other operators as needed
976
+ }
977
+ }
978
+
979
+ if (conditions.length > 0) {
980
+ const joinOperator = group.type === 'and' ? ' AND ' : ' OR ';
981
+ qb.andWhere(\`(\${conditions.join(joinOperator)})\`, params);
982
+ }
983
+ }
984
+
985
+ /**
986
+ * Execute query and return paginated result
987
+ */
988
+ static async execute<T extends ObjectLiteral>(
989
+ qb: SelectQueryBuilder<T>,
990
+ query: FilterQuery
991
+ ): Promise<FilterResult<T>> {
992
+ const [data, total] = await qb.getManyAndCount();
993
+
994
+ const page = query.pagination?.page || 1;
995
+ const limit = query.pagination?.limit || data.length;
996
+ const totalPages = Math.ceil(total / limit);
997
+
998
+ return {
999
+ data,
1000
+ total,
1001
+ page,
1002
+ limit,
1003
+ totalPages,
1004
+ hasNext: page < totalPages,
1005
+ hasPrev: page > 1,
1006
+ };
1007
+ }
1008
+ }
1009
+ `;
1010
+ await (0, file_utils_1.writeFile)(path.join(filtersPath, 'typeorm.adapter.ts'), content);
1011
+ console.log(chalk_1.default.green(' ✓ TypeORM adapter'));
1012
+ }
1013
+ async function generatePrismaAdapter(filtersPath) {
1014
+ const content = `import { FilterCondition, FilterQuery, FilterResult, FilterGroup } from './filter.types';
1015
+
1016
+ /**
1017
+ * Prisma adapter for filter queries
1018
+ */
1019
+ export class PrismaFilterAdapter {
1020
+ /**
1021
+ * Convert filter query to Prisma where clause
1022
+ */
1023
+ static toWhereClause(query: FilterQuery): Record<string, any> {
1024
+ const where: Record<string, any> = {};
1025
+
1026
+ for (const condition of query.conditions) {
1027
+ if ('type' in condition) {
1028
+ // It's a filter group
1029
+ const group = condition as unknown as FilterGroup;
1030
+ const groupConditions = this.groupToWhere(group);
1031
+
1032
+ if (group.type === 'and') {
1033
+ where.AND = where.AND || [];
1034
+ where.AND.push(groupConditions);
1035
+ } else {
1036
+ where.OR = where.OR || [];
1037
+ where.OR.push(groupConditions);
1038
+ }
1039
+ } else {
1040
+ const prismaCondition = this.conditionToWhere(condition);
1041
+ Object.assign(where, prismaCondition);
1042
+ }
1043
+ }
1044
+
1045
+ // Apply search
1046
+ if (query.search) {
1047
+ const searchConditions = query.search.fields.map(field => ({
1048
+ [field]: { contains: query.search!.query, mode: 'insensitive' },
1049
+ }));
1050
+
1051
+ if (query.search.mode === 'all') {
1052
+ where.AND = [...(where.AND || []), ...searchConditions];
1053
+ } else {
1054
+ where.OR = [...(where.OR || []), ...searchConditions];
1055
+ }
1056
+ }
1057
+
1058
+ return where;
1059
+ }
1060
+
1061
+ /**
1062
+ * Convert a single condition to Prisma format
1063
+ */
1064
+ private static conditionToWhere(condition: FilterCondition): Record<string, any> {
1065
+ const { field, operator, value } = condition;
1066
+
1067
+ switch (operator) {
1068
+ case 'eq':
1069
+ return { [field]: value };
1070
+ case 'ne':
1071
+ return { [field]: { not: value } };
1072
+ case 'gt':
1073
+ return { [field]: { gt: value } };
1074
+ case 'gte':
1075
+ return { [field]: { gte: value } };
1076
+ case 'lt':
1077
+ return { [field]: { lt: value } };
1078
+ case 'lte':
1079
+ return { [field]: { lte: value } };
1080
+ case 'in':
1081
+ return { [field]: { in: value } };
1082
+ case 'nin':
1083
+ return { [field]: { notIn: value } };
1084
+ case 'like':
1085
+ return { [field]: { contains: value.replace(/%/g, '') } };
1086
+ case 'ilike':
1087
+ return { [field]: { contains: value.replace(/%/g, ''), mode: 'insensitive' } };
1088
+ case 'between':
1089
+ return { [field]: { gte: value[0], lte: value[1] } };
1090
+ case 'isNull':
1091
+ return { [field]: null };
1092
+ case 'isNotNull':
1093
+ return { [field]: { not: null } };
1094
+ case 'contains':
1095
+ return { [field]: { contains: value, mode: 'insensitive' } };
1096
+ case 'startsWith':
1097
+ return { [field]: { startsWith: value, mode: 'insensitive' } };
1098
+ case 'endsWith':
1099
+ return { [field]: { endsWith: value, mode: 'insensitive' } };
1100
+ default:
1101
+ return {};
1102
+ }
1103
+ }
1104
+
1105
+ /**
1106
+ * Convert filter group to Prisma format
1107
+ */
1108
+ private static groupToWhere(group: FilterGroup): Record<string, any> {
1109
+ const conditions = group.conditions.map(c => {
1110
+ if ('type' in c) {
1111
+ return this.groupToWhere(c as FilterGroup);
1112
+ }
1113
+ return this.conditionToWhere(c as FilterCondition);
1114
+ });
1115
+
1116
+ return group.type === 'and' ? { AND: conditions } : { OR: conditions };
1117
+ }
1118
+
1119
+ /**
1120
+ * Convert filter query to Prisma orderBy
1121
+ */
1122
+ static toOrderBy(query: FilterQuery): Record<string, 'asc' | 'desc'>[] {
1123
+ if (!query.sort?.length) return [];
1124
+
1125
+ return query.sort.map(sort => ({
1126
+ [sort.field]: sort.direction.toLowerCase() as 'asc' | 'desc',
1127
+ }));
1128
+ }
1129
+
1130
+ /**
1131
+ * Convert filter query to Prisma include
1132
+ */
1133
+ static toInclude(query: FilterQuery): Record<string, boolean> | undefined {
1134
+ if (!query.include?.length) return undefined;
1135
+
1136
+ const include: Record<string, boolean> = {};
1137
+ for (const relation of query.include) {
1138
+ include[relation] = true;
1139
+ }
1140
+ return include;
1141
+ }
1142
+
1143
+ /**
1144
+ * Convert filter query to Prisma select
1145
+ */
1146
+ static toSelect(query: FilterQuery): Record<string, boolean> | undefined {
1147
+ if (!query.select?.length) return undefined;
1148
+
1149
+ const select: Record<string, boolean> = {};
1150
+ for (const field of query.select) {
1151
+ select[field] = true;
1152
+ }
1153
+ return select;
1154
+ }
1155
+
1156
+ /**
1157
+ * Build complete Prisma query args
1158
+ */
1159
+ static toQueryArgs(query: FilterQuery): {
1160
+ where?: Record<string, any>;
1161
+ orderBy?: Record<string, 'asc' | 'desc'>[];
1162
+ include?: Record<string, boolean>;
1163
+ select?: Record<string, boolean>;
1164
+ skip?: number;
1165
+ take?: number;
1166
+ } {
1167
+ const args: any = {};
1168
+
1169
+ const where = this.toWhereClause(query);
1170
+ if (Object.keys(where).length > 0) {
1171
+ args.where = where;
1172
+ }
1173
+
1174
+ const orderBy = this.toOrderBy(query);
1175
+ if (orderBy.length > 0) {
1176
+ args.orderBy = orderBy;
1177
+ }
1178
+
1179
+ const include = this.toInclude(query);
1180
+ if (include) {
1181
+ args.include = include;
1182
+ }
1183
+
1184
+ const select = this.toSelect(query);
1185
+ if (select) {
1186
+ args.select = select;
1187
+ }
1188
+
1189
+ if (query.pagination) {
1190
+ args.skip = query.pagination.offset ?? (query.pagination.page - 1) * query.pagination.limit;
1191
+ args.take = query.pagination.limit;
1192
+ }
1193
+
1194
+ return args;
1195
+ }
1196
+
1197
+ /**
1198
+ * Execute query and return paginated result
1199
+ */
1200
+ static async execute<T>(
1201
+ model: any,
1202
+ query: FilterQuery
1203
+ ): Promise<FilterResult<T>> {
1204
+ const args = this.toQueryArgs(query);
1205
+
1206
+ const [data, total] = await Promise.all([
1207
+ model.findMany(args),
1208
+ model.count({ where: args.where }),
1209
+ ]);
1210
+
1211
+ const page = query.pagination?.page || 1;
1212
+ const limit = query.pagination?.limit || data.length;
1213
+ const totalPages = Math.ceil(total / limit);
1214
+
1215
+ return {
1216
+ data,
1217
+ total,
1218
+ page,
1219
+ limit,
1220
+ totalPages,
1221
+ hasNext: page < totalPages,
1222
+ hasPrev: page > 1,
1223
+ };
1224
+ }
1225
+ }
1226
+ `;
1227
+ await (0, file_utils_1.writeFile)(path.join(filtersPath, 'prisma.adapter.ts'), content);
1228
+ console.log(chalk_1.default.green(' ✓ Prisma adapter'));
1229
+ }
1230
+ async function generateFilterDTOTemplates(filtersPath) {
1231
+ const content = `import { IsOptional, IsString, IsInt, Min, Max, IsIn, IsArray, ValidateNested } from 'class-validator';
1232
+ import { Type, Transform } from 'class-transformer';
1233
+ import { ApiPropertyOptional } from '@nestjs/swagger';
1234
+
1235
+ /**
1236
+ * Base pagination query DTO
1237
+ */
1238
+ export class PaginationQueryDto {
1239
+ @ApiPropertyOptional({ minimum: 1, default: 1 })
1240
+ @IsOptional()
1241
+ @Type(() => Number)
1242
+ @IsInt()
1243
+ @Min(1)
1244
+ page?: number = 1;
1245
+
1246
+ @ApiPropertyOptional({ minimum: 1, maximum: 100, default: 10 })
1247
+ @IsOptional()
1248
+ @Type(() => Number)
1249
+ @IsInt()
1250
+ @Min(1)
1251
+ @Max(100)
1252
+ limit?: number = 10;
1253
+ }
1254
+
1255
+ /**
1256
+ * Base sorting query DTO
1257
+ */
1258
+ export class SortQueryDto {
1259
+ @ApiPropertyOptional({ description: 'Field to sort by' })
1260
+ @IsOptional()
1261
+ @IsString()
1262
+ sortBy?: string;
1263
+
1264
+ @ApiPropertyOptional({ enum: ['ASC', 'DESC'], default: 'ASC' })
1265
+ @IsOptional()
1266
+ @IsIn(['ASC', 'DESC', 'asc', 'desc'])
1267
+ @Transform(({ value }) => value?.toUpperCase())
1268
+ sortOrder?: 'ASC' | 'DESC' = 'ASC';
1269
+ }
1270
+
1271
+ /**
1272
+ * Base search query DTO
1273
+ */
1274
+ export class SearchQueryDto {
1275
+ @ApiPropertyOptional({ description: 'Search query string' })
1276
+ @IsOptional()
1277
+ @IsString()
1278
+ q?: string;
1279
+
1280
+ @ApiPropertyOptional({ description: 'Fields to search in', type: [String] })
1281
+ @IsOptional()
1282
+ @IsArray()
1283
+ @IsString({ each: true })
1284
+ @Transform(({ value }) => (typeof value === 'string' ? value.split(',') : value))
1285
+ searchFields?: string[];
1286
+ }
1287
+
1288
+ /**
1289
+ * Combined filter query DTO
1290
+ */
1291
+ export class BaseFilterQueryDto extends PaginationQueryDto {
1292
+ @ApiPropertyOptional({ description: 'Field to sort by' })
1293
+ @IsOptional()
1294
+ @IsString()
1295
+ sortBy?: string;
1296
+
1297
+ @ApiPropertyOptional({ enum: ['ASC', 'DESC'], default: 'ASC' })
1298
+ @IsOptional()
1299
+ @IsIn(['ASC', 'DESC', 'asc', 'desc'])
1300
+ @Transform(({ value }) => value?.toUpperCase())
1301
+ sortOrder?: 'ASC' | 'DESC' = 'ASC';
1302
+
1303
+ @ApiPropertyOptional({ description: 'Search query string' })
1304
+ @IsOptional()
1305
+ @IsString()
1306
+ search?: string;
1307
+
1308
+ @ApiPropertyOptional({ description: 'Relations to include' })
1309
+ @IsOptional()
1310
+ @IsArray()
1311
+ @IsString({ each: true })
1312
+ @Transform(({ value }) => (typeof value === 'string' ? value.split(',') : value))
1313
+ include?: string[];
1314
+
1315
+ @ApiPropertyOptional({ description: 'Fields to select' })
1316
+ @IsOptional()
1317
+ @IsArray()
1318
+ @IsString({ each: true })
1319
+ @Transform(({ value }) => (typeof value === 'string' ? value.split(',') : value))
1320
+ select?: string[];
1321
+ }
1322
+
1323
+ /**
1324
+ * Date range filter DTO
1325
+ */
1326
+ export class DateRangeFilterDto {
1327
+ @ApiPropertyOptional({ description: 'Start date' })
1328
+ @IsOptional()
1329
+ @Type(() => Date)
1330
+ from?: Date;
1331
+
1332
+ @ApiPropertyOptional({ description: 'End date' })
1333
+ @IsOptional()
1334
+ @Type(() => Date)
1335
+ to?: Date;
1336
+
1337
+ @ApiPropertyOptional({
1338
+ description: 'Preset date range',
1339
+ enum: ['today', 'yesterday', 'thisWeek', 'lastWeek', 'thisMonth', 'lastMonth', 'last7Days', 'last30Days'],
1340
+ })
1341
+ @IsOptional()
1342
+ @IsString()
1343
+ preset?: string;
1344
+ }
1345
+
1346
+ /**
1347
+ * Numeric range filter DTO
1348
+ */
1349
+ export class NumericRangeFilterDto {
1350
+ @ApiPropertyOptional({ description: 'Minimum value' })
1351
+ @IsOptional()
1352
+ @Type(() => Number)
1353
+ min?: number;
1354
+
1355
+ @ApiPropertyOptional({ description: 'Maximum value' })
1356
+ @IsOptional()
1357
+ @Type(() => Number)
1358
+ max?: number;
1359
+ }
1360
+
1361
+ /**
1362
+ * String filter DTO
1363
+ */
1364
+ export class StringFilterDto {
1365
+ @ApiPropertyOptional({ description: 'Exact match' })
1366
+ @IsOptional()
1367
+ @IsString()
1368
+ eq?: string;
1369
+
1370
+ @ApiPropertyOptional({ description: 'Contains substring' })
1371
+ @IsOptional()
1372
+ @IsString()
1373
+ contains?: string;
1374
+
1375
+ @ApiPropertyOptional({ description: 'Starts with' })
1376
+ @IsOptional()
1377
+ @IsString()
1378
+ startsWith?: string;
1379
+
1380
+ @ApiPropertyOptional({ description: 'Ends with' })
1381
+ @IsOptional()
1382
+ @IsString()
1383
+ endsWith?: string;
1384
+
1385
+ @ApiPropertyOptional({ description: 'In list', type: [String] })
1386
+ @IsOptional()
1387
+ @IsArray()
1388
+ @IsString({ each: true })
1389
+ in?: string[];
1390
+ }
1391
+
1392
+ /**
1393
+ * Factory to create entity-specific filter DTOs
1394
+ */
1395
+ export function createFilterDto<T>(
1396
+ entityName: string,
1397
+ filterableFields: (keyof T)[]
1398
+ ): any {
1399
+ // This is a placeholder - actual implementation would use
1400
+ // class-transformer and class-validator dynamically
1401
+ return BaseFilterQueryDto;
1402
+ }
1403
+ `;
1404
+ await (0, file_utils_1.writeFile)(path.join(filtersPath, 'filter.dto.ts'), content);
1405
+ console.log(chalk_1.default.green(' ✓ Filter DTOs'));
1406
+ }
1407
+ //# sourceMappingURL=filter-dsl.js.map