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.
- package/README.md +118 -9
- package/ddd.schema.json +11 -1
- package/dist/commands/doctor-enhanced.d.ts +1 -0
- package/dist/commands/doctor-enhanced.js +94 -79
- package/dist/commands/doctor-enhanced.js.map +1 -1
- package/dist/commands/doctor.d.ts +9 -0
- package/dist/commands/doctor.js +68 -21
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/generate-all.js +238 -176
- package/dist/commands/generate-all.js.map +1 -1
- package/dist/commands/generate-controller.d.ts +1 -0
- package/dist/commands/generate-controller.js +71 -0
- package/dist/commands/generate-controller.js.map +1 -0
- package/dist/commands/generate-domain-service.d.ts +1 -1
- package/dist/commands/generate-domain-service.js +6 -4
- package/dist/commands/generate-domain-service.js.map +1 -1
- package/dist/commands/generate-dto.d.ts +4 -0
- package/dist/commands/generate-dto.js +162 -0
- package/dist/commands/generate-dto.js.map +1 -0
- package/dist/commands/generate-entity.js +44 -35
- package/dist/commands/generate-entity.js.map +1 -1
- package/dist/commands/generate-event.js +10 -4
- package/dist/commands/generate-event.js.map +1 -1
- package/dist/commands/generate-migration-deployment.d.ts +15 -0
- package/dist/commands/generate-migration-deployment.js +326 -0
- package/dist/commands/generate-migration-deployment.js.map +1 -0
- package/dist/commands/generate-migration-deployment.spec.d.ts +1 -0
- package/dist/commands/generate-migration-deployment.spec.js +337 -0
- package/dist/commands/generate-migration-deployment.spec.js.map +1 -0
- package/dist/commands/generate-module.js +41 -31
- package/dist/commands/generate-module.js.map +1 -1
- package/dist/commands/generate-query.js +23 -4
- package/dist/commands/generate-query.js.map +1 -1
- package/dist/commands/generate-repository.d.ts +1 -1
- package/dist/commands/generate-repository.js +17 -34
- package/dist/commands/generate-repository.js.map +1 -1
- package/dist/commands/generate-service.js +19 -4
- package/dist/commands/generate-service.js.map +1 -1
- package/dist/commands/generate-usecase.js +12 -31
- package/dist/commands/generate-usecase.js.map +1 -1
- package/dist/commands/generate-value-object.js +3 -14
- package/dist/commands/generate-value-object.js.map +1 -1
- package/dist/commands/init-project.d.ts +3 -0
- package/dist/commands/init-project.js +19 -9
- package/dist/commands/init-project.js.map +1 -1
- package/dist/commands/interactive-scaffold.js +10 -0
- package/dist/commands/interactive-scaffold.js.map +1 -1
- package/dist/commands/migration.d.ts +3 -0
- package/dist/commands/migration.js +83 -48
- package/dist/commands/migration.js.map +1 -1
- package/dist/commands/recipe.d.ts +2 -0
- package/dist/commands/recipe.js +282 -91
- package/dist/commands/recipe.js.map +1 -1
- package/dist/commands/recipes/banklink-connector-contract.recipe.d.ts +1 -0
- package/dist/commands/recipes/banklink-connector-contract.recipe.js +161 -0
- package/dist/commands/recipes/banklink-connector-contract.recipe.js.map +1 -0
- package/dist/commands/recipes/bounded-json-http.recipe.d.ts +2 -0
- package/dist/commands/recipes/bounded-json-http.recipe.js +53 -0
- package/dist/commands/recipes/bounded-json-http.recipe.js.map +1 -0
- package/dist/commands/recipes/business-reference-identifiers.recipe.d.ts +1 -0
- package/dist/commands/recipes/business-reference-identifiers.recipe.js +223 -0
- package/dist/commands/recipes/business-reference-identifiers.recipe.js.map +1 -0
- package/dist/commands/recipes/event-backbone.recipe.d.ts +1 -0
- package/dist/commands/recipes/event-backbone.recipe.js +1118 -0
- package/dist/commands/recipes/event-backbone.recipe.js.map +1 -0
- package/dist/commands/recipes/external-projection-worker.recipe.d.ts +6 -0
- package/dist/commands/recipes/external-projection-worker.recipe.js +211 -0
- package/dist/commands/recipes/external-projection-worker.recipe.js.map +1 -0
- package/dist/commands/recipes/index.d.ts +12 -0
- package/dist/commands/recipes/index.js +12 -0
- package/dist/commands/recipes/index.js.map +1 -1
- package/dist/commands/recipes/jest-cjs-compat.recipe.d.ts +3 -0
- package/dist/commands/recipes/jest-cjs-compat.recipe.js +130 -0
- package/dist/commands/recipes/jest-cjs-compat.recipe.js.map +1 -0
- package/dist/commands/recipes/joi-env.recipe.d.ts +1 -0
- package/dist/commands/recipes/joi-env.recipe.js +301 -0
- package/dist/commands/recipes/joi-env.recipe.js.map +1 -0
- package/dist/commands/recipes/kafka-consumer.recipe.d.ts +1 -0
- package/dist/commands/recipes/kafka-consumer.recipe.js +65 -0
- package/dist/commands/recipes/kafka-consumer.recipe.js.map +1 -0
- package/dist/commands/recipes/oauth2-client-credentials.recipe.d.ts +1 -0
- package/dist/commands/recipes/oauth2-client-credentials.recipe.js +60 -0
- package/dist/commands/recipes/oauth2-client-credentials.recipe.js.map +1 -0
- package/dist/commands/recipes/oauth2-token-introspection.recipe.d.ts +1 -0
- package/dist/commands/recipes/oauth2-token-introspection.recipe.js +67 -0
- package/dist/commands/recipes/oauth2-token-introspection.recipe.js.map +1 -0
- package/dist/commands/recipes/oidc-bff-session.recipe.d.ts +1 -0
- package/dist/commands/recipes/oidc-bff-session.recipe.js +57 -0
- package/dist/commands/recipes/oidc-bff-session.recipe.js.map +1 -0
- package/dist/commands/recipes/oidc-dashboard.recipe.d.ts +1 -0
- package/dist/commands/recipes/oidc-dashboard.recipe.js +463 -0
- package/dist/commands/recipes/oidc-dashboard.recipe.js.map +1 -0
- package/dist/commands/recipes/oidc-resource-server.recipe.d.ts +1 -0
- package/dist/commands/recipes/oidc-resource-server.recipe.js +85 -0
- package/dist/commands/recipes/oidc-resource-server.recipe.js.map +1 -0
- package/dist/commands/recipes/platform-context.recipe.d.ts +1 -0
- package/dist/commands/recipes/platform-context.recipe.js +229 -0
- package/dist/commands/recipes/platform-context.recipe.js.map +1 -0
- package/dist/commands/recipes/platform-parc-authorization.recipe.d.ts +1 -0
- package/dist/commands/recipes/platform-parc-authorization.recipe.js +339 -0
- package/dist/commands/recipes/platform-parc-authorization.recipe.js.map +1 -0
- package/dist/commands/recipes/platform-parc-bound-authorization.recipe.d.ts +1 -0
- package/dist/commands/recipes/platform-parc-bound-authorization.recipe.js +56 -0
- package/dist/commands/recipes/platform-parc-bound-authorization.recipe.js.map +1 -0
- package/dist/commands/recipes/platform-service-access-request-context.recipe.d.ts +1 -0
- package/dist/commands/recipes/platform-service-access-request-context.recipe.js +103 -0
- package/dist/commands/recipes/platform-service-access-request-context.recipe.js.map +1 -0
- package/dist/commands/recipes/platform-service-runtime.recipe.d.ts +1 -0
- package/dist/commands/recipes/platform-service-runtime.recipe.js +415 -0
- package/dist/commands/recipes/platform-service-runtime.recipe.js.map +1 -0
- package/dist/index.js +82 -11
- package/dist/index.js.map +1 -1
- package/dist/templates/ai-context/CLAUDE.md.hbs +54 -3
- package/dist/templates/ai-context/conventions.md.hbs +20 -8
- package/dist/templates/command/create-command.hbs +8 -3
- package/dist/templates/command/delete-command.hbs +1 -1
- package/dist/templates/command/update-command.hbs +1 -1
- package/dist/templates/controller/application-controller.hbs +7 -0
- package/dist/templates/controller/controller.hbs +6 -0
- package/dist/templates/dto/create-dto.hbs +17 -5
- package/dist/templates/dto/filter-dto.hbs +1 -1
- package/dist/templates/dto/response-dto.hbs +1 -1
- package/dist/templates/entity/entity.hbs +4 -0
- package/dist/templates/mapper/mapper.hbs +6 -2
- package/dist/templates/orm-entity/orm-entity.hbs +9 -1
- package/dist/templates/prisma/prisma-mapper.hbs +6 -0
- package/dist/templates/prisma/prisma-repository.hbs +57 -5
- package/dist/templates/prisma/prisma-schema.hbs +2 -0
- package/dist/templates/query/get-all.query.hbs +10 -2
- package/dist/templates/recipes/bounded-json-http/README.md.hbs +36 -0
- package/dist/templates/recipes/external-projection-worker/README.md.hbs +157 -0
- package/dist/templates/recipes/external-projection-worker/canonical-json.ts.hbs +123 -0
- package/dist/templates/recipes/external-projection-worker/enqueuer.ts.hbs +167 -0
- package/dist/templates/recipes/external-projection-worker/index.ts.hbs +6 -0
- package/dist/templates/recipes/external-projection-worker/migration.ts.hbs +88 -0
- package/dist/templates/recipes/external-projection-worker/module.ts.hbs +36 -0
- package/dist/templates/recipes/external-projection-worker/store.ts.hbs +354 -0
- package/dist/templates/recipes/external-projection-worker/types.ts.hbs +134 -0
- package/dist/templates/recipes/external-projection-worker/worker.ts.hbs +296 -0
- package/dist/templates/recipes/kafka-consumer/README.md.hbs +50 -0
- package/dist/templates/recipes/kafka-consumer/kafka-consumer.ts.hbs +135 -0
- package/dist/templates/recipes/kafka-consumer/migration.ts.hbs +17 -0
- package/dist/templates/recipes/oauth2-client-credentials/README.md.hbs +40 -0
- package/dist/templates/recipes/oauth2-client-credentials/client.spec.ts.hbs +234 -0
- package/dist/templates/recipes/oauth2-client-credentials/client.ts.hbs +210 -0
- package/dist/templates/recipes/oauth2-client-credentials/index.ts.hbs +2 -0
- package/dist/templates/recipes/oauth2-client-credentials/module.ts.hbs +58 -0
- package/dist/templates/recipes/oauth2-token-introspection/README.md.hbs +158 -0
- package/dist/templates/recipes/oauth2-token-introspection/client.spec.ts.hbs +350 -0
- package/dist/templates/recipes/oauth2-token-introspection/client.ts.hbs +241 -0
- package/dist/templates/recipes/oauth2-token-introspection/http-client.spec.ts.hbs +411 -0
- package/dist/templates/recipes/oauth2-token-introspection/http-client.ts.hbs +343 -0
- package/dist/templates/recipes/oauth2-token-introspection/index.ts.hbs +3 -0
- package/dist/templates/recipes/oauth2-token-introspection/module.ts.hbs +58 -0
- package/dist/templates/recipes/oidc-bff-session/README.md.hbs +96 -0
- package/dist/templates/recipes/oidc-bff-session/client.test.ts.hbs +663 -0
- package/dist/templates/recipes/oidc-bff-session/client.ts.hbs +635 -0
- package/dist/templates/recipes/oidc-bff-session/index.ts.hbs +3 -0
- package/dist/templates/recipes/oidc-bff-session/store.integration.test.ts.hbs +480 -0
- package/dist/templates/recipes/oidc-bff-session/store.test.ts.hbs +87 -0
- package/dist/templates/recipes/oidc-bff-session/store.ts.hbs +549 -0
- package/dist/templates/recipes/oidc-bff-session/valkey.test.ts.hbs +158 -0
- package/dist/templates/recipes/oidc-bff-session/valkey.ts.hbs +148 -0
- package/dist/templates/recipes/oidc-resource-server/README.md.hbs +74 -0
- package/dist/templates/recipes/oidc-resource-server/index.ts.hbs +2 -0
- package/dist/templates/recipes/oidc-resource-server/module.ts.hbs +51 -0
- package/dist/templates/recipes/oidc-resource-server/verifier.spec.ts.hbs +186 -0
- package/dist/templates/recipes/oidc-resource-server/verifier.ts.hbs +440 -0
- package/dist/templates/recipes/platform-parc-bound-authorization/README.md.hbs +92 -0
- package/dist/templates/recipes/platform-parc-bound-authorization/client.ts.hbs +142 -0
- package/dist/templates/recipes/platform-service-access-request-context/docs.md.hbs +61 -0
- package/dist/templates/recipes/platform-service-access-request-context/env.example.hbs +7 -0
- package/dist/templates/recipes/platform-service-access-request-context/index.ts.hbs +7 -0
- package/dist/templates/recipes/platform-service-access-request-context/request-context-headers.ts.hbs +61 -0
- package/dist/templates/recipes/platform-service-access-request-context/request-context-introspection.parser.ts.hbs +132 -0
- package/dist/templates/recipes/platform-service-access-request-context/request-context-introspection.types.ts.hbs +40 -0
- package/dist/templates/recipes/platform-service-access-request-context/service-access-request-context.client.ts.hbs +45 -0
- package/dist/templates/recipes/platform-service-access-request-context/service-access-request-context.config.ts.hbs +142 -0
- package/dist/templates/recipes/platform-service-access-request-context/service-access-request-context.guard.ts.hbs +77 -0
- package/dist/templates/recipes/platform-service-access-request-context/service-access-request-context.module.ts.hbs +17 -0
- package/dist/templates/repository/repository.hbs +43 -5
- package/dist/templates/resolver/graphql-input.hbs +23 -6
- package/dist/templates/resolver/resolver.hbs +4 -0
- package/dist/templates/service/domain-service.hbs +2 -2
- package/dist/templates/test/controller.spec.hbs +5 -1
- package/dist/templates/test/prisma-repository.spec.hbs +192 -0
- package/dist/templates/test/repository.spec.hbs +41 -2
- package/dist/templates/test/usecase.spec.hbs +24 -5
- package/dist/templates/usecase/application-usecase.hbs +17 -0
- package/dist/templates/usecase/create-usecase.hbs +34 -3
- package/dist/templates/usecase/update-usecase.hbs +15 -1
- package/dist/utils/config.utils.d.ts +2 -0
- package/dist/utils/config.utils.js +14 -2
- package/dist/utils/config.utils.js.map +1 -1
- package/dist/utils/dependency.utils.d.ts +18 -2
- package/dist/utils/dependency.utils.js +144 -15
- package/dist/utils/dependency.utils.js.map +1 -1
- package/dist/utils/field.utils.d.ts +6 -0
- package/dist/utils/field.utils.js +78 -24
- package/dist/utils/field.utils.js.map +1 -1
- package/dist/utils/field.utils.spec.d.ts +1 -0
- package/dist/utils/field.utils.spec.js +38 -0
- package/dist/utils/field.utils.spec.js.map +1 -0
- package/dist/utils/file.utils.d.ts +39 -3
- package/dist/utils/file.utils.js +150 -13
- package/dist/utils/file.utils.js.map +1 -1
- package/dist/utils/naming.utils.js +5 -4
- package/dist/utils/naming.utils.js.map +1 -1
- package/dist/utils/static-recipe.utils.d.ts +4 -0
- package/dist/utils/static-recipe.utils.js +148 -0
- package/dist/utils/static-recipe.utils.js.map +1 -0
- package/dist/utils/template-engine.utils.js +83 -68
- package/dist/utils/template-engine.utils.js.map +1 -1
- package/package.json +20 -11
|
@@ -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
|
|
21
|
-
{{#each
|
|
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.
|
|
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
|
|
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}};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Injectable } from "@nestjs/common";
|
|
2
|
-
import { {{entityNamePascal}}Entity } from "@modules/{{moduleNameKebab}}/domain/entities/{{entityNameKebab}}.entity";
|
|
2
|
+
import { {{entityNamePascal}}Entity } from "@modules/{{moduleNameKebab}}/application/domain/entities/{{entityNameKebab}}.entity";
|
|
3
3
|
import { {{entityNamePascal}}OrmEntity } from "@modules/{{moduleNameKebab}}/infrastructure/orm-entities/{{entityNameKebab}}.orm-entity";
|
|
4
4
|
import { {{entityNamePascal}}ResponseDto } from "@modules/{{moduleNameKebab}}/application/dto/responses/{{entityNameKebab}}.response.dto";
|
|
5
5
|
|
|
@@ -15,7 +15,9 @@ export class {{entityNamePascal}}Mapper {
|
|
|
15
15
|
isActive: ormEntity.isActive,
|
|
16
16
|
createdAt: ormEntity.createdAt,
|
|
17
17
|
updatedAt: ormEntity.updatedAt,
|
|
18
|
-
|
|
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
|
-
|
|
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:
|
|
11
|
-
orderDirection: "
|
|
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:
|
|
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) {
|
|
@@ -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 {
|
|
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
|
|
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.
|