nestjs-ddd-cli 3.2.1 → 3.3.0

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 (214) hide show
  1. package/README.md +118 -9
  2. package/ddd.schema.json +11 -1
  3. package/dist/commands/doctor-enhanced.d.ts +1 -0
  4. package/dist/commands/doctor-enhanced.js +94 -79
  5. package/dist/commands/doctor-enhanced.js.map +1 -1
  6. package/dist/commands/doctor.d.ts +9 -0
  7. package/dist/commands/doctor.js +68 -21
  8. package/dist/commands/doctor.js.map +1 -1
  9. package/dist/commands/generate-all.js +238 -176
  10. package/dist/commands/generate-all.js.map +1 -1
  11. package/dist/commands/generate-controller.d.ts +1 -0
  12. package/dist/commands/generate-controller.js +71 -0
  13. package/dist/commands/generate-controller.js.map +1 -0
  14. package/dist/commands/generate-domain-service.d.ts +1 -1
  15. package/dist/commands/generate-domain-service.js +6 -4
  16. package/dist/commands/generate-domain-service.js.map +1 -1
  17. package/dist/commands/generate-dto.d.ts +4 -0
  18. package/dist/commands/generate-dto.js +162 -0
  19. package/dist/commands/generate-dto.js.map +1 -0
  20. package/dist/commands/generate-entity.js +44 -35
  21. package/dist/commands/generate-entity.js.map +1 -1
  22. package/dist/commands/generate-event.js +10 -4
  23. package/dist/commands/generate-event.js.map +1 -1
  24. package/dist/commands/generate-migration-deployment.d.ts +15 -0
  25. package/dist/commands/generate-migration-deployment.js +326 -0
  26. package/dist/commands/generate-migration-deployment.js.map +1 -0
  27. package/dist/commands/generate-migration-deployment.spec.d.ts +1 -0
  28. package/dist/commands/generate-migration-deployment.spec.js +337 -0
  29. package/dist/commands/generate-migration-deployment.spec.js.map +1 -0
  30. package/dist/commands/generate-module.js +41 -31
  31. package/dist/commands/generate-module.js.map +1 -1
  32. package/dist/commands/generate-query.js +23 -4
  33. package/dist/commands/generate-query.js.map +1 -1
  34. package/dist/commands/generate-repository.d.ts +1 -1
  35. package/dist/commands/generate-repository.js +17 -34
  36. package/dist/commands/generate-repository.js.map +1 -1
  37. package/dist/commands/generate-service.js +19 -4
  38. package/dist/commands/generate-service.js.map +1 -1
  39. package/dist/commands/generate-usecase.js +12 -31
  40. package/dist/commands/generate-usecase.js.map +1 -1
  41. package/dist/commands/generate-value-object.js +3 -14
  42. package/dist/commands/generate-value-object.js.map +1 -1
  43. package/dist/commands/init-project.d.ts +3 -0
  44. package/dist/commands/init-project.js +19 -9
  45. package/dist/commands/init-project.js.map +1 -1
  46. package/dist/commands/interactive-scaffold.js +10 -0
  47. package/dist/commands/interactive-scaffold.js.map +1 -1
  48. package/dist/commands/migration.d.ts +3 -0
  49. package/dist/commands/migration.js +83 -48
  50. package/dist/commands/migration.js.map +1 -1
  51. package/dist/commands/recipe.d.ts +2 -0
  52. package/dist/commands/recipe.js +282 -91
  53. package/dist/commands/recipe.js.map +1 -1
  54. package/dist/commands/recipes/banklink-connector-contract.recipe.d.ts +1 -0
  55. package/dist/commands/recipes/banklink-connector-contract.recipe.js +161 -0
  56. package/dist/commands/recipes/banklink-connector-contract.recipe.js.map +1 -0
  57. package/dist/commands/recipes/bounded-json-http.recipe.d.ts +2 -0
  58. package/dist/commands/recipes/bounded-json-http.recipe.js +53 -0
  59. package/dist/commands/recipes/bounded-json-http.recipe.js.map +1 -0
  60. package/dist/commands/recipes/business-reference-identifiers.recipe.d.ts +1 -0
  61. package/dist/commands/recipes/business-reference-identifiers.recipe.js +223 -0
  62. package/dist/commands/recipes/business-reference-identifiers.recipe.js.map +1 -0
  63. package/dist/commands/recipes/event-backbone.recipe.d.ts +1 -0
  64. package/dist/commands/recipes/event-backbone.recipe.js +1118 -0
  65. package/dist/commands/recipes/event-backbone.recipe.js.map +1 -0
  66. package/dist/commands/recipes/external-projection-worker.recipe.d.ts +6 -0
  67. package/dist/commands/recipes/external-projection-worker.recipe.js +211 -0
  68. package/dist/commands/recipes/external-projection-worker.recipe.js.map +1 -0
  69. package/dist/commands/recipes/index.d.ts +12 -0
  70. package/dist/commands/recipes/index.js +12 -0
  71. package/dist/commands/recipes/index.js.map +1 -1
  72. package/dist/commands/recipes/jest-cjs-compat.recipe.d.ts +3 -0
  73. package/dist/commands/recipes/jest-cjs-compat.recipe.js +130 -0
  74. package/dist/commands/recipes/jest-cjs-compat.recipe.js.map +1 -0
  75. package/dist/commands/recipes/joi-env.recipe.d.ts +1 -0
  76. package/dist/commands/recipes/joi-env.recipe.js +301 -0
  77. package/dist/commands/recipes/joi-env.recipe.js.map +1 -0
  78. package/dist/commands/recipes/kafka-consumer.recipe.d.ts +1 -0
  79. package/dist/commands/recipes/kafka-consumer.recipe.js +65 -0
  80. package/dist/commands/recipes/kafka-consumer.recipe.js.map +1 -0
  81. package/dist/commands/recipes/oauth2-client-credentials.recipe.d.ts +1 -0
  82. package/dist/commands/recipes/oauth2-client-credentials.recipe.js +60 -0
  83. package/dist/commands/recipes/oauth2-client-credentials.recipe.js.map +1 -0
  84. package/dist/commands/recipes/oauth2-token-introspection.recipe.d.ts +1 -0
  85. package/dist/commands/recipes/oauth2-token-introspection.recipe.js +67 -0
  86. package/dist/commands/recipes/oauth2-token-introspection.recipe.js.map +1 -0
  87. package/dist/commands/recipes/oidc-bff-session.recipe.d.ts +1 -0
  88. package/dist/commands/recipes/oidc-bff-session.recipe.js +57 -0
  89. package/dist/commands/recipes/oidc-bff-session.recipe.js.map +1 -0
  90. package/dist/commands/recipes/oidc-dashboard.recipe.d.ts +1 -0
  91. package/dist/commands/recipes/oidc-dashboard.recipe.js +463 -0
  92. package/dist/commands/recipes/oidc-dashboard.recipe.js.map +1 -0
  93. package/dist/commands/recipes/oidc-resource-server.recipe.d.ts +1 -0
  94. package/dist/commands/recipes/oidc-resource-server.recipe.js +85 -0
  95. package/dist/commands/recipes/oidc-resource-server.recipe.js.map +1 -0
  96. package/dist/commands/recipes/platform-context.recipe.d.ts +1 -0
  97. package/dist/commands/recipes/platform-context.recipe.js +229 -0
  98. package/dist/commands/recipes/platform-context.recipe.js.map +1 -0
  99. package/dist/commands/recipes/platform-parc-authorization.recipe.d.ts +1 -0
  100. package/dist/commands/recipes/platform-parc-authorization.recipe.js +339 -0
  101. package/dist/commands/recipes/platform-parc-authorization.recipe.js.map +1 -0
  102. package/dist/commands/recipes/platform-parc-bound-authorization.recipe.d.ts +1 -0
  103. package/dist/commands/recipes/platform-parc-bound-authorization.recipe.js +56 -0
  104. package/dist/commands/recipes/platform-parc-bound-authorization.recipe.js.map +1 -0
  105. package/dist/commands/recipes/platform-service-access-request-context.recipe.d.ts +1 -0
  106. package/dist/commands/recipes/platform-service-access-request-context.recipe.js +103 -0
  107. package/dist/commands/recipes/platform-service-access-request-context.recipe.js.map +1 -0
  108. package/dist/commands/recipes/platform-service-runtime.recipe.d.ts +1 -0
  109. package/dist/commands/recipes/platform-service-runtime.recipe.js +415 -0
  110. package/dist/commands/recipes/platform-service-runtime.recipe.js.map +1 -0
  111. package/dist/index.js +82 -11
  112. package/dist/index.js.map +1 -1
  113. package/dist/templates/ai-context/CLAUDE.md.hbs +54 -3
  114. package/dist/templates/ai-context/conventions.md.hbs +20 -8
  115. package/dist/templates/command/create-command.hbs +8 -3
  116. package/dist/templates/command/delete-command.hbs +1 -1
  117. package/dist/templates/command/update-command.hbs +1 -1
  118. package/dist/templates/controller/application-controller.hbs +7 -0
  119. package/dist/templates/controller/controller.hbs +6 -0
  120. package/dist/templates/dto/create-dto.hbs +17 -5
  121. package/dist/templates/dto/filter-dto.hbs +1 -1
  122. package/dist/templates/dto/response-dto.hbs +1 -1
  123. package/dist/templates/entity/entity.hbs +4 -0
  124. package/dist/templates/mapper/mapper.hbs +6 -2
  125. package/dist/templates/orm-entity/orm-entity.hbs +9 -1
  126. package/dist/templates/prisma/prisma-mapper.hbs +6 -0
  127. package/dist/templates/prisma/prisma-repository.hbs +57 -5
  128. package/dist/templates/prisma/prisma-schema.hbs +2 -0
  129. package/dist/templates/query/get-all.query.hbs +10 -2
  130. package/dist/templates/recipes/bounded-json-http/README.md.hbs +36 -0
  131. package/dist/templates/recipes/external-projection-worker/README.md.hbs +157 -0
  132. package/dist/templates/recipes/external-projection-worker/canonical-json.ts.hbs +123 -0
  133. package/dist/templates/recipes/external-projection-worker/enqueuer.ts.hbs +167 -0
  134. package/dist/templates/recipes/external-projection-worker/index.ts.hbs +6 -0
  135. package/dist/templates/recipes/external-projection-worker/migration.ts.hbs +88 -0
  136. package/dist/templates/recipes/external-projection-worker/module.ts.hbs +36 -0
  137. package/dist/templates/recipes/external-projection-worker/store.ts.hbs +354 -0
  138. package/dist/templates/recipes/external-projection-worker/types.ts.hbs +134 -0
  139. package/dist/templates/recipes/external-projection-worker/worker.ts.hbs +296 -0
  140. package/dist/templates/recipes/kafka-consumer/README.md.hbs +50 -0
  141. package/dist/templates/recipes/kafka-consumer/kafka-consumer.ts.hbs +135 -0
  142. package/dist/templates/recipes/kafka-consumer/migration.ts.hbs +17 -0
  143. package/dist/templates/recipes/oauth2-client-credentials/README.md.hbs +40 -0
  144. package/dist/templates/recipes/oauth2-client-credentials/client.spec.ts.hbs +234 -0
  145. package/dist/templates/recipes/oauth2-client-credentials/client.ts.hbs +210 -0
  146. package/dist/templates/recipes/oauth2-client-credentials/index.ts.hbs +2 -0
  147. package/dist/templates/recipes/oauth2-client-credentials/module.ts.hbs +58 -0
  148. package/dist/templates/recipes/oauth2-token-introspection/README.md.hbs +158 -0
  149. package/dist/templates/recipes/oauth2-token-introspection/client.spec.ts.hbs +350 -0
  150. package/dist/templates/recipes/oauth2-token-introspection/client.ts.hbs +241 -0
  151. package/dist/templates/recipes/oauth2-token-introspection/http-client.spec.ts.hbs +411 -0
  152. package/dist/templates/recipes/oauth2-token-introspection/http-client.ts.hbs +343 -0
  153. package/dist/templates/recipes/oauth2-token-introspection/index.ts.hbs +3 -0
  154. package/dist/templates/recipes/oauth2-token-introspection/module.ts.hbs +58 -0
  155. package/dist/templates/recipes/oidc-bff-session/README.md.hbs +96 -0
  156. package/dist/templates/recipes/oidc-bff-session/client.test.ts.hbs +663 -0
  157. package/dist/templates/recipes/oidc-bff-session/client.ts.hbs +635 -0
  158. package/dist/templates/recipes/oidc-bff-session/index.ts.hbs +3 -0
  159. package/dist/templates/recipes/oidc-bff-session/store.integration.test.ts.hbs +480 -0
  160. package/dist/templates/recipes/oidc-bff-session/store.test.ts.hbs +87 -0
  161. package/dist/templates/recipes/oidc-bff-session/store.ts.hbs +549 -0
  162. package/dist/templates/recipes/oidc-bff-session/valkey.test.ts.hbs +158 -0
  163. package/dist/templates/recipes/oidc-bff-session/valkey.ts.hbs +148 -0
  164. package/dist/templates/recipes/oidc-resource-server/README.md.hbs +74 -0
  165. package/dist/templates/recipes/oidc-resource-server/index.ts.hbs +2 -0
  166. package/dist/templates/recipes/oidc-resource-server/module.ts.hbs +51 -0
  167. package/dist/templates/recipes/oidc-resource-server/verifier.spec.ts.hbs +186 -0
  168. package/dist/templates/recipes/oidc-resource-server/verifier.ts.hbs +440 -0
  169. package/dist/templates/recipes/platform-parc-bound-authorization/README.md.hbs +92 -0
  170. package/dist/templates/recipes/platform-parc-bound-authorization/client.ts.hbs +142 -0
  171. package/dist/templates/recipes/platform-service-access-request-context/docs.md.hbs +61 -0
  172. package/dist/templates/recipes/platform-service-access-request-context/env.example.hbs +7 -0
  173. package/dist/templates/recipes/platform-service-access-request-context/index.ts.hbs +7 -0
  174. package/dist/templates/recipes/platform-service-access-request-context/request-context-headers.ts.hbs +61 -0
  175. package/dist/templates/recipes/platform-service-access-request-context/request-context-introspection.parser.ts.hbs +132 -0
  176. package/dist/templates/recipes/platform-service-access-request-context/request-context-introspection.types.ts.hbs +40 -0
  177. package/dist/templates/recipes/platform-service-access-request-context/service-access-request-context.client.ts.hbs +45 -0
  178. package/dist/templates/recipes/platform-service-access-request-context/service-access-request-context.config.ts.hbs +142 -0
  179. package/dist/templates/recipes/platform-service-access-request-context/service-access-request-context.guard.ts.hbs +77 -0
  180. package/dist/templates/recipes/platform-service-access-request-context/service-access-request-context.module.ts.hbs +17 -0
  181. package/dist/templates/repository/repository.hbs +43 -5
  182. package/dist/templates/resolver/graphql-input.hbs +23 -6
  183. package/dist/templates/resolver/resolver.hbs +4 -0
  184. package/dist/templates/service/domain-service.hbs +2 -2
  185. package/dist/templates/test/controller.spec.hbs +5 -1
  186. package/dist/templates/test/prisma-repository.spec.hbs +192 -0
  187. package/dist/templates/test/repository.spec.hbs +41 -2
  188. package/dist/templates/test/usecase.spec.hbs +24 -5
  189. package/dist/templates/usecase/application-usecase.hbs +17 -0
  190. package/dist/templates/usecase/create-usecase.hbs +34 -3
  191. package/dist/templates/usecase/update-usecase.hbs +15 -1
  192. package/dist/utils/config.utils.d.ts +2 -0
  193. package/dist/utils/config.utils.js +14 -2
  194. package/dist/utils/config.utils.js.map +1 -1
  195. package/dist/utils/dependency.utils.d.ts +18 -2
  196. package/dist/utils/dependency.utils.js +144 -15
  197. package/dist/utils/dependency.utils.js.map +1 -1
  198. package/dist/utils/field.utils.d.ts +6 -0
  199. package/dist/utils/field.utils.js +78 -24
  200. package/dist/utils/field.utils.js.map +1 -1
  201. package/dist/utils/field.utils.spec.d.ts +1 -0
  202. package/dist/utils/field.utils.spec.js +38 -0
  203. package/dist/utils/field.utils.spec.js.map +1 -0
  204. package/dist/utils/file.utils.d.ts +39 -3
  205. package/dist/utils/file.utils.js +150 -13
  206. package/dist/utils/file.utils.js.map +1 -1
  207. package/dist/utils/naming.utils.js +5 -4
  208. package/dist/utils/naming.utils.js.map +1 -1
  209. package/dist/utils/static-recipe.utils.d.ts +4 -0
  210. package/dist/utils/static-recipe.utils.js +148 -0
  211. package/dist/utils/static-recipe.utils.js.map +1 -0
  212. package/dist/utils/template-engine.utils.js +83 -68
  213. package/dist/utils/template-engine.utils.js.map +1 -1
  214. package/package.json +20 -11
package/README.md CHANGED
@@ -28,8 +28,8 @@ npm install -g nestjs-ddd-cli
28
28
  # Initialize a new project
29
29
  ddd init my-project
30
30
 
31
- # Generate complete CRUD with typed fields
32
- ddd scaffold User -m users --fields "name:string email:string:unique age:number:optional"
31
+ # Generate a financial-safe aggregate without a generic DELETE operation
32
+ ddd scaffold Invoice -m billing --fields "amount:money tenantId:uuid:serverOwned" --no-delete
33
33
 
34
34
  # Apply authentication recipe
35
35
  ddd recipe auth-jwt --install-deps
@@ -41,6 +41,48 @@ ddd security-patterns
41
41
  ddd observability
42
42
  ```
43
43
 
44
+ ## OAuth2 token introspection
45
+
46
+ Generate a bounded RFC7662 client and an owning Nest module:
47
+
48
+ ```sh
49
+ ddd recipe oauth2-token-introspection --path <service> --dry-run
50
+ ddd recipe oauth2-token-introspection --path <service>
51
+ ```
52
+
53
+ The client rechecks the provider on every call and returns immutable evidence.
54
+ The consuming service supplies principal/session/permission policy. HTTPS is the
55
+ default; verified private HTTP requires explicit opt-in. See the
56
+ [generated operator guide](src/templates/recipes/oauth2-token-introspection/README.md.hbs)
57
+ for async registration, limits, failure handling and provider compatibility.
58
+
59
+ ## Framework-independent bounded JSON transport
60
+
61
+ ```sh
62
+ ddd recipe bounded-json-http --path <application> --dry-run
63
+ ddd recipe bounded-json-http --path <application>
64
+ ```
65
+
66
+ Emit the same fixed-origin HTTP client without a Nest module or dependencies.
67
+ The consumer owns its server-only wrapper, exact response validation and domain
68
+ admission. GET, form POST and JSON POST/PATCH/PUT have bounded responses and
69
+ deadlines, with no redirects or retries. See the
70
+ [generated guide](src/templates/recipes/bounded-json-http/README.md.hbs).
71
+
72
+ ## Confidential OIDC BFF sessions
73
+
74
+ ```sh
75
+ ddd recipe oidc-bff-session --path <app> --dry-run
76
+ ddd recipe oidc-bff-session --path <app>
77
+ ```
78
+
79
+ This emits provider-neutral verified authorization-code/S256 transport, opaque
80
+ Lua sessions and durable token-retirement leases. Exact issuer/endpoints,
81
+ credentials, cookie/routes, domain admission and worker scheduling remain the
82
+ consumer's responsibility. Runtime dependencies and storage custody controls
83
+ are explicit in the [generated guide](src/templates/recipes/oidc-bff-session/README.md.hbs).
84
+ The recipe never installs dependencies or rewrites application configuration.
85
+
44
86
  ## Commands
45
87
 
46
88
  ### Core Commands
@@ -48,11 +90,15 @@ ddd observability
48
90
  | Command | Description |
49
91
  |---------|-------------|
50
92
  | `ddd init <name>` | Initialize new NestJS project with DDD structure |
51
- | `ddd scaffold <entity> -m <module>` | Generate complete CRUD with all layers |
93
+ | `ddd scaffold <entity> -m <module>` | Generate a complete aggregate stack |
52
94
  | `ddd generate <type> <name> -m <module>` | Generate individual components |
53
95
  | `ddd recipe [name]` | Apply common patterns (auth, caching, etc.) |
54
96
  | `ddd shared` | Generate shared utilities and base classes |
55
97
 
98
+ ```bash
99
+ ddd generate dto Product -m inventory --kind create
100
+ ```
101
+
56
102
  ### Enterprise Commands
57
103
 
58
104
  | Command | Description |
@@ -80,7 +126,7 @@ ddd observability
80
126
  Generate complete typed entities, DTOs, and migrations:
81
127
 
82
128
  ```bash
83
- ddd scaffold Product -m inventory --fields "name:string price:number:optional sku:string:unique"
129
+ ddd scaffold Product -m inventory --fields "name:string price:money:optional sku:string:unique"
84
130
  ```
85
131
 
86
132
  **Field Format:** `name:type:modifier1:modifier2`
@@ -89,10 +135,37 @@ ddd scaffold Product -m inventory --fields "name:string price:number:optional sk
89
135
  |-------|-----------|
90
136
  | `string`, `text` | `optional` |
91
137
  | `number`, `int`, `float`, `decimal` | `unique` |
138
+ | `money` (exact decimal-backed TypeScript string) | `serverOwned` (`internal` alias) |
92
139
  | `boolean` | `relation` |
93
140
  | `date`, `datetime`, `timestamp` | `OneToOne`, `OneToMany`, `ManyToOne`, `ManyToMany` |
94
141
  | `uuid`, `json`, `enum` | |
95
142
 
143
+ Use `money` for financial values that must not pass through JavaScript floating-point
144
+ numbers. It generates `string` in domain, TypeORM, request, and response types,
145
+ decimal-string validation and OpenAPI metadata, plus `decimal`/Prisma `Decimal`
146
+ storage. Existing `decimal` fields remain TypeScript `number` for compatibility.
147
+
148
+ Use `serverOwned` for fields populated from trusted application context rather than
149
+ the request body:
150
+
151
+ ```bash
152
+ ddd scaffold Invoice -m billing \
153
+ --fields "amount:money tenantId:uuid:serverOwned bookId:uuid:serverOwned"
154
+ ```
155
+
156
+ Server-owned fields remain in domain, persistence, mapper, and response generation,
157
+ but are omitted from create/update request DTOs and request mass-assignment. The
158
+ generated create command accepts a second typed `serverOwnedFields` argument and
159
+ fails closed until the controller/application layer supplies it from authenticated
160
+ context.
161
+
162
+ For an aggregate with no generic deletion capability:
163
+
164
+ ```bash
165
+ ddd scaffold LedgerEntry -m ledger --fields "amount:money" --no-delete
166
+ # Equivalent project-wide default: features.delete = false in .dddrc.json
167
+ ```
168
+
96
169
  **Relations:**
97
170
  ```bash
98
171
  # ManyToOne relation
@@ -140,6 +213,11 @@ ddd recipe
140
213
 
141
214
  # Apply with dependency installation
142
215
  ddd recipe auth-jwt --install-deps
216
+ ddd recipe event-backbone --install-deps
217
+ ddd recipe oidc-dashboard --install-deps
218
+ ddd recipe platform-service-access-request-context
219
+ ddd recipe external-projection-worker --migration-timestamp 1790000000000 --install-deps
220
+ ddd recipe jest-cjs-compat --path /path/to/service --dry-run
143
221
  ```
144
222
 
145
223
  | Recipe | Description |
@@ -149,6 +227,15 @@ ddd recipe auth-jwt --install-deps
149
227
  | `soft-delete` | Base entity and repository with soft delete |
150
228
  | `audit-log` | Entity change tracking with sensitive data masking |
151
229
  | `caching` | Redis caching with decorators |
230
+ | `event-backbone` | Postgres event store/outbox source of truth with Pulsar transport |
231
+ | `business-reference-identifiers` | Human-readable sidecar references beside canonical IDs |
232
+ | `platform-service-runtime` | Service manifest, capabilities, actions, events, dependencies, and health contract |
233
+ | `platform-service-access-request-context` | Fail-closed Service Access lease introspection before route-specific PARC authorization |
234
+ | `banklink-connector-contract` | BankLink NestJS control-plane and Go sidecar connector boundary contract |
235
+ | `oidc-dashboard` | OIDC broker integration for internal dashboards and admin APIs |
236
+ | `external-projection-worker` | Postgres-backed external projection intents with atomic enqueue, canonical idempotency, fenced leases, and bounded retry outcomes |
237
+ | `jest-cjs-compat` | Node/Jest-only CommonJS transformation of ESM Nest configuration; production TypeScript and dependency versions stay intact |
238
+ | `bounded-json-http` | Fixed-origin GET/form/JSON transport without framework dependencies or domain admission policy |
152
239
 
153
240
  ## Generated Structure
154
241
 
@@ -192,13 +279,15 @@ Create `.dddrc.json` in your project root:
192
279
  "database": "postgres",
193
280
  "naming": {
194
281
  "table": "snake_case",
195
- "dto": "snake_case",
282
+ "dto": "camelCase",
196
283
  "file": "kebab-case"
197
284
  },
198
285
  "features": {
199
286
  "swagger": true,
200
287
  "pagination": true,
288
+ "delete": true,
201
289
  "softDelete": true,
290
+ "hardDelete": false,
202
291
  "timestamps": true,
203
292
  "tests": false,
204
293
  "events": false
@@ -211,6 +300,23 @@ Create `.dddrc.json` in your project root:
211
300
  }
212
301
  ```
213
302
 
303
+ `features.delete` controls whether aggregate scaffolds emit the generic DELETE
304
+ controller route, delete command/use case, and repository `delete`/`hardDelete`
305
+ surface. It defaults to `true` for compatibility. Set it to `false`, or pass
306
+ `--no-delete` to `ddd scaffold`/`ddd generate all`, for a non-destructive aggregate
307
+ scaffold. This does not remove existing generated files and does not disable
308
+ soft-delete columns or active-row filtering.
309
+
310
+ `features.softDelete` controls the generated domain field, ORM column or Prisma
311
+ field, active-row filters, and repository delete behavior. When it is `false`,
312
+ none of the generated code references `deletedAt`/`deleted_at` and the ordinary
313
+ `delete` method performs the ORM's normal delete operation.
314
+
315
+ `features.hardDelete` is disabled by default. Set it to `true` only when a
316
+ soft-delete repository must expose a separately named `hardDelete` compatibility
317
+ helper. It never changes the ordinary `delete` method, and it is omitted when
318
+ `softDelete` is disabled because ordinary deletion is already physical.
319
+
214
320
  ## Real-World Examples
215
321
 
216
322
  ### E-commerce with Security
@@ -224,8 +330,8 @@ ddd security-patterns
224
330
  ddd recipe auth-jwt --install-deps
225
331
 
226
332
  # Domain modules
227
- ddd scaffold Product -m inventory --fields "name:string price:decimal sku:string:unique stock:int"
228
- ddd scaffold Order -m orders --fields "total:decimal status:enum:pending,paid,shipped customerId:uuid"
333
+ ddd scaffold Product -m inventory --fields "name:string price:money sku:string:unique stock:int"
334
+ ddd scaffold Order -m orders --fields "total:money status:enum:pending,paid,shipped customerId:uuid"
229
335
  ddd scaffold Customer -m customers --fields "email:string:unique name:string"
230
336
 
231
337
  # Enterprise features
@@ -247,7 +353,7 @@ ddd metrics-prometheus
247
353
  ddd health-probes
248
354
 
249
355
  # Domain
250
- ddd scaffold Transaction -m payments --fields "amount:decimal currency:string status:enum:pending,completed,failed"
356
+ ddd scaffold Transaction -m payments --fields "amount:money currency:string status:enum:pending,completed,failed"
251
357
  ddd circuit-breaker
252
358
  ```
253
359
 
@@ -282,7 +388,7 @@ All generated APIs follow RESTful conventions with security:
282
388
  | GET | `/entities/:id` | Get single entity |
283
389
  | POST | `/entities` | Create new entity |
284
390
  | PUT | `/entities/:id` | Update entity |
285
- | DELETE | `/entities/:id` | Soft delete entity |
391
+ | DELETE | `/entities/:id` | Soft delete entity (omitted with `--no-delete`) |
286
392
 
287
393
  ### Pagination
288
394
 
@@ -338,6 +444,9 @@ npm link
338
444
 
339
445
  # Test locally
340
446
  ddd scaffold Test -m test-module --fields "name:string"
447
+
448
+ # External projection database proof (requires a disposable PostgreSQL URL)
449
+ EXTERNAL_PROJECTION_TEST_DATABASE_URL=postgres://... bun run test:external-projection-postgres
341
450
  ```
342
451
 
343
452
  ## License
package/ddd.schema.json CHANGED
@@ -29,7 +29,7 @@
29
29
  "dto": {
30
30
  "type": "string",
31
31
  "enum": ["snake_case", "camelCase"],
32
- "default": "snake_case",
32
+ "default": "camelCase",
33
33
  "description": "DTO property naming convention"
34
34
  },
35
35
  "file": {
@@ -54,11 +54,21 @@
54
54
  "default": true,
55
55
  "description": "Include pagination support in list queries"
56
56
  },
57
+ "delete": {
58
+ "type": "boolean",
59
+ "default": true,
60
+ "description": "Generate the generic aggregate deletion surface"
61
+ },
57
62
  "softDelete": {
58
63
  "type": "boolean",
59
64
  "default": true,
60
65
  "description": "Use soft delete pattern"
61
66
  },
67
+ "hardDelete": {
68
+ "type": "boolean",
69
+ "default": false,
70
+ "description": "Generate an explicit hardDelete compatibility helper when softDelete is enabled"
71
+ },
62
72
  "timestamps": {
63
73
  "type": "boolean",
64
74
  "default": true,
@@ -1,4 +1,5 @@
1
1
  export interface DoctorOptions {
2
+ app?: string;
2
3
  fix?: boolean;
3
4
  verbose?: boolean;
4
5
  }
@@ -40,7 +40,10 @@ exports.runEnhancedDoctor = runEnhancedDoctor;
40
40
  const fs = __importStar(require("fs"));
41
41
  const path = __importStar(require("path"));
42
42
  const chalk_1 = __importDefault(require("chalk"));
43
+ const config_utils_1 = require("../utils/config.utils");
44
+ const doctor_1 = require("./doctor");
43
45
  async function runEnhancedDoctor(basePath, options = {}) {
46
+ const doctorPaths = await resolveDoctorPaths(basePath, options.app);
44
47
  const report = {
45
48
  passed: 0,
46
49
  warnings: 0,
@@ -49,14 +52,14 @@ async function runEnhancedDoctor(basePath, options = {}) {
49
52
  };
50
53
  console.log(chalk_1.default.bold.blue('\n🩺 Running Enhanced DDD Doctor...\n'));
51
54
  // Run all diagnostic checks
52
- await checkProjectStructure(basePath, report);
53
- await checkDddPatterns(basePath, report);
54
- await checkTypeScriptStrictness(basePath, report);
55
- await checkNamingConventions(basePath, report);
56
- await checkTestCoverage(basePath, report);
57
- await checkImportPatterns(basePath, report);
58
- await checkEntityIntegrity(basePath, report);
59
- await checkModuleBoundaries(basePath, report);
55
+ await checkProjectStructure(doctorPaths, report);
56
+ await checkDddPatterns(doctorPaths.modulesPath, report);
57
+ await checkTypeScriptStrictness(doctorPaths.projectRoot, report);
58
+ await checkNamingConventions(doctorPaths.modulesPath, report);
59
+ await checkTestCoverage(doctorPaths.modulesPath, report);
60
+ await checkImportPatterns(doctorPaths.modulesPath, report);
61
+ await checkEntityIntegrity(doctorPaths.modulesPath, report);
62
+ await checkModuleBoundaries(doctorPaths.modulesPath, report);
60
63
  // Print results
61
64
  printReport(report, options.verbose);
62
65
  // Auto-fix if requested
@@ -65,26 +68,32 @@ async function runEnhancedDoctor(basePath, options = {}) {
65
68
  }
66
69
  return report;
67
70
  }
68
- async function checkProjectStructure(basePath, report) {
69
- const requiredDirs = [
70
- 'src/modules',
71
- 'src/shared',
72
- ];
71
+ async function resolveDoctorPaths(basePath, app) {
72
+ const projectRoot = await (0, doctor_1.resolveNestProjectPath)(basePath, app);
73
+ const config = await (0, config_utils_1.loadConfig)(projectRoot);
74
+ return {
75
+ projectRoot,
76
+ modulesPath: path.resolve(projectRoot, config.paths.modules),
77
+ sharedPath: path.resolve(projectRoot, config.paths.shared),
78
+ };
79
+ }
80
+ async function checkProjectStructure(paths, report) {
81
+ const requiredDirs = [paths.modulesPath, paths.sharedPath];
73
82
  const recommendedDirs = [
74
- 'src/shared/domain',
75
- 'src/shared/infrastructure',
76
- 'src/shared/application',
83
+ path.join(paths.sharedPath, 'domain'),
84
+ path.join(paths.sharedPath, 'infrastructure'),
85
+ path.join(paths.sharedPath, 'application'),
77
86
  ];
78
- for (const dir of requiredDirs) {
79
- const fullPath = path.join(basePath, dir);
87
+ for (const fullPath of requiredDirs) {
88
+ const displayPath = displayProjectPath(paths.projectRoot, fullPath);
80
89
  if (!fs.existsSync(fullPath)) {
81
90
  report.errors++;
82
91
  report.results.push({
83
92
  category: 'Structure',
84
93
  rule: 'required-directory',
85
94
  severity: 'error',
86
- message: `Missing required directory: ${dir}`,
87
- suggestion: `Run: mkdir -p ${dir}`,
95
+ message: `Missing required directory: ${displayPath}`,
96
+ suggestion: `Run: mkdir -p ${fullPath}`,
88
97
  autoFixable: true,
89
98
  });
90
99
  }
@@ -92,30 +101,34 @@ async function checkProjectStructure(basePath, report) {
92
101
  report.passed++;
93
102
  }
94
103
  }
95
- for (const dir of recommendedDirs) {
96
- const fullPath = path.join(basePath, dir);
104
+ for (const fullPath of recommendedDirs) {
105
+ const displayPath = displayProjectPath(paths.projectRoot, fullPath);
97
106
  if (!fs.existsSync(fullPath)) {
98
107
  report.warnings++;
99
108
  report.results.push({
100
109
  category: 'Structure',
101
110
  rule: 'recommended-directory',
102
111
  severity: 'warning',
103
- message: `Missing recommended directory: ${dir}`,
104
- suggestion: `Consider adding ${dir} for better DDD organization`,
112
+ message: `Missing recommended directory: ${displayPath}`,
113
+ suggestion: `Consider adding ${displayPath} for better DDD organization`,
105
114
  });
106
115
  }
107
116
  }
108
117
  }
109
- async function checkDddPatterns(basePath, report) {
110
- const modulesPath = path.join(basePath, 'src/modules');
118
+ function displayProjectPath(projectRoot, targetPath) {
119
+ const relativePath = path.relative(projectRoot, targetPath);
120
+ return relativePath && !relativePath.startsWith('..') ? relativePath : targetPath;
121
+ }
122
+ async function checkDddPatterns(modulesPath, report) {
111
123
  if (!fs.existsSync(modulesPath))
112
124
  return;
113
- const modules = fs.readdirSync(modulesPath).filter(f => fs.statSync(path.join(modulesPath, f)).isDirectory());
125
+ const modules = fs
126
+ .readdirSync(modulesPath)
127
+ .filter((f) => fs.statSync(path.join(modulesPath, f)).isDirectory());
114
128
  for (const moduleName of modules) {
115
129
  const modulePath = path.join(modulesPath, moduleName);
116
- // Check for domain layer
117
- const domainPath = path.join(modulePath, 'domain');
118
- if (!fs.existsSync(domainPath)) {
130
+ const domainPaths = getDomainLayerPaths(modulePath);
131
+ if (domainPaths.length === 0) {
119
132
  report.warnings++;
120
133
  report.results.push({
121
134
  category: 'DDD Pattern',
@@ -123,22 +136,29 @@ async function checkDddPatterns(basePath, report) {
123
136
  severity: 'warning',
124
137
  message: `Module "${moduleName}" missing domain layer`,
125
138
  file: modulePath,
126
- suggestion: 'Add domain/ directory with entities, value-objects, and domain services',
139
+ suggestion: 'Add application/domain/ (CLI layout) or domain/ (classic layout) with domain types',
127
140
  });
128
141
  }
129
- // Check for entity immutability patterns
130
- const entitiesPath = path.join(domainPath, 'entities');
131
- if (fs.existsSync(entitiesPath)) {
132
- await checkEntityImmutability(entitiesPath, moduleName, report);
142
+ for (const domainPath of domainPaths) {
143
+ // Check for entity immutability patterns
144
+ const entitiesPath = path.join(domainPath, 'entities');
145
+ if (fs.existsSync(entitiesPath)) {
146
+ await checkEntityImmutability(entitiesPath, report);
147
+ }
148
+ // Check for proper aggregate roots
149
+ await checkAggregateRoots(domainPath, report);
133
150
  }
134
- // Check for proper aggregate roots
135
- await checkAggregateRoots(domainPath, moduleName, report);
136
151
  // Check repository interface in domain layer
137
- await checkRepositoryPattern(modulePath, moduleName, report);
152
+ await checkRepositoryPattern(modulePath, moduleName, domainPaths, report);
138
153
  }
139
154
  }
140
- async function checkEntityImmutability(entitiesPath, moduleName, report) {
141
- const files = fs.readdirSync(entitiesPath).filter(f => f.endsWith('.ts'));
155
+ function getDomainLayerPaths(modulePath) {
156
+ return ['application/domain', 'domain']
157
+ .map((relativePath) => path.join(modulePath, relativePath))
158
+ .filter((domainPath) => fs.existsSync(domainPath));
159
+ }
160
+ async function checkEntityImmutability(entitiesPath, report) {
161
+ const files = fs.readdirSync(entitiesPath).filter((f) => f.endsWith('.ts'));
142
162
  for (const file of files) {
143
163
  const filePath = path.join(entitiesPath, file);
144
164
  const content = fs.readFileSync(filePath, 'utf-8');
@@ -172,7 +192,7 @@ async function checkEntityImmutability(entitiesPath, moduleName, report) {
172
192
  }
173
193
  }
174
194
  }
175
- async function checkAggregateRoots(domainPath, moduleName, report) {
195
+ async function checkAggregateRoots(domainPath, report) {
176
196
  if (!fs.existsSync(domainPath))
177
197
  return;
178
198
  const files = getAllTypeScriptFiles(domainPath);
@@ -180,10 +200,8 @@ async function checkAggregateRoots(domainPath, moduleName, report) {
180
200
  const content = fs.readFileSync(file, 'utf-8');
181
201
  // Check if entity extends AggregateRoot when it should
182
202
  if (content.includes('class ') && content.includes('Entity')) {
183
- const hasAggregateRoot = content.includes('extends AggregateRoot') ||
184
- content.includes('extends BaseEntity');
185
- const hasDomainEvents = content.includes('domainEvents') ||
186
- content.includes('addDomainEvent');
203
+ const hasAggregateRoot = content.includes('extends AggregateRoot') || content.includes('extends BaseEntity');
204
+ const hasDomainEvents = content.includes('domainEvents') || content.includes('addDomainEvent');
187
205
  if (hasDomainEvents && !hasAggregateRoot) {
188
206
  report.warnings++;
189
207
  report.results.push({
@@ -198,15 +216,12 @@ async function checkAggregateRoots(domainPath, moduleName, report) {
198
216
  }
199
217
  }
200
218
  }
201
- async function checkRepositoryPattern(modulePath, moduleName, report) {
202
- const domainPath = path.join(modulePath, 'domain');
219
+ async function checkRepositoryPattern(modulePath, moduleName, domainPaths, report) {
203
220
  const infraPath = path.join(modulePath, 'infrastructure');
204
221
  // Check for repository interface in domain layer
205
- const domainRepoFiles = fs.existsSync(domainPath)
206
- ? getAllTypeScriptFiles(domainPath).filter(f => f.includes('repository'))
207
- : [];
222
+ const domainRepoFiles = domainPaths.flatMap((domainPath) => getAllTypeScriptFiles(domainPath).filter((file) => file.includes('repository')));
208
223
  const infraRepoFiles = fs.existsSync(infraPath)
209
- ? getAllTypeScriptFiles(infraPath).filter(f => f.includes('repository'))
224
+ ? getAllTypeScriptFiles(infraPath).filter((f) => f.includes('repository'))
210
225
  : [];
211
226
  if (infraRepoFiles.length > 0 && domainRepoFiles.length === 0) {
212
227
  report.warnings++;
@@ -270,8 +285,7 @@ async function checkTypeScriptStrictness(basePath, report) {
270
285
  });
271
286
  }
272
287
  }
273
- async function checkNamingConventions(basePath, report) {
274
- const modulesPath = path.join(basePath, 'src/modules');
288
+ async function checkNamingConventions(modulesPath, report) {
275
289
  if (!fs.existsSync(modulesPath))
276
290
  return;
277
291
  const files = getAllTypeScriptFiles(modulesPath);
@@ -295,8 +309,9 @@ async function checkNamingConventions(basePath, report) {
295
309
  if (classMatch) {
296
310
  for (const match of classMatch) {
297
311
  const className = match.replace('export class ', '');
312
+ const firstCharacter = className[0];
298
313
  // Check PascalCase
299
- if (className[0] !== className[0].toUpperCase()) {
314
+ if (firstCharacter && firstCharacter !== firstCharacter.toUpperCase()) {
300
315
  report.errors++;
301
316
  report.results.push({
302
317
  category: 'Naming',
@@ -310,17 +325,17 @@ async function checkNamingConventions(basePath, report) {
310
325
  }
311
326
  }
312
327
  }
313
- async function checkTestCoverage(basePath, report) {
314
- const modulesPath = path.join(basePath, 'src/modules');
328
+ async function checkTestCoverage(modulesPath, report) {
315
329
  if (!fs.existsSync(modulesPath))
316
330
  return;
317
- const modules = fs.readdirSync(modulesPath).filter(f => fs.statSync(path.join(modulesPath, f)).isDirectory());
331
+ const modules = fs
332
+ .readdirSync(modulesPath)
333
+ .filter((f) => fs.statSync(path.join(modulesPath, f)).isDirectory());
318
334
  for (const moduleName of modules) {
319
335
  const modulePath = path.join(modulesPath, moduleName);
320
- const sourceFiles = getAllTypeScriptFiles(modulePath).filter(f => !f.includes('.spec.ts') && !f.includes('.test.ts'));
321
- const testFiles = getAllTypeScriptFiles(modulePath).filter(f => f.includes('.spec.ts') || f.includes('.test.ts'));
336
+ const sourceFiles = getAllTypeScriptFiles(modulePath).filter((f) => !f.includes('.spec.ts') && !f.includes('.test.ts'));
322
337
  // Check for service tests
323
- const services = sourceFiles.filter(f => f.includes('.service.ts'));
338
+ const services = sourceFiles.filter((f) => f.includes('.service.ts'));
324
339
  for (const service of services) {
325
340
  const testFile = service.replace('.service.ts', '.service.spec.ts');
326
341
  if (!fs.existsSync(testFile)) {
@@ -336,7 +351,7 @@ async function checkTestCoverage(basePath, report) {
336
351
  }
337
352
  }
338
353
  // Check for domain entity tests
339
- const entities = sourceFiles.filter(f => f.includes('.entity.ts'));
354
+ const entities = sourceFiles.filter((f) => f.includes('.entity.ts'));
340
355
  for (const entity of entities) {
341
356
  const testFile = entity.replace('.entity.ts', '.entity.spec.ts');
342
357
  if (!fs.existsSync(testFile)) {
@@ -351,17 +366,15 @@ async function checkTestCoverage(basePath, report) {
351
366
  }
352
367
  }
353
368
  }
354
- async function checkImportPatterns(basePath, report) {
355
- const modulesPath = path.join(basePath, 'src/modules');
369
+ async function checkImportPatterns(modulesPath, report) {
356
370
  if (!fs.existsSync(modulesPath))
357
371
  return;
358
- const modules = fs.readdirSync(modulesPath).filter(f => fs.statSync(path.join(modulesPath, f)).isDirectory());
372
+ const modules = fs
373
+ .readdirSync(modulesPath)
374
+ .filter((f) => fs.statSync(path.join(modulesPath, f)).isDirectory());
359
375
  for (const moduleName of modules) {
360
376
  const modulePath = path.join(modulesPath, moduleName);
361
- const domainPath = path.join(modulePath, 'domain');
362
- if (!fs.existsSync(domainPath))
363
- continue;
364
- const domainFiles = getAllTypeScriptFiles(domainPath);
377
+ const domainFiles = getDomainLayerPaths(modulePath).flatMap((domainPath) => getAllTypeScriptFiles(domainPath));
365
378
  for (const file of domainFiles) {
366
379
  const content = fs.readFileSync(file, 'utf-8');
367
380
  // Check for infrastructure imports in domain layer (violation)
@@ -395,11 +408,10 @@ async function checkImportPatterns(basePath, report) {
395
408
  }
396
409
  }
397
410
  }
398
- async function checkEntityIntegrity(basePath, report) {
399
- const modulesPath = path.join(basePath, 'src/modules');
411
+ async function checkEntityIntegrity(modulesPath, report) {
400
412
  if (!fs.existsSync(modulesPath))
401
413
  return;
402
- const files = getAllTypeScriptFiles(modulesPath).filter(f => f.includes('.entity.ts'));
414
+ const files = getAllTypeScriptFiles(modulesPath).filter((f) => f.includes('.entity.ts'));
403
415
  for (const file of files) {
404
416
  const content = fs.readFileSync(file, 'utf-8');
405
417
  // Check for ID field
@@ -430,11 +442,12 @@ async function checkEntityIntegrity(basePath, report) {
430
442
  }
431
443
  }
432
444
  }
433
- async function checkModuleBoundaries(basePath, report) {
434
- const modulesPath = path.join(basePath, 'src/modules');
445
+ async function checkModuleBoundaries(modulesPath, report) {
435
446
  if (!fs.existsSync(modulesPath))
436
447
  return;
437
- const modules = fs.readdirSync(modulesPath).filter(f => fs.statSync(path.join(modulesPath, f)).isDirectory());
448
+ const modules = fs
449
+ .readdirSync(modulesPath)
450
+ .filter((f) => fs.statSync(path.join(modulesPath, f)).isDirectory());
438
451
  for (const moduleName of modules) {
439
452
  const modulePath = path.join(modulesPath, moduleName);
440
453
  const files = getAllTypeScriptFiles(modulePath);
@@ -482,7 +495,7 @@ function getAllTypeScriptFiles(dir) {
482
495
  return files;
483
496
  }
484
497
  async function applyFixes(report) {
485
- const fixableResults = report.results.filter(r => r.autoFixable);
498
+ const fixableResults = report.results.filter((r) => r.autoFixable);
486
499
  if (fixableResults.length === 0) {
487
500
  console.log(chalk_1.default.gray('\nNo auto-fixable issues found.'));
488
501
  return;
@@ -508,10 +521,12 @@ function printReport(report, verbose) {
508
521
  for (const [category, results] of byCategory) {
509
522
  console.log(chalk_1.default.bold(`\nšŸ“ ${category}`));
510
523
  for (const result of results) {
511
- const icon = result.severity === 'error' ? 'āŒ' :
512
- result.severity === 'warning' ? 'āš ļø' : 'ā„¹ļø';
513
- const color = result.severity === 'error' ? chalk_1.default.red :
514
- result.severity === 'warning' ? chalk_1.default.yellow : chalk_1.default.gray;
524
+ const icon = result.severity === 'error' ? 'āŒ' : result.severity === 'warning' ? 'āš ļø' : 'ā„¹ļø';
525
+ const color = result.severity === 'error'
526
+ ? chalk_1.default.red
527
+ : result.severity === 'warning'
528
+ ? chalk_1.default.yellow
529
+ : chalk_1.default.gray;
515
530
  console.log(color(` ${icon} ${result.message}`));
516
531
  if (verbose) {
517
532
  if (result.file) {