nestjs-ddd-cli 3.2.2 → 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 (210) 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 +54 -28
  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 +7 -2
  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 +4 -2
  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 +229 -85
  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 +11 -0
  70. package/dist/commands/recipes/index.js +11 -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.js +1 -1
  76. package/dist/commands/recipes/kafka-consumer.recipe.d.ts +1 -0
  77. package/dist/commands/recipes/kafka-consumer.recipe.js +65 -0
  78. package/dist/commands/recipes/kafka-consumer.recipe.js.map +1 -0
  79. package/dist/commands/recipes/oauth2-client-credentials.recipe.d.ts +1 -0
  80. package/dist/commands/recipes/oauth2-client-credentials.recipe.js +60 -0
  81. package/dist/commands/recipes/oauth2-client-credentials.recipe.js.map +1 -0
  82. package/dist/commands/recipes/oauth2-token-introspection.recipe.d.ts +1 -0
  83. package/dist/commands/recipes/oauth2-token-introspection.recipe.js +67 -0
  84. package/dist/commands/recipes/oauth2-token-introspection.recipe.js.map +1 -0
  85. package/dist/commands/recipes/oidc-bff-session.recipe.d.ts +1 -0
  86. package/dist/commands/recipes/oidc-bff-session.recipe.js +57 -0
  87. package/dist/commands/recipes/oidc-bff-session.recipe.js.map +1 -0
  88. package/dist/commands/recipes/oidc-dashboard.recipe.d.ts +1 -0
  89. package/dist/commands/recipes/oidc-dashboard.recipe.js +463 -0
  90. package/dist/commands/recipes/oidc-dashboard.recipe.js.map +1 -0
  91. package/dist/commands/recipes/oidc-resource-server.recipe.d.ts +1 -0
  92. package/dist/commands/recipes/oidc-resource-server.recipe.js +85 -0
  93. package/dist/commands/recipes/oidc-resource-server.recipe.js.map +1 -0
  94. package/dist/commands/recipes/platform-context.recipe.d.ts +1 -0
  95. package/dist/commands/recipes/platform-context.recipe.js +229 -0
  96. package/dist/commands/recipes/platform-context.recipe.js.map +1 -0
  97. package/dist/commands/recipes/platform-parc-authorization.recipe.d.ts +1 -0
  98. package/dist/commands/recipes/platform-parc-authorization.recipe.js +339 -0
  99. package/dist/commands/recipes/platform-parc-authorization.recipe.js.map +1 -0
  100. package/dist/commands/recipes/platform-parc-bound-authorization.recipe.d.ts +1 -0
  101. package/dist/commands/recipes/platform-parc-bound-authorization.recipe.js +56 -0
  102. package/dist/commands/recipes/platform-parc-bound-authorization.recipe.js.map +1 -0
  103. package/dist/commands/recipes/platform-service-access-request-context.recipe.d.ts +1 -0
  104. package/dist/commands/recipes/platform-service-access-request-context.recipe.js +103 -0
  105. package/dist/commands/recipes/platform-service-access-request-context.recipe.js.map +1 -0
  106. package/dist/commands/recipes/platform-service-runtime.recipe.d.ts +1 -0
  107. package/dist/commands/recipes/platform-service-runtime.recipe.js +415 -0
  108. package/dist/commands/recipes/platform-service-runtime.recipe.js.map +1 -0
  109. package/dist/index.js +77 -9
  110. package/dist/index.js.map +1 -1
  111. package/dist/templates/ai-context/CLAUDE.md.hbs +39 -3
  112. package/dist/templates/ai-context/conventions.md.hbs +13 -8
  113. package/dist/templates/command/create-command.hbs +8 -3
  114. package/dist/templates/command/delete-command.hbs +1 -1
  115. package/dist/templates/command/update-command.hbs +1 -1
  116. package/dist/templates/controller/application-controller.hbs +7 -0
  117. package/dist/templates/controller/controller.hbs +6 -0
  118. package/dist/templates/dto/create-dto.hbs +17 -5
  119. package/dist/templates/dto/filter-dto.hbs +1 -1
  120. package/dist/templates/dto/response-dto.hbs +1 -1
  121. package/dist/templates/entity/entity.hbs +4 -0
  122. package/dist/templates/mapper/mapper.hbs +5 -1
  123. package/dist/templates/orm-entity/orm-entity.hbs +9 -1
  124. package/dist/templates/prisma/prisma-mapper.hbs +6 -0
  125. package/dist/templates/prisma/prisma-repository.hbs +57 -5
  126. package/dist/templates/prisma/prisma-schema.hbs +2 -0
  127. package/dist/templates/query/get-all.query.hbs +10 -2
  128. package/dist/templates/recipes/bounded-json-http/README.md.hbs +36 -0
  129. package/dist/templates/recipes/external-projection-worker/README.md.hbs +157 -0
  130. package/dist/templates/recipes/external-projection-worker/canonical-json.ts.hbs +123 -0
  131. package/dist/templates/recipes/external-projection-worker/enqueuer.ts.hbs +167 -0
  132. package/dist/templates/recipes/external-projection-worker/index.ts.hbs +6 -0
  133. package/dist/templates/recipes/external-projection-worker/migration.ts.hbs +88 -0
  134. package/dist/templates/recipes/external-projection-worker/module.ts.hbs +36 -0
  135. package/dist/templates/recipes/external-projection-worker/store.ts.hbs +354 -0
  136. package/dist/templates/recipes/external-projection-worker/types.ts.hbs +134 -0
  137. package/dist/templates/recipes/external-projection-worker/worker.ts.hbs +296 -0
  138. package/dist/templates/recipes/kafka-consumer/README.md.hbs +50 -0
  139. package/dist/templates/recipes/kafka-consumer/kafka-consumer.ts.hbs +135 -0
  140. package/dist/templates/recipes/kafka-consumer/migration.ts.hbs +17 -0
  141. package/dist/templates/recipes/oauth2-client-credentials/README.md.hbs +40 -0
  142. package/dist/templates/recipes/oauth2-client-credentials/client.spec.ts.hbs +234 -0
  143. package/dist/templates/recipes/oauth2-client-credentials/client.ts.hbs +210 -0
  144. package/dist/templates/recipes/oauth2-client-credentials/index.ts.hbs +2 -0
  145. package/dist/templates/recipes/oauth2-client-credentials/module.ts.hbs +58 -0
  146. package/dist/templates/recipes/oauth2-token-introspection/README.md.hbs +158 -0
  147. package/dist/templates/recipes/oauth2-token-introspection/client.spec.ts.hbs +350 -0
  148. package/dist/templates/recipes/oauth2-token-introspection/client.ts.hbs +241 -0
  149. package/dist/templates/recipes/oauth2-token-introspection/http-client.spec.ts.hbs +411 -0
  150. package/dist/templates/recipes/oauth2-token-introspection/http-client.ts.hbs +343 -0
  151. package/dist/templates/recipes/oauth2-token-introspection/index.ts.hbs +3 -0
  152. package/dist/templates/recipes/oauth2-token-introspection/module.ts.hbs +58 -0
  153. package/dist/templates/recipes/oidc-bff-session/README.md.hbs +96 -0
  154. package/dist/templates/recipes/oidc-bff-session/client.test.ts.hbs +663 -0
  155. package/dist/templates/recipes/oidc-bff-session/client.ts.hbs +635 -0
  156. package/dist/templates/recipes/oidc-bff-session/index.ts.hbs +3 -0
  157. package/dist/templates/recipes/oidc-bff-session/store.integration.test.ts.hbs +480 -0
  158. package/dist/templates/recipes/oidc-bff-session/store.test.ts.hbs +87 -0
  159. package/dist/templates/recipes/oidc-bff-session/store.ts.hbs +549 -0
  160. package/dist/templates/recipes/oidc-bff-session/valkey.test.ts.hbs +158 -0
  161. package/dist/templates/recipes/oidc-bff-session/valkey.ts.hbs +148 -0
  162. package/dist/templates/recipes/oidc-resource-server/README.md.hbs +74 -0
  163. package/dist/templates/recipes/oidc-resource-server/index.ts.hbs +2 -0
  164. package/dist/templates/recipes/oidc-resource-server/module.ts.hbs +51 -0
  165. package/dist/templates/recipes/oidc-resource-server/verifier.spec.ts.hbs +186 -0
  166. package/dist/templates/recipes/oidc-resource-server/verifier.ts.hbs +440 -0
  167. package/dist/templates/recipes/platform-parc-bound-authorization/README.md.hbs +92 -0
  168. package/dist/templates/recipes/platform-parc-bound-authorization/client.ts.hbs +142 -0
  169. package/dist/templates/recipes/platform-service-access-request-context/docs.md.hbs +61 -0
  170. package/dist/templates/recipes/platform-service-access-request-context/env.example.hbs +7 -0
  171. package/dist/templates/recipes/platform-service-access-request-context/index.ts.hbs +7 -0
  172. package/dist/templates/recipes/platform-service-access-request-context/request-context-headers.ts.hbs +61 -0
  173. package/dist/templates/recipes/platform-service-access-request-context/request-context-introspection.parser.ts.hbs +132 -0
  174. package/dist/templates/recipes/platform-service-access-request-context/request-context-introspection.types.ts.hbs +40 -0
  175. package/dist/templates/recipes/platform-service-access-request-context/service-access-request-context.client.ts.hbs +45 -0
  176. package/dist/templates/recipes/platform-service-access-request-context/service-access-request-context.config.ts.hbs +142 -0
  177. package/dist/templates/recipes/platform-service-access-request-context/service-access-request-context.guard.ts.hbs +77 -0
  178. package/dist/templates/recipes/platform-service-access-request-context/service-access-request-context.module.ts.hbs +17 -0
  179. package/dist/templates/repository/repository.hbs +42 -4
  180. package/dist/templates/resolver/graphql-input.hbs +23 -6
  181. package/dist/templates/resolver/resolver.hbs +4 -0
  182. package/dist/templates/service/domain-service.hbs +2 -2
  183. package/dist/templates/test/controller.spec.hbs +5 -1
  184. package/dist/templates/test/prisma-repository.spec.hbs +192 -0
  185. package/dist/templates/test/repository.spec.hbs +40 -1
  186. package/dist/templates/test/usecase.spec.hbs +23 -4
  187. package/dist/templates/usecase/application-usecase.hbs +17 -0
  188. package/dist/templates/usecase/create-usecase.hbs +34 -3
  189. package/dist/templates/usecase/update-usecase.hbs +15 -1
  190. package/dist/utils/config.utils.d.ts +2 -0
  191. package/dist/utils/config.utils.js +14 -2
  192. package/dist/utils/config.utils.js.map +1 -1
  193. package/dist/utils/dependency.utils.d.ts +16 -2
  194. package/dist/utils/dependency.utils.js +100 -12
  195. package/dist/utils/dependency.utils.js.map +1 -1
  196. package/dist/utils/field.utils.d.ts +6 -0
  197. package/dist/utils/field.utils.js +50 -8
  198. package/dist/utils/field.utils.js.map +1 -1
  199. package/dist/utils/field.utils.spec.d.ts +1 -0
  200. package/dist/utils/field.utils.spec.js +38 -0
  201. package/dist/utils/field.utils.spec.js.map +1 -0
  202. package/dist/utils/file.utils.d.ts +23 -1
  203. package/dist/utils/file.utils.js +37 -1
  204. package/dist/utils/file.utils.js.map +1 -1
  205. package/dist/utils/naming.utils.js +5 -4
  206. package/dist/utils/naming.utils.js.map +1 -1
  207. package/dist/utils/static-recipe.utils.d.ts +4 -0
  208. package/dist/utils/static-recipe.utils.js +148 -0
  209. package/dist/utils/static-recipe.utils.js.map +1 -0
  210. package/package.json +12 -5
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  Body,
3
3
  Controller,
4
+ {{#if deleteEnabled}}
4
5
  Delete,
6
+ {{/if}}
5
7
  Get,
6
8
  HttpCode,
7
9
  HttpStatus,
@@ -15,7 +17,9 @@ import { CommandBus, QueryBus } from "@nestjs/cqrs";
15
17
  import { ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from "@nestjs/swagger";
16
18
  import { Create{{entityNamePascal}}Command } from "@modules/{{moduleNameKebab}}/application/commands/create-{{entityNameKebab}}.command";
17
19
  import { Update{{entityNamePascal}}Command } from "@modules/{{moduleNameKebab}}/application/commands/update-{{entityNameKebab}}.command";
20
+ {{#if deleteEnabled}}
18
21
  import { Delete{{entityNamePascal}}Command } from "@modules/{{moduleNameKebab}}/application/commands/delete-{{entityNameKebab}}.command";
22
+ {{/if}}
19
23
  import { Get{{entityNamePascal}}ByIdQuery } from "@modules/{{moduleNameKebab}}/application/queries/get-{{entityNameKebab}}-by-id.query";
20
24
  import { GetAll{{entityNamePluralPascal}}Query } from "@modules/{{moduleNameKebab}}/application/queries/get-all-{{entityNamePluralKebab}}.query";
21
25
  import { Create{{entityNamePascal}}Dto } from "@modules/{{moduleNameKebab}}/application/dto/requests/create-{{entityNameKebab}}.dto";
@@ -77,6 +81,7 @@ export class {{entityNamePascal}}Controller {
77
81
  return this.commandBus.execute(command);
78
82
  }
79
83
 
84
+ {{#if deleteEnabled}}
80
85
  @Delete(":id")
81
86
  @HttpCode(HttpStatus.NO_CONTENT)
82
87
  @ApiOperation({ summary: "Delete {{entityNameCamel}}" })
@@ -87,4 +92,5 @@ export class {{entityNamePascal}}Controller {
87
92
  const command = new Delete{{entityNamePascal}}Command(id);
88
93
  return this.commandBus.execute(command);
89
94
  }
95
+ {{/if}}
90
96
  }
@@ -3,33 +3,44 @@ import {
3
3
  IsArray,
4
4
  IsBoolean,
5
5
  IsDate,
6
+ IsDecimal,
6
7
  IsDefined,
7
8
  IsEmail,
8
9
  IsIn,
9
10
  IsInt,
10
11
  IsNotEmpty,
11
12
  IsNumber,
13
+ IsObject,
12
14
  IsOptional,
13
15
  IsString,
14
16
  IsUUID,
15
17
  IsUrl,
18
+ ArrayMaxSize,
19
+ Matches,
20
+ Max,
21
+ MaxLength,
22
+ Min,
23
+ MinLength,
16
24
  } from "class-validator";
17
25
  import { Type } from "class-transformer";
18
26
 
19
27
  export class Create{{entityNamePascal}}Dto {
20
- {{#if hasFields}}
21
- {{#each fields}}
28
+ {{#if hasRequestFields}}
29
+ {{#each requestFields}}
22
30
  {{#if this.isOptional}}
23
- @ApiPropertyOptional({ description: "{{this.description}}"{{#if this.example}}, example: "{{this.example}}"{{/if}} })
31
+ @ApiPropertyOptional({ description: "{{this.description}}"{{#if this.isMoney}}, type: String, format: "decimal"{{/if}}{{#if this.example}}, example: "{{this.example}}"{{/if}} })
24
32
  {{else}}
25
- @ApiProperty({ description: "{{this.description}}"{{#if this.example}}, example: "{{this.example}}"{{/if}} })
33
+ @ApiProperty({ description: "{{this.description}}"{{#if this.isMoney}}, type: String, format: "decimal"{{/if}}{{#if this.example}}, example: "{{this.example}}"{{/if}} })
26
34
  {{/if}}
27
35
  {{#each this.validators}}
28
36
  {{this}}
29
37
  {{/each}}
30
- {{this.snakeCase}}{{#if this.isOptional}}?{{/if}}: {{this.tsType}};
38
+ {{this.camelCase}}{{#if this.isOptional}}?{{/if}}: {{this.tsType}};
31
39
 
32
40
  {{/each}}
41
+ {{else}}
42
+ {{#if hasFields}}
43
+ // This aggregate has no client-writable scaffold fields.
33
44
  {{else}}
34
45
  // Add your DTO properties here in snake_case
35
46
  // Example:
@@ -39,4 +50,5 @@ export class Create{{entityNamePascal}}Dto {
39
50
  // @IsDefined()
40
51
  // name: string;
41
52
  {{/if}}
53
+ {{/if}}
42
54
  }
@@ -1,7 +1,7 @@
1
1
  import { ApiPropertyOptional } from "@nestjs/swagger";
2
2
  import { IsOptional, IsString, IsBoolean, IsDate, IsUUID } from "class-validator";
3
3
  import { Type } from "class-transformer";
4
- import { PaginationQueryDto } from "./pagination.query.dto";
4
+ import { PaginationQueryDto } from "./pagination-query.dto";
5
5
 
6
6
  export class {{entityNamePascal}}FilterDto extends PaginationQueryDto {
7
7
  @ApiPropertyOptional({ description: "Search by ID" })
@@ -8,7 +8,7 @@ export class {{entityNamePascal}}ResponseDto {
8
8
 
9
9
  {{#if hasFields}}
10
10
  {{#each fields}}
11
- @ApiProperty({ description: "{{this.description}}"{{#if this.example}}, example: "{{this.example}}"{{/if}} })
11
+ @ApiProperty({ description: "{{this.description}}"{{#if this.isMoney}}, type: String, format: "decimal"{{/if}}{{#if this.example}}, example: "{{this.example}}"{{/if}} })
12
12
  @Expose()
13
13
  {{this.camelCase}}: {{this.tsType}};
14
14
 
@@ -3,7 +3,9 @@ export interface {{entityNamePascal}}EntityProps {
3
3
  isActive?: boolean;
4
4
  createdAt?: Date;
5
5
  updatedAt?: Date;
6
+ {{#if softDelete}}
6
7
  deletedAt?: Date;
8
+ {{/if}}
7
9
  {{#if hasFields}}
8
10
  {{#each fields}}
9
11
  {{this.camelCase}}{{#if this.isOptional}}?{{/if}}: {{this.tsType}};
@@ -19,7 +21,9 @@ export class {{entityNamePascal}}Entity {
19
21
  public readonly isActive?: boolean;
20
22
  public readonly createdAt?: Date;
21
23
  public readonly updatedAt?: Date;
24
+ {{#if softDelete}}
22
25
  public readonly deletedAt?: Date;
26
+ {{/if}}
23
27
  {{#if hasFields}}
24
28
  {{#each fields}}
25
29
  public readonly {{this.camelCase}}{{#if this.isOptional}}?{{/if}}: {{this.tsType}};
@@ -15,7 +15,9 @@ export class {{entityNamePascal}}Mapper {
15
15
  isActive: ormEntity.isActive,
16
16
  createdAt: ormEntity.createdAt,
17
17
  updatedAt: ormEntity.updatedAt,
18
- deletedAt: ormEntity.deletedAt,
18
+ {{#if softDelete}}
19
+ deletedAt: ormEntity.deletedAt ?? undefined,
20
+ {{/if}}
19
21
  // Map additional fields from ORM entity (snake_case) to domain entity (camelCase)
20
22
  {{#each fields}}
21
23
  {{this.camelCase}}: ormEntity.{{this.snakeCase}},
@@ -33,7 +35,9 @@ export class {{entityNamePascal}}Mapper {
33
35
  isActive: domainEntity.isActive,
34
36
  createdAt: domainEntity.createdAt,
35
37
  updatedAt: domainEntity.updatedAt,
38
+ {{#if softDelete}}
36
39
  deletedAt: domainEntity.deletedAt,
40
+ {{/if}}
37
41
  // Map additional fields from domain entity (camelCase) to ORM entity (snake_case)
38
42
  {{#each fields}}
39
43
  {{this.snakeCase}}: domainEntity.{{this.camelCase}},
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  Column,
3
3
  CreateDateColumn,
4
+ {{#if softDelete}}
4
5
  DeleteDateColumn,
6
+ {{/if}}
5
7
  Entity,
6
8
  PrimaryGeneratedColumn,
7
9
  UpdateDateColumn,
@@ -49,8 +51,12 @@ export class {{entityNamePascal}}OrmEntity {
49
51
  {{this.camelCase}}: {{this.relationTarget}}OrmEntity[];
50
52
 
51
53
  {{/if}}
54
+ {{else}}
55
+ {{#if this.isMoney}}
56
+ @Column({ type: "decimal"{{#if this.isUnique}}, unique: true{{/if}}{{#if this.isOptional}}, nullable: true{{/if}} })
52
57
  {{else}}
53
58
  @Column({{#if this.isUnique}}{ unique: true{{#if this.isOptional}}, nullable: true{{/if}} }{{else}}{{#if this.isOptional}}{ nullable: true }{{/if}}{{/if}})
59
+ {{/if}}
54
60
  {{this.snakeCase}}{{#if this.isOptional}}?{{/if}}: {{this.tsType}};
55
61
 
56
62
  {{/if}}
@@ -70,6 +76,8 @@ export class {{entityNamePascal}}OrmEntity {
70
76
  @UpdateDateColumn({ name: "updated_at" })
71
77
  updatedAt: Date;
72
78
 
79
+ {{#if softDelete}}
73
80
  @DeleteDateColumn({ name: "deleted_at" })
74
- deletedAt?: Date;
81
+ deletedAt?: Date | null;
82
+ {{/if}}
75
83
  }
@@ -11,10 +11,16 @@ export class {{entityNamePascal}}Mapper {
11
11
  isActive: prismaEntity.is_active,
12
12
  createdAt: prismaEntity.created_at,
13
13
  updatedAt: prismaEntity.updated_at,
14
+ {{#if softDelete}}
14
15
  deletedAt: prismaEntity.deleted_at ?? undefined,
16
+ {{/if}}
15
17
  {{#if hasFields}}
16
18
  {{#each fields}}
19
+ {{#if this.isMoney}}
20
+ {{this.camelCase}}: prismaEntity.{{this.snakeCase}}{{#if this.isOptional}}?{{/if}}.toString(),
21
+ {{else}}
17
22
  {{this.camelCase}}: prismaEntity.{{this.snakeCase}},
23
+ {{/if}}
18
24
  {{/each}}
19
25
  {{else}}
20
26
  // Map your Prisma fields to domain entity
@@ -4,11 +4,23 @@ import { {{entityNamePascal}}Entity } from "@modules/{{moduleNameKebab}}/applica
4
4
  import { {{entityNamePascal}}Mapper } from "@modules/{{moduleNameKebab}}/infrastructure/mappers/{{entityNameKebab}}.mapper";
5
5
  import { Prisma } from "@prisma/client";
6
6
 
7
- export interface PaginationOptions {
7
+ const {{entityNameCamel}}OrderFields = [
8
+ "id",
9
+ {{#each fields}}
10
+ "{{this.snakeCase}}",
11
+ {{/each}}
12
+ "is_active",
13
+ "created_at",
14
+ "updated_at",
15
+ ] as const satisfies readonly (keyof Prisma.{{entityNamePascal}}OrderByWithRelationInput)[];
16
+
17
+ export type {{entityNamePascal}}OrderField = (typeof {{entityNameCamel}}OrderFields)[number];
18
+
19
+ export interface {{entityNamePascal}}PaginationOptions {
8
20
  skip: number;
9
21
  take: number;
10
- orderBy: string;
11
- orderDirection: "asc" | "desc";
22
+ orderBy: {{entityNamePascal}}OrderField;
23
+ orderDirection: "ASC" | "DESC";
12
24
  }
13
25
 
14
26
  @Injectable()
@@ -18,6 +30,10 @@ export class {{entityNamePascal}}Repository {
18
30
  private readonly mapper: {{entityNamePascal}}Mapper,
19
31
  ) {}
20
32
 
33
+ static isOrderByField(value: string): value is {{entityNamePascal}}OrderField {
34
+ return {{entityNameCamel}}OrderFields.some((field) => field === value);
35
+ }
36
+
21
37
  async create(entity: {{entityNamePascal}}Entity): Promise<{{entityNamePascal}}Entity> {
22
38
  const data = this.mapper.toPrismaCreate(entity);
23
39
  const created = await this.prisma.{{entityNameCamel}}.create({ data });
@@ -26,7 +42,11 @@ export class {{entityNamePascal}}Repository {
26
42
 
27
43
  async findById(id: string): Promise<{{entityNamePascal}}Entity | null> {
28
44
  const record = await this.prisma.{{entityNameCamel}}.findFirst({
45
+ {{#if softDelete}}
29
46
  where: { id, deleted_at: null },
47
+ {{else}}
48
+ where: { id },
49
+ {{/if}}
30
50
  });
31
51
 
32
52
  if (!record) {
@@ -37,28 +57,41 @@ export class {{entityNamePascal}}Repository {
37
57
  }
38
58
 
39
59
  async findAll(): Promise<{{entityNamePascal}}Entity[]> {
60
+ {{#if softDelete}}
40
61
  const records = await this.prisma.{{entityNameCamel}}.findMany({
41
62
  where: { deleted_at: null },
42
63
  });
64
+ {{else}}
65
+ const records = await this.prisma.{{entityNameCamel}}.findMany();
66
+ {{/if}}
43
67
 
44
68
  return records.map((record) => this.mapper.toDomainEntity(record));
45
69
  }
46
70
 
47
71
  async findAllPaginated(
48
- options: PaginationOptions,
72
+ options: {{entityNamePascal}}PaginationOptions,
49
73
  ): Promise<[{{entityNamePascal}}Entity[], number]> {
50
74
  const { skip, take, orderBy, orderDirection } = options;
75
+ const order: Prisma.{{entityNamePascal}}OrderByWithRelationInput = {
76
+ [orderBy]: orderDirection.toLowerCase() as Prisma.SortOrder,
77
+ };
51
78
 
52
79
  const [records, total] = await Promise.all([
53
80
  this.prisma.{{entityNameCamel}}.findMany({
81
+ {{#if softDelete}}
54
82
  where: { deleted_at: null },
83
+ {{/if}}
55
84
  skip,
56
85
  take,
57
- orderBy: { [orderBy]: orderDirection },
86
+ orderBy: order,
58
87
  }),
88
+ {{#if softDelete}}
59
89
  this.prisma.{{entityNameCamel}}.count({
60
90
  where: { deleted_at: null },
61
91
  }),
92
+ {{else}}
93
+ this.prisma.{{entityNameCamel}}.count(),
94
+ {{/if}}
62
95
  ]);
63
96
 
64
97
  const entities = records.map((record) => this.mapper.toDomainEntity(record));
@@ -74,7 +107,9 @@ export class {{entityNamePascal}}Repository {
74
107
  return this.mapper.toDomainEntity(updated);
75
108
  }
76
109
 
110
+ {{#if deleteEnabled}}
77
111
  async delete(id: string): Promise<void> {
112
+ {{#if softDelete}}
78
113
  await this.prisma.{{entityNameCamel}}.update({
79
114
  where: { id },
80
115
  data: {
@@ -82,17 +117,30 @@ export class {{entityNamePascal}}Repository {
82
117
  is_active: false,
83
118
  },
84
119
  });
120
+ {{else}}
121
+ await this.prisma.{{entityNameCamel}}.delete({
122
+ where: { id },
123
+ });
124
+ {{/if}}
85
125
  }
86
126
 
127
+ {{#if hardDelete}}
87
128
  async hardDelete(id: string): Promise<void> {
88
129
  await this.prisma.{{entityNameCamel}}.delete({
89
130
  where: { id },
90
131
  });
91
132
  }
92
133
 
134
+ {{/if}}
135
+
136
+ {{/if}}
93
137
  async exists(id: string): Promise<boolean> {
94
138
  const count = await this.prisma.{{entityNameCamel}}.count({
139
+ {{#if softDelete}}
95
140
  where: { id, deleted_at: null },
141
+ {{else}}
142
+ where: { id },
143
+ {{/if}}
96
144
  });
97
145
  return count > 0;
98
146
  }
@@ -102,7 +150,11 @@ export class {{entityNamePascal}}Repository {
102
150
  value: Prisma.{{entityNamePascal}}WhereInput[K],
103
151
  ): Promise<{{entityNamePascal}}Entity | null> {
104
152
  const record = await this.prisma.{{entityNameCamel}}.findFirst({
153
+ {{#if softDelete}}
105
154
  where: { [field]: value, deleted_at: null },
155
+ {{else}}
156
+ where: { [field]: value },
157
+ {{/if}}
106
158
  });
107
159
 
108
160
  if (!record) {
@@ -14,7 +14,9 @@ model {{entityNamePascal}} {
14
14
  is_active Boolean @default(true)
15
15
  created_at DateTime @default(now())
16
16
  updated_at DateTime @updatedAt
17
+ {{#if softDelete}}
17
18
  deleted_at DateTime?
19
+ {{/if}}
18
20
 
19
21
  @@map("{{tableName}}")
20
22
  }
@@ -22,14 +22,22 @@ export class GetAll{{entityNamePluralPascal}}Handler
22
22
  query: GetAll{{entityNamePluralPascal}}Query,
23
23
  ): Promise<PaginatedResponseDto<{{entityNamePascal}}ResponseDto>> {
24
24
  const { pagination } = query;
25
- const { page = 1, limit = 10, sortBy = "createdAt", sortOrder = "DESC" } = pagination;
25
+ const {
26
+ page = 1,
27
+ limit = 10,
28
+ sortBy = {{#if isPrisma}}"created_at"{{else}}"createdAt"{{/if}},
29
+ sortOrder = "DESC",
30
+ } = pagination;
26
31
 
27
32
  const skip = (page - 1) * limit;
33
+ const orderBy = {{entityNamePascal}}Repository.isOrderByField(sortBy)
34
+ ? sortBy
35
+ : {{#if isPrisma}}"created_at"{{else}}"createdAt"{{/if}};
28
36
 
29
37
  const [entities, total] = await this.repository.findAllPaginated({
30
38
  skip,
31
39
  take: limit,
32
- orderBy: sortBy,
40
+ orderBy,
33
41
  orderDirection: sortOrder,
34
42
  });
35
43
 
@@ -0,0 +1,36 @@
1
+ # Bounded JSON HTTP transport
2
+
3
+ ```sh
4
+ ddd recipe bounded-json-http --path <application> --dry-run
5
+ ddd recipe bounded-json-http --path <application>
6
+ ```
7
+
8
+ This emits only the reusable transport and this handoff. It requires the native
9
+ Fetch, Headers, Web Streams and TextEncoder APIs supplied by Node 18+ or Bun. It
10
+ does not emit a Nest module, install packages, register routes, validate a human
11
+ identity or grant domain permissions. Import the client directly from
12
+ `src/shared/auth/oauth2-token-introspection/bounded-json-http.client.ts`.
13
+
14
+ The output path and unchanged template are shared with the OAuth recipes. A
15
+ matching existing file is preserved; a conflicting file, symlink or unsafe
16
+ destination rejects the complete plan before writes. Dry-run performs the same
17
+ preflight without creating files or directories. If later filesystem operations
18
+ fail, rollback preserves a concurrent owner's successor content and reports a
19
+ rollback conflict.
20
+
21
+ Supply a fixed, validated origin from deployment configuration. HTTPS is the
22
+ default; `trustedInternalHttp: true` is an explicit operator-owned exception for
23
+ a proven private HTTP service. Dynamic path segments are encoded individually.
24
+ Do not accept origins, authorization headers or arbitrary paths from callers.
25
+ Constructor headers are snapshotted and private. Request policies use bounded
26
+ deadlines and byte limits, refuse redirects and do not retry or positively cache
27
+ responses. Errors expose a safe category/status, never credentials or bodies.
28
+
29
+ `get`, `postForm`, `postJson`, `putJson` and `patchJson` return immutable JSON evidence. The
30
+ consumer must validate its exact schema and distinguish an authoritative denial
31
+ from an unavailable or malformed provider. A successful transport response does
32
+ not admit an account, link identities, complete a callback or authorize a write.
33
+ Provider-specific token and membership policy belongs to the owning application.
34
+ Use the owning application's tests to prove those decisions and verify this
35
+ mechanism under its supported runtime. Next.js consumers should add their own
36
+ `server-only` wrapper; the framework-neutral generated file has no Next import.
@@ -0,0 +1,157 @@
1
+ # External projection worker
2
+
3
+ Generated by `ddd recipe external-projection-worker`. This is generic
4
+ PostgreSQL/TypeORM infrastructure for committing an external-system projection
5
+ intent with domain state, then reconciling it outside the request transaction.
6
+ It does not define provider calls, business roles, authorization, Kafka policy,
7
+ or a source-of-truth aggregate.
8
+
9
+ ## Generate and integrate
10
+
11
+ Use a reviewed, explicit migration timestamp so every checkout emits the same
12
+ migration identity:
13
+
14
+ ```bash
15
+ ddd recipe external-projection-worker \
16
+ --migration-timestamp 1790000000000 \
17
+ --dry-run
18
+ ddd recipe external-projection-worker \
19
+ --migration-timestamp 1790000000000 \
20
+ --install-deps
21
+ ```
22
+
23
+ The recipe supports only `.dddrc.json` configurations with `orm: "typeorm"`
24
+ and `database: "postgres"`. It honors `paths.shared` and `paths.migrations`,
25
+ preflights every output before writing, refuses symlink traversal, and will not
26
+ create a second migration under a different timestamp. An identical rerun is a
27
+ no-op; an adapted target is left untouched and reported as a conflict. Recipe
28
+ generation is locked per project, and dependency installation begins only after
29
+ the timestamp, schema, symlink, migration, and target conflict checks pass.
30
+
31
+ 1. Review and register the generated migration in the service's existing
32
+ migration runner before enabling the worker.
33
+ 2. Generate the application adapter with `ddd generate service`. Implement
34
+ `ExternalProjectionHandler`, export it from its owning module, and import that
35
+ module through `ExternalProjectionWorkerModule.register(...)`. Do not
36
+ redeclare the handler in the worker module. The imported composition must
37
+ also expose TypeORM's default `DataSource` provider to the worker module.
38
+ 3. Enable Nest shutdown hooks. Supply bounded settings from validated service
39
+ configuration. Keep `enabled: false` until the migration is applied.
40
+ 4. Export `ExternalProjectionWorkerModule` only from the composition root that
41
+ owns this worker. Run one logical projection name per handler contract.
42
+
43
+ ```ts
44
+ ExternalProjectionWorkerModule.register(
45
+ {
46
+ projectionName: 'directory-v1',
47
+ enabled: true,
48
+ pollIntervalMs: 1_000,
49
+ batchSize: 25,
50
+ leaseSeconds: 30,
51
+ maxAttempts: 8,
52
+ retryBaseMs: 1_000,
53
+ retryMaxMs: 300_000,
54
+ },
55
+ DirectoryProjectionHandler,
56
+ [DirectoryProjectionHandlerModule],
57
+ );
58
+ ```
59
+
60
+ ## Atomic enqueue is mandatory
61
+
62
+ `enqueue` intentionally accepts an `EntityManager`, not a `DataSource`, and
63
+ rejects a manager without an active transaction-bound `QueryRunner`. Call it
64
+ inside the same transaction that commits the owning domain change:
65
+
66
+ ```ts
67
+ await dataSource.transaction(async (manager) => {
68
+ await writeOwnedDomainState(manager, command);
69
+ await projectionEnqueuer.enqueue(manager, {
70
+ projectionName: 'directory-v1',
71
+ targetKey: command.subjectId,
72
+ operationKind: 'upsert',
73
+ idempotencyKey: command.commandId,
74
+ payload: command.publicProjection,
75
+ });
76
+ });
77
+ ```
78
+
79
+ The payload accepts only bounded plain JSON. Keys are sorted recursively before
80
+ hashing. Repeating a projection/idempotency key with the same projection name,
81
+ target, operation and payload returns the stored operation; reusing it for
82
+ different content fails closed. Never place credentials, tokens, clinical
83
+ documents, or other secrets in projection payloads or error codes.
84
+
85
+ Enqueue takes an exact `(projection_name, target_key)` row lock through the
86
+ durable target-head table before allocating `enqueue_sequence`. This prevents a
87
+ later transaction from committing a visible same-target operation while an
88
+ earlier enqueue is still uncommitted. If one owning transaction enqueues several
89
+ targets, call `enqueue` in stable projection-name/target-key order so independent
90
+ transactions cannot create a lock-order cycle. Direct inserts bypass this
91
+ contract and are unsupported.
92
+
93
+ Canonical application JSON remains limited to 256 KiB and 10,000 nodes. The
94
+ database allows up to 4 MiB for `jsonb::text` because PostgreSQL expands valid
95
+ JavaScript exponent notation (for example `1e+308`) when rendering `jsonb`.
96
+ The larger database envelope is representation headroom, not a larger public
97
+ payload limit.
98
+
99
+ ## Lifecycle and external effects
100
+
101
+ - `pending`: committed intent awaiting a worker.
102
+ - `processing`: one live lease owns the attempt. Claims use `FOR UPDATE SKIP
103
+ LOCKED`; renewal and completion require the current owner, token and an
104
+ unexpired lease.
105
+ - `retry_wait`: a known retryable failure, with a bounded next-attempt time.
106
+ - `uncertain`: the external result is unknown. On the next attempt, the handler
107
+ must read back using stable provider identifiers before repeating a write.
108
+ - `blocked`: terminal operator action is required.
109
+ - `applied`: reconciliation completed and may retain a non-secret provider
110
+ reference.
111
+
112
+ Each worker is bound to exactly one configured `projectionName`; it cannot
113
+ claim another handler's rows. Transactional target-head locking serializes
114
+ sequence allocation, then claims enforce `(projection_name, target_key)`
115
+ `enqueue_sequence` order: a later operation cannot overtake any earlier
116
+ operation that has not reached `applied`. A `blocked` operation deliberately
117
+ remains an ordering barrier until an owning operator resolves it.
118
+ Attempts exhausted by repeated lease expiry are changed to `blocked` before
119
+ another handler call.
120
+
121
+ The handler has separate entry points. `execute` receives only fresh or known
122
+ retryable work. `reconcile` receives `uncertain_recovery` and
123
+ `expired_lease_recovery` claims, including their prior status and prior error
124
+ when one exists. Reconciliation must read the external system with stable
125
+ identifiers before deciding whether any write is still required. The worker
126
+ never routes recovery claims through `execute`; a retryable reconciliation
127
+ failure remains `uncertain` so the following attempt also enters `reconcile`.
128
+ An unclassified handler exception is also `uncertain`, because it may have been
129
+ raised after the external system accepted a write. The same rule applies when
130
+ the handler returns but the final lease-renewal call throws: the operation must
131
+ be read back through `reconcile` and must never be dispatched to `execute`
132
+ again.
133
+
134
+ This is at-least-once orchestration, not exactly-once external delivery. The
135
+ handler must pass the durable operation id or another stable idempotency key to
136
+ the external provider when supported. A lost lease fences database completion;
137
+ it cannot undo an external side effect. Do not convert an unknown timeout into
138
+ success, and do not blindly repeat non-idempotent writes.
139
+
140
+ The worker runs one batch at a time per process, renews leases during handler
141
+ execution, stops scheduling before shutdown, and waits for the active batch.
142
+ Multiple replicas may run because claims use row locks and `SKIP LOCKED`.
143
+ An optional result reference is trimmed and must be a non-empty, control-free
144
+ string of at most 500 characters.
145
+
146
+ ## Acceptance before enablement
147
+
148
+ Prove against disposable PostgreSQL: migration up/down, same-key replay,
149
+ changed-payload conflict, an uncommitted-earlier/concurrent-later same-target
150
+ enqueue schedule, expanded exponent-number payloads, concurrent distinct claims,
151
+ expired-lease reclaim, same-target ordering, cross-projection routing, handler
152
+ and final-renewal uncertainty recovery, stale-owner completion rejection,
153
+ renewal, every disposition, crash-driven max-attempt blocking, and graceful
154
+ shutdown. Also prove the concrete provider adapter's
155
+ create/update/delete/readback behavior and its own target-ordering requirements.
156
+ Alert on old `pending`, `retry_wait`, `uncertain`, `blocked`, expired
157
+ `processing`, attempt count, and worker poll failures.