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,3941 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.applyRecipe = applyRecipe;
40
+ exports.listRecipes = listRecipes;
41
+ const path = __importStar(require("path"));
42
+ const chalk_1 = __importDefault(require("chalk"));
43
+ const file_utils_1 = require("../utils/file.utils");
44
+ const dependency_utils_1 = require("../utils/dependency.utils");
45
+ // Import recipe implementations from separate files
46
+ const middleware_recipe_1 = require("./recipes/middleware.recipe");
47
+ const websocket_recipe_1 = require("./recipes/websocket.recipe");
48
+ const multi_tenancy_recipe_1 = require("./recipes/multi-tenancy.recipe");
49
+ const oauth2_recipe_1 = require("./recipes/oauth2.recipe");
50
+ const message_queue_recipe_1 = require("./recipes/message-queue.recipe");
51
+ const elasticsearch_recipe_1 = require("./recipes/elasticsearch.recipe");
52
+ const event_sourcing_recipe_1 = require("./recipes/event-sourcing.recipe");
53
+ const AVAILABLE_RECIPES = {
54
+ 'auth-jwt': {
55
+ name: 'JWT Authentication',
56
+ description: 'JWT-based authentication with guards and decorators',
57
+ dependencies: ['@nestjs/jwt', '@nestjs/passport', 'passport', 'passport-jwt', 'bcrypt'],
58
+ devDependencies: ['@types/passport-jwt', '@types/bcrypt'],
59
+ },
60
+ 'pagination': {
61
+ name: 'Pagination Utilities',
62
+ description: 'Shared pagination DTOs and utilities',
63
+ dependencies: [],
64
+ devDependencies: [],
65
+ },
66
+ 'soft-delete': {
67
+ name: 'Soft Delete',
68
+ description: 'Soft delete base class and repository mixin',
69
+ dependencies: [],
70
+ devDependencies: [],
71
+ },
72
+ 'audit-log': {
73
+ name: 'Audit Logging',
74
+ description: 'Track entity changes with audit log',
75
+ dependencies: [],
76
+ devDependencies: [],
77
+ },
78
+ 'caching': {
79
+ name: 'Redis Caching',
80
+ description: 'Redis-based caching with decorators',
81
+ dependencies: ['@nestjs/cache-manager', 'cache-manager', 'cache-manager-redis-store', 'redis'],
82
+ devDependencies: ['@types/cache-manager-redis-store'],
83
+ },
84
+ 'file-upload': {
85
+ name: 'File Upload',
86
+ description: 'File upload service with local/S3 storage support',
87
+ dependencies: ['@nestjs/platform-express', 'multer', '@aws-sdk/client-s3', '@aws-sdk/s3-request-presigner', 'uuid'],
88
+ devDependencies: ['@types/multer', '@types/uuid'],
89
+ },
90
+ 'notifications': {
91
+ name: 'Notifications',
92
+ description: 'Multi-channel notification system (email, push, SMS)',
93
+ dependencies: ['@nestjs/bull', 'bull', 'nodemailer', 'handlebars'],
94
+ devDependencies: ['@types/nodemailer'],
95
+ },
96
+ 'webhooks': {
97
+ name: 'Webhooks',
98
+ description: 'Webhook management with retry and signature verification',
99
+ dependencies: ['axios', 'crypto'],
100
+ devDependencies: [],
101
+ },
102
+ 'filtering': {
103
+ name: 'Advanced Filtering',
104
+ description: 'Query builder utilities with operators (eq, gt, lt, like, in, between)',
105
+ dependencies: [],
106
+ devDependencies: [],
107
+ },
108
+ 'rate-limiting': {
109
+ name: 'Rate Limiting',
110
+ description: 'Throttling with decorators, Redis-based rate limiting, and IP tracking',
111
+ dependencies: ['@nestjs/throttler', 'ioredis'],
112
+ devDependencies: [],
113
+ },
114
+ 'health': {
115
+ name: 'Health Checks & Monitoring',
116
+ description: 'Health endpoints, structured logging, and monitoring utilities',
117
+ dependencies: ['@nestjs/terminus', 'pino', 'pino-pretty', 'nestjs-pino'],
118
+ devDependencies: [],
119
+ },
120
+ 'api-versioning': {
121
+ name: 'API Versioning',
122
+ description: 'URL/Header-based versioning with deprecation support and migration helpers',
123
+ dependencies: [],
124
+ devDependencies: [],
125
+ },
126
+ 'test-factories': {
127
+ name: 'Test Factories & Fixtures',
128
+ description: 'Faker-based factories, test data builders, database seeders, and mock utilities',
129
+ dependencies: ['@faker-js/faker'],
130
+ devDependencies: [],
131
+ },
132
+ 'middleware': {
133
+ name: 'Middleware, Guards & Interceptors',
134
+ description: 'Common middleware, guards, and interceptors for NestJS applications',
135
+ dependencies: [],
136
+ devDependencies: [],
137
+ },
138
+ 'websocket': {
139
+ name: 'WebSocket & Real-Time',
140
+ description: 'Socket.IO gateway, rooms, presence tracking, and real-time events',
141
+ dependencies: ['@nestjs/websockets', '@nestjs/platform-socket.io', 'socket.io'],
142
+ devDependencies: [],
143
+ },
144
+ 'multi-tenancy': {
145
+ name: 'Multi-Tenancy',
146
+ description: 'SaaS multi-tenant architecture with tenant isolation and scoped repositories',
147
+ dependencies: [],
148
+ devDependencies: [],
149
+ },
150
+ 'oauth2': {
151
+ name: 'OAuth2 & Social Login',
152
+ description: 'OAuth2/OIDC with Google, GitHub, and enterprise SSO support',
153
+ dependencies: ['@nestjs/passport', 'passport-google-oauth20', 'passport-github2', 'openid-client'],
154
+ devDependencies: ['@types/passport-google-oauth20', '@types/passport-github2'],
155
+ },
156
+ 'message-queue': {
157
+ name: 'Message Queue (RabbitMQ)',
158
+ description: 'RabbitMQ integration with producers, consumers, dead letters, and retry patterns',
159
+ dependencies: ['@nestjs/microservices', 'amqplib', 'amqp-connection-manager'],
160
+ devDependencies: ['@types/amqplib'],
161
+ },
162
+ 'elasticsearch': {
163
+ name: 'Elasticsearch Search',
164
+ description: 'Full-text search with Elasticsearch, indexing, and autocomplete',
165
+ dependencies: ['@nestjs/elasticsearch', '@elastic/elasticsearch'],
166
+ devDependencies: [],
167
+ },
168
+ 'event-sourcing': {
169
+ name: 'Event Sourcing',
170
+ description: 'Event store, projections, snapshots, and event replay capabilities',
171
+ dependencies: ['@nestjs/cqrs'],
172
+ devDependencies: [],
173
+ },
174
+ };
175
+ async function applyRecipe(recipeName, options) {
176
+ const recipe = AVAILABLE_RECIPES[recipeName];
177
+ if (!recipe) {
178
+ console.log(chalk_1.default.red(`Unknown recipe: ${recipeName}`));
179
+ console.log(chalk_1.default.yellow('\nAvailable recipes:'));
180
+ Object.entries(AVAILABLE_RECIPES).forEach(([key, value]) => {
181
+ console.log(chalk_1.default.cyan(` ${key.padEnd(15)} - ${value.description}`));
182
+ });
183
+ return;
184
+ }
185
+ console.log(chalk_1.default.blue(`\n🧪 Applying recipe: ${recipe.name}`));
186
+ const basePath = options.path || process.cwd();
187
+ // Install dependencies if requested
188
+ if (options.installDeps && recipe.dependencies.length > 0) {
189
+ console.log(chalk_1.default.cyan(' Installing dependencies...'));
190
+ await (0, dependency_utils_1.installDependencies)(basePath, recipe.dependencies);
191
+ if (recipe.devDependencies.length > 0) {
192
+ await (0, dependency_utils_1.installDependencies)(basePath, recipe.devDependencies, true);
193
+ }
194
+ }
195
+ // Apply the specific recipe
196
+ switch (recipeName) {
197
+ case 'auth-jwt':
198
+ await applyAuthJwtRecipe(basePath);
199
+ break;
200
+ case 'pagination':
201
+ await applyPaginationRecipe(basePath);
202
+ break;
203
+ case 'soft-delete':
204
+ await applySoftDeleteRecipe(basePath);
205
+ break;
206
+ case 'audit-log':
207
+ await applyAuditLogRecipe(basePath);
208
+ break;
209
+ case 'caching':
210
+ await applyCachingRecipe(basePath);
211
+ break;
212
+ case 'file-upload':
213
+ await applyFileUploadRecipe(basePath);
214
+ break;
215
+ case 'notifications':
216
+ await applyNotificationsRecipe(basePath);
217
+ break;
218
+ case 'webhooks':
219
+ await applyWebhooksRecipe(basePath);
220
+ break;
221
+ case 'filtering':
222
+ await applyFilteringRecipe(basePath);
223
+ break;
224
+ case 'rate-limiting':
225
+ await applyRateLimitingRecipe(basePath);
226
+ break;
227
+ case 'health':
228
+ await applyHealthRecipe(basePath);
229
+ break;
230
+ case 'api-versioning':
231
+ await applyApiVersioningRecipe(basePath);
232
+ break;
233
+ case 'test-factories':
234
+ await applyTestFactoriesRecipe(basePath);
235
+ break;
236
+ case 'middleware':
237
+ await (0, middleware_recipe_1.applyMiddlewareRecipe)(basePath);
238
+ break;
239
+ case 'websocket':
240
+ await (0, websocket_recipe_1.applyWebSocketRecipe)(basePath);
241
+ break;
242
+ case 'multi-tenancy':
243
+ await (0, multi_tenancy_recipe_1.applyMultiTenancyRecipe)(basePath);
244
+ break;
245
+ case 'oauth2':
246
+ await (0, oauth2_recipe_1.applyOAuth2Recipe)(basePath);
247
+ break;
248
+ case 'message-queue':
249
+ await (0, message_queue_recipe_1.applyMessageQueueRecipe)(basePath);
250
+ break;
251
+ case 'elasticsearch':
252
+ await (0, elasticsearch_recipe_1.applyElasticsearchRecipe)(basePath);
253
+ break;
254
+ case 'event-sourcing':
255
+ await (0, event_sourcing_recipe_1.applyEventSourcingRecipe)(basePath);
256
+ break;
257
+ }
258
+ console.log(chalk_1.default.green(`\n✅ Recipe '${recipe.name}' applied successfully!`));
259
+ if (recipe.dependencies.length > 0 && !options.installDeps) {
260
+ console.log(chalk_1.default.yellow('\n📦 Required dependencies (run with --install-deps to auto-install):'));
261
+ console.log(chalk_1.default.cyan(` npm install ${recipe.dependencies.join(' ')}`));
262
+ if (recipe.devDependencies.length > 0) {
263
+ console.log(chalk_1.default.cyan(` npm install -D ${recipe.devDependencies.join(' ')}`));
264
+ }
265
+ }
266
+ }
267
+ async function applyAuthJwtRecipe(basePath) {
268
+ const sharedPath = path.join(basePath, 'src/shared');
269
+ const authPath = path.join(sharedPath, 'auth');
270
+ await (0, file_utils_1.ensureDir)(authPath);
271
+ await (0, file_utils_1.ensureDir)(path.join(authPath, 'guards'));
272
+ await (0, file_utils_1.ensureDir)(path.join(authPath, 'decorators'));
273
+ await (0, file_utils_1.ensureDir)(path.join(authPath, 'strategies'));
274
+ // JWT Strategy
275
+ const jwtStrategyContent = `import { Injectable, UnauthorizedException } from "@nestjs/common";
276
+ import { PassportStrategy } from "@nestjs/passport";
277
+ import { ExtractJwt, Strategy } from "passport-jwt";
278
+ import { ConfigService } from "@nestjs/config";
279
+
280
+ export interface JwtPayload {
281
+ sub: string;
282
+ email: string;
283
+ roles?: string[];
284
+ }
285
+
286
+ @Injectable()
287
+ export class JwtStrategy extends PassportStrategy(Strategy) {
288
+ constructor(private configService: ConfigService) {
289
+ super({
290
+ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
291
+ ignoreExpiration: false,
292
+ secretOrKey: configService.get<string>("JWT_SECRET"),
293
+ });
294
+ }
295
+
296
+ async validate(payload: JwtPayload) {
297
+ if (!payload.sub) {
298
+ throw new UnauthorizedException();
299
+ }
300
+ return { userId: payload.sub, email: payload.email, roles: payload.roles };
301
+ }
302
+ }
303
+ `;
304
+ await (0, file_utils_1.writeFile)(path.join(authPath, 'strategies/jwt.strategy.ts'), jwtStrategyContent);
305
+ // Auth Guard
306
+ const authGuardContent = `import { Injectable, ExecutionContext, UnauthorizedException } from "@nestjs/common";
307
+ import { AuthGuard } from "@nestjs/passport";
308
+ import { Reflector } from "@nestjs/core";
309
+ import { IS_PUBLIC_KEY } from "../decorators/public.decorator";
310
+
311
+ @Injectable()
312
+ export class JwtAuthGuard extends AuthGuard("jwt") {
313
+ constructor(private reflector: Reflector) {
314
+ super();
315
+ }
316
+
317
+ canActivate(context: ExecutionContext) {
318
+ const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
319
+ context.getHandler(),
320
+ context.getClass(),
321
+ ]);
322
+
323
+ if (isPublic) {
324
+ return true;
325
+ }
326
+
327
+ return super.canActivate(context);
328
+ }
329
+
330
+ handleRequest(err: any, user: any, info: any) {
331
+ if (err || !user) {
332
+ throw err || new UnauthorizedException();
333
+ }
334
+ return user;
335
+ }
336
+ }
337
+ `;
338
+ await (0, file_utils_1.writeFile)(path.join(authPath, 'guards/jwt-auth.guard.ts'), authGuardContent);
339
+ // Roles Guard
340
+ const rolesGuardContent = `import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
341
+ import { Reflector } from "@nestjs/core";
342
+ import { ROLES_KEY } from "../decorators/roles.decorator";
343
+
344
+ @Injectable()
345
+ export class RolesGuard implements CanActivate {
346
+ constructor(private reflector: Reflector) {}
347
+
348
+ canActivate(context: ExecutionContext): boolean {
349
+ const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
350
+ context.getHandler(),
351
+ context.getClass(),
352
+ ]);
353
+
354
+ if (!requiredRoles) {
355
+ return true;
356
+ }
357
+
358
+ const { user } = context.switchToHttp().getRequest();
359
+ return requiredRoles.some((role) => user.roles?.includes(role));
360
+ }
361
+ }
362
+ `;
363
+ await (0, file_utils_1.writeFile)(path.join(authPath, 'guards/roles.guard.ts'), rolesGuardContent);
364
+ // Decorators
365
+ const publicDecoratorContent = `import { SetMetadata } from "@nestjs/common";
366
+
367
+ export const IS_PUBLIC_KEY = "isPublic";
368
+ export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
369
+ `;
370
+ await (0, file_utils_1.writeFile)(path.join(authPath, 'decorators/public.decorator.ts'), publicDecoratorContent);
371
+ const rolesDecoratorContent = `import { SetMetadata } from "@nestjs/common";
372
+
373
+ export const ROLES_KEY = "roles";
374
+ export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
375
+ `;
376
+ await (0, file_utils_1.writeFile)(path.join(authPath, 'decorators/roles.decorator.ts'), rolesDecoratorContent);
377
+ const currentUserDecoratorContent = `import { createParamDecorator, ExecutionContext } from "@nestjs/common";
378
+
379
+ export interface CurrentUserData {
380
+ userId: string;
381
+ email: string;
382
+ roles?: string[];
383
+ }
384
+
385
+ export const CurrentUser = createParamDecorator(
386
+ (data: keyof CurrentUserData | undefined, ctx: ExecutionContext) => {
387
+ const request = ctx.switchToHttp().getRequest();
388
+ const user = request.user as CurrentUserData;
389
+
390
+ return data ? user?.[data] : user;
391
+ }
392
+ );
393
+ `;
394
+ await (0, file_utils_1.writeFile)(path.join(authPath, 'decorators/current-user.decorator.ts'), currentUserDecoratorContent);
395
+ // Index exports
396
+ const indexContent = `// Guards
397
+ export * from "./guards/jwt-auth.guard";
398
+ export * from "./guards/roles.guard";
399
+
400
+ // Decorators
401
+ export * from "./decorators/public.decorator";
402
+ export * from "./decorators/roles.decorator";
403
+ export * from "./decorators/current-user.decorator";
404
+
405
+ // Strategies
406
+ export * from "./strategies/jwt.strategy";
407
+ `;
408
+ await (0, file_utils_1.writeFile)(path.join(authPath, 'index.ts'), indexContent);
409
+ console.log(chalk_1.default.green(' ✓ JWT Strategy'));
410
+ console.log(chalk_1.default.green(' ✓ Auth Guards (JWT, Roles)'));
411
+ console.log(chalk_1.default.green(' ✓ Decorators (Public, Roles, CurrentUser)'));
412
+ }
413
+ async function applyPaginationRecipe(basePath) {
414
+ const sharedPath = path.join(basePath, 'src/shared');
415
+ const paginationPath = path.join(sharedPath, 'pagination');
416
+ await (0, file_utils_1.ensureDir)(paginationPath);
417
+ const paginationDtoContent = `import { ApiPropertyOptional } from "@nestjs/swagger";
418
+ import { IsOptional, IsInt, Min, Max, IsString, IsIn } from "class-validator";
419
+ import { Type } from "class-transformer";
420
+
421
+ export class PaginationQueryDto {
422
+ @ApiPropertyOptional({ description: "Page number", default: 1, minimum: 1 })
423
+ @IsOptional()
424
+ @Type(() => Number)
425
+ @IsInt()
426
+ @Min(1)
427
+ page?: number = 1;
428
+
429
+ @ApiPropertyOptional({ description: "Items per page", default: 10, minimum: 1, maximum: 100 })
430
+ @IsOptional()
431
+ @Type(() => Number)
432
+ @IsInt()
433
+ @Min(1)
434
+ @Max(100)
435
+ limit?: number = 10;
436
+
437
+ @ApiPropertyOptional({ description: "Field to sort by", default: "createdAt" })
438
+ @IsOptional()
439
+ @IsString()
440
+ sortBy?: string = "createdAt";
441
+
442
+ @ApiPropertyOptional({ description: "Sort order", enum: ["ASC", "DESC"], default: "DESC" })
443
+ @IsOptional()
444
+ @IsIn(["ASC", "DESC"])
445
+ sortOrder?: "ASC" | "DESC" = "DESC";
446
+
447
+ get skip(): number {
448
+ return ((this.page || 1) - 1) * (this.limit || 10);
449
+ }
450
+
451
+ get take(): number {
452
+ return this.limit || 10;
453
+ }
454
+ }
455
+
456
+ export class PaginationMeta {
457
+ total: number;
458
+ page: number;
459
+ limit: number;
460
+ totalPages: number;
461
+ hasNextPage: boolean;
462
+ hasPreviousPage: boolean;
463
+
464
+ constructor(total: number, page: number, limit: number) {
465
+ this.total = total;
466
+ this.page = page;
467
+ this.limit = limit;
468
+ this.totalPages = Math.ceil(total / limit);
469
+ this.hasNextPage = page < this.totalPages;
470
+ this.hasPreviousPage = page > 1;
471
+ }
472
+ }
473
+
474
+ export class PaginatedResponseDto<T> {
475
+ items: T[];
476
+ meta: PaginationMeta;
477
+
478
+ constructor(items: T[], total: number, page: number, limit: number) {
479
+ this.items = items;
480
+ this.meta = new PaginationMeta(total, page, limit);
481
+ }
482
+ }
483
+ `;
484
+ await (0, file_utils_1.writeFile)(path.join(paginationPath, 'pagination.dto.ts'), paginationDtoContent);
485
+ const indexContent = `export * from "./pagination.dto";
486
+ `;
487
+ await (0, file_utils_1.writeFile)(path.join(paginationPath, 'index.ts'), indexContent);
488
+ console.log(chalk_1.default.green(' ✓ PaginationQueryDto'));
489
+ console.log(chalk_1.default.green(' ✓ PaginationMeta'));
490
+ console.log(chalk_1.default.green(' ✓ PaginatedResponseDto'));
491
+ }
492
+ async function applySoftDeleteRecipe(basePath) {
493
+ const sharedPath = path.join(basePath, 'src/shared');
494
+ const basePath2 = path.join(sharedPath, 'base');
495
+ await (0, file_utils_1.ensureDir)(basePath2);
496
+ const baseEntityContent = `import {
497
+ PrimaryGeneratedColumn,
498
+ CreateDateColumn,
499
+ UpdateDateColumn,
500
+ DeleteDateColumn,
501
+ Column,
502
+ } from "typeorm";
503
+
504
+ export abstract class BaseOrmEntity {
505
+ @PrimaryGeneratedColumn("uuid")
506
+ id: string;
507
+
508
+ @Column({ name: "is_active", default: true })
509
+ isActive: boolean;
510
+
511
+ @CreateDateColumn({ name: "created_at" })
512
+ createdAt: Date;
513
+
514
+ @UpdateDateColumn({ name: "updated_at" })
515
+ updatedAt: Date;
516
+
517
+ @DeleteDateColumn({ name: "deleted_at" })
518
+ deletedAt?: Date;
519
+ }
520
+ `;
521
+ await (0, file_utils_1.writeFile)(path.join(basePath2, 'base-orm.entity.ts'), baseEntityContent);
522
+ const baseRepoContent = `import { Repository, FindOptionsWhere, DeepPartial } from "typeorm";
523
+ import { BaseOrmEntity } from "./base-orm.entity";
524
+
525
+ export abstract class BaseRepository<T extends BaseOrmEntity> {
526
+ constructor(protected readonly repository: Repository<T>) {}
527
+
528
+ async findById(id: string): Promise<T | null> {
529
+ return this.repository.findOne({
530
+ where: { id, deletedAt: null } as FindOptionsWhere<T>,
531
+ });
532
+ }
533
+
534
+ async findAll(): Promise<T[]> {
535
+ return this.repository.find({
536
+ where: { deletedAt: null } as FindOptionsWhere<T>,
537
+ });
538
+ }
539
+
540
+ async create(data: DeepPartial<T>): Promise<T> {
541
+ const entity = this.repository.create(data);
542
+ return this.repository.save(entity);
543
+ }
544
+
545
+ async update(id: string, data: DeepPartial<T>): Promise<T | null> {
546
+ await this.repository.update(id, data as any);
547
+ return this.findById(id);
548
+ }
549
+
550
+ async softDelete(id: string): Promise<void> {
551
+ await this.repository.update(id, {
552
+ deletedAt: new Date(),
553
+ isActive: false,
554
+ } as any);
555
+ }
556
+
557
+ async hardDelete(id: string): Promise<void> {
558
+ await this.repository.delete(id);
559
+ }
560
+
561
+ async restore(id: string): Promise<void> {
562
+ await this.repository.update(id, {
563
+ deletedAt: null,
564
+ isActive: true,
565
+ } as any);
566
+ }
567
+
568
+ async exists(id: string): Promise<boolean> {
569
+ const count = await this.repository.count({
570
+ where: { id, deletedAt: null } as FindOptionsWhere<T>,
571
+ });
572
+ return count > 0;
573
+ }
574
+ }
575
+ `;
576
+ await (0, file_utils_1.writeFile)(path.join(basePath2, 'base.repository.ts'), baseRepoContent);
577
+ const indexContent = `export * from "./base-orm.entity";
578
+ export * from "./base.repository";
579
+ `;
580
+ await (0, file_utils_1.writeFile)(path.join(basePath2, 'index.ts'), indexContent);
581
+ console.log(chalk_1.default.green(' ✓ BaseOrmEntity with soft delete'));
582
+ console.log(chalk_1.default.green(' ✓ BaseRepository with CRUD + soft delete'));
583
+ }
584
+ async function applyAuditLogRecipe(basePath) {
585
+ const sharedPath = path.join(basePath, 'src/shared');
586
+ const auditPath = path.join(sharedPath, 'audit');
587
+ await (0, file_utils_1.ensureDir)(auditPath);
588
+ const auditEntityContent = `import {
589
+ Entity,
590
+ PrimaryGeneratedColumn,
591
+ Column,
592
+ CreateDateColumn,
593
+ } from "typeorm";
594
+
595
+ export type AuditAction = "CREATE" | "UPDATE" | "DELETE" | "RESTORE";
596
+
597
+ @Entity("audit_logs")
598
+ export class AuditLogEntity {
599
+ @PrimaryGeneratedColumn("uuid")
600
+ id: string;
601
+
602
+ @Column()
603
+ entityName: string;
604
+
605
+ @Column()
606
+ entityId: string;
607
+
608
+ @Column()
609
+ action: AuditAction;
610
+
611
+ @Column({ type: "jsonb", nullable: true })
612
+ oldValues?: Record<string, any>;
613
+
614
+ @Column({ type: "jsonb", nullable: true })
615
+ newValues?: Record<string, any>;
616
+
617
+ @Column({ nullable: true })
618
+ userId?: string;
619
+
620
+ @Column({ nullable: true })
621
+ userEmail?: string;
622
+
623
+ @CreateDateColumn()
624
+ createdAt: Date;
625
+ }
626
+ `;
627
+ await (0, file_utils_1.writeFile)(path.join(auditPath, 'audit-log.entity.ts'), auditEntityContent);
628
+ const auditServiceContent = `import { Injectable } from "@nestjs/common";
629
+ import { InjectRepository } from "@nestjs/typeorm";
630
+ import { Repository } from "typeorm";
631
+ import { AuditLogEntity, AuditAction } from "./audit-log.entity";
632
+
633
+ export interface AuditContext {
634
+ userId?: string;
635
+ userEmail?: string;
636
+ }
637
+
638
+ @Injectable()
639
+ export class AuditService {
640
+ constructor(
641
+ @InjectRepository(AuditLogEntity)
642
+ private readonly auditRepository: Repository<AuditLogEntity>
643
+ ) {}
644
+
645
+ async log(
646
+ entityName: string,
647
+ entityId: string,
648
+ action: AuditAction,
649
+ oldValues: Record<string, any> | null,
650
+ newValues: Record<string, any> | null,
651
+ context?: AuditContext
652
+ ): Promise<void> {
653
+ const auditLog = this.auditRepository.create({
654
+ entityName,
655
+ entityId,
656
+ action,
657
+ oldValues: oldValues || undefined,
658
+ newValues: newValues || undefined,
659
+ userId: context?.userId,
660
+ userEmail: context?.userEmail,
661
+ });
662
+
663
+ await this.auditRepository.save(auditLog);
664
+ }
665
+
666
+ async getAuditHistory(entityName: string, entityId: string): Promise<AuditLogEntity[]> {
667
+ return this.auditRepository.find({
668
+ where: { entityName, entityId },
669
+ order: { createdAt: "DESC" },
670
+ });
671
+ }
672
+ }
673
+ `;
674
+ await (0, file_utils_1.writeFile)(path.join(auditPath, 'audit.service.ts'), auditServiceContent);
675
+ const indexContent = `export * from "./audit-log.entity";
676
+ export * from "./audit.service";
677
+ `;
678
+ await (0, file_utils_1.writeFile)(path.join(auditPath, 'index.ts'), indexContent);
679
+ console.log(chalk_1.default.green(' ✓ AuditLogEntity'));
680
+ console.log(chalk_1.default.green(' ✓ AuditService'));
681
+ }
682
+ async function applyCachingRecipe(basePath) {
683
+ const sharedPath = path.join(basePath, 'src/shared');
684
+ const cachePath = path.join(sharedPath, 'cache');
685
+ await (0, file_utils_1.ensureDir)(cachePath);
686
+ const cacheDecoratorContent = `import { SetMetadata } from "@nestjs/common";
687
+
688
+ export const CACHE_KEY = "cache_key";
689
+ export const CACHE_TTL = "cache_ttl";
690
+
691
+ export interface CacheOptions {
692
+ key?: string;
693
+ ttl?: number; // seconds
694
+ }
695
+
696
+ export const Cacheable = (options: CacheOptions = {}) => {
697
+ return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
698
+ SetMetadata(CACHE_KEY, options.key || \`\${target.constructor.name}:\${propertyKey}\`)(
699
+ target,
700
+ propertyKey,
701
+ descriptor
702
+ );
703
+ SetMetadata(CACHE_TTL, options.ttl || 300)(target, propertyKey, descriptor);
704
+ };
705
+ };
706
+
707
+ export const CacheInvalidate = (keys: string[]) => SetMetadata("cache_invalidate", keys);
708
+ `;
709
+ await (0, file_utils_1.writeFile)(path.join(cachePath, 'cache.decorator.ts'), cacheDecoratorContent);
710
+ const cacheInterceptorContent = `import {
711
+ Injectable,
712
+ NestInterceptor,
713
+ ExecutionContext,
714
+ CallHandler,
715
+ } from "@nestjs/common";
716
+ import { Reflector } from "@nestjs/core";
717
+ import { Observable, of } from "rxjs";
718
+ import { tap } from "rxjs/operators";
719
+ import { CACHE_MANAGER } from "@nestjs/cache-manager";
720
+ import { Inject } from "@nestjs/common";
721
+ import { Cache } from "cache-manager";
722
+ import { CACHE_KEY, CACHE_TTL } from "./cache.decorator";
723
+
724
+ @Injectable()
725
+ export class CacheInterceptor implements NestInterceptor {
726
+ constructor(
727
+ @Inject(CACHE_MANAGER) private cacheManager: Cache,
728
+ private reflector: Reflector
729
+ ) {}
730
+
731
+ async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
732
+ const cacheKey = this.reflector.get<string>(CACHE_KEY, context.getHandler());
733
+ const cacheTtl = this.reflector.get<number>(CACHE_TTL, context.getHandler());
734
+
735
+ if (!cacheKey) {
736
+ return next.handle();
737
+ }
738
+
739
+ const request = context.switchToHttp().getRequest();
740
+ const fullKey = \`\${cacheKey}:\${JSON.stringify(request.params)}:\${JSON.stringify(request.query)}\`;
741
+
742
+ const cachedData = await this.cacheManager.get(fullKey);
743
+
744
+ if (cachedData) {
745
+ return of(cachedData);
746
+ }
747
+
748
+ return next.handle().pipe(
749
+ tap(async (data) => {
750
+ await this.cacheManager.set(fullKey, data, cacheTtl * 1000);
751
+ })
752
+ );
753
+ }
754
+ }
755
+ `;
756
+ await (0, file_utils_1.writeFile)(path.join(cachePath, 'cache.interceptor.ts'), cacheInterceptorContent);
757
+ const indexContent = `export * from "./cache.decorator";
758
+ export * from "./cache.interceptor";
759
+ `;
760
+ await (0, file_utils_1.writeFile)(path.join(cachePath, 'index.ts'), indexContent);
761
+ console.log(chalk_1.default.green(' ✓ Cache decorators (Cacheable, CacheInvalidate)'));
762
+ console.log(chalk_1.default.green(' ✓ CacheInterceptor'));
763
+ }
764
+ async function applyFileUploadRecipe(basePath) {
765
+ const sharedPath = path.join(basePath, 'src/shared');
766
+ const uploadPath = path.join(sharedPath, 'upload');
767
+ await (0, file_utils_1.ensureDir)(uploadPath);
768
+ await (0, file_utils_1.ensureDir)(path.join(uploadPath, 'strategies'));
769
+ // Storage interface
770
+ const storageInterfaceContent = `export interface StorageProvider {
771
+ upload(file: Express.Multer.File, path?: string): Promise<UploadResult>;
772
+ delete(fileKey: string): Promise<void>;
773
+ getSignedUrl(fileKey: string, expiresIn?: number): Promise<string>;
774
+ }
775
+
776
+ export interface UploadResult {
777
+ key: string;
778
+ url: string;
779
+ size: number;
780
+ mimeType: string;
781
+ originalName: string;
782
+ }
783
+
784
+ export interface UploadOptions {
785
+ maxSize?: number; // bytes
786
+ allowedMimeTypes?: string[];
787
+ path?: string;
788
+ }
789
+ `;
790
+ await (0, file_utils_1.writeFile)(path.join(uploadPath, 'storage.interface.ts'), storageInterfaceContent);
791
+ // Local storage strategy
792
+ const localStorageContent = `import { Injectable } from "@nestjs/common";
793
+ import { StorageProvider, UploadResult } from "../storage.interface";
794
+ import * as fs from "fs/promises";
795
+ import * as path from "path";
796
+ import { v4 as uuid } from "uuid";
797
+
798
+ @Injectable()
799
+ export class LocalStorageStrategy implements StorageProvider {
800
+ private readonly uploadDir: string;
801
+ private readonly baseUrl: string;
802
+
803
+ constructor() {
804
+ this.uploadDir = process.env.UPLOAD_DIR || "./uploads";
805
+ this.baseUrl = process.env.UPLOAD_BASE_URL || "http://localhost:3000/uploads";
806
+ }
807
+
808
+ async upload(file: Express.Multer.File, filePath?: string): Promise<UploadResult> {
809
+ const ext = path.extname(file.originalname);
810
+ const fileName = \`\${uuid()}\${ext}\`;
811
+ const relativePath = filePath ? \`\${filePath}/\${fileName}\` : fileName;
812
+ const fullPath = path.join(this.uploadDir, relativePath);
813
+
814
+ await fs.mkdir(path.dirname(fullPath), { recursive: true });
815
+ await fs.writeFile(fullPath, file.buffer);
816
+
817
+ return {
818
+ key: relativePath,
819
+ url: \`\${this.baseUrl}/\${relativePath}\`,
820
+ size: file.size,
821
+ mimeType: file.mimetype,
822
+ originalName: file.originalname,
823
+ };
824
+ }
825
+
826
+ async delete(fileKey: string): Promise<void> {
827
+ const fullPath = path.join(this.uploadDir, fileKey);
828
+ await fs.unlink(fullPath);
829
+ }
830
+
831
+ async getSignedUrl(fileKey: string, _expiresIn?: number): Promise<string> {
832
+ // Local storage doesn't need signed URLs
833
+ return \`\${this.baseUrl}/\${fileKey}\`;
834
+ }
835
+ }
836
+ `;
837
+ await (0, file_utils_1.writeFile)(path.join(uploadPath, 'strategies/local.strategy.ts'), localStorageContent);
838
+ // S3 storage strategy
839
+ const s3StorageContent = `import { Injectable } from "@nestjs/common";
840
+ import { StorageProvider, UploadResult } from "../storage.interface";
841
+ import { S3Client, PutObjectCommand, DeleteObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
842
+ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
843
+ import { v4 as uuid } from "uuid";
844
+ import * as path from "path";
845
+
846
+ @Injectable()
847
+ export class S3StorageStrategy implements StorageProvider {
848
+ private readonly s3Client: S3Client;
849
+ private readonly bucket: string;
850
+ private readonly region: string;
851
+
852
+ constructor() {
853
+ this.region = process.env.AWS_REGION || "us-east-1";
854
+ this.bucket = process.env.AWS_S3_BUCKET || "uploads";
855
+
856
+ this.s3Client = new S3Client({
857
+ region: this.region,
858
+ credentials: {
859
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID || "",
860
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || "",
861
+ },
862
+ });
863
+ }
864
+
865
+ async upload(file: Express.Multer.File, filePath?: string): Promise<UploadResult> {
866
+ const ext = path.extname(file.originalname);
867
+ const fileName = \`\${uuid()}\${ext}\`;
868
+ const key = filePath ? \`\${filePath}/\${fileName}\` : fileName;
869
+
870
+ await this.s3Client.send(
871
+ new PutObjectCommand({
872
+ Bucket: this.bucket,
873
+ Key: key,
874
+ Body: file.buffer,
875
+ ContentType: file.mimetype,
876
+ Metadata: {
877
+ originalName: file.originalname,
878
+ },
879
+ })
880
+ );
881
+
882
+ return {
883
+ key,
884
+ url: \`https://\${this.bucket}.s3.\${this.region}.amazonaws.com/\${key}\`,
885
+ size: file.size,
886
+ mimeType: file.mimetype,
887
+ originalName: file.originalname,
888
+ };
889
+ }
890
+
891
+ async delete(fileKey: string): Promise<void> {
892
+ await this.s3Client.send(
893
+ new DeleteObjectCommand({
894
+ Bucket: this.bucket,
895
+ Key: fileKey,
896
+ })
897
+ );
898
+ }
899
+
900
+ async getSignedUrl(fileKey: string, expiresIn: number = 3600): Promise<string> {
901
+ const command = new GetObjectCommand({
902
+ Bucket: this.bucket,
903
+ Key: fileKey,
904
+ });
905
+
906
+ return getSignedUrl(this.s3Client, command, { expiresIn });
907
+ }
908
+ }
909
+ `;
910
+ await (0, file_utils_1.writeFile)(path.join(uploadPath, 'strategies/s3.strategy.ts'), s3StorageContent);
911
+ // Upload service
912
+ const uploadServiceContent = `import { Injectable, BadRequestException } from "@nestjs/common";
913
+ import { StorageProvider, UploadResult, UploadOptions } from "./storage.interface";
914
+ import { LocalStorageStrategy } from "./strategies/local.strategy";
915
+ import { S3StorageStrategy } from "./strategies/s3.strategy";
916
+
917
+ @Injectable()
918
+ export class UploadService {
919
+ private readonly storageProvider: StorageProvider;
920
+
921
+ constructor() {
922
+ const storageType = process.env.STORAGE_TYPE || "local";
923
+ this.storageProvider =
924
+ storageType === "s3" ? new S3StorageStrategy() : new LocalStorageStrategy();
925
+ }
926
+
927
+ async uploadFile(
928
+ file: Express.Multer.File,
929
+ options: UploadOptions = {}
930
+ ): Promise<UploadResult> {
931
+ this.validateFile(file, options);
932
+ return this.storageProvider.upload(file, options.path);
933
+ }
934
+
935
+ async uploadFiles(
936
+ files: Express.Multer.File[],
937
+ options: UploadOptions = {}
938
+ ): Promise<UploadResult[]> {
939
+ return Promise.all(files.map((file) => this.uploadFile(file, options)));
940
+ }
941
+
942
+ async deleteFile(fileKey: string): Promise<void> {
943
+ return this.storageProvider.delete(fileKey);
944
+ }
945
+
946
+ async getSignedUrl(fileKey: string, expiresIn?: number): Promise<string> {
947
+ return this.storageProvider.getSignedUrl(fileKey, expiresIn);
948
+ }
949
+
950
+ private validateFile(file: Express.Multer.File, options: UploadOptions): void {
951
+ if (options.maxSize && file.size > options.maxSize) {
952
+ throw new BadRequestException(
953
+ \`File size exceeds maximum allowed size of \${options.maxSize} bytes\`
954
+ );
955
+ }
956
+
957
+ if (
958
+ options.allowedMimeTypes &&
959
+ !options.allowedMimeTypes.includes(file.mimetype)
960
+ ) {
961
+ throw new BadRequestException(
962
+ \`File type \${file.mimetype} is not allowed. Allowed types: \${options.allowedMimeTypes.join(", ")}\`
963
+ );
964
+ }
965
+ }
966
+ }
967
+ `;
968
+ await (0, file_utils_1.writeFile)(path.join(uploadPath, 'upload.service.ts'), uploadServiceContent);
969
+ // Index exports
970
+ const indexContent = `export * from "./storage.interface";
971
+ export * from "./upload.service";
972
+ export * from "./strategies/local.strategy";
973
+ export * from "./strategies/s3.strategy";
974
+ `;
975
+ await (0, file_utils_1.writeFile)(path.join(uploadPath, 'index.ts'), indexContent);
976
+ console.log(chalk_1.default.green(' ✓ StorageProvider interface'));
977
+ console.log(chalk_1.default.green(' ✓ LocalStorageStrategy'));
978
+ console.log(chalk_1.default.green(' ✓ S3StorageStrategy'));
979
+ console.log(chalk_1.default.green(' ✓ UploadService'));
980
+ }
981
+ async function applyNotificationsRecipe(basePath) {
982
+ const sharedPath = path.join(basePath, 'src/shared');
983
+ const notifPath = path.join(sharedPath, 'notifications');
984
+ await (0, file_utils_1.ensureDir)(notifPath);
985
+ await (0, file_utils_1.ensureDir)(path.join(notifPath, 'channels'));
986
+ await (0, file_utils_1.ensureDir)(path.join(notifPath, 'templates'));
987
+ // Notification interface
988
+ const notifInterfaceContent = `export interface NotificationChannel {
989
+ send(notification: Notification): Promise<void>;
990
+ }
991
+
992
+ export interface Notification {
993
+ recipient: string;
994
+ subject: string;
995
+ content: string;
996
+ template?: string;
997
+ data?: Record<string, any>;
998
+ metadata?: Record<string, any>;
999
+ }
1000
+
1001
+ export type NotificationType = "email" | "push" | "sms" | "in-app";
1002
+
1003
+ export interface NotificationOptions {
1004
+ channels: NotificationType[];
1005
+ priority?: "low" | "normal" | "high";
1006
+ scheduledAt?: Date;
1007
+ }
1008
+ `;
1009
+ await (0, file_utils_1.writeFile)(path.join(notifPath, 'notification.interface.ts'), notifInterfaceContent);
1010
+ // Email channel
1011
+ const emailChannelContent = `import { Injectable } from "@nestjs/common";
1012
+ import { NotificationChannel, Notification } from "../notification.interface";
1013
+ import * as nodemailer from "nodemailer";
1014
+ import * as Handlebars from "handlebars";
1015
+ import * as fs from "fs/promises";
1016
+ import * as path from "path";
1017
+
1018
+ @Injectable()
1019
+ export class EmailChannel implements NotificationChannel {
1020
+ private transporter: nodemailer.Transporter;
1021
+ private templateCache: Map<string, HandlebarsTemplateDelegate> = new Map();
1022
+
1023
+ constructor() {
1024
+ this.transporter = nodemailer.createTransport({
1025
+ host: process.env.SMTP_HOST,
1026
+ port: parseInt(process.env.SMTP_PORT || "587"),
1027
+ secure: process.env.SMTP_SECURE === "true",
1028
+ auth: {
1029
+ user: process.env.SMTP_USER,
1030
+ pass: process.env.SMTP_PASS,
1031
+ },
1032
+ });
1033
+ }
1034
+
1035
+ async send(notification: Notification): Promise<void> {
1036
+ let html = notification.content;
1037
+
1038
+ if (notification.template) {
1039
+ const template = await this.loadTemplate(notification.template);
1040
+ html = template(notification.data || {});
1041
+ }
1042
+
1043
+ await this.transporter.sendMail({
1044
+ from: process.env.SMTP_FROM || "noreply@example.com",
1045
+ to: notification.recipient,
1046
+ subject: notification.subject,
1047
+ html,
1048
+ });
1049
+ }
1050
+
1051
+ private async loadTemplate(templateName: string): Promise<HandlebarsTemplateDelegate> {
1052
+ if (this.templateCache.has(templateName)) {
1053
+ return this.templateCache.get(templateName)!;
1054
+ }
1055
+
1056
+ const templatePath = path.join(
1057
+ process.cwd(),
1058
+ "src/shared/notifications/templates",
1059
+ \`\${templateName}.hbs\`
1060
+ );
1061
+
1062
+ const templateContent = await fs.readFile(templatePath, "utf-8");
1063
+ const compiled = Handlebars.compile(templateContent);
1064
+ this.templateCache.set(templateName, compiled);
1065
+
1066
+ return compiled;
1067
+ }
1068
+ }
1069
+ `;
1070
+ await (0, file_utils_1.writeFile)(path.join(notifPath, 'channels/email.channel.ts'), emailChannelContent);
1071
+ // In-app channel
1072
+ const inAppChannelContent = `import { Injectable } from "@nestjs/common";
1073
+ import { NotificationChannel, Notification } from "../notification.interface";
1074
+ import { EventEmitter2 } from "@nestjs/event-emitter";
1075
+
1076
+ @Injectable()
1077
+ export class InAppChannel implements NotificationChannel {
1078
+ constructor(private readonly eventEmitter: EventEmitter2) {}
1079
+
1080
+ async send(notification: Notification): Promise<void> {
1081
+ this.eventEmitter.emit("notification.created", {
1082
+ userId: notification.recipient,
1083
+ subject: notification.subject,
1084
+ content: notification.content,
1085
+ metadata: notification.metadata,
1086
+ createdAt: new Date(),
1087
+ });
1088
+ }
1089
+ }
1090
+ `;
1091
+ await (0, file_utils_1.writeFile)(path.join(notifPath, 'channels/in-app.channel.ts'), inAppChannelContent);
1092
+ // Notification service
1093
+ const notifServiceContent = `import { Injectable, Logger } from "@nestjs/common";
1094
+ import { InjectQueue } from "@nestjs/bull";
1095
+ import { Queue } from "bull";
1096
+ import { Notification, NotificationOptions, NotificationType } from "./notification.interface";
1097
+ import { EmailChannel } from "./channels/email.channel";
1098
+ import { InAppChannel } from "./channels/in-app.channel";
1099
+
1100
+ @Injectable()
1101
+ export class NotificationService {
1102
+ private readonly logger = new Logger(NotificationService.name);
1103
+
1104
+ constructor(
1105
+ @InjectQueue("notifications") private notificationQueue: Queue,
1106
+ private readonly emailChannel: EmailChannel,
1107
+ private readonly inAppChannel: InAppChannel
1108
+ ) {}
1109
+
1110
+ async send(notification: Notification, options: NotificationOptions): Promise<void> {
1111
+ if (options.scheduledAt) {
1112
+ const delay = options.scheduledAt.getTime() - Date.now();
1113
+ await this.notificationQueue.add(
1114
+ "send",
1115
+ { notification, options },
1116
+ { delay, priority: this.getPriority(options.priority) }
1117
+ );
1118
+ return;
1119
+ }
1120
+
1121
+ await this.processNotification(notification, options);
1122
+ }
1123
+
1124
+ async processNotification(notification: Notification, options: NotificationOptions): Promise<void> {
1125
+ for (const channel of options.channels) {
1126
+ try {
1127
+ await this.sendToChannel(channel, notification);
1128
+ } catch (error) {
1129
+ this.logger.error(\`Failed to send notification via \${channel}\`, error);
1130
+ }
1131
+ }
1132
+ }
1133
+
1134
+ private async sendToChannel(channel: NotificationType, notification: Notification): Promise<void> {
1135
+ switch (channel) {
1136
+ case "email":
1137
+ await this.emailChannel.send(notification);
1138
+ break;
1139
+ case "in-app":
1140
+ await this.inAppChannel.send(notification);
1141
+ break;
1142
+ case "push":
1143
+ // Implement push notification
1144
+ this.logger.warn("Push notifications not yet implemented");
1145
+ break;
1146
+ case "sms":
1147
+ // Implement SMS notification
1148
+ this.logger.warn("SMS notifications not yet implemented");
1149
+ break;
1150
+ }
1151
+ }
1152
+
1153
+ private getPriority(priority?: "low" | "normal" | "high"): number {
1154
+ switch (priority) {
1155
+ case "high":
1156
+ return 1;
1157
+ case "low":
1158
+ return 3;
1159
+ default:
1160
+ return 2;
1161
+ }
1162
+ }
1163
+ }
1164
+ `;
1165
+ await (0, file_utils_1.writeFile)(path.join(notifPath, 'notification.service.ts'), notifServiceContent);
1166
+ // Sample template
1167
+ const welcomeTemplateContent = `<!DOCTYPE html>
1168
+ <html>
1169
+ <head>
1170
+ <style>
1171
+ body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
1172
+ .container { max-width: 600px; margin: 0 auto; padding: 20px; }
1173
+ .header { background: #007bff; color: white; padding: 20px; text-align: center; }
1174
+ .content { padding: 20px; background: #f9f9f9; }
1175
+ .footer { text-align: center; padding: 20px; color: #666; font-size: 12px; }
1176
+ </style>
1177
+ </head>
1178
+ <body>
1179
+ <div class="container">
1180
+ <div class="header">
1181
+ <h1>Welcome, {{name}}!</h1>
1182
+ </div>
1183
+ <div class="content">
1184
+ <p>Thank you for joining us. We're excited to have you on board.</p>
1185
+ {{#if actionUrl}}
1186
+ <p>
1187
+ <a href="{{actionUrl}}" style="background: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">
1188
+ {{actionText}}
1189
+ </a>
1190
+ </p>
1191
+ {{/if}}
1192
+ </div>
1193
+ <div class="footer">
1194
+ <p>&copy; {{year}} Your Company. All rights reserved.</p>
1195
+ </div>
1196
+ </div>
1197
+ </body>
1198
+ </html>
1199
+ `;
1200
+ await (0, file_utils_1.writeFile)(path.join(notifPath, 'templates/welcome.hbs'), welcomeTemplateContent);
1201
+ // Index exports
1202
+ const indexContent = `export * from "./notification.interface";
1203
+ export * from "./notification.service";
1204
+ export * from "./channels/email.channel";
1205
+ export * from "./channels/in-app.channel";
1206
+ `;
1207
+ await (0, file_utils_1.writeFile)(path.join(notifPath, 'index.ts'), indexContent);
1208
+ console.log(chalk_1.default.green(' ✓ NotificationService'));
1209
+ console.log(chalk_1.default.green(' ✓ EmailChannel'));
1210
+ console.log(chalk_1.default.green(' ✓ InAppChannel'));
1211
+ console.log(chalk_1.default.green(' ✓ Welcome email template'));
1212
+ }
1213
+ async function applyWebhooksRecipe(basePath) {
1214
+ const sharedPath = path.join(basePath, 'src/shared');
1215
+ const webhookPath = path.join(sharedPath, 'webhooks');
1216
+ await (0, file_utils_1.ensureDir)(webhookPath);
1217
+ // Webhook entity
1218
+ const webhookEntityContent = `import {
1219
+ Entity,
1220
+ PrimaryGeneratedColumn,
1221
+ Column,
1222
+ CreateDateColumn,
1223
+ UpdateDateColumn,
1224
+ } from "typeorm";
1225
+
1226
+ export type WebhookEvent =
1227
+ | "entity.created"
1228
+ | "entity.updated"
1229
+ | "entity.deleted"
1230
+ | "user.registered"
1231
+ | "order.completed"
1232
+ | "payment.received";
1233
+
1234
+ @Entity("webhooks")
1235
+ export class WebhookEntity {
1236
+ @PrimaryGeneratedColumn("uuid")
1237
+ id: string;
1238
+
1239
+ @Column()
1240
+ name: string;
1241
+
1242
+ @Column()
1243
+ url: string;
1244
+
1245
+ @Column("simple-array")
1246
+ events: WebhookEvent[];
1247
+
1248
+ @Column({ nullable: true })
1249
+ secret?: string;
1250
+
1251
+ @Column({ default: true })
1252
+ isActive: boolean;
1253
+
1254
+ @Column({ default: 0 })
1255
+ failureCount: number;
1256
+
1257
+ @Column({ type: "timestamp", nullable: true })
1258
+ lastTriggeredAt?: Date;
1259
+
1260
+ @CreateDateColumn()
1261
+ createdAt: Date;
1262
+
1263
+ @UpdateDateColumn()
1264
+ updatedAt: Date;
1265
+ }
1266
+ `;
1267
+ await (0, file_utils_1.writeFile)(path.join(webhookPath, 'webhook.entity.ts'), webhookEntityContent);
1268
+ // Webhook delivery entity
1269
+ const deliveryEntityContent = `import {
1270
+ Entity,
1271
+ PrimaryGeneratedColumn,
1272
+ Column,
1273
+ CreateDateColumn,
1274
+ ManyToOne,
1275
+ JoinColumn,
1276
+ } from "typeorm";
1277
+ import { WebhookEntity } from "./webhook.entity";
1278
+
1279
+ export type DeliveryStatus = "pending" | "success" | "failed" | "retrying";
1280
+
1281
+ @Entity("webhook_deliveries")
1282
+ export class WebhookDeliveryEntity {
1283
+ @PrimaryGeneratedColumn("uuid")
1284
+ id: string;
1285
+
1286
+ @ManyToOne(() => WebhookEntity)
1287
+ @JoinColumn({ name: "webhook_id" })
1288
+ webhook: WebhookEntity;
1289
+
1290
+ @Column()
1291
+ webhookId: string;
1292
+
1293
+ @Column()
1294
+ event: string;
1295
+
1296
+ @Column({ type: "jsonb" })
1297
+ payload: Record<string, any>;
1298
+
1299
+ @Column({ default: "pending" })
1300
+ status: DeliveryStatus;
1301
+
1302
+ @Column({ nullable: true })
1303
+ responseStatus?: number;
1304
+
1305
+ @Column({ type: "text", nullable: true })
1306
+ responseBody?: string;
1307
+
1308
+ @Column({ default: 0 })
1309
+ attempts: number;
1310
+
1311
+ @Column({ type: "timestamp", nullable: true })
1312
+ nextRetryAt?: Date;
1313
+
1314
+ @CreateDateColumn()
1315
+ createdAt: Date;
1316
+ }
1317
+ `;
1318
+ await (0, file_utils_1.writeFile)(path.join(webhookPath, 'webhook-delivery.entity.ts'), deliveryEntityContent);
1319
+ // Webhook service
1320
+ const webhookServiceContent = `import { Injectable, Logger } from "@nestjs/common";
1321
+ import { InjectRepository } from "@nestjs/typeorm";
1322
+ import { Repository } from "typeorm";
1323
+ import { WebhookEntity, WebhookEvent } from "./webhook.entity";
1324
+ import { WebhookDeliveryEntity } from "./webhook-delivery.entity";
1325
+ import axios from "axios";
1326
+ import * as crypto from "crypto";
1327
+
1328
+ @Injectable()
1329
+ export class WebhookService {
1330
+ private readonly logger = new Logger(WebhookService.name);
1331
+ private readonly maxRetries = 3;
1332
+ private readonly retryDelays = [60, 300, 900]; // seconds
1333
+
1334
+ constructor(
1335
+ @InjectRepository(WebhookEntity)
1336
+ private readonly webhookRepo: Repository<WebhookEntity>,
1337
+ @InjectRepository(WebhookDeliveryEntity)
1338
+ private readonly deliveryRepo: Repository<WebhookDeliveryEntity>
1339
+ ) {}
1340
+
1341
+ async registerWebhook(
1342
+ name: string,
1343
+ url: string,
1344
+ events: WebhookEvent[],
1345
+ secret?: string
1346
+ ): Promise<WebhookEntity> {
1347
+ const webhook = this.webhookRepo.create({
1348
+ name,
1349
+ url,
1350
+ events,
1351
+ secret: secret || this.generateSecret(),
1352
+ });
1353
+ return this.webhookRepo.save(webhook);
1354
+ }
1355
+
1356
+ async trigger(event: WebhookEvent, payload: Record<string, any>): Promise<void> {
1357
+ const webhooks = await this.webhookRepo.find({
1358
+ where: { isActive: true },
1359
+ });
1360
+
1361
+ const relevantWebhooks = webhooks.filter((w) => w.events.includes(event));
1362
+
1363
+ for (const webhook of relevantWebhooks) {
1364
+ await this.deliver(webhook, event, payload);
1365
+ }
1366
+ }
1367
+
1368
+ private async deliver(
1369
+ webhook: WebhookEntity,
1370
+ event: string,
1371
+ payload: Record<string, any>
1372
+ ): Promise<void> {
1373
+ const delivery = this.deliveryRepo.create({
1374
+ webhookId: webhook.id,
1375
+ event,
1376
+ payload,
1377
+ status: "pending",
1378
+ });
1379
+ await this.deliveryRepo.save(delivery);
1380
+
1381
+ await this.attemptDelivery(webhook, delivery);
1382
+ }
1383
+
1384
+ private async attemptDelivery(
1385
+ webhook: WebhookEntity,
1386
+ delivery: WebhookDeliveryEntity
1387
+ ): Promise<void> {
1388
+ try {
1389
+ const signature = this.generateSignature(delivery.payload, webhook.secret);
1390
+
1391
+ const response = await axios.post(webhook.url, delivery.payload, {
1392
+ headers: {
1393
+ "Content-Type": "application/json",
1394
+ "X-Webhook-Signature": signature,
1395
+ "X-Webhook-Event": delivery.event,
1396
+ "X-Webhook-Delivery": delivery.id,
1397
+ },
1398
+ timeout: 30000,
1399
+ });
1400
+
1401
+ delivery.status = "success";
1402
+ delivery.responseStatus = response.status;
1403
+ delivery.responseBody = JSON.stringify(response.data).slice(0, 1000);
1404
+ delivery.attempts += 1;
1405
+
1406
+ webhook.lastTriggeredAt = new Date();
1407
+ webhook.failureCount = 0;
1408
+
1409
+ await Promise.all([
1410
+ this.deliveryRepo.save(delivery),
1411
+ this.webhookRepo.save(webhook),
1412
+ ]);
1413
+
1414
+ this.logger.log(\`Webhook delivered successfully: \${delivery.id}\`);
1415
+ } catch (error: any) {
1416
+ delivery.attempts += 1;
1417
+ delivery.responseStatus = error.response?.status;
1418
+ delivery.responseBody = error.message;
1419
+
1420
+ if (delivery.attempts < this.maxRetries) {
1421
+ delivery.status = "retrying";
1422
+ delivery.nextRetryAt = new Date(
1423
+ Date.now() + this.retryDelays[delivery.attempts - 1] * 1000
1424
+ );
1425
+ } else {
1426
+ delivery.status = "failed";
1427
+ webhook.failureCount += 1;
1428
+
1429
+ if (webhook.failureCount >= 10) {
1430
+ webhook.isActive = false;
1431
+ this.logger.warn(\`Webhook disabled due to failures: \${webhook.id}\`);
1432
+ }
1433
+ }
1434
+
1435
+ await Promise.all([
1436
+ this.deliveryRepo.save(delivery),
1437
+ this.webhookRepo.save(webhook),
1438
+ ]);
1439
+
1440
+ this.logger.error(\`Webhook delivery failed: \${delivery.id}\`, error.message);
1441
+ }
1442
+ }
1443
+
1444
+ private generateSignature(payload: Record<string, any>, secret?: string): string {
1445
+ if (!secret) return "";
1446
+ const hmac = crypto.createHmac("sha256", secret);
1447
+ hmac.update(JSON.stringify(payload));
1448
+ return \`sha256=\${hmac.digest("hex")}\`;
1449
+ }
1450
+
1451
+ private generateSecret(): string {
1452
+ return crypto.randomBytes(32).toString("hex");
1453
+ }
1454
+
1455
+ verifySignature(
1456
+ payload: string,
1457
+ signature: string,
1458
+ secret: string
1459
+ ): boolean {
1460
+ const expected = this.generateSignature(JSON.parse(payload), secret);
1461
+ return crypto.timingSafeEqual(
1462
+ Buffer.from(signature),
1463
+ Buffer.from(expected)
1464
+ );
1465
+ }
1466
+
1467
+ async getDeliveries(webhookId: string): Promise<WebhookDeliveryEntity[]> {
1468
+ return this.deliveryRepo.find({
1469
+ where: { webhookId },
1470
+ order: { createdAt: "DESC" },
1471
+ take: 100,
1472
+ });
1473
+ }
1474
+
1475
+ async retryDelivery(deliveryId: string): Promise<void> {
1476
+ const delivery = await this.deliveryRepo.findOne({
1477
+ where: { id: deliveryId },
1478
+ relations: ["webhook"],
1479
+ });
1480
+
1481
+ if (!delivery) {
1482
+ throw new Error("Delivery not found");
1483
+ }
1484
+
1485
+ delivery.status = "pending";
1486
+ delivery.attempts = 0;
1487
+ await this.deliveryRepo.save(delivery);
1488
+
1489
+ const webhook = await this.webhookRepo.findOne({
1490
+ where: { id: delivery.webhookId },
1491
+ });
1492
+
1493
+ if (webhook) {
1494
+ await this.attemptDelivery(webhook, delivery);
1495
+ }
1496
+ }
1497
+ }
1498
+ `;
1499
+ await (0, file_utils_1.writeFile)(path.join(webhookPath, 'webhook.service.ts'), webhookServiceContent);
1500
+ // Index exports
1501
+ const indexContent = `export * from "./webhook.entity";
1502
+ export * from "./webhook-delivery.entity";
1503
+ export * from "./webhook.service";
1504
+ `;
1505
+ await (0, file_utils_1.writeFile)(path.join(webhookPath, 'index.ts'), indexContent);
1506
+ console.log(chalk_1.default.green(' ✓ WebhookEntity'));
1507
+ console.log(chalk_1.default.green(' ✓ WebhookDeliveryEntity'));
1508
+ console.log(chalk_1.default.green(' ✓ WebhookService with retry logic'));
1509
+ console.log(chalk_1.default.green(' ✓ Signature verification'));
1510
+ }
1511
+ async function applyTestFactoriesRecipe(basePath) {
1512
+ const testPath = path.join(basePath, 'test');
1513
+ const factoriesPath = path.join(testPath, 'factories');
1514
+ const fixturesPath = path.join(testPath, 'fixtures');
1515
+ const mocksPath = path.join(testPath, 'mocks');
1516
+ const utilsPath = path.join(testPath, 'utils');
1517
+ await (0, file_utils_1.ensureDir)(factoriesPath);
1518
+ await (0, file_utils_1.ensureDir)(fixturesPath);
1519
+ await (0, file_utils_1.ensureDir)(mocksPath);
1520
+ await (0, file_utils_1.ensureDir)(utilsPath);
1521
+ // Base factory class
1522
+ const baseFactoryContent = `import { faker } from "@faker-js/faker";
1523
+
1524
+ /**
1525
+ * Base factory class for creating test data
1526
+ *
1527
+ * Usage:
1528
+ * const factory = new UserFactory();
1529
+ * const user = factory.make(); // Single instance
1530
+ * const users = factory.makeMany(5); // Multiple instances
1531
+ * const saved = await factory.create(); // Persisted to DB
1532
+ */
1533
+ export abstract class BaseFactory<T> {
1534
+ protected abstract definition(): T;
1535
+
1536
+ /**
1537
+ * Create a single instance without persisting
1538
+ */
1539
+ make(overrides: Partial<T> = {}): T {
1540
+ return { ...this.definition(), ...overrides };
1541
+ }
1542
+
1543
+ /**
1544
+ * Create multiple instances without persisting
1545
+ */
1546
+ makeMany(count: number, overrides: Partial<T> = {}): T[] {
1547
+ return Array.from({ length: count }, () => this.make(overrides));
1548
+ }
1549
+
1550
+ /**
1551
+ * Create and persist a single instance
1552
+ * Override this in subclass to implement persistence
1553
+ */
1554
+ async create(overrides: Partial<T> = {}): Promise<T> {
1555
+ return this.make(overrides);
1556
+ }
1557
+
1558
+ /**
1559
+ * Create and persist multiple instances
1560
+ */
1561
+ async createMany(count: number, overrides: Partial<T> = {}): Promise<T[]> {
1562
+ return Promise.all(
1563
+ Array.from({ length: count }, () => this.create(overrides))
1564
+ );
1565
+ }
1566
+
1567
+ /**
1568
+ * Create instance with specific state
1569
+ */
1570
+ state(stateOverrides: Partial<T>): this {
1571
+ const original = this.definition.bind(this);
1572
+ this.definition = () => ({ ...original(), ...stateOverrides });
1573
+ return this;
1574
+ }
1575
+ }
1576
+
1577
+ /**
1578
+ * Factory with repository support for database persistence
1579
+ */
1580
+ export abstract class PersistentFactory<T, R = any> extends BaseFactory<T> {
1581
+ constructor(protected repository?: R) {
1582
+ super();
1583
+ }
1584
+
1585
+ setRepository(repository: R): this {
1586
+ this.repository = repository;
1587
+ return this;
1588
+ }
1589
+
1590
+ abstract persist(entity: T): Promise<T>;
1591
+
1592
+ async create(overrides: Partial<T> = {}): Promise<T> {
1593
+ const entity = this.make(overrides);
1594
+ if (this.repository) {
1595
+ return this.persist(entity);
1596
+ }
1597
+ return entity;
1598
+ }
1599
+ }
1600
+
1601
+ // Re-export faker for convenience
1602
+ export { faker };
1603
+ `;
1604
+ await (0, file_utils_1.writeFile)(path.join(factoriesPath, 'base.factory.ts'), baseFactoryContent);
1605
+ // Example user factory
1606
+ const exampleFactoryContent = `import { faker } from "@faker-js/faker";
1607
+ import { BaseFactory, PersistentFactory } from "./base.factory";
1608
+
1609
+ /**
1610
+ * Example interfaces - replace with your actual entity types
1611
+ */
1612
+ interface User {
1613
+ id: string;
1614
+ email: string;
1615
+ firstName: string;
1616
+ lastName: string;
1617
+ password: string;
1618
+ isActive: boolean;
1619
+ role: "admin" | "user" | "guest";
1620
+ createdAt: Date;
1621
+ updatedAt: Date;
1622
+ }
1623
+
1624
+ interface Post {
1625
+ id: string;
1626
+ title: string;
1627
+ content: string;
1628
+ authorId: string;
1629
+ isPublished: boolean;
1630
+ tags: string[];
1631
+ createdAt: Date;
1632
+ }
1633
+
1634
+ /**
1635
+ * User Factory
1636
+ */
1637
+ export class UserFactory extends BaseFactory<User> {
1638
+ protected definition(): User {
1639
+ return {
1640
+ id: faker.string.uuid(),
1641
+ email: faker.internet.email().toLowerCase(),
1642
+ firstName: faker.person.firstName(),
1643
+ lastName: faker.person.lastName(),
1644
+ password: faker.internet.password({ length: 12 }),
1645
+ isActive: true,
1646
+ role: "user",
1647
+ createdAt: faker.date.past(),
1648
+ updatedAt: new Date(),
1649
+ };
1650
+ }
1651
+
1652
+ /**
1653
+ * Create an admin user
1654
+ */
1655
+ admin(): this {
1656
+ return this.state({ role: "admin" });
1657
+ }
1658
+
1659
+ /**
1660
+ * Create an inactive user
1661
+ */
1662
+ inactive(): this {
1663
+ return this.state({ isActive: false });
1664
+ }
1665
+
1666
+ /**
1667
+ * Create a user with specific email domain
1668
+ */
1669
+ withDomain(domain: string): this {
1670
+ return this.state({
1671
+ email: \`\${faker.internet.username()}@\${domain}\`.toLowerCase(),
1672
+ });
1673
+ }
1674
+
1675
+ /**
1676
+ * Create a recently registered user
1677
+ */
1678
+ recent(): this {
1679
+ return this.state({
1680
+ createdAt: faker.date.recent({ days: 7 }),
1681
+ });
1682
+ }
1683
+ }
1684
+
1685
+ /**
1686
+ * Post Factory
1687
+ */
1688
+ export class PostFactory extends BaseFactory<Post> {
1689
+ protected definition(): Post {
1690
+ return {
1691
+ id: faker.string.uuid(),
1692
+ title: faker.lorem.sentence(),
1693
+ content: faker.lorem.paragraphs(3),
1694
+ authorId: faker.string.uuid(),
1695
+ isPublished: false,
1696
+ tags: faker.helpers.arrayElements(
1697
+ ["tech", "news", "tutorial", "review", "opinion"],
1698
+ { min: 1, max: 3 }
1699
+ ),
1700
+ createdAt: faker.date.past(),
1701
+ };
1702
+ }
1703
+
1704
+ /**
1705
+ * Create a published post
1706
+ */
1707
+ published(): this {
1708
+ return this.state({ isPublished: true });
1709
+ }
1710
+
1711
+ /**
1712
+ * Create a post by specific author
1713
+ */
1714
+ byAuthor(authorId: string): this {
1715
+ return this.state({ authorId });
1716
+ }
1717
+
1718
+ /**
1719
+ * Create a post with specific tags
1720
+ */
1721
+ withTags(...tags: string[]): this {
1722
+ return this.state({ tags });
1723
+ }
1724
+ }
1725
+
1726
+ // Export singleton instances for convenience
1727
+ export const userFactory = new UserFactory();
1728
+ export const postFactory = new PostFactory();
1729
+ `;
1730
+ await (0, file_utils_1.writeFile)(path.join(factoriesPath, 'example.factory.ts'), exampleFactoryContent);
1731
+ // Test data builder pattern
1732
+ const builderContent = `import { faker } from "@faker-js/faker";
1733
+
1734
+ /**
1735
+ * Test Data Builder Pattern
1736
+ *
1737
+ * More flexible than factories for complex object construction
1738
+ *
1739
+ * Usage:
1740
+ * const user = new UserBuilder()
1741
+ * .withEmail("test@example.com")
1742
+ * .asAdmin()
1743
+ * .build();
1744
+ */
1745
+ export class TestDataBuilder<T> {
1746
+ protected data: Partial<T> = {};
1747
+
1748
+ constructor(protected defaults: () => T) {}
1749
+
1750
+ /**
1751
+ * Set a specific field value
1752
+ */
1753
+ with<K extends keyof T>(key: K, value: T[K]): this {
1754
+ this.data[key] = value;
1755
+ return this;
1756
+ }
1757
+
1758
+ /**
1759
+ * Set multiple fields at once
1760
+ */
1761
+ withOverrides(overrides: Partial<T>): this {
1762
+ this.data = { ...this.data, ...overrides };
1763
+ return this;
1764
+ }
1765
+
1766
+ /**
1767
+ * Build the final object
1768
+ */
1769
+ build(): T {
1770
+ return { ...this.defaults(), ...this.data };
1771
+ }
1772
+
1773
+ /**
1774
+ * Build multiple objects
1775
+ */
1776
+ buildMany(count: number): T[] {
1777
+ return Array.from({ length: count }, () => this.build());
1778
+ }
1779
+
1780
+ /**
1781
+ * Reset builder state
1782
+ */
1783
+ reset(): this {
1784
+ this.data = {};
1785
+ return this;
1786
+ }
1787
+ }
1788
+
1789
+ /**
1790
+ * Example User Builder
1791
+ */
1792
+ export class UserBuilder extends TestDataBuilder<{
1793
+ id: string;
1794
+ email: string;
1795
+ name: string;
1796
+ role: string;
1797
+ isVerified: boolean;
1798
+ }> {
1799
+ constructor() {
1800
+ super(() => ({
1801
+ id: faker.string.uuid(),
1802
+ email: faker.internet.email(),
1803
+ name: faker.person.fullName(),
1804
+ role: "user",
1805
+ isVerified: false,
1806
+ }));
1807
+ }
1808
+
1809
+ withEmail(email: string): this {
1810
+ return this.with("email", email);
1811
+ }
1812
+
1813
+ withName(name: string): this {
1814
+ return this.with("name", name);
1815
+ }
1816
+
1817
+ asAdmin(): this {
1818
+ return this.with("role", "admin");
1819
+ }
1820
+
1821
+ verified(): this {
1822
+ return this.with("isVerified", true);
1823
+ }
1824
+ }
1825
+ `;
1826
+ await (0, file_utils_1.writeFile)(path.join(factoriesPath, 'builder.ts'), builderContent);
1827
+ // Database fixture utilities
1828
+ const fixtureUtilsContent = `import { DataSource, Repository, ObjectLiteral } from "typeorm";
1829
+
1830
+ /**
1831
+ * Database Fixture Manager
1832
+ *
1833
+ * Handles test database setup, seeding, and cleanup
1834
+ */
1835
+ export class FixtureManager {
1836
+ private loadedFixtures: Map<string, any[]> = new Map();
1837
+
1838
+ constructor(private dataSource: DataSource) {}
1839
+
1840
+ /**
1841
+ * Load fixtures from a fixture class
1842
+ */
1843
+ async load<T extends ObjectLiteral>(
1844
+ repository: Repository<T>,
1845
+ data: Partial<T>[]
1846
+ ): Promise<T[]> {
1847
+ const entities = data.map((item) => repository.create(item));
1848
+ const saved = await repository.save(entities);
1849
+
1850
+ const key = repository.metadata.name;
1851
+ this.loadedFixtures.set(key, [
1852
+ ...(this.loadedFixtures.get(key) || []),
1853
+ ...saved,
1854
+ ]);
1855
+
1856
+ return saved;
1857
+ }
1858
+
1859
+ /**
1860
+ * Get loaded fixtures by entity name
1861
+ */
1862
+ get<T>(entityName: string): T[] {
1863
+ return (this.loadedFixtures.get(entityName) || []) as T[];
1864
+ }
1865
+
1866
+ /**
1867
+ * Clear all data from specified tables
1868
+ */
1869
+ async clear(...entityNames: string[]): Promise<void> {
1870
+ for (const name of entityNames) {
1871
+ const repository = this.dataSource.getRepository(name);
1872
+ await repository.clear();
1873
+ this.loadedFixtures.delete(name);
1874
+ }
1875
+ }
1876
+
1877
+ /**
1878
+ * Clear all loaded fixtures
1879
+ */
1880
+ async clearAll(): Promise<void> {
1881
+ // Disable foreign key checks temporarily
1882
+ await this.dataSource.query("SET CONSTRAINTS ALL DEFERRED");
1883
+
1884
+ for (const [name] of this.loadedFixtures) {
1885
+ try {
1886
+ const repository = this.dataSource.getRepository(name);
1887
+ await repository.clear();
1888
+ } catch (error) {
1889
+ console.warn(\`Failed to clear \${name}:\`, error);
1890
+ }
1891
+ }
1892
+
1893
+ await this.dataSource.query("SET CONSTRAINTS ALL IMMEDIATE");
1894
+ this.loadedFixtures.clear();
1895
+ }
1896
+
1897
+ /**
1898
+ * Reset database to clean state
1899
+ */
1900
+ async reset(): Promise<void> {
1901
+ await this.clearAll();
1902
+ await this.dataSource.synchronize(true);
1903
+ }
1904
+ }
1905
+
1906
+ /**
1907
+ * Fixture definition interface
1908
+ */
1909
+ export interface Fixture<T = any> {
1910
+ name: string;
1911
+ entity: new () => T;
1912
+ data: () => Partial<T>[];
1913
+ dependencies?: string[];
1914
+ }
1915
+
1916
+ /**
1917
+ * Fixture loader for managing fixture dependencies
1918
+ */
1919
+ export class FixtureLoader {
1920
+ private fixtures: Map<string, Fixture> = new Map();
1921
+ private loaded: Set<string> = new Set();
1922
+
1923
+ register(fixture: Fixture): this {
1924
+ this.fixtures.set(fixture.name, fixture);
1925
+ return this;
1926
+ }
1927
+
1928
+ async load(
1929
+ name: string,
1930
+ manager: FixtureManager,
1931
+ dataSource: DataSource
1932
+ ): Promise<any[]> {
1933
+ if (this.loaded.has(name)) {
1934
+ return manager.get(name);
1935
+ }
1936
+
1937
+ const fixture = this.fixtures.get(name);
1938
+ if (!fixture) {
1939
+ throw new Error(\`Fixture "\${name}" not found\`);
1940
+ }
1941
+
1942
+ // Load dependencies first
1943
+ if (fixture.dependencies) {
1944
+ for (const dep of fixture.dependencies) {
1945
+ await this.load(dep, manager, dataSource);
1946
+ }
1947
+ }
1948
+
1949
+ const repository = dataSource.getRepository(fixture.entity);
1950
+ const result = await manager.load(repository, fixture.data());
1951
+ this.loaded.add(name);
1952
+
1953
+ return result;
1954
+ }
1955
+
1956
+ reset(): void {
1957
+ this.loaded.clear();
1958
+ }
1959
+ }
1960
+ `;
1961
+ await (0, file_utils_1.writeFile)(path.join(fixturesPath, 'fixture.utils.ts'), fixtureUtilsContent);
1962
+ // Example fixtures
1963
+ const exampleFixturesContent = `import { faker } from "@faker-js/faker";
1964
+ import { Fixture } from "./fixture.utils";
1965
+
1966
+ /**
1967
+ * Example fixture definitions
1968
+ * Replace with your actual entities
1969
+ */
1970
+
1971
+ export const usersFixture: Fixture = {
1972
+ name: "users",
1973
+ entity: class User {} as any, // Replace with actual entity
1974
+ data: () => [
1975
+ {
1976
+ id: "user-1",
1977
+ email: "admin@example.com",
1978
+ firstName: "Admin",
1979
+ lastName: "User",
1980
+ role: "admin",
1981
+ isActive: true,
1982
+ },
1983
+ {
1984
+ id: "user-2",
1985
+ email: "user@example.com",
1986
+ firstName: "Regular",
1987
+ lastName: "User",
1988
+ role: "user",
1989
+ isActive: true,
1990
+ },
1991
+ {
1992
+ id: "user-3",
1993
+ email: "inactive@example.com",
1994
+ firstName: "Inactive",
1995
+ lastName: "User",
1996
+ role: "user",
1997
+ isActive: false,
1998
+ },
1999
+ ],
2000
+ };
2001
+
2002
+ export const postsFixture: Fixture = {
2003
+ name: "posts",
2004
+ entity: class Post {} as any, // Replace with actual entity
2005
+ dependencies: ["users"],
2006
+ data: () => [
2007
+ {
2008
+ id: "post-1",
2009
+ title: "First Post",
2010
+ content: "This is the first post content",
2011
+ authorId: "user-1",
2012
+ isPublished: true,
2013
+ },
2014
+ {
2015
+ id: "post-2",
2016
+ title: "Draft Post",
2017
+ content: "This is a draft",
2018
+ authorId: "user-2",
2019
+ isPublished: false,
2020
+ },
2021
+ ],
2022
+ };
2023
+
2024
+ /**
2025
+ * Generate dynamic fixtures with faker
2026
+ */
2027
+ export function generateUsersFixture(count: number): Fixture {
2028
+ return {
2029
+ name: \`users_\${count}\`,
2030
+ entity: class User {} as any,
2031
+ data: () =>
2032
+ Array.from({ length: count }, (_, i) => ({
2033
+ id: faker.string.uuid(),
2034
+ email: faker.internet.email(),
2035
+ firstName: faker.person.firstName(),
2036
+ lastName: faker.person.lastName(),
2037
+ role: i === 0 ? "admin" : "user",
2038
+ isActive: faker.datatype.boolean({ probability: 0.9 }),
2039
+ })),
2040
+ };
2041
+ }
2042
+ `;
2043
+ await (0, file_utils_1.writeFile)(path.join(fixturesPath, 'example.fixtures.ts'), exampleFixturesContent);
2044
+ // Mock utilities
2045
+ const mockUtilsContent = `/**
2046
+ * Mock Utilities for Testing
2047
+ */
2048
+
2049
+ /**
2050
+ * Create a mock repository with common TypeORM methods
2051
+ */
2052
+ export function createMockRepository<T = any>() {
2053
+ return {
2054
+ find: jest.fn().mockResolvedValue([]),
2055
+ findOne: jest.fn().mockResolvedValue(null),
2056
+ findOneBy: jest.fn().mockResolvedValue(null),
2057
+ save: jest.fn().mockImplementation((entity) => Promise.resolve({ id: "mock-id", ...entity })),
2058
+ create: jest.fn().mockImplementation((dto) => dto),
2059
+ update: jest.fn().mockResolvedValue({ affected: 1 }),
2060
+ delete: jest.fn().mockResolvedValue({ affected: 1 }),
2061
+ softDelete: jest.fn().mockResolvedValue({ affected: 1 }),
2062
+ restore: jest.fn().mockResolvedValue({ affected: 1 }),
2063
+ count: jest.fn().mockResolvedValue(0),
2064
+ createQueryBuilder: jest.fn(() => createMockQueryBuilder()),
2065
+ manager: {
2066
+ transaction: jest.fn((cb) => cb({
2067
+ save: jest.fn(),
2068
+ remove: jest.fn(),
2069
+ })),
2070
+ },
2071
+ };
2072
+ }
2073
+
2074
+ /**
2075
+ * Create a mock query builder
2076
+ */
2077
+ export function createMockQueryBuilder<T = any>() {
2078
+ const qb: any = {
2079
+ select: jest.fn().mockReturnThis(),
2080
+ addSelect: jest.fn().mockReturnThis(),
2081
+ where: jest.fn().mockReturnThis(),
2082
+ andWhere: jest.fn().mockReturnThis(),
2083
+ orWhere: jest.fn().mockReturnThis(),
2084
+ orderBy: jest.fn().mockReturnThis(),
2085
+ addOrderBy: jest.fn().mockReturnThis(),
2086
+ skip: jest.fn().mockReturnThis(),
2087
+ take: jest.fn().mockReturnThis(),
2088
+ leftJoin: jest.fn().mockReturnThis(),
2089
+ leftJoinAndSelect: jest.fn().mockReturnThis(),
2090
+ innerJoin: jest.fn().mockReturnThis(),
2091
+ innerJoinAndSelect: jest.fn().mockReturnThis(),
2092
+ groupBy: jest.fn().mockReturnThis(),
2093
+ having: jest.fn().mockReturnThis(),
2094
+ getOne: jest.fn().mockResolvedValue(null),
2095
+ getMany: jest.fn().mockResolvedValue([]),
2096
+ getManyAndCount: jest.fn().mockResolvedValue([[], 0]),
2097
+ getCount: jest.fn().mockResolvedValue(0),
2098
+ getRawOne: jest.fn().mockResolvedValue(null),
2099
+ getRawMany: jest.fn().mockResolvedValue([]),
2100
+ execute: jest.fn().mockResolvedValue({ affected: 1 }),
2101
+ };
2102
+ return qb;
2103
+ }
2104
+
2105
+ /**
2106
+ * Create a mock Prisma client
2107
+ */
2108
+ export function createMockPrismaClient() {
2109
+ const createModelMock = () => ({
2110
+ findUnique: jest.fn().mockResolvedValue(null),
2111
+ findFirst: jest.fn().mockResolvedValue(null),
2112
+ findMany: jest.fn().mockResolvedValue([]),
2113
+ create: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: "mock-id", ...data })),
2114
+ createMany: jest.fn().mockResolvedValue({ count: 0 }),
2115
+ update: jest.fn().mockImplementation(({ data }) => Promise.resolve(data)),
2116
+ updateMany: jest.fn().mockResolvedValue({ count: 0 }),
2117
+ delete: jest.fn().mockResolvedValue({}),
2118
+ deleteMany: jest.fn().mockResolvedValue({ count: 0 }),
2119
+ count: jest.fn().mockResolvedValue(0),
2120
+ aggregate: jest.fn().mockResolvedValue({}),
2121
+ groupBy: jest.fn().mockResolvedValue([]),
2122
+ });
2123
+
2124
+ return {
2125
+ $connect: jest.fn().mockResolvedValue(undefined),
2126
+ $disconnect: jest.fn().mockResolvedValue(undefined),
2127
+ $transaction: jest.fn((cb) => cb({})),
2128
+ // Add model mocks as needed
2129
+ user: createModelMock(),
2130
+ post: createModelMock(),
2131
+ };
2132
+ }
2133
+
2134
+ /**
2135
+ * Create a mock service with all methods as jest functions
2136
+ */
2137
+ export function createMockService<T extends object>(
2138
+ methods: (keyof T)[]
2139
+ ): jest.Mocked<T> {
2140
+ const mock: any = {};
2141
+ for (const method of methods) {
2142
+ mock[method] = jest.fn();
2143
+ }
2144
+ return mock;
2145
+ }
2146
+
2147
+ /**
2148
+ * Create a mock request object
2149
+ */
2150
+ export function createMockRequest(overrides: any = {}) {
2151
+ return {
2152
+ user: { id: "user-1", email: "test@example.com", roles: ["user"] },
2153
+ params: {},
2154
+ query: {},
2155
+ body: {},
2156
+ headers: {},
2157
+ ip: "127.0.0.1",
2158
+ ...overrides,
2159
+ };
2160
+ }
2161
+
2162
+ /**
2163
+ * Create a mock response object
2164
+ */
2165
+ export function createMockResponse() {
2166
+ const res: any = {
2167
+ status: jest.fn().mockReturnThis(),
2168
+ json: jest.fn().mockReturnThis(),
2169
+ send: jest.fn().mockReturnThis(),
2170
+ setHeader: jest.fn().mockReturnThis(),
2171
+ cookie: jest.fn().mockReturnThis(),
2172
+ clearCookie: jest.fn().mockReturnThis(),
2173
+ };
2174
+ return res;
2175
+ }
2176
+
2177
+ /**
2178
+ * Create a mock execution context for guards/interceptors
2179
+ */
2180
+ export function createMockExecutionContext(request: any = {}, response: any = {}) {
2181
+ return {
2182
+ switchToHttp: () => ({
2183
+ getRequest: () => createMockRequest(request),
2184
+ getResponse: () => createMockResponse(),
2185
+ }),
2186
+ getHandler: () => jest.fn(),
2187
+ getClass: () => jest.fn(),
2188
+ getArgs: () => [],
2189
+ getArgByIndex: () => ({}),
2190
+ switchToRpc: () => ({}),
2191
+ switchToWs: () => ({}),
2192
+ getType: () => "http",
2193
+ };
2194
+ }
2195
+ `;
2196
+ await (0, file_utils_1.writeFile)(path.join(mocksPath, 'mock.utils.ts'), mockUtilsContent);
2197
+ // Test setup utilities
2198
+ const testSetupContent = `import { Test, TestingModule } from "@nestjs/testing";
2199
+ import { INestApplication, ValidationPipe } from "@nestjs/common";
2200
+ import { DataSource } from "typeorm";
2201
+
2202
+ /**
2203
+ * Test Setup Utilities
2204
+ */
2205
+
2206
+ /**
2207
+ * Create a testing module with common configuration
2208
+ */
2209
+ export async function createTestingModule(
2210
+ imports: any[] = [],
2211
+ providers: any[] = [],
2212
+ controllers: any[] = []
2213
+ ): Promise<TestingModule> {
2214
+ return Test.createTestingModule({
2215
+ imports,
2216
+ providers,
2217
+ controllers,
2218
+ }).compile();
2219
+ }
2220
+
2221
+ /**
2222
+ * Create and configure a test application
2223
+ */
2224
+ export async function createTestApp(
2225
+ module: TestingModule,
2226
+ options: {
2227
+ validation?: boolean;
2228
+ prefix?: string;
2229
+ } = {}
2230
+ ): Promise<INestApplication> {
2231
+ const app = module.createNestApplication();
2232
+
2233
+ if (options.validation !== false) {
2234
+ app.useGlobalPipes(
2235
+ new ValidationPipe({
2236
+ whitelist: true,
2237
+ transform: true,
2238
+ forbidNonWhitelisted: true,
2239
+ })
2240
+ );
2241
+ }
2242
+
2243
+ if (options.prefix) {
2244
+ app.setGlobalPrefix(options.prefix);
2245
+ }
2246
+
2247
+ await app.init();
2248
+ return app;
2249
+ }
2250
+
2251
+ /**
2252
+ * Clean up test resources
2253
+ */
2254
+ export async function cleanupTest(
2255
+ app?: INestApplication,
2256
+ dataSource?: DataSource
2257
+ ): Promise<void> {
2258
+ if (dataSource?.isInitialized) {
2259
+ await dataSource.destroy();
2260
+ }
2261
+ if (app) {
2262
+ await app.close();
2263
+ }
2264
+ }
2265
+
2266
+ /**
2267
+ * Wait for condition with timeout
2268
+ */
2269
+ export async function waitFor(
2270
+ condition: () => boolean | Promise<boolean>,
2271
+ timeout = 5000,
2272
+ interval = 100
2273
+ ): Promise<void> {
2274
+ const start = Date.now();
2275
+ while (Date.now() - start < timeout) {
2276
+ if (await condition()) return;
2277
+ await new Promise((r) => setTimeout(r, interval));
2278
+ }
2279
+ throw new Error(\`Condition not met within \${timeout}ms\`);
2280
+ }
2281
+
2282
+ /**
2283
+ * Retry async operation
2284
+ */
2285
+ export async function retry<T>(
2286
+ fn: () => Promise<T>,
2287
+ maxAttempts = 3,
2288
+ delay = 100
2289
+ ): Promise<T> {
2290
+ let lastError: Error | undefined;
2291
+ for (let i = 0; i < maxAttempts; i++) {
2292
+ try {
2293
+ return await fn();
2294
+ } catch (error) {
2295
+ lastError = error as Error;
2296
+ if (i < maxAttempts - 1) {
2297
+ await new Promise((r) => setTimeout(r, delay));
2298
+ }
2299
+ }
2300
+ }
2301
+ throw lastError;
2302
+ }
2303
+ `;
2304
+ await (0, file_utils_1.writeFile)(path.join(utilsPath, 'test-setup.utils.ts'), testSetupContent);
2305
+ // Index exports
2306
+ const factoriesIndexContent = `export * from "./base.factory";
2307
+ export * from "./example.factory";
2308
+ export * from "./builder";
2309
+ `;
2310
+ await (0, file_utils_1.writeFile)(path.join(factoriesPath, 'index.ts'), factoriesIndexContent);
2311
+ const fixturesIndexContent = `export * from "./fixture.utils";
2312
+ export * from "./example.fixtures";
2313
+ `;
2314
+ await (0, file_utils_1.writeFile)(path.join(fixturesPath, 'index.ts'), fixturesIndexContent);
2315
+ const mocksIndexContent = `export * from "./mock.utils";
2316
+ `;
2317
+ await (0, file_utils_1.writeFile)(path.join(mocksPath, 'index.ts'), mocksIndexContent);
2318
+ const utilsIndexContent = `export * from "./test-setup.utils";
2319
+ `;
2320
+ await (0, file_utils_1.writeFile)(path.join(utilsPath, 'index.ts'), utilsIndexContent);
2321
+ // Main test index
2322
+ const mainIndexContent = `export * from "./factories";
2323
+ export * from "./fixtures";
2324
+ export * from "./mocks";
2325
+ export * from "./utils";
2326
+ `;
2327
+ await (0, file_utils_1.writeFile)(path.join(testPath, 'index.ts'), mainIndexContent);
2328
+ console.log(chalk_1.default.green(' ✓ Base factory class with state support'));
2329
+ console.log(chalk_1.default.green(' ✓ Example factories (User, Post) with fluent API'));
2330
+ console.log(chalk_1.default.green(' ✓ Test data builder pattern'));
2331
+ console.log(chalk_1.default.green(' ✓ Database fixture manager with dependencies'));
2332
+ console.log(chalk_1.default.green(' ✓ Mock utilities (Repository, QueryBuilder, Prisma, Request/Response)'));
2333
+ console.log(chalk_1.default.green(' ✓ Test setup utilities (createTestApp, cleanup, waitFor, retry)'));
2334
+ }
2335
+ async function applyApiVersioningRecipe(basePath) {
2336
+ const sharedPath = path.join(basePath, 'src/shared');
2337
+ const versioningPath = path.join(sharedPath, 'versioning');
2338
+ await (0, file_utils_1.ensureDir)(versioningPath);
2339
+ await (0, file_utils_1.ensureDir)(path.join(versioningPath, 'decorators'));
2340
+ await (0, file_utils_1.ensureDir)(path.join(versioningPath, 'interceptors'));
2341
+ // Version configuration
2342
+ const versionConfigContent = `/**
2343
+ * API Versioning Configuration
2344
+ *
2345
+ * Supports multiple versioning strategies:
2346
+ * - URI: /api/v1/users, /api/v2/users
2347
+ * - Header: X-API-Version: 1
2348
+ * - Media Type: Accept: application/vnd.api.v1+json
2349
+ * - Query: /api/users?version=1
2350
+ */
2351
+
2352
+ export enum VersioningStrategy {
2353
+ URI = "uri",
2354
+ HEADER = "header",
2355
+ MEDIA_TYPE = "media_type",
2356
+ QUERY = "query",
2357
+ }
2358
+
2359
+ export interface ApiVersion {
2360
+ version: string;
2361
+ deprecatedAt?: Date;
2362
+ sunsetAt?: Date;
2363
+ replacedBy?: string;
2364
+ changelog?: string;
2365
+ }
2366
+
2367
+ export const API_VERSIONS: ApiVersion[] = [
2368
+ {
2369
+ version: "1",
2370
+ deprecatedAt: undefined,
2371
+ sunsetAt: undefined,
2372
+ replacedBy: undefined,
2373
+ },
2374
+ // Add new versions here:
2375
+ // {
2376
+ // version: "2",
2377
+ // deprecatedAt: undefined,
2378
+ // sunsetAt: undefined,
2379
+ // replacedBy: undefined,
2380
+ // },
2381
+ ];
2382
+
2383
+ export const CURRENT_VERSION = "1";
2384
+ export const SUPPORTED_VERSIONS = API_VERSIONS.map((v) => v.version);
2385
+ export const DEFAULT_VERSION = "1";
2386
+
2387
+ /**
2388
+ * Get version info by version number
2389
+ */
2390
+ export function getVersionInfo(version: string): ApiVersion | undefined {
2391
+ return API_VERSIONS.find((v) => v.version === version);
2392
+ }
2393
+
2394
+ /**
2395
+ * Check if a version is deprecated
2396
+ */
2397
+ export function isVersionDeprecated(version: string): boolean {
2398
+ const info = getVersionInfo(version);
2399
+ return info?.deprecatedAt ? new Date() >= info.deprecatedAt : false;
2400
+ }
2401
+
2402
+ /**
2403
+ * Check if a version is sunset (no longer supported)
2404
+ */
2405
+ export function isVersionSunset(version: string): boolean {
2406
+ const info = getVersionInfo(version);
2407
+ return info?.sunsetAt ? new Date() >= info.sunsetAt : false;
2408
+ }
2409
+
2410
+ /**
2411
+ * Get days until sunset for a version
2412
+ */
2413
+ export function getDaysUntilSunset(version: string): number | null {
2414
+ const info = getVersionInfo(version);
2415
+ if (!info?.sunsetAt) return null;
2416
+ const diff = info.sunsetAt.getTime() - Date.now();
2417
+ return Math.ceil(diff / (1000 * 60 * 60 * 24));
2418
+ }
2419
+ `;
2420
+ await (0, file_utils_1.writeFile)(path.join(versioningPath, 'version.config.ts'), versionConfigContent);
2421
+ // Version decorator
2422
+ const versionDecoratorContent = `import { SetMetadata, applyDecorators } from "@nestjs/common";
2423
+ import { ApiHeader, ApiOperation } from "@nestjs/swagger";
2424
+
2425
+ export const API_VERSION_KEY = "api_version";
2426
+ export const DEPRECATED_VERSION_KEY = "deprecated_version";
2427
+ export const MIN_VERSION_KEY = "min_version";
2428
+ export const MAX_VERSION_KEY = "max_version";
2429
+
2430
+ /**
2431
+ * Mark a controller or method as available in specific version(s)
2432
+ * @param versions - Version(s) this endpoint is available in
2433
+ */
2434
+ export const ApiVersion = (...versions: string[]) =>
2435
+ SetMetadata(API_VERSION_KEY, versions);
2436
+
2437
+ /**
2438
+ * Mark an endpoint as deprecated
2439
+ * @param message - Deprecation message
2440
+ * @param replacedBy - The new endpoint/version to use
2441
+ * @param sunsetDate - When this endpoint will be removed
2442
+ */
2443
+ export const DeprecatedVersion = (
2444
+ message: string,
2445
+ replacedBy?: string,
2446
+ sunsetDate?: Date
2447
+ ) =>
2448
+ applyDecorators(
2449
+ SetMetadata(DEPRECATED_VERSION_KEY, { message, replacedBy, sunsetDate }),
2450
+ ApiOperation({
2451
+ deprecated: true,
2452
+ description: \`**DEPRECATED**: \${message}\${replacedBy ? \` Use \${replacedBy} instead.\` : ""}\`,
2453
+ })
2454
+ );
2455
+
2456
+ /**
2457
+ * Require minimum API version
2458
+ */
2459
+ export const MinVersion = (version: string) =>
2460
+ SetMetadata(MIN_VERSION_KEY, version);
2461
+
2462
+ /**
2463
+ * Require maximum API version (useful for sunset endpoints)
2464
+ */
2465
+ export const MaxVersion = (version: string) =>
2466
+ SetMetadata(MAX_VERSION_KEY, version);
2467
+
2468
+ /**
2469
+ * Combined decorator for versioned endpoint with Swagger docs
2470
+ */
2471
+ export const VersionedEndpoint = (version: string, deprecated = false) =>
2472
+ applyDecorators(
2473
+ ApiVersion(version),
2474
+ ApiHeader({
2475
+ name: "X-API-Version",
2476
+ description: \`API Version (current: \${version})\`,
2477
+ required: false,
2478
+ }),
2479
+ ...(deprecated ? [DeprecatedVersion(\`This endpoint is deprecated in v\${version}\`)] : [])
2480
+ );
2481
+ `;
2482
+ await (0, file_utils_1.writeFile)(path.join(versioningPath, 'decorators/version.decorator.ts'), versionDecoratorContent);
2483
+ // Version interceptor
2484
+ const versionInterceptorContent = `import {
2485
+ Injectable,
2486
+ NestInterceptor,
2487
+ ExecutionContext,
2488
+ CallHandler,
2489
+ } from "@nestjs/common";
2490
+ import { Observable } from "rxjs";
2491
+ import { tap } from "rxjs/operators";
2492
+ import { Reflector } from "@nestjs/core";
2493
+ import {
2494
+ DEPRECATED_VERSION_KEY,
2495
+ API_VERSION_KEY,
2496
+ } from "../decorators/version.decorator";
2497
+ import { getVersionInfo, isVersionDeprecated } from "../version.config";
2498
+
2499
+ @Injectable()
2500
+ export class VersionHeaderInterceptor implements NestInterceptor {
2501
+ constructor(private reflector: Reflector) {}
2502
+
2503
+ intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
2504
+ const response = context.switchToHttp().getResponse();
2505
+ const request = context.switchToHttp().getRequest();
2506
+
2507
+ // Get current API version from request
2508
+ const apiVersion = request.apiVersion || request.headers["x-api-version"] || "1";
2509
+
2510
+ // Get version metadata from handler/controller
2511
+ const versions = this.reflector.getAllAndOverride<string[]>(API_VERSION_KEY, [
2512
+ context.getHandler(),
2513
+ context.getClass(),
2514
+ ]);
2515
+
2516
+ const deprecationInfo = this.reflector.getAllAndOverride<any>(
2517
+ DEPRECATED_VERSION_KEY,
2518
+ [context.getHandler(), context.getClass()]
2519
+ );
2520
+
2521
+ return next.handle().pipe(
2522
+ tap(() => {
2523
+ // Always add current version header
2524
+ response.setHeader("X-API-Version", apiVersion);
2525
+
2526
+ // Add supported versions header
2527
+ if (versions) {
2528
+ response.setHeader("X-API-Supported-Versions", versions.join(", "));
2529
+ }
2530
+
2531
+ // Add deprecation headers if applicable
2532
+ if (deprecationInfo || isVersionDeprecated(apiVersion)) {
2533
+ const versionInfo = getVersionInfo(apiVersion);
2534
+
2535
+ response.setHeader("Deprecation", "true");
2536
+
2537
+ if (deprecationInfo?.sunsetDate || versionInfo?.sunsetAt) {
2538
+ const sunsetDate = deprecationInfo?.sunsetDate || versionInfo?.sunsetAt;
2539
+ response.setHeader("Sunset", sunsetDate.toUTCString());
2540
+ }
2541
+
2542
+ if (deprecationInfo?.replacedBy || versionInfo?.replacedBy) {
2543
+ const replacement = deprecationInfo?.replacedBy || versionInfo?.replacedBy;
2544
+ response.setHeader("Link", \`<\${replacement}>; rel="successor-version"\`);
2545
+ }
2546
+
2547
+ // Add warning header for deprecated APIs
2548
+ const message = deprecationInfo?.message || "This API version is deprecated";
2549
+ response.setHeader(
2550
+ "Warning",
2551
+ \`299 - "\${message}"\`
2552
+ );
2553
+ }
2554
+ })
2555
+ );
2556
+ }
2557
+ }
2558
+ `;
2559
+ await (0, file_utils_1.writeFile)(path.join(versioningPath, 'interceptors/version-header.interceptor.ts'), versionInterceptorContent);
2560
+ // Version guard
2561
+ const versionGuardContent = `import {
2562
+ Injectable,
2563
+ CanActivate,
2564
+ ExecutionContext,
2565
+ HttpException,
2566
+ HttpStatus,
2567
+ } from "@nestjs/common";
2568
+ import { Reflector } from "@nestjs/core";
2569
+ import {
2570
+ API_VERSION_KEY,
2571
+ MIN_VERSION_KEY,
2572
+ MAX_VERSION_KEY,
2573
+ } from "../decorators/version.decorator";
2574
+ import {
2575
+ SUPPORTED_VERSIONS,
2576
+ isVersionSunset,
2577
+ getVersionInfo,
2578
+ } from "../version.config";
2579
+
2580
+ @Injectable()
2581
+ export class ApiVersionGuard implements CanActivate {
2582
+ constructor(private reflector: Reflector) {}
2583
+
2584
+ canActivate(context: ExecutionContext): boolean {
2585
+ const request = context.switchToHttp().getRequest();
2586
+
2587
+ // Extract version from various sources
2588
+ const version = this.extractVersion(request);
2589
+ request.apiVersion = version;
2590
+
2591
+ // Check if version is supported
2592
+ if (!SUPPORTED_VERSIONS.includes(version)) {
2593
+ throw new HttpException(
2594
+ {
2595
+ statusCode: HttpStatus.BAD_REQUEST,
2596
+ error: "Unsupported API Version",
2597
+ message: \`API version '\${version}' is not supported. Supported versions: \${SUPPORTED_VERSIONS.join(", ")}\`,
2598
+ supportedVersions: SUPPORTED_VERSIONS,
2599
+ },
2600
+ HttpStatus.BAD_REQUEST
2601
+ );
2602
+ }
2603
+
2604
+ // Check if version is sunset
2605
+ if (isVersionSunset(version)) {
2606
+ const versionInfo = getVersionInfo(version);
2607
+ throw new HttpException(
2608
+ {
2609
+ statusCode: HttpStatus.GONE,
2610
+ error: "API Version Sunset",
2611
+ message: \`API version '\${version}' is no longer supported.\`,
2612
+ sunsetDate: versionInfo?.sunsetAt,
2613
+ replacedBy: versionInfo?.replacedBy,
2614
+ },
2615
+ HttpStatus.GONE
2616
+ );
2617
+ }
2618
+
2619
+ // Check version constraints from decorators
2620
+ const allowedVersions = this.reflector.getAllAndOverride<string[]>(
2621
+ API_VERSION_KEY,
2622
+ [context.getHandler(), context.getClass()]
2623
+ );
2624
+
2625
+ if (allowedVersions && !allowedVersions.includes(version)) {
2626
+ throw new HttpException(
2627
+ {
2628
+ statusCode: HttpStatus.BAD_REQUEST,
2629
+ error: "Version Not Available",
2630
+ message: \`This endpoint is not available in API version '\${version}'. Available in: \${allowedVersions.join(", ")}\`,
2631
+ },
2632
+ HttpStatus.BAD_REQUEST
2633
+ );
2634
+ }
2635
+
2636
+ // Check min version
2637
+ const minVersion = this.reflector.getAllAndOverride<string>(MIN_VERSION_KEY, [
2638
+ context.getHandler(),
2639
+ context.getClass(),
2640
+ ]);
2641
+
2642
+ if (minVersion && this.compareVersions(version, minVersion) < 0) {
2643
+ throw new HttpException(
2644
+ {
2645
+ statusCode: HttpStatus.BAD_REQUEST,
2646
+ error: "Version Too Low",
2647
+ message: \`This endpoint requires API version \${minVersion} or higher.\`,
2648
+ },
2649
+ HttpStatus.BAD_REQUEST
2650
+ );
2651
+ }
2652
+
2653
+ // Check max version
2654
+ const maxVersion = this.reflector.getAllAndOverride<string>(MAX_VERSION_KEY, [
2655
+ context.getHandler(),
2656
+ context.getClass(),
2657
+ ]);
2658
+
2659
+ if (maxVersion && this.compareVersions(version, maxVersion) > 0) {
2660
+ throw new HttpException(
2661
+ {
2662
+ statusCode: HttpStatus.GONE,
2663
+ error: "Endpoint Removed",
2664
+ message: \`This endpoint was removed in API version \${maxVersion}.\`,
2665
+ },
2666
+ HttpStatus.GONE
2667
+ );
2668
+ }
2669
+
2670
+ return true;
2671
+ }
2672
+
2673
+ private extractVersion(request: any): string {
2674
+ // Priority: URL param > Header > Query > Default
2675
+
2676
+ // 1. URL versioning: /api/v1/...
2677
+ const urlMatch = request.url?.match(/\\/v(\\d+)\\//);
2678
+ if (urlMatch) return urlMatch[1];
2679
+
2680
+ // 2. Header versioning: X-API-Version
2681
+ const headerVersion = request.headers["x-api-version"];
2682
+ if (headerVersion) return headerVersion;
2683
+
2684
+ // 3. Accept header versioning: application/vnd.api.v1+json
2685
+ const acceptHeader = request.headers["accept"];
2686
+ if (acceptHeader) {
2687
+ const acceptMatch = acceptHeader.match(/vnd\\.api\\.v(\\d+)/);
2688
+ if (acceptMatch) return acceptMatch[1];
2689
+ }
2690
+
2691
+ // 4. Query parameter: ?version=1
2692
+ if (request.query?.version) return request.query.version;
2693
+
2694
+ // 5. Default version
2695
+ return "1";
2696
+ }
2697
+
2698
+ private compareVersions(a: string, b: string): number {
2699
+ const numA = parseInt(a, 10);
2700
+ const numB = parseInt(b, 10);
2701
+ return numA - numB;
2702
+ }
2703
+ }
2704
+ `;
2705
+ await (0, file_utils_1.writeFile)(path.join(versioningPath, 'guards/version.guard.ts'), versionGuardContent);
2706
+ // Versioning module
2707
+ const versioningModuleContent = `import { Module, Global } from "@nestjs/common";
2708
+ import { APP_GUARD, APP_INTERCEPTOR } from "@nestjs/core";
2709
+ import { ApiVersionGuard } from "./guards/version.guard";
2710
+ import { VersionHeaderInterceptor } from "./interceptors/version-header.interceptor";
2711
+
2712
+ @Global()
2713
+ @Module({
2714
+ providers: [
2715
+ {
2716
+ provide: APP_GUARD,
2717
+ useClass: ApiVersionGuard,
2718
+ },
2719
+ {
2720
+ provide: APP_INTERCEPTOR,
2721
+ useClass: VersionHeaderInterceptor,
2722
+ },
2723
+ ],
2724
+ exports: [],
2725
+ })
2726
+ export class VersioningModule {}
2727
+
2728
+ /**
2729
+ * NestJS built-in versioning configuration helper
2730
+ * Use in main.ts:
2731
+ *
2732
+ * import { VersioningType } from "@nestjs/common";
2733
+ *
2734
+ * app.enableVersioning({
2735
+ * type: VersioningType.URI, // /v1/users
2736
+ * // OR
2737
+ * type: VersioningType.HEADER, // X-API-Version: 1
2738
+ * header: "X-API-Version",
2739
+ * // OR
2740
+ * type: VersioningType.MEDIA_TYPE, // Accept: application/vnd.api.v1+json
2741
+ * key: "v=",
2742
+ * });
2743
+ */
2744
+ export const VERSIONING_CONFIG = {
2745
+ uri: {
2746
+ type: "URI" as const,
2747
+ prefix: "v",
2748
+ },
2749
+ header: {
2750
+ type: "HEADER" as const,
2751
+ header: "X-API-Version",
2752
+ },
2753
+ mediaType: {
2754
+ type: "MEDIA_TYPE" as const,
2755
+ key: "v=",
2756
+ },
2757
+ };
2758
+ `;
2759
+ await (0, file_utils_1.writeFile)(path.join(versioningPath, 'versioning.module.ts'), versioningModuleContent);
2760
+ // Version migration helper
2761
+ const migrationHelperContent = `/**
2762
+ * API Version Migration Helper
2763
+ *
2764
+ * Utilities for migrating between API versions
2765
+ */
2766
+
2767
+ export interface MigrationRule<TFrom, TTo> {
2768
+ fromVersion: string;
2769
+ toVersion: string;
2770
+ transform: (data: TFrom) => TTo;
2771
+ }
2772
+
2773
+ export class VersionMigrator<T = any> {
2774
+ private rules: MigrationRule<any, any>[] = [];
2775
+
2776
+ /**
2777
+ * Register a migration rule
2778
+ */
2779
+ register<TFrom, TTo>(
2780
+ fromVersion: string,
2781
+ toVersion: string,
2782
+ transform: (data: TFrom) => TTo
2783
+ ): this {
2784
+ this.rules.push({ fromVersion, toVersion, transform });
2785
+ return this;
2786
+ }
2787
+
2788
+ /**
2789
+ * Migrate data from one version to another
2790
+ */
2791
+ migrate(data: T, fromVersion: string, toVersion: string): T {
2792
+ if (fromVersion === toVersion) return data;
2793
+
2794
+ const path = this.findMigrationPath(fromVersion, toVersion);
2795
+ if (!path) {
2796
+ throw new Error(
2797
+ \`No migration path found from v\${fromVersion} to v\${toVersion}\`
2798
+ );
2799
+ }
2800
+
2801
+ let result = data;
2802
+ for (const rule of path) {
2803
+ result = rule.transform(result);
2804
+ }
2805
+
2806
+ return result;
2807
+ }
2808
+
2809
+ private findMigrationPath(
2810
+ from: string,
2811
+ to: string
2812
+ ): MigrationRule<any, any>[] | null {
2813
+ // Simple linear path finding
2814
+ const path: MigrationRule<any, any>[] = [];
2815
+ let current = from;
2816
+
2817
+ while (current !== to) {
2818
+ const rule = this.rules.find((r) => r.fromVersion === current);
2819
+ if (!rule) return null;
2820
+ path.push(rule);
2821
+ current = rule.toVersion;
2822
+ }
2823
+
2824
+ return path;
2825
+ }
2826
+ }
2827
+
2828
+ /**
2829
+ * Example usage:
2830
+ *
2831
+ * const userMigrator = new VersionMigrator<UserDto>();
2832
+ *
2833
+ * userMigrator
2834
+ * .register("1", "2", (v1User) => ({
2835
+ * ...v1User,
2836
+ * fullName: \`\${v1User.firstName} \${v1User.lastName}\`,
2837
+ * }))
2838
+ * .register("2", "3", (v2User) => ({
2839
+ * ...v2User,
2840
+ * email: v2User.email.toLowerCase(),
2841
+ * }));
2842
+ *
2843
+ * const v3User = userMigrator.migrate(v1User, "1", "3");
2844
+ */
2845
+
2846
+ /**
2847
+ * Decorator for automatic response transformation based on version
2848
+ */
2849
+ export function TransformForVersion<T>(
2850
+ migrator: VersionMigrator<T>,
2851
+ targetVersion: string
2852
+ ) {
2853
+ return function (
2854
+ target: any,
2855
+ propertyKey: string,
2856
+ descriptor: PropertyDescriptor
2857
+ ) {
2858
+ const originalMethod = descriptor.value;
2859
+
2860
+ descriptor.value = async function (...args: any[]) {
2861
+ const result = await originalMethod.apply(this, args);
2862
+ const request = args.find((arg) => arg?.apiVersion);
2863
+ const currentVersion = request?.apiVersion || "1";
2864
+
2865
+ if (currentVersion !== targetVersion) {
2866
+ return migrator.migrate(result, targetVersion, currentVersion);
2867
+ }
2868
+
2869
+ return result;
2870
+ };
2871
+
2872
+ return descriptor;
2873
+ };
2874
+ }
2875
+ `;
2876
+ await (0, file_utils_1.writeFile)(path.join(versioningPath, 'migration.helper.ts'), migrationHelperContent);
2877
+ // Create guards directory and index
2878
+ await (0, file_utils_1.ensureDir)(path.join(versioningPath, 'guards'));
2879
+ const guardsIndexContent = `export * from "./version.guard";
2880
+ `;
2881
+ await (0, file_utils_1.writeFile)(path.join(versioningPath, 'guards/index.ts'), guardsIndexContent);
2882
+ // Index exports
2883
+ const indexContent = `export * from "./version.config";
2884
+ export * from "./versioning.module";
2885
+ export * from "./migration.helper";
2886
+ export * from "./decorators/version.decorator";
2887
+ export * from "./guards/version.guard";
2888
+ export * from "./interceptors/version-header.interceptor";
2889
+ `;
2890
+ await (0, file_utils_1.writeFile)(path.join(versioningPath, 'index.ts'), indexContent);
2891
+ // Decorators index
2892
+ const decoratorsIndexContent = `export * from "./version.decorator";
2893
+ `;
2894
+ await (0, file_utils_1.writeFile)(path.join(versioningPath, 'decorators/index.ts'), decoratorsIndexContent);
2895
+ // Interceptors index
2896
+ const interceptorsIndexContent = `export * from "./version-header.interceptor";
2897
+ `;
2898
+ await (0, file_utils_1.writeFile)(path.join(versioningPath, 'interceptors/index.ts'), interceptorsIndexContent);
2899
+ console.log(chalk_1.default.green(' ✓ Version configuration (URI/Header/MediaType/Query support)'));
2900
+ console.log(chalk_1.default.green(' ✓ Version decorators (@ApiVersion, @DeprecatedVersion, @MinVersion)'));
2901
+ console.log(chalk_1.default.green(' ✓ Version guard with sunset support'));
2902
+ console.log(chalk_1.default.green(' ✓ Version header interceptor (Deprecation, Sunset, Warning headers)'));
2903
+ console.log(chalk_1.default.green(' ✓ Migration helper for version transformations'));
2904
+ }
2905
+ async function applyHealthRecipe(basePath) {
2906
+ const sharedPath = path.join(basePath, 'src/shared');
2907
+ const healthPath = path.join(sharedPath, 'health');
2908
+ const loggingPath = path.join(sharedPath, 'logging');
2909
+ await (0, file_utils_1.ensureDir)(healthPath);
2910
+ await (0, file_utils_1.ensureDir)(loggingPath);
2911
+ // Health controller
2912
+ const healthControllerContent = `import { Controller, Get } from "@nestjs/common";
2913
+ import {
2914
+ HealthCheck,
2915
+ HealthCheckService,
2916
+ HttpHealthIndicator,
2917
+ TypeOrmHealthIndicator,
2918
+ MemoryHealthIndicator,
2919
+ DiskHealthIndicator,
2920
+ } from "@nestjs/terminus";
2921
+ import { ApiTags, ApiOperation } from "@nestjs/swagger";
2922
+ import { SkipThrottle } from "@nestjs/throttler";
2923
+ import { Public } from "../auth/decorators/public.decorator";
2924
+
2925
+ @ApiTags("Health")
2926
+ @Controller("health")
2927
+ @SkipThrottle()
2928
+ export class HealthController {
2929
+ constructor(
2930
+ private health: HealthCheckService,
2931
+ private http: HttpHealthIndicator,
2932
+ private db: TypeOrmHealthIndicator,
2933
+ private memory: MemoryHealthIndicator,
2934
+ private disk: DiskHealthIndicator
2935
+ ) {}
2936
+
2937
+ @Get()
2938
+ @Public()
2939
+ @HealthCheck()
2940
+ @ApiOperation({ summary: "Basic health check" })
2941
+ check() {
2942
+ return this.health.check([
2943
+ () => this.db.pingCheck("database"),
2944
+ ]);
2945
+ }
2946
+
2947
+ @Get("ready")
2948
+ @Public()
2949
+ @HealthCheck()
2950
+ @ApiOperation({ summary: "Readiness check - is the service ready to receive traffic?" })
2951
+ readiness() {
2952
+ return this.health.check([
2953
+ () => this.db.pingCheck("database"),
2954
+ () => this.memory.checkHeap("memory_heap", 300 * 1024 * 1024), // 300MB
2955
+ ]);
2956
+ }
2957
+
2958
+ @Get("live")
2959
+ @Public()
2960
+ @HealthCheck()
2961
+ @ApiOperation({ summary: "Liveness check - is the service alive?" })
2962
+ liveness() {
2963
+ return this.health.check([
2964
+ () => this.memory.checkRSS("memory_rss", 500 * 1024 * 1024), // 500MB
2965
+ ]);
2966
+ }
2967
+
2968
+ @Get("detailed")
2969
+ @HealthCheck()
2970
+ @ApiOperation({ summary: "Detailed health check with all indicators" })
2971
+ detailed() {
2972
+ return this.health.check([
2973
+ () => this.db.pingCheck("database"),
2974
+ () => this.memory.checkHeap("memory_heap", 300 * 1024 * 1024),
2975
+ () => this.memory.checkRSS("memory_rss", 500 * 1024 * 1024),
2976
+ () =>
2977
+ this.disk.checkStorage("disk", {
2978
+ path: "/",
2979
+ thresholdPercent: 0.9, // 90% threshold
2980
+ }),
2981
+ ]);
2982
+ }
2983
+ }
2984
+ `;
2985
+ await (0, file_utils_1.writeFile)(path.join(healthPath, 'health.controller.ts'), healthControllerContent);
2986
+ // Health module
2987
+ const healthModuleContent = `import { Module } from "@nestjs/common";
2988
+ import { TerminusModule } from "@nestjs/terminus";
2989
+ import { HttpModule } from "@nestjs/axios";
2990
+ import { HealthController } from "./health.controller";
2991
+
2992
+ @Module({
2993
+ imports: [TerminusModule, HttpModule],
2994
+ controllers: [HealthController],
2995
+ })
2996
+ export class HealthModule {}
2997
+ `;
2998
+ await (0, file_utils_1.writeFile)(path.join(healthPath, 'health.module.ts'), healthModuleContent);
2999
+ // Custom health indicator example
3000
+ const customIndicatorContent = `import { Injectable } from "@nestjs/common";
3001
+ import {
3002
+ HealthIndicator,
3003
+ HealthIndicatorResult,
3004
+ HealthCheckError,
3005
+ } from "@nestjs/terminus";
3006
+ import Redis from "ioredis";
3007
+
3008
+ @Injectable()
3009
+ export class RedisHealthIndicator extends HealthIndicator {
3010
+ private redis: Redis;
3011
+
3012
+ constructor() {
3013
+ super();
3014
+ this.redis = new Redis({
3015
+ host: process.env.REDIS_HOST || "localhost",
3016
+ port: parseInt(process.env.REDIS_PORT || "6379"),
3017
+ password: process.env.REDIS_PASSWORD,
3018
+ maxRetriesPerRequest: 1,
3019
+ lazyConnect: true,
3020
+ });
3021
+ }
3022
+
3023
+ async isHealthy(key: string): Promise<HealthIndicatorResult> {
3024
+ try {
3025
+ await this.redis.ping();
3026
+ return this.getStatus(key, true);
3027
+ } catch (error) {
3028
+ throw new HealthCheckError(
3029
+ "Redis check failed",
3030
+ this.getStatus(key, false, { error: (error as Error).message })
3031
+ );
3032
+ }
3033
+ }
3034
+ }
3035
+
3036
+ @Injectable()
3037
+ export class ExternalServiceHealthIndicator extends HealthIndicator {
3038
+ async isHealthy(key: string, url: string): Promise<HealthIndicatorResult> {
3039
+ try {
3040
+ const start = Date.now();
3041
+ const response = await fetch(url, {
3042
+ method: "HEAD",
3043
+ signal: AbortSignal.timeout(5000),
3044
+ });
3045
+ const latency = Date.now() - start;
3046
+
3047
+ if (response.ok) {
3048
+ return this.getStatus(key, true, { latency: \`\${latency}ms\` });
3049
+ }
3050
+
3051
+ throw new Error(\`HTTP \${response.status}\`);
3052
+ } catch (error) {
3053
+ throw new HealthCheckError(
3054
+ \`\${key} check failed\`,
3055
+ this.getStatus(key, false, { error: (error as Error).message })
3056
+ );
3057
+ }
3058
+ }
3059
+ }
3060
+ `;
3061
+ await (0, file_utils_1.writeFile)(path.join(healthPath, 'custom.indicators.ts'), customIndicatorContent);
3062
+ // Logging configuration
3063
+ const loggingConfigContent = `import { Params } from "nestjs-pino";
3064
+
3065
+ export const loggerConfig: Params = {
3066
+ pinoHttp: {
3067
+ level: process.env.LOG_LEVEL || "info",
3068
+ transport:
3069
+ process.env.NODE_ENV !== "production"
3070
+ ? {
3071
+ target: "pino-pretty",
3072
+ options: {
3073
+ colorize: true,
3074
+ singleLine: true,
3075
+ translateTime: "SYS:standard",
3076
+ ignore: "pid,hostname",
3077
+ },
3078
+ }
3079
+ : undefined,
3080
+ autoLogging: {
3081
+ ignore: (req) => {
3082
+ // Don't log health check requests
3083
+ return req.url?.includes("/health") || false;
3084
+ },
3085
+ },
3086
+ redact: {
3087
+ paths: [
3088
+ "req.headers.authorization",
3089
+ "req.headers.cookie",
3090
+ "res.headers['set-cookie']",
3091
+ "body.password",
3092
+ "body.token",
3093
+ "body.secret",
3094
+ ],
3095
+ censor: "[REDACTED]",
3096
+ },
3097
+ customProps: (req) => ({
3098
+ requestId: req.id,
3099
+ userAgent: req.headers["user-agent"],
3100
+ }),
3101
+ customLogLevel: (req, res, err) => {
3102
+ if (res.statusCode >= 500 || err) return "error";
3103
+ if (res.statusCode >= 400) return "warn";
3104
+ return "info";
3105
+ },
3106
+ customSuccessMessage: (req, res) => {
3107
+ return \`\${req.method} \${req.url} \${res.statusCode}\`;
3108
+ },
3109
+ customErrorMessage: (req, res, err) => {
3110
+ return \`\${req.method} \${req.url} failed: \${err.message}\`;
3111
+ },
3112
+ },
3113
+ };
3114
+ `;
3115
+ await (0, file_utils_1.writeFile)(path.join(loggingPath, 'logger.config.ts'), loggingConfigContent);
3116
+ // Logging module
3117
+ const loggingModuleContent = `import { Module } from "@nestjs/common";
3118
+ import { LoggerModule as PinoLoggerModule } from "nestjs-pino";
3119
+ import { loggerConfig } from "./logger.config";
3120
+
3121
+ @Module({
3122
+ imports: [PinoLoggerModule.forRoot(loggerConfig)],
3123
+ exports: [PinoLoggerModule],
3124
+ })
3125
+ export class LoggingModule {}
3126
+ `;
3127
+ await (0, file_utils_1.writeFile)(path.join(loggingPath, 'logging.module.ts'), loggingModuleContent);
3128
+ // Request context
3129
+ const requestContextContent = `import { Injectable, NestMiddleware } from "@nestjs/common";
3130
+ import { Request, Response, NextFunction } from "express";
3131
+ import { v4 as uuid } from "uuid";
3132
+ import { AsyncLocalStorage } from "async_hooks";
3133
+
3134
+ export interface RequestContext {
3135
+ requestId: string;
3136
+ userId?: string;
3137
+ startTime: number;
3138
+ path: string;
3139
+ method: string;
3140
+ }
3141
+
3142
+ export const requestContextStorage = new AsyncLocalStorage<RequestContext>();
3143
+
3144
+ @Injectable()
3145
+ export class RequestContextMiddleware implements NestMiddleware {
3146
+ use(req: Request, res: Response, next: NextFunction) {
3147
+ const requestId = (req.headers["x-request-id"] as string) || uuid();
3148
+ const userId = (req as any).user?.id;
3149
+
3150
+ const context: RequestContext = {
3151
+ requestId,
3152
+ userId,
3153
+ startTime: Date.now(),
3154
+ path: req.path,
3155
+ method: req.method,
3156
+ };
3157
+
3158
+ // Set request ID header for response
3159
+ res.setHeader("X-Request-Id", requestId);
3160
+
3161
+ requestContextStorage.run(context, () => {
3162
+ next();
3163
+ });
3164
+ }
3165
+ }
3166
+
3167
+ /**
3168
+ * Get current request context
3169
+ */
3170
+ export function getRequestContext(): RequestContext | undefined {
3171
+ return requestContextStorage.getStore();
3172
+ }
3173
+
3174
+ /**
3175
+ * Get current request ID
3176
+ */
3177
+ export function getRequestId(): string | undefined {
3178
+ return getRequestContext()?.requestId;
3179
+ }
3180
+ `;
3181
+ await (0, file_utils_1.writeFile)(path.join(loggingPath, 'request-context.ts'), requestContextContent);
3182
+ // Metrics utility
3183
+ const metricsContent = `import { Injectable, Logger } from "@nestjs/common";
3184
+
3185
+ export interface MetricData {
3186
+ name: string;
3187
+ value: number;
3188
+ tags?: Record<string, string>;
3189
+ timestamp?: Date;
3190
+ }
3191
+
3192
+ @Injectable()
3193
+ export class MetricsService {
3194
+ private readonly logger = new Logger(MetricsService.name);
3195
+ private metrics: Map<string, number[]> = new Map();
3196
+
3197
+ /**
3198
+ * Record a metric value
3199
+ */
3200
+ record(name: string, value: number, tags?: Record<string, string>): void {
3201
+ const key = this.getKey(name, tags);
3202
+
3203
+ if (!this.metrics.has(key)) {
3204
+ this.metrics.set(key, []);
3205
+ }
3206
+
3207
+ const values = this.metrics.get(key)!;
3208
+ values.push(value);
3209
+
3210
+ // Keep only last 1000 values
3211
+ if (values.length > 1000) {
3212
+ values.shift();
3213
+ }
3214
+ }
3215
+
3216
+ /**
3217
+ * Increment a counter
3218
+ */
3219
+ increment(name: string, tags?: Record<string, string>): void {
3220
+ this.record(name, 1, tags);
3221
+ }
3222
+
3223
+ /**
3224
+ * Record timing in milliseconds
3225
+ */
3226
+ timing(name: string, duration: number, tags?: Record<string, string>): void {
3227
+ this.record(\`\${name}.timing\`, duration, tags);
3228
+ }
3229
+
3230
+ /**
3231
+ * Measure execution time of a function
3232
+ */
3233
+ async measure<T>(
3234
+ name: string,
3235
+ fn: () => Promise<T>,
3236
+ tags?: Record<string, string>
3237
+ ): Promise<T> {
3238
+ const start = Date.now();
3239
+ try {
3240
+ const result = await fn();
3241
+ this.timing(name, Date.now() - start, { ...tags, status: "success" });
3242
+ return result;
3243
+ } catch (error) {
3244
+ this.timing(name, Date.now() - start, { ...tags, status: "error" });
3245
+ throw error;
3246
+ }
3247
+ }
3248
+
3249
+ /**
3250
+ * Get metric statistics
3251
+ */
3252
+ getStats(name: string, tags?: Record<string, string>): {
3253
+ count: number;
3254
+ min: number;
3255
+ max: number;
3256
+ avg: number;
3257
+ p50: number;
3258
+ p95: number;
3259
+ p99: number;
3260
+ } | null {
3261
+ const key = this.getKey(name, tags);
3262
+ const values = this.metrics.get(key);
3263
+
3264
+ if (!values || values.length === 0) return null;
3265
+
3266
+ const sorted = [...values].sort((a, b) => a - b);
3267
+ const count = sorted.length;
3268
+
3269
+ return {
3270
+ count,
3271
+ min: sorted[0],
3272
+ max: sorted[count - 1],
3273
+ avg: sorted.reduce((a, b) => a + b, 0) / count,
3274
+ p50: sorted[Math.floor(count * 0.5)],
3275
+ p95: sorted[Math.floor(count * 0.95)],
3276
+ p99: sorted[Math.floor(count * 0.99)],
3277
+ };
3278
+ }
3279
+
3280
+ /**
3281
+ * Get all metrics
3282
+ */
3283
+ getAllMetrics(): Record<string, any> {
3284
+ const result: Record<string, any> = {};
3285
+
3286
+ for (const [key] of this.metrics) {
3287
+ result[key] = this.getStats(key);
3288
+ }
3289
+
3290
+ return result;
3291
+ }
3292
+
3293
+ private getKey(name: string, tags?: Record<string, string>): string {
3294
+ if (!tags) return name;
3295
+ const tagStr = Object.entries(tags)
3296
+ .sort(([a], [b]) => a.localeCompare(b))
3297
+ .map(([k, v]) => \`\${k}=\${v}\`)
3298
+ .join(",");
3299
+ return \`\${name}{\${tagStr}}\`;
3300
+ }
3301
+ }
3302
+ `;
3303
+ await (0, file_utils_1.writeFile)(path.join(loggingPath, 'metrics.service.ts'), metricsContent);
3304
+ // Index exports
3305
+ const healthIndexContent = `export * from "./health.controller";
3306
+ export * from "./health.module";
3307
+ export * from "./custom.indicators";
3308
+ `;
3309
+ await (0, file_utils_1.writeFile)(path.join(healthPath, 'index.ts'), healthIndexContent);
3310
+ const loggingIndexContent = `export * from "./logger.config";
3311
+ export * from "./logging.module";
3312
+ export * from "./request-context";
3313
+ export * from "./metrics.service";
3314
+ `;
3315
+ await (0, file_utils_1.writeFile)(path.join(loggingPath, 'index.ts'), loggingIndexContent);
3316
+ console.log(chalk_1.default.green(' ✓ Health controller (/health, /health/ready, /health/live)'));
3317
+ console.log(chalk_1.default.green(' ✓ Custom health indicators (Redis, External Services)'));
3318
+ console.log(chalk_1.default.green(' ✓ Pino logging with redaction'));
3319
+ console.log(chalk_1.default.green(' ✓ Request context with AsyncLocalStorage'));
3320
+ console.log(chalk_1.default.green(' ✓ Metrics service with percentiles'));
3321
+ }
3322
+ async function applyRateLimitingRecipe(basePath) {
3323
+ const sharedPath = path.join(basePath, 'src/shared');
3324
+ const rateLimitPath = path.join(sharedPath, 'rate-limit');
3325
+ await (0, file_utils_1.ensureDir)(rateLimitPath);
3326
+ await (0, file_utils_1.ensureDir)(path.join(rateLimitPath, 'guards'));
3327
+ await (0, file_utils_1.ensureDir)(path.join(rateLimitPath, 'decorators'));
3328
+ // Custom throttler decorator
3329
+ const throttleDecoratorContent = `import { SetMetadata, applyDecorators } from "@nestjs/common";
3330
+ import { Throttle, SkipThrottle } from "@nestjs/throttler";
3331
+
3332
+ export const RATE_LIMIT_KEY = "rate_limit_config";
3333
+
3334
+ export interface RateLimitConfig {
3335
+ ttl: number; // Time window in seconds
3336
+ limit: number; // Max requests in time window
3337
+ blockDuration?: number; // How long to block after exceeding limit (seconds)
3338
+ }
3339
+
3340
+ /**
3341
+ * Apply rate limiting to a route
3342
+ * @param limit - Max requests allowed
3343
+ * @param ttl - Time window in seconds (default: 60)
3344
+ */
3345
+ export const RateLimit = (limit: number, ttl: number = 60) =>
3346
+ applyDecorators(
3347
+ Throttle({ default: { limit, ttl: ttl * 1000 } }),
3348
+ SetMetadata(RATE_LIMIT_KEY, { limit, ttl })
3349
+ );
3350
+
3351
+ /**
3352
+ * Skip rate limiting for this route
3353
+ */
3354
+ export { SkipThrottle };
3355
+
3356
+ /**
3357
+ * Stricter rate limit for sensitive operations
3358
+ */
3359
+ export const StrictRateLimit = () => RateLimit(5, 60);
3360
+
3361
+ /**
3362
+ * Relaxed rate limit for public endpoints
3363
+ */
3364
+ export const RelaxedRateLimit = () => RateLimit(100, 60);
3365
+
3366
+ /**
3367
+ * Rate limit by user ID instead of IP
3368
+ */
3369
+ export const RATE_LIMIT_BY_USER = "rate_limit_by_user";
3370
+ export const RateLimitByUser = () => SetMetadata(RATE_LIMIT_BY_USER, true);
3371
+ `;
3372
+ await (0, file_utils_1.writeFile)(path.join(rateLimitPath, 'decorators/throttle.decorator.ts'), throttleDecoratorContent);
3373
+ // Custom throttler guard
3374
+ const throttlerGuardContent = `import { Injectable, ExecutionContext } from "@nestjs/common";
3375
+ import { ThrottlerGuard, ThrottlerException } from "@nestjs/throttler";
3376
+ import { Reflector } from "@nestjs/core";
3377
+ import { RATE_LIMIT_BY_USER } from "../decorators/throttle.decorator";
3378
+
3379
+ @Injectable()
3380
+ export class CustomThrottlerGuard extends ThrottlerGuard {
3381
+ constructor(
3382
+ options: any,
3383
+ storageService: any,
3384
+ private readonly reflector: Reflector
3385
+ ) {
3386
+ super(options, storageService, reflector);
3387
+ }
3388
+
3389
+ /**
3390
+ * Generate tracking key based on IP or user ID
3391
+ */
3392
+ protected async getTracker(req: Record<string, any>): Promise<string> {
3393
+ const byUser = this.reflector.get<boolean>(
3394
+ RATE_LIMIT_BY_USER,
3395
+ this.context?.getHandler()
3396
+ );
3397
+
3398
+ if (byUser && req.user?.id) {
3399
+ return \`user_\${req.user.id}\`;
3400
+ }
3401
+
3402
+ // Use X-Forwarded-For if behind proxy, otherwise use IP
3403
+ const forwarded = req.headers["x-forwarded-for"];
3404
+ const ip = forwarded
3405
+ ? (Array.isArray(forwarded) ? forwarded[0] : forwarded.split(",")[0])
3406
+ : req.ip || req.connection?.remoteAddress;
3407
+
3408
+ return \`ip_\${ip}\`;
3409
+ }
3410
+
3411
+ private context?: ExecutionContext;
3412
+
3413
+ async canActivate(context: ExecutionContext): Promise<boolean> {
3414
+ this.context = context;
3415
+ return super.canActivate(context);
3416
+ }
3417
+
3418
+ /**
3419
+ * Custom error response
3420
+ */
3421
+ protected throwThrottlingException(context: ExecutionContext): void {
3422
+ const req = context.switchToHttp().getRequest();
3423
+ const res = context.switchToHttp().getResponse();
3424
+
3425
+ // Add retry-after header
3426
+ const retryAfter = 60; // Default retry after 60 seconds
3427
+ res.header("Retry-After", retryAfter.toString());
3428
+ res.header("X-RateLimit-Reset", new Date(Date.now() + retryAfter * 1000).toISOString());
3429
+
3430
+ throw new ThrottlerException("Too many requests. Please try again later.");
3431
+ }
3432
+ }
3433
+ `;
3434
+ await (0, file_utils_1.writeFile)(path.join(rateLimitPath, 'guards/throttler.guard.ts'), throttlerGuardContent);
3435
+ // Redis throttler storage
3436
+ const redisStorageContent = `import { Injectable, OnModuleDestroy } from "@nestjs/common";
3437
+ import { ThrottlerStorage } from "@nestjs/throttler";
3438
+ import Redis from "ioredis";
3439
+
3440
+ export interface ThrottlerStorageRecord {
3441
+ totalHits: number;
3442
+ timeToExpire: number;
3443
+ isBlocked: boolean;
3444
+ timeToBlockExpire: number;
3445
+ }
3446
+
3447
+ @Injectable()
3448
+ export class RedisThrottlerStorage implements ThrottlerStorage, OnModuleDestroy {
3449
+ private redis: Redis;
3450
+ private prefix = "throttle:";
3451
+
3452
+ constructor() {
3453
+ this.redis = new Redis({
3454
+ host: process.env.REDIS_HOST || "localhost",
3455
+ port: parseInt(process.env.REDIS_PORT || "6379"),
3456
+ password: process.env.REDIS_PASSWORD,
3457
+ keyPrefix: this.prefix,
3458
+ });
3459
+ }
3460
+
3461
+ async onModuleDestroy() {
3462
+ await this.redis.quit();
3463
+ }
3464
+
3465
+ async increment(
3466
+ key: string,
3467
+ ttl: number,
3468
+ limit: number,
3469
+ blockDuration: number,
3470
+ throttlerName: string
3471
+ ): Promise<ThrottlerStorageRecord> {
3472
+ const fullKey = \`\${throttlerName}:\${key}\`;
3473
+
3474
+ // Check if blocked
3475
+ const blockedUntil = await this.redis.get(\`blocked:\${fullKey}\`);
3476
+ if (blockedUntil) {
3477
+ const timeToBlockExpire = parseInt(blockedUntil) - Date.now();
3478
+ if (timeToBlockExpire > 0) {
3479
+ return {
3480
+ totalHits: limit + 1,
3481
+ timeToExpire: 0,
3482
+ isBlocked: true,
3483
+ timeToBlockExpire,
3484
+ };
3485
+ }
3486
+ }
3487
+
3488
+ // Increment counter
3489
+ const multi = this.redis.multi();
3490
+ multi.incr(fullKey);
3491
+ multi.pttl(fullKey);
3492
+
3493
+ const results = await multi.exec();
3494
+ const totalHits = results?.[0]?.[1] as number || 1;
3495
+ let timeToExpire = results?.[1]?.[1] as number || -1;
3496
+
3497
+ // Set TTL if this is a new key
3498
+ if (timeToExpire === -1) {
3499
+ await this.redis.pexpire(fullKey, ttl);
3500
+ timeToExpire = ttl;
3501
+ }
3502
+
3503
+ // Block if limit exceeded
3504
+ let isBlocked = false;
3505
+ let timeToBlockExpire = 0;
3506
+
3507
+ if (totalHits > limit && blockDuration > 0) {
3508
+ const blockUntil = Date.now() + blockDuration;
3509
+ await this.redis.set(\`blocked:\${fullKey}\`, blockUntil.toString(), "PX", blockDuration);
3510
+ isBlocked = true;
3511
+ timeToBlockExpire = blockDuration;
3512
+ }
3513
+
3514
+ return {
3515
+ totalHits,
3516
+ timeToExpire,
3517
+ isBlocked,
3518
+ timeToBlockExpire,
3519
+ };
3520
+ }
3521
+
3522
+ /**
3523
+ * Get current rate limit status for a key
3524
+ */
3525
+ async getStatus(key: string, throttlerName: string): Promise<{ hits: number; ttl: number } | null> {
3526
+ const fullKey = \`\${throttlerName}:\${key}\`;
3527
+ const [hits, ttl] = await Promise.all([
3528
+ this.redis.get(fullKey),
3529
+ this.redis.pttl(fullKey),
3530
+ ]);
3531
+
3532
+ if (!hits) return null;
3533
+
3534
+ return {
3535
+ hits: parseInt(hits),
3536
+ ttl: Math.max(0, ttl),
3537
+ };
3538
+ }
3539
+
3540
+ /**
3541
+ * Reset rate limit for a key
3542
+ */
3543
+ async reset(key: string, throttlerName: string): Promise<void> {
3544
+ const fullKey = \`\${throttlerName}:\${key}\`;
3545
+ await this.redis.del(fullKey, \`blocked:\${fullKey}\`);
3546
+ }
3547
+ }
3548
+ `;
3549
+ await (0, file_utils_1.writeFile)(path.join(rateLimitPath, 'redis-throttler.storage.ts'), redisStorageContent);
3550
+ // Rate limit module
3551
+ const moduleContent = `import { Module, Global } from "@nestjs/common";
3552
+ import { ThrottlerModule } from "@nestjs/throttler";
3553
+ import { APP_GUARD } from "@nestjs/core";
3554
+ import { CustomThrottlerGuard } from "./guards/throttler.guard";
3555
+ import { RedisThrottlerStorage } from "./redis-throttler.storage";
3556
+
3557
+ @Global()
3558
+ @Module({
3559
+ imports: [
3560
+ ThrottlerModule.forRoot({
3561
+ throttlers: [
3562
+ {
3563
+ name: "default",
3564
+ ttl: parseInt(process.env.THROTTLE_TTL || "60") * 1000,
3565
+ limit: parseInt(process.env.THROTTLE_LIMIT || "100"),
3566
+ },
3567
+ {
3568
+ name: "strict",
3569
+ ttl: 60000,
3570
+ limit: 5,
3571
+ },
3572
+ {
3573
+ name: "auth",
3574
+ ttl: 300000, // 5 minutes
3575
+ limit: 5, // 5 attempts
3576
+ },
3577
+ ],
3578
+ storage: new RedisThrottlerStorage(),
3579
+ }),
3580
+ ],
3581
+ providers: [
3582
+ {
3583
+ provide: APP_GUARD,
3584
+ useClass: CustomThrottlerGuard,
3585
+ },
3586
+ ],
3587
+ exports: [ThrottlerModule],
3588
+ })
3589
+ export class RateLimitModule {}
3590
+ `;
3591
+ await (0, file_utils_1.writeFile)(path.join(rateLimitPath, 'rate-limit.module.ts'), moduleContent);
3592
+ // Index exports
3593
+ const indexContent = `export * from "./decorators/throttle.decorator";
3594
+ export * from "./guards/throttler.guard";
3595
+ export * from "./redis-throttler.storage";
3596
+ export * from "./rate-limit.module";
3597
+ `;
3598
+ await (0, file_utils_1.writeFile)(path.join(rateLimitPath, 'index.ts'), indexContent);
3599
+ // Decorators index
3600
+ const decoratorsIndexContent = `export * from "./throttle.decorator";
3601
+ `;
3602
+ await (0, file_utils_1.writeFile)(path.join(rateLimitPath, 'decorators/index.ts'), decoratorsIndexContent);
3603
+ // Guards index
3604
+ const guardsIndexContent = `export * from "./throttler.guard";
3605
+ `;
3606
+ await (0, file_utils_1.writeFile)(path.join(rateLimitPath, 'guards/index.ts'), guardsIndexContent);
3607
+ console.log(chalk_1.default.green(' ✓ Rate limit decorators (@RateLimit, @StrictRateLimit, @RateLimitByUser)'));
3608
+ console.log(chalk_1.default.green(' ✓ Custom throttler guard with IP/User tracking'));
3609
+ console.log(chalk_1.default.green(' ✓ Redis-based throttler storage'));
3610
+ console.log(chalk_1.default.green(' ✓ Rate limit module'));
3611
+ }
3612
+ async function applyFilteringRecipe(basePath) {
3613
+ const sharedPath = path.join(basePath, 'src/shared');
3614
+ const filterPath = path.join(sharedPath, 'filtering');
3615
+ await (0, file_utils_1.ensureDir)(filterPath);
3616
+ // Filter operators and types
3617
+ const filterTypesContent = `export type FilterOperator =
3618
+ | "eq" // Equal
3619
+ | "ne" // Not equal
3620
+ | "gt" // Greater than
3621
+ | "gte" // Greater than or equal
3622
+ | "lt" // Less than
3623
+ | "lte" // Less than or equal
3624
+ | "in" // In array
3625
+ | "nin" // Not in array
3626
+ | "like" // Contains (case insensitive)
3627
+ | "ilike" // Contains (PostgreSQL)
3628
+ | "between" // Between two values
3629
+ | "isNull" // Is null
3630
+ | "isNotNull"; // Is not null
3631
+
3632
+ export interface FilterCondition {
3633
+ field: string;
3634
+ operator: FilterOperator;
3635
+ value?: any;
3636
+ }
3637
+
3638
+ export interface QueryOptions {
3639
+ page?: number;
3640
+ limit?: number;
3641
+ sortBy?: string;
3642
+ sortOrder?: "ASC" | "DESC";
3643
+ search?: string;
3644
+ searchFields?: string[];
3645
+ filters?: Record<string, any>;
3646
+ }
3647
+
3648
+ export interface PaginatedResult<T> {
3649
+ items: T[];
3650
+ meta: {
3651
+ total: number;
3652
+ page: number;
3653
+ limit: number;
3654
+ totalPages: number;
3655
+ hasNextPage: boolean;
3656
+ hasPreviousPage: boolean;
3657
+ };
3658
+ }
3659
+ `;
3660
+ await (0, file_utils_1.writeFile)(path.join(filterPath, 'filter.types.ts'), filterTypesContent);
3661
+ // TypeORM Query Builder
3662
+ const typeormBuilderContent = `import { SelectQueryBuilder, Brackets } from "typeorm";
3663
+ import { FilterCondition, QueryOptions, PaginatedResult } from "./filter.types";
3664
+
3665
+ /**
3666
+ * Parse filter parameters from query string
3667
+ * Supports format: field__operator=value (e.g., age__gte=18, name__like=john)
3668
+ */
3669
+ export function parseFilters(query: Record<string, any>): FilterCondition[] {
3670
+ const conditions: FilterCondition[] = [];
3671
+ const operatorMap: Record<string, string> = {
3672
+ eq: "eq", ne: "ne", gt: "gt", gte: "gte", lt: "lt", lte: "lte",
3673
+ in: "in", nin: "nin", like: "like", ilike: "ilike",
3674
+ between: "between", isNull: "isNull", isNotNull: "isNotNull",
3675
+ };
3676
+
3677
+ for (const [key, value] of Object.entries(query)) {
3678
+ if (value === undefined || value === null || value === "") continue;
3679
+ if (["page", "limit", "sortBy", "sortOrder", "search", "searchFields"].includes(key)) continue;
3680
+
3681
+ const parts = key.split("__");
3682
+ const field = parts[0];
3683
+ const operatorKey = parts[1] || "eq";
3684
+ const operator = operatorMap[operatorKey] || "eq";
3685
+
3686
+ conditions.push({ field, operator: operator as any, value });
3687
+ }
3688
+
3689
+ return conditions;
3690
+ }
3691
+
3692
+ /**
3693
+ * Apply filters to TypeORM QueryBuilder
3694
+ */
3695
+ export function applyFilters<T>(
3696
+ qb: SelectQueryBuilder<T>,
3697
+ conditions: FilterCondition[],
3698
+ alias: string
3699
+ ): SelectQueryBuilder<T> {
3700
+ for (const { field, operator, value } of conditions) {
3701
+ const paramKey = \`\${field}_\${Date.now()}_\${Math.random().toString(36).slice(2)}\`;
3702
+ const col = \`\${alias}.\${field}\`;
3703
+
3704
+ switch (operator) {
3705
+ case "eq":
3706
+ qb.andWhere(\`\${col} = :\${paramKey}\`, { [paramKey]: value });
3707
+ break;
3708
+ case "ne":
3709
+ qb.andWhere(\`\${col} != :\${paramKey}\`, { [paramKey]: value });
3710
+ break;
3711
+ case "gt":
3712
+ qb.andWhere(\`\${col} > :\${paramKey}\`, { [paramKey]: value });
3713
+ break;
3714
+ case "gte":
3715
+ qb.andWhere(\`\${col} >= :\${paramKey}\`, { [paramKey]: value });
3716
+ break;
3717
+ case "lt":
3718
+ qb.andWhere(\`\${col} < :\${paramKey}\`, { [paramKey]: value });
3719
+ break;
3720
+ case "lte":
3721
+ qb.andWhere(\`\${col} <= :\${paramKey}\`, { [paramKey]: value });
3722
+ break;
3723
+ case "in":
3724
+ qb.andWhere(\`\${col} IN (:...\${paramKey})\`, {
3725
+ [paramKey]: Array.isArray(value) ? value : value.split(",")
3726
+ });
3727
+ break;
3728
+ case "nin":
3729
+ qb.andWhere(\`\${col} NOT IN (:...\${paramKey})\`, {
3730
+ [paramKey]: Array.isArray(value) ? value : value.split(",")
3731
+ });
3732
+ break;
3733
+ case "like":
3734
+ qb.andWhere(\`LOWER(\${col}) LIKE LOWER(:\${paramKey})\`, { [paramKey]: \`%\${value}%\` });
3735
+ break;
3736
+ case "ilike":
3737
+ qb.andWhere(\`\${col} ILIKE :\${paramKey}\`, { [paramKey]: \`%\${value}%\` });
3738
+ break;
3739
+ case "between":
3740
+ const [min, max] = Array.isArray(value) ? value : value.split(",");
3741
+ qb.andWhere(\`\${col} BETWEEN :\${paramKey}_min AND :\${paramKey}_max\`, {
3742
+ [\`\${paramKey}_min\`]: min, [\`\${paramKey}_max\`]: max
3743
+ });
3744
+ break;
3745
+ case "isNull":
3746
+ qb.andWhere(\`\${col} IS NULL\`);
3747
+ break;
3748
+ case "isNotNull":
3749
+ qb.andWhere(\`\${col} IS NOT NULL\`);
3750
+ break;
3751
+ }
3752
+ }
3753
+ return qb;
3754
+ }
3755
+
3756
+ /**
3757
+ * Apply search across multiple fields
3758
+ */
3759
+ export function applySearch<T>(
3760
+ qb: SelectQueryBuilder<T>,
3761
+ search: string,
3762
+ fields: string[],
3763
+ alias: string
3764
+ ): SelectQueryBuilder<T> {
3765
+ if (!search || fields.length === 0) return qb;
3766
+
3767
+ qb.andWhere(new Brackets((sub) => {
3768
+ for (const field of fields) {
3769
+ sub.orWhere(\`LOWER(\${alias}.\${field}) LIKE LOWER(:search)\`, { search: \`%\${search}%\` });
3770
+ }
3771
+ }));
3772
+
3773
+ return qb;
3774
+ }
3775
+
3776
+ /**
3777
+ * Execute filtered, paginated query
3778
+ */
3779
+ export async function executeQuery<T>(
3780
+ qb: SelectQueryBuilder<T>,
3781
+ options: QueryOptions,
3782
+ alias: string,
3783
+ defaultSearchFields: string[] = []
3784
+ ): Promise<PaginatedResult<T>> {
3785
+ const { page = 1, limit = 10, sortBy = "createdAt", sortOrder = "DESC" } = options;
3786
+
3787
+ // Apply filters
3788
+ if (options.filters) {
3789
+ applyFilters(qb, parseFilters(options.filters), alias);
3790
+ }
3791
+
3792
+ // Apply search
3793
+ if (options.search) {
3794
+ applySearch(qb, options.search, options.searchFields || defaultSearchFields, alias);
3795
+ }
3796
+
3797
+ // Get total before pagination
3798
+ const total = await qb.getCount();
3799
+
3800
+ // Apply pagination
3801
+ qb.skip((page - 1) * limit).take(limit).orderBy(\`\${alias}.\${sortBy}\`, sortOrder);
3802
+
3803
+ const items = await qb.getMany();
3804
+ const totalPages = Math.ceil(total / limit);
3805
+
3806
+ return {
3807
+ items,
3808
+ meta: {
3809
+ total,
3810
+ page,
3811
+ limit,
3812
+ totalPages,
3813
+ hasNextPage: page < totalPages,
3814
+ hasPreviousPage: page > 1,
3815
+ },
3816
+ };
3817
+ }
3818
+ `;
3819
+ await (0, file_utils_1.writeFile)(path.join(filterPath, 'typeorm-query-builder.ts'), typeormBuilderContent);
3820
+ // Prisma Query Builder
3821
+ const prismaBuilderContent = `import { FilterCondition, QueryOptions, PaginatedResult } from "./filter.types";
3822
+
3823
+ /**
3824
+ * Parse filters to Prisma where conditions
3825
+ */
3826
+ export function parseFiltersToPrisma(query: Record<string, any>): Record<string, any> {
3827
+ const where: Record<string, any> = {};
3828
+
3829
+ for (const [key, value] of Object.entries(query)) {
3830
+ if (value === undefined || value === null || value === "") continue;
3831
+ if (["page", "limit", "sortBy", "sortOrder", "search", "searchFields"].includes(key)) continue;
3832
+
3833
+ const parts = key.split("__");
3834
+ const field = parts[0];
3835
+ const operator = parts[1] || "eq";
3836
+
3837
+ where[field] = convertOperator(operator, value);
3838
+ }
3839
+
3840
+ return where;
3841
+ }
3842
+
3843
+ function convertOperator(op: string, value: any): any {
3844
+ switch (op) {
3845
+ case "eq": return value;
3846
+ case "ne": return { not: value };
3847
+ case "gt": return { gt: value };
3848
+ case "gte": return { gte: value };
3849
+ case "lt": return { lt: value };
3850
+ case "lte": return { lte: value };
3851
+ case "in": return { in: Array.isArray(value) ? value : value.split(",") };
3852
+ case "nin": return { notIn: Array.isArray(value) ? value : value.split(",") };
3853
+ case "like":
3854
+ case "contains": return { contains: value, mode: "insensitive" };
3855
+ case "startsWith": return { startsWith: value, mode: "insensitive" };
3856
+ case "endsWith": return { endsWith: value, mode: "insensitive" };
3857
+ case "isNull": return value === "true" ? null : { not: null };
3858
+ case "between":
3859
+ const [min, max] = Array.isArray(value) ? value : value.split(",");
3860
+ return { gte: min, lte: max };
3861
+ default: return value;
3862
+ }
3863
+ }
3864
+
3865
+ /**
3866
+ * Build search conditions for Prisma
3867
+ */
3868
+ export function buildSearchCondition(search: string, fields: string[]): Record<string, any> {
3869
+ if (!search || fields.length === 0) return {};
3870
+ return { OR: fields.map((field) => ({ [field]: { contains: search, mode: "insensitive" } })) };
3871
+ }
3872
+
3873
+ /**
3874
+ * Execute filtered, paginated Prisma query
3875
+ */
3876
+ export async function executePrismaQuery<T>(
3877
+ model: any,
3878
+ options: QueryOptions,
3879
+ defaultSearchFields: string[] = []
3880
+ ): Promise<PaginatedResult<T>> {
3881
+ const { page = 1, limit = 10, sortBy = "created_at", sortOrder = "DESC" } = options;
3882
+
3883
+ // Build where
3884
+ const filterWhere = options.filters ? parseFiltersToPrisma(options.filters) : {};
3885
+ const searchWhere = options.search
3886
+ ? buildSearchCondition(options.search, options.searchFields || defaultSearchFields)
3887
+ : {};
3888
+
3889
+ const where = {
3890
+ ...filterWhere,
3891
+ ...searchWhere,
3892
+ deleted_at: null,
3893
+ };
3894
+
3895
+ const [items, total] = await Promise.all([
3896
+ model.findMany({
3897
+ where,
3898
+ skip: (page - 1) * limit,
3899
+ take: limit,
3900
+ orderBy: { [sortBy]: sortOrder.toLowerCase() },
3901
+ }),
3902
+ model.count({ where }),
3903
+ ]);
3904
+
3905
+ const totalPages = Math.ceil(total / limit);
3906
+
3907
+ return {
3908
+ items,
3909
+ meta: {
3910
+ total,
3911
+ page,
3912
+ limit,
3913
+ totalPages,
3914
+ hasNextPage: page < totalPages,
3915
+ hasPreviousPage: page > 1,
3916
+ },
3917
+ };
3918
+ }
3919
+ `;
3920
+ await (0, file_utils_1.writeFile)(path.join(filterPath, 'prisma-query-builder.ts'), prismaBuilderContent);
3921
+ // Index exports
3922
+ const indexContent = `export * from "./filter.types";
3923
+ export * from "./typeorm-query-builder";
3924
+ export * from "./prisma-query-builder";
3925
+ `;
3926
+ await (0, file_utils_1.writeFile)(path.join(filterPath, 'index.ts'), indexContent);
3927
+ console.log(chalk_1.default.green(' ✓ Filter types and interfaces'));
3928
+ console.log(chalk_1.default.green(' ✓ TypeORM query builder utilities'));
3929
+ console.log(chalk_1.default.green(' ✓ Prisma query builder utilities'));
3930
+ }
3931
+ function listRecipes() {
3932
+ console.log(chalk_1.default.blue('\\n📚 Available Recipes:\\n'));
3933
+ Object.entries(AVAILABLE_RECIPES).forEach(([key, value]) => {
3934
+ console.log(chalk_1.default.cyan(` ${key.padEnd(15)}`), '-', value.description);
3935
+ if (value.dependencies.length > 0) {
3936
+ console.log(chalk_1.default.gray(` Dependencies: ${value.dependencies.join(', ')}`));
3937
+ }
3938
+ });
3939
+ console.log(chalk_1.default.yellow('\nUsage: ddd recipe <recipe-name> [--install-deps]'));
3940
+ }
3941
+ //# sourceMappingURL=recipe.js.map