@pattern-stack/codegen 0.2.0 → 0.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 +9 -4
- package/dist/src/cli/index.js +136 -128
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/index.d.ts +16 -0
- package/dist/src/index.js +25 -0
- package/dist/src/index.js.map +1 -1
- package/package.json +10 -1
- package/templates/entity/new/backend/application/commands/create.ejs.t +38 -1
- package/templates/entity/new/backend/application/commands/delete.ejs.t +41 -1
- package/templates/entity/new/backend/application/commands/update.ejs.t +42 -1
- package/templates/entity/new/backend/database/repository.ejs.t +33 -3
- package/templates/entity/new/backend/domain/repository-interface.ejs.t +6 -3
- package/templates/entity/new/backend/modules/core/module.ejs.t +6 -0
- package/templates/entity/new/backend/presentation/controller.ejs.t +32 -10
- package/templates/entity/new/clean-lite-ps/controller.ejs.t +72 -11
- package/templates/entity/new/clean-lite-ps/entity.ejs.t +16 -2
- package/templates/entity/new/clean-lite-ps/index.ejs.t +1 -1
- package/templates/entity/new/clean-lite-ps/module.ejs.t +45 -2
- package/templates/entity/new/clean-lite-ps/prompt-extension.js +459 -98
- package/templates/entity/new/clean-lite-ps/repository.ejs.t +57 -4
- package/templates/entity/new/clean-lite-ps/search-controller.ejs.t +50 -0
- package/templates/entity/new/clean-lite-ps/service.ejs.t +98 -1
- package/templates/entity/new/clean-lite-ps/use-cases/create.ejs.t +150 -0
- package/templates/entity/new/clean-lite-ps/use-cases/delete.ejs.t +70 -0
- package/templates/entity/new/clean-lite-ps/use-cases/find-by-id-with-fields.ejs.t +19 -0
- package/templates/entity/new/clean-lite-ps/use-cases/find-by-id.ejs.t +7 -3
- package/templates/entity/new/clean-lite-ps/use-cases/list-with-fields.ejs.t +17 -0
- package/templates/entity/new/clean-lite-ps/use-cases/search.ejs.t +63 -0
- package/templates/entity/new/clean-lite-ps/use-cases/update.ejs.t +153 -0
- package/templates/entity/new/prompt.js +284 -41
- package/templates/relationship/new/entity.ejs.t +2 -2
- package/templates/relationship/new/prompt.js +3 -7
- package/templates/relationship/new/service.ejs.t +1 -1
- package/templates/subsystem/bridge/generated-keep.ejs.t +4 -0
- package/templates/subsystem/bridge/prompt.js +36 -0
- package/templates/subsystem/bridge-config/codegen-config-bridge-block.ejs.t +20 -0
- package/templates/subsystem/bridge-config/prompt.js +20 -0
- package/templates/subsystem/events/domain-events.schema.ejs.t +81 -0
- package/templates/subsystem/events/generated-keep.ejs.t +4 -0
- package/templates/subsystem/events/prompt.js +39 -0
- package/templates/subsystem/events-config/codegen-config-events-block.ejs.t +26 -0
- package/templates/subsystem/events-config/prompt.js +20 -0
- package/templates/subsystem/jobs/job-orchestration.schema.ejs.t +221 -0
- package/templates/subsystem/jobs/main-hook.ejs.t +11 -0
- package/templates/subsystem/jobs/prompt.js +40 -0
- package/templates/subsystem/jobs/worker.ejs.t +82 -0
- package/templates/subsystem/jobs-config/codegen-config-jobs-block.ejs.t +55 -0
- package/templates/subsystem/jobs-config/prompt.js +20 -0
- package/templates/subsystem/sync/prompt.js +43 -0
- package/templates/subsystem/sync/sync-audit.schema.ejs.t +195 -0
- package/templates/subsystem/sync-config/codegen-config-sync-block.ejs.t +29 -0
- package/templates/subsystem/sync-config/prompt.js +22 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pattern-stack/codegen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Entity-driven code generation for full-stack TypeScript applications",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -62,7 +62,9 @@
|
|
|
62
62
|
"chalk": "^5.6.2",
|
|
63
63
|
"clipanion": "^4.0.0-rc.4",
|
|
64
64
|
"drizzle-orm": "^0.45.2",
|
|
65
|
+
"glob": "^13.0.6",
|
|
65
66
|
"ora": "^9.3.0",
|
|
67
|
+
"pluralize": "^8.0.0",
|
|
66
68
|
"yaml": "^2.8.3",
|
|
67
69
|
"zod": "^3.22.4"
|
|
68
70
|
},
|
|
@@ -95,15 +97,22 @@
|
|
|
95
97
|
"optional": true
|
|
96
98
|
}
|
|
97
99
|
},
|
|
100
|
+
"workspaces": [
|
|
101
|
+
"packages/*"
|
|
102
|
+
],
|
|
98
103
|
"devDependencies": {
|
|
99
104
|
"@cubejs-client/core": "^1.0.0",
|
|
100
105
|
"@nestjs/common": "10",
|
|
101
106
|
"@nestjs/core": "10",
|
|
107
|
+
"@nestjs/testing": "^10",
|
|
102
108
|
"@types/bun": "latest",
|
|
109
|
+
"@types/ejs": "^3.1.5",
|
|
103
110
|
"@types/node": "^25.6.0",
|
|
111
|
+
"@types/pluralize": "^0.0.33",
|
|
104
112
|
"bullmq": "^5.0.0",
|
|
105
113
|
"class-transformer": "^0.5.1",
|
|
106
114
|
"class-validator": "^0.14.0",
|
|
115
|
+
"ejs": "^5.0.2",
|
|
107
116
|
"hygen": "^6.2.11",
|
|
108
117
|
"ioredis": "^5.3.0",
|
|
109
118
|
"reflect-metadata": "^0.2.2",
|
|
@@ -21,15 +21,27 @@ import { <%= repositoryToken %> } from '<%= imports.constants %>';
|
|
|
21
21
|
import type { Create<%= className %>Input, I<%= className %>Repository } from '<%= imports.domain %>';
|
|
22
22
|
import { <%= className %> } from '<%= imports.domain %>';
|
|
23
23
|
import type { Create<%= className %>Dto } from '<%= imports.schemas %>';
|
|
24
|
+
<% if (hasEmits && createEventType) { -%>
|
|
25
|
+
import { TYPED_EVENT_BUS, TypedEventBus } from '<%= eventsTokenImport %>';
|
|
26
|
+
import { DRIZZLE } from '<%= drizzleTokenImport %>';
|
|
27
|
+
import type { DrizzleClient } from '<%= drizzleTypeImport %>';
|
|
28
|
+
<% } -%>
|
|
24
29
|
|
|
25
30
|
@Injectable()
|
|
26
31
|
export class <%= createCommandClass %> {
|
|
27
32
|
constructor(
|
|
28
33
|
@Inject(<%= repositoryToken %>)
|
|
29
34
|
private readonly <%= camelName %>Repository: I<%= className %>Repository,
|
|
35
|
+
<% if (hasEmits && createEventType) { -%>
|
|
36
|
+
@Inject(TYPED_EVENT_BUS) private readonly typedEvents: TypedEventBus,
|
|
37
|
+
@Inject(DRIZZLE) private readonly db: DrizzleClient,
|
|
38
|
+
<% } -%>
|
|
30
39
|
) {}
|
|
31
40
|
|
|
32
|
-
async execute(
|
|
41
|
+
async execute(
|
|
42
|
+
dto: Create<%= className %>Dto,
|
|
43
|
+
<%= hasEmits && createEventType ? 'opts' : '_opts' %>?: { actor?: { tenantId?: string | null; userId?: string } },
|
|
44
|
+
): Promise<<%= className %>> {
|
|
33
45
|
// TODO: Add pre-create validation and business rules here
|
|
34
46
|
|
|
35
47
|
// Map DTO to domain input
|
|
@@ -45,11 +57,36 @@ export class <%= createCommandClass %> {
|
|
|
45
57
|
<% }) -%>
|
|
46
58
|
};
|
|
47
59
|
|
|
60
|
+
<% if (hasEmits && createEventType) { -%>
|
|
61
|
+
return this.db.transaction(async (tx) => {
|
|
62
|
+
const entity = await this.<%= camelName %>Repository.create(input, tx);
|
|
63
|
+
// TODO: verify payload mapping against events/<%= createEventType.type %>.yaml
|
|
64
|
+
await this.typedEvents.publish(
|
|
65
|
+
'<%= createEventType.type %>',
|
|
66
|
+
entity.id,
|
|
67
|
+
{
|
|
68
|
+
<% createEventType.payloadMap.forEach((p) => { -%>
|
|
69
|
+
<%= p.camelKey %>: <%- p.expression %>,<% if (p.todo) { %> // TODO: <%= p.todo %><% } %>
|
|
70
|
+
|
|
71
|
+
<% }) -%>
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
tx,
|
|
75
|
+
metadata: opts?.actor
|
|
76
|
+
? { tenantId: opts.actor.tenantId, userId: opts.actor.userId }
|
|
77
|
+
: undefined,
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
// TODO: Add post-create side effects here (non-event hooks, notifications, etc.)
|
|
81
|
+
return entity;
|
|
82
|
+
});
|
|
83
|
+
<% } else { -%>
|
|
48
84
|
const created = await this.<%= camelName %>Repository.create(input);
|
|
49
85
|
|
|
50
86
|
// TODO: Add post-create side effects here (events, notifications, etc.)
|
|
51
87
|
|
|
52
88
|
return created;
|
|
89
|
+
<% } -%>
|
|
53
90
|
}
|
|
54
91
|
}
|
|
55
92
|
<% } -%>
|
|
@@ -20,18 +20,57 @@ import { Inject, Injectable, NotFoundException } from '@nestjs/common';
|
|
|
20
20
|
import { <%= repositoryToken %> } from '<%= imports.constants %>';
|
|
21
21
|
import type { I<%= className %>Repository } from '<%= imports.domain %>';
|
|
22
22
|
import { <%= className %> } from '<%= imports.domain %>';
|
|
23
|
+
<% if (hasEmits && deleteEventType) { -%>
|
|
24
|
+
import { TYPED_EVENT_BUS, TypedEventBus } from '<%= eventsTokenImport %>';
|
|
25
|
+
import { DRIZZLE } from '<%= drizzleTokenImport %>';
|
|
26
|
+
import type { DrizzleClient } from '<%= drizzleTypeImport %>';
|
|
27
|
+
<% } -%>
|
|
23
28
|
|
|
24
29
|
@Injectable()
|
|
25
30
|
export class <%= deleteCommandClass %> {
|
|
26
31
|
constructor(
|
|
27
32
|
@Inject(<%= repositoryToken %>)
|
|
28
33
|
private readonly <%= camelName %>Repository: I<%= className %>Repository,
|
|
34
|
+
<% if (hasEmits && deleteEventType) { -%>
|
|
35
|
+
@Inject(TYPED_EVENT_BUS) private readonly typedEvents: TypedEventBus,
|
|
36
|
+
@Inject(DRIZZLE) private readonly db: DrizzleClient,
|
|
37
|
+
<% } -%>
|
|
29
38
|
) {}
|
|
30
39
|
|
|
31
|
-
async execute(
|
|
40
|
+
async execute(
|
|
41
|
+
id: string,
|
|
42
|
+
<%= hasEmits && deleteEventType ? 'opts' : '_opts' %>?: { actor?: { tenantId?: string | null; userId?: string } },
|
|
43
|
+
): Promise<<%= className %>> {
|
|
32
44
|
// TODO: Add pre-delete validation here
|
|
33
45
|
// e.g., check for dependent records, verify user permissions
|
|
34
46
|
|
|
47
|
+
<% if (hasEmits && deleteEventType) { -%>
|
|
48
|
+
return this.db.transaction(async (tx) => {
|
|
49
|
+
const entity = await this.<%= camelName %>Repository.delete(id, tx);
|
|
50
|
+
if (!entity) {
|
|
51
|
+
throw new NotFoundException(`<%= className %> with id ${id} not found`);
|
|
52
|
+
}
|
|
53
|
+
// TODO: verify payload mapping against events/<%= deleteEventType.type %>.yaml
|
|
54
|
+
await this.typedEvents.publish(
|
|
55
|
+
'<%= deleteEventType.type %>',
|
|
56
|
+
entity.id,
|
|
57
|
+
{
|
|
58
|
+
<% deleteEventType.payloadMap.forEach((p) => { -%>
|
|
59
|
+
<%= p.camelKey %>: <%- p.expression %>,<% if (p.todo) { %> // TODO: <%= p.todo %><% } %>
|
|
60
|
+
|
|
61
|
+
<% }) -%>
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
tx,
|
|
65
|
+
metadata: opts?.actor
|
|
66
|
+
? { tenantId: opts.actor.tenantId, userId: opts.actor.userId }
|
|
67
|
+
: undefined,
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
// TODO: Add post-delete side effects here (cleanup, etc.)
|
|
71
|
+
return entity;
|
|
72
|
+
});
|
|
73
|
+
<% } else { -%>
|
|
35
74
|
const deleted = await this.<%= camelName %>Repository.delete(id);
|
|
36
75
|
if (!deleted) {
|
|
37
76
|
throw new NotFoundException(`<%= className %> with id ${id} not found`);
|
|
@@ -40,6 +79,7 @@ export class <%= deleteCommandClass %> {
|
|
|
40
79
|
// TODO: Add post-delete side effects here (events, cleanup, etc.)
|
|
41
80
|
|
|
42
81
|
return deleted;
|
|
82
|
+
<% } -%>
|
|
43
83
|
}
|
|
44
84
|
}
|
|
45
85
|
<% } -%>
|
|
@@ -21,15 +21,28 @@ import { <%= repositoryToken %> } from '<%= imports.constants %>';
|
|
|
21
21
|
import type { I<%= className %>Repository, Update<%= className %>Input } from '<%= imports.domain %>';
|
|
22
22
|
import { <%= className %> } from '<%= imports.domain %>';
|
|
23
23
|
import type { Update<%= className %>Dto } from '<%= imports.schemas %>';
|
|
24
|
+
<% if (hasEmits && updateEventType) { -%>
|
|
25
|
+
import { TYPED_EVENT_BUS, TypedEventBus } from '<%= eventsTokenImport %>';
|
|
26
|
+
import { DRIZZLE } from '<%= drizzleTokenImport %>';
|
|
27
|
+
import type { DrizzleClient } from '<%= drizzleTypeImport %>';
|
|
28
|
+
<% } -%>
|
|
24
29
|
|
|
25
30
|
@Injectable()
|
|
26
31
|
export class <%= updateCommandClass %> {
|
|
27
32
|
constructor(
|
|
28
33
|
@Inject(<%= repositoryToken %>)
|
|
29
34
|
private readonly <%= camelName %>Repository: I<%= className %>Repository,
|
|
35
|
+
<% if (hasEmits && updateEventType) { -%>
|
|
36
|
+
@Inject(TYPED_EVENT_BUS) private readonly typedEvents: TypedEventBus,
|
|
37
|
+
@Inject(DRIZZLE) private readonly db: DrizzleClient,
|
|
38
|
+
<% } -%>
|
|
30
39
|
) {}
|
|
31
40
|
|
|
32
|
-
async execute(
|
|
41
|
+
async execute(
|
|
42
|
+
id: string,
|
|
43
|
+
dto: Update<%= className %>Dto,
|
|
44
|
+
<%= hasEmits && updateEventType ? 'opts' : '_opts' %>?: { actor?: { tenantId?: string | null; userId?: string } },
|
|
45
|
+
): Promise<<%= className %>> {
|
|
33
46
|
const existing = await this.<%= camelName %>Repository.findById(id);
|
|
34
47
|
if (!existing) {
|
|
35
48
|
throw new NotFoundException(`<%= className %> with id ${id} not found`);
|
|
@@ -45,6 +58,33 @@ export class <%= updateCommandClass %> {
|
|
|
45
58
|
<% }) -%>
|
|
46
59
|
};
|
|
47
60
|
|
|
61
|
+
<% if (hasEmits && updateEventType) { -%>
|
|
62
|
+
return this.db.transaction(async (tx) => {
|
|
63
|
+
const entity = await this.<%= camelName %>Repository.update(id, input, tx);
|
|
64
|
+
if (!entity) {
|
|
65
|
+
throw new NotFoundException(`<%= className %> with id ${id} not found`);
|
|
66
|
+
}
|
|
67
|
+
// TODO: verify payload mapping against events/<%= updateEventType.type %>.yaml
|
|
68
|
+
await this.typedEvents.publish(
|
|
69
|
+
'<%= updateEventType.type %>',
|
|
70
|
+
entity.id,
|
|
71
|
+
{
|
|
72
|
+
<% updateEventType.payloadMap.forEach((p) => { -%>
|
|
73
|
+
<%= p.camelKey %>: <%- p.expression %>,<% if (p.todo) { %> // TODO: <%= p.todo %><% } %>
|
|
74
|
+
|
|
75
|
+
<% }) -%>
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
tx,
|
|
79
|
+
metadata: opts?.actor
|
|
80
|
+
? { tenantId: opts.actor.tenantId, userId: opts.actor.userId }
|
|
81
|
+
: undefined,
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
// TODO: Add post-update side effects here (non-event hooks, cache invalidation, etc.)
|
|
85
|
+
return entity;
|
|
86
|
+
});
|
|
87
|
+
<% } else { -%>
|
|
48
88
|
const updated = await this.<%= camelName %>Repository.update(id, input);
|
|
49
89
|
if (!updated) {
|
|
50
90
|
throw new NotFoundException(`<%= className %> with id ${id} not found`);
|
|
@@ -53,6 +93,7 @@ export class <%= updateCommandClass %> {
|
|
|
53
93
|
// TODO: Add post-update side effects here (events, cache invalidation, etc.)
|
|
54
94
|
|
|
55
95
|
return updated;
|
|
96
|
+
<% } -%>
|
|
56
97
|
}
|
|
57
98
|
}
|
|
58
99
|
<% } -%>
|
|
@@ -184,6 +184,9 @@ import { DRIZZLE } from '<%= imports.repositoryToConstants %>';
|
|
|
184
184
|
<% if (hasEntityRefFields) { -%>
|
|
185
185
|
import type { EntityType } from '<%= locations.dbSchemaServer.import %>';
|
|
186
186
|
<% } -%>
|
|
187
|
+
<% if (hasEmits && (createEventType || updateEventType || deleteEventType)) { -%>
|
|
188
|
+
import type { DrizzleTransaction } from '<%= eventsTokenImport %>';
|
|
189
|
+
<% } -%>
|
|
187
190
|
import type {
|
|
188
191
|
Create<%= className %>Input,
|
|
189
192
|
I<%= className %>Repository,
|
|
@@ -209,11 +212,16 @@ import { <%= plural %> } from '<%= locations.dbSchemaServer.import %>';
|
|
|
209
212
|
export class <%= className %>Repository implements I<%= className %>Repository {
|
|
210
213
|
constructor(@Inject(DRIZZLE) private readonly db: DrizzleDB) {}
|
|
211
214
|
|
|
212
|
-
async create(input: Create<%= className %>Input): Promise<<%= className %>> {
|
|
215
|
+
async create(input: Create<%= className %>Input<%= (hasEmits && createEventType) ? ', tx?: DrizzleTransaction' : '' %>): Promise<<%= className %>> {
|
|
213
216
|
<% if (hasTimestamps) { -%>
|
|
214
217
|
const now = new Date();
|
|
215
218
|
<% } -%>
|
|
219
|
+
<% if (hasEmits && createEventType) { -%>
|
|
220
|
+
const runner = tx ?? this.db;
|
|
221
|
+
const result = await runner
|
|
222
|
+
<% } else { -%>
|
|
216
223
|
const result = await this.db
|
|
224
|
+
<% } -%>
|
|
217
225
|
.insert(<%= plural %>)
|
|
218
226
|
.values({
|
|
219
227
|
<% fields.forEach((field) => { -%>
|
|
@@ -261,8 +269,13 @@ export class <%= className %>Repository implements I<%= className %>Repository {
|
|
|
261
269
|
<% } -%>
|
|
262
270
|
}
|
|
263
271
|
|
|
264
|
-
async update(id: string, input: Update<%= className %>Input): Promise<<%= className %> | null> {
|
|
272
|
+
async update(id: string, input: Update<%= className %>Input<%= (hasEmits && updateEventType) ? ', tx?: DrizzleTransaction' : '' %>): Promise<<%= className %> | null> {
|
|
273
|
+
<% if (hasEmits && updateEventType) { -%>
|
|
274
|
+
const runner = tx ?? this.db;
|
|
275
|
+
const result = await runner
|
|
276
|
+
<% } else { -%>
|
|
265
277
|
const result = await this.db
|
|
278
|
+
<% } -%>
|
|
266
279
|
.update(<%= plural %>)
|
|
267
280
|
.set({
|
|
268
281
|
...input,
|
|
@@ -277,7 +290,23 @@ export class <%= className %>Repository implements I<%= className %>Repository {
|
|
|
277
290
|
return record ? <%= className %>.fromRecord(record) : null;
|
|
278
291
|
}
|
|
279
292
|
|
|
280
|
-
async delete(id: string): Promise<<%= className %> | null> {
|
|
293
|
+
async delete(id: string<%= (hasEmits && deleteEventType) ? ', tx?: DrizzleTransaction' : '' %>): Promise<<%= className %> | null> {
|
|
294
|
+
<% if (hasEmits && deleteEventType) { -%>
|
|
295
|
+
const runner = tx ?? this.db;
|
|
296
|
+
<% if (hasSoftDelete) { -%>
|
|
297
|
+
// Soft delete - set deletedAt timestamp
|
|
298
|
+
const result = await runner
|
|
299
|
+
.update(<%= plural %>)
|
|
300
|
+
.set({ deletedAt: new Date() })
|
|
301
|
+
.where(eq(<%= plural %>.id, id))
|
|
302
|
+
.returning();
|
|
303
|
+
<% } else { -%>
|
|
304
|
+
const result = await runner
|
|
305
|
+
.delete(<%= plural %>)
|
|
306
|
+
.where(eq(<%= plural %>.id, id))
|
|
307
|
+
.returning();
|
|
308
|
+
<% } -%>
|
|
309
|
+
<% } else { -%>
|
|
281
310
|
<% if (hasSoftDelete) { -%>
|
|
282
311
|
// Soft delete - set deletedAt timestamp
|
|
283
312
|
const result = await this.db
|
|
@@ -290,6 +319,7 @@ export class <%= className %>Repository implements I<%= className %>Repository {
|
|
|
290
319
|
.delete(<%= plural %>)
|
|
291
320
|
.where(eq(<%= plural %>.id, id))
|
|
292
321
|
.returning();
|
|
322
|
+
<% } -%>
|
|
293
323
|
<% } -%>
|
|
294
324
|
|
|
295
325
|
const record = result[0];
|
|
@@ -13,6 +13,9 @@ import type { <%= className %> } from './<%= name %>.entity';
|
|
|
13
13
|
<% if (hasEntityRefFields) { -%>
|
|
14
14
|
import type { EntityType } from '<%= locations.dbSchemaServer.import %>';
|
|
15
15
|
<% } -%>
|
|
16
|
+
<% if (hasEmits && (createEventType || updateEventType || deleteEventType)) { -%>
|
|
17
|
+
import type { DrizzleTransaction } from '<%= eventsTokenImport %>';
|
|
18
|
+
<% } -%>
|
|
16
19
|
<% if (hasRelationships) { -%>
|
|
17
20
|
|
|
18
21
|
/**
|
|
@@ -44,11 +47,11 @@ export type Create<%= className %>Input = {
|
|
|
44
47
|
export type Update<%= className %>Input = Partial<Create<%= className %>Input>;
|
|
45
48
|
|
|
46
49
|
export interface I<%= className %>Repository {
|
|
47
|
-
create(input: Create<%= className %>Input): Promise<<%= className %>>;
|
|
50
|
+
create(input: Create<%= className %>Input<%= (hasEmits && createEventType) ? ', tx?: DrizzleTransaction' : '' %>): Promise<<%= className %>>;
|
|
48
51
|
findById(id: string<%= hasRelationships ? `, include?: ${className}With` : '' %>): Promise<<%= className %> | null>;
|
|
49
52
|
findAll(<%= hasRelationships ? `include?: ${className}With` : '' %>): Promise<<%= className %>[]>;
|
|
50
|
-
update(id: string, input: Update<%= className %>Input): Promise<<%= className %> | null>;
|
|
51
|
-
delete(id: string): Promise<<%= className %> | null>;
|
|
53
|
+
update(id: string, input: Update<%= className %>Input<%= (hasEmits && updateEventType) ? ', tx?: DrizzleTransaction' : '' %>): Promise<<%= className %> | null>;
|
|
54
|
+
delete(id: string<%= (hasEmits && deleteEventType) ? ', tx?: DrizzleTransaction' : '' %>): Promise<<%= className %> | null>;
|
|
52
55
|
<% if (hasSoftDelete) { -%>
|
|
53
56
|
// Soft delete recovery methods
|
|
54
57
|
restore(id: string): Promise<<%= className %> | null>;
|
|
@@ -6,6 +6,12 @@ force: true
|
|
|
6
6
|
/**
|
|
7
7
|
* <%= classNamePlural %> Module
|
|
8
8
|
* Generated by entity codegen - do not edit directly
|
|
9
|
+
<% if (hasEmits) { -%>
|
|
10
|
+
*
|
|
11
|
+
* EVT-7: This entity emits typed domain events. Use-cases depend on
|
|
12
|
+
* TYPED_EVENT_BUS + DRIZZLE. Ensure EventsModule is registered in the
|
|
13
|
+
* root AppModule (global) so these tokens resolve at runtime.
|
|
14
|
+
<% } -%>
|
|
9
15
|
*/
|
|
10
16
|
|
|
11
17
|
import { Module } from '@nestjs/common';
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
Controller,
|
|
15
15
|
Delete,
|
|
16
16
|
Get,
|
|
17
|
+
Headers,
|
|
17
18
|
Param,
|
|
18
19
|
ParseUUIDPipe,
|
|
19
20
|
Post,
|
|
@@ -75,21 +76,31 @@ export class <%= classNamePlural %>Controller {
|
|
|
75
76
|
|
|
76
77
|
@Post()
|
|
77
78
|
@UsePipes(new ZodValidationPipe(create<%= className %>Schema))
|
|
78
|
-
async create(
|
|
79
|
-
|
|
79
|
+
async create(
|
|
80
|
+
@Body() dto: Create<%= className %>Dto,
|
|
81
|
+
@Headers('x-tenant-id') tenantId?: string,
|
|
82
|
+
@Headers('x-user-id') userId?: string,
|
|
83
|
+
): Promise<<%= className %>> {
|
|
84
|
+
return this.create<%= className %>Command.execute(dto, { actor: { tenantId, userId } });
|
|
80
85
|
}
|
|
81
86
|
|
|
82
87
|
@Put(':id')
|
|
83
88
|
async update(
|
|
84
89
|
@Param('id', ParseUUIDPipe) id: string,
|
|
85
90
|
@Body(new ZodValidationPipe(update<%= className %>Schema)) dto: Update<%= className %>Dto,
|
|
91
|
+
@Headers('x-tenant-id') tenantId?: string,
|
|
92
|
+
@Headers('x-user-id') userId?: string,
|
|
86
93
|
): Promise<<%= className %>> {
|
|
87
|
-
return this.update<%= className %>Command.execute(id, dto);
|
|
94
|
+
return this.update<%= className %>Command.execute(id, dto, { actor: { tenantId, userId } });
|
|
88
95
|
}
|
|
89
96
|
|
|
90
97
|
@Delete(':id')
|
|
91
|
-
async delete(
|
|
92
|
-
|
|
98
|
+
async delete(
|
|
99
|
+
@Param('id', ParseUUIDPipe) id: string,
|
|
100
|
+
@Headers('x-tenant-id') tenantId?: string,
|
|
101
|
+
@Headers('x-user-id') userId?: string,
|
|
102
|
+
): Promise<<%= className %>> {
|
|
103
|
+
return this.delete<%= className %>Command.execute(id, { actor: { tenantId, userId } });
|
|
93
104
|
}
|
|
94
105
|
<% if (hasRelationships) { -%>
|
|
95
106
|
|
|
@@ -120,6 +131,7 @@ import {
|
|
|
120
131
|
Get,
|
|
121
132
|
Body,
|
|
122
133
|
Delete,
|
|
134
|
+
Headers,
|
|
123
135
|
Param,
|
|
124
136
|
ParseUUIDPipe,
|
|
125
137
|
Post,
|
|
@@ -181,21 +193,31 @@ export class <%= classNamePlural %>Controller {
|
|
|
181
193
|
|
|
182
194
|
@Post()
|
|
183
195
|
@UsePipes(new ZodValidationPipe(create<%= className %>Schema))
|
|
184
|
-
async create(
|
|
185
|
-
|
|
196
|
+
async create(
|
|
197
|
+
@Body() dto: Create<%= className %>Dto,
|
|
198
|
+
@Headers('x-tenant-id') tenantId?: string,
|
|
199
|
+
@Headers('x-user-id') userId?: string,
|
|
200
|
+
): Promise<<%= className %>> {
|
|
201
|
+
return this.create<%= className %>Command.execute(dto, { actor: { tenantId, userId } });
|
|
186
202
|
}
|
|
187
203
|
|
|
188
204
|
@Put(':id')
|
|
189
205
|
async update(
|
|
190
206
|
@Param('id', ParseUUIDPipe) id: string,
|
|
191
207
|
@Body(new ZodValidationPipe(update<%= className %>Schema)) dto: Update<%= className %>Dto,
|
|
208
|
+
@Headers('x-tenant-id') tenantId?: string,
|
|
209
|
+
@Headers('x-user-id') userId?: string,
|
|
192
210
|
): Promise<<%= className %>> {
|
|
193
|
-
return this.update<%= className %>Command.execute(id, dto);
|
|
211
|
+
return this.update<%= className %>Command.execute(id, dto, { actor: { tenantId, userId } });
|
|
194
212
|
}
|
|
195
213
|
|
|
196
214
|
@Delete(':id')
|
|
197
|
-
async delete(
|
|
198
|
-
|
|
215
|
+
async delete(
|
|
216
|
+
@Param('id', ParseUUIDPipe) id: string,
|
|
217
|
+
@Headers('x-tenant-id') tenantId?: string,
|
|
218
|
+
@Headers('x-user-id') userId?: string,
|
|
219
|
+
): Promise<<%= className %>> {
|
|
220
|
+
return this.delete<%= className %>Command.execute(id, { actor: { tenantId, userId } });
|
|
199
221
|
}
|
|
200
222
|
}
|
|
201
223
|
<% } -%>
|
|
@@ -3,11 +3,24 @@ to: "<%= typeof clpOutputPaths !== 'undefined' ? clpOutputPaths.controller : nul
|
|
|
3
3
|
skip_if: "<%= typeof clpOutputPaths === 'undefined' %>"
|
|
4
4
|
force: true
|
|
5
5
|
---
|
|
6
|
-
import { Controller, Get, Param } from '@nestjs/common';
|
|
6
|
+
import { Controller, Get<% if (generateWrites) { %>, Post, Patch, Delete, Body, Headers<% } %>, NotFoundException, Param, ParseUUIDPipe } from '@nestjs/common';
|
|
7
7
|
import { <%= classNames.findByIdUseCase %> } from './use-cases/find-<%= entityName %>-by-id.use-case';
|
|
8
8
|
import { <%= classNames.listUseCase %> } from './use-cases/list-<%= entityNamePlural %>.use-case';
|
|
9
|
+
<% if (eavEnabled) { -%>
|
|
10
|
+
import { <%= classNames.findByIdWithFieldsUseCase %> } from './use-cases/find-<%= entityName %>-by-id-with-fields.use-case';
|
|
11
|
+
import { <%= classNames.listWithFieldsUseCase %> } from './use-cases/list-<%= entityNamePlural %>-with-fields.use-case';
|
|
12
|
+
<% } -%>
|
|
13
|
+
<% if (generateWrites) { -%>
|
|
14
|
+
import { ZodValidationPipe } from '@shared/pipes/zod-validation.pipe';
|
|
15
|
+
import { <%= classNames.createUseCase %> } from './use-cases/create-<%= entityName %>.use-case';
|
|
16
|
+
import { <%= classNames.updateUseCase %> } from './use-cases/update-<%= entityName %>.use-case';
|
|
17
|
+
import { <%= classNames.deleteUseCase %> } from './use-cases/delete-<%= entityName %>.use-case';
|
|
18
|
+
import { <%= classNames.createSchema %> } from './dto/create-<%= entityName %>.dto';
|
|
19
|
+
import type { <%= classNames.createDto %> } from './dto/create-<%= entityName %>.dto';
|
|
20
|
+
import { <%= classNames.updateSchema %> } from './dto/update-<%= entityName %>.dto';
|
|
21
|
+
import type { <%= classNames.updateDto %> } from './dto/update-<%= entityName %>.dto';
|
|
22
|
+
<% } -%>
|
|
9
23
|
import type { <%= classNames.entity %> } from './<%= entityName %>.entity';
|
|
10
|
-
// Write use cases must be hand-written. Import them here when ready.
|
|
11
24
|
|
|
12
25
|
@Controller('<%= entityNamePlural %>')
|
|
13
26
|
export class <%= classNames.controller %> {
|
|
@@ -15,23 +28,71 @@ export class <%= classNames.controller %> {
|
|
|
15
28
|
// All routes go through use cases (ADR-003 — no controller → service shortcuts)
|
|
16
29
|
private readonly findByIdUseCase: <%= classNames.findByIdUseCase %>,
|
|
17
30
|
private readonly listUseCase: <%= classNames.listUseCase %>,
|
|
18
|
-
|
|
31
|
+
<% if (eavEnabled) { -%>
|
|
32
|
+
private readonly findByIdWithFieldsUseCase: <%= classNames.findByIdWithFieldsUseCase %>,
|
|
33
|
+
private readonly listWithFieldsUseCase: <%= classNames.listWithFieldsUseCase %>,
|
|
34
|
+
<% } -%>
|
|
35
|
+
<% if (generateWrites) { -%>
|
|
36
|
+
private readonly createUseCase: <%= classNames.createUseCase %>,
|
|
37
|
+
private readonly updateUseCase: <%= classNames.updateUseCase %>,
|
|
38
|
+
private readonly deleteUseCase: <%= classNames.deleteUseCase %>,
|
|
39
|
+
<% } -%>
|
|
19
40
|
) {}
|
|
20
41
|
|
|
21
42
|
@Get()
|
|
22
43
|
async getAll(): Promise<<%= classNames.entity %>[]> {
|
|
23
44
|
return this.listUseCase.execute();
|
|
24
45
|
}
|
|
25
|
-
|
|
46
|
+
<% if (eavEnabled) { %>
|
|
47
|
+
@Get('with-fields')
|
|
48
|
+
async getAllWithFields(): Promise<Array<<%= classNames.entity %> & { fields: Record<string, unknown> }>> {
|
|
49
|
+
return this.listWithFieldsUseCase.execute();
|
|
50
|
+
}
|
|
51
|
+
<% } %>
|
|
26
52
|
@Get(':id')
|
|
27
|
-
async getById(@Param('id') id: string): Promise<<%= classNames.entity
|
|
53
|
+
async getById(@Param('id', ParseUUIDPipe) id: string): Promise<<%= classNames.entity %>> {
|
|
54
|
+
// Use case throws NotFoundException on null/undefined (D2)
|
|
28
55
|
return this.findByIdUseCase.execute(id);
|
|
29
56
|
}
|
|
57
|
+
<% if (eavEnabled) { %>
|
|
58
|
+
@Get(':id/with-fields')
|
|
59
|
+
async getByIdWithFields(
|
|
60
|
+
@Param('id', ParseUUIDPipe) id: string,
|
|
61
|
+
): Promise<<%= classNames.entity %> & { fields: Record<string, unknown> }> {
|
|
62
|
+
const entity = await this.findByIdWithFieldsUseCase.execute(id);
|
|
63
|
+
if (!entity) throw new NotFoundException(`<%= classNames.entity %> ${id} not found`);
|
|
64
|
+
return entity;
|
|
65
|
+
}
|
|
66
|
+
<% } %>
|
|
67
|
+
<% if (generateWrites) { %>
|
|
68
|
+
@Post()
|
|
69
|
+
async create(
|
|
70
|
+
@Body(new ZodValidationPipe(<%= classNames.createSchema %>)) dto: <%= classNames.createDto %>,
|
|
71
|
+
@Headers('x-tenant-id') tenantId?: string,
|
|
72
|
+
@Headers('x-user-id') userId?: string,
|
|
73
|
+
): Promise<<%= classNames.entity %>> {
|
|
74
|
+
return this.createUseCase.execute(dto, { actor: { tenantId, userId } });
|
|
75
|
+
}
|
|
30
76
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
77
|
+
@Patch(':id')
|
|
78
|
+
async update(
|
|
79
|
+
@Param('id', ParseUUIDPipe) id: string,
|
|
80
|
+
@Body(new ZodValidationPipe(<%= classNames.updateSchema %>)) dto: <%= classNames.updateDto %>,
|
|
81
|
+
@Headers('x-tenant-id') tenantId?: string,
|
|
82
|
+
@Headers('x-user-id') userId?: string,
|
|
83
|
+
): Promise<<%= classNames.entity %>> {
|
|
84
|
+
const entity = await this.updateUseCase.execute(id, dto, { actor: { tenantId, userId } });
|
|
85
|
+
if (!entity) throw new NotFoundException(`<%= classNames.entity %> ${id} not found`);
|
|
86
|
+
return entity;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@Delete(':id')
|
|
90
|
+
async remove(
|
|
91
|
+
@Param('id', ParseUUIDPipe) id: string,
|
|
92
|
+
@Headers('x-tenant-id') tenantId?: string,
|
|
93
|
+
@Headers('x-user-id') userId?: string,
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
return this.deleteUseCase.execute(id, { actor: { tenantId, userId } });
|
|
96
|
+
}
|
|
97
|
+
<% } %>
|
|
37
98
|
}
|
|
@@ -14,7 +14,9 @@ import { relations, type InferSelectModel } from 'drizzle-orm';
|
|
|
14
14
|
import { type InferSelectModel } from 'drizzle-orm';
|
|
15
15
|
<%_ } _%>
|
|
16
16
|
<%_ clpBelongsTo.forEach(rel => { _%>
|
|
17
|
+
<%_ if (rel.relatedTable !== entityNamePlural) { _%>
|
|
17
18
|
import { <%= rel.relatedTable %> } from '<%= rel.importPath %>';
|
|
19
|
+
<%_ } _%>
|
|
18
20
|
<%_ }) _%>
|
|
19
21
|
|
|
20
22
|
export const <%= entityNamePlural %> = pgTable(
|
|
@@ -22,11 +24,23 @@ export const <%= entityNamePlural %> = pgTable(
|
|
|
22
24
|
{
|
|
23
25
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
24
26
|
<%_ clpBelongsTo.forEach(rel => { _%>
|
|
25
|
-
|
|
27
|
+
<%_ if (hasSoftDelete) { _%>
|
|
28
|
+
// WARNING: on_delete: '<%= rel.onDeleteYaml %>' is a no-op when this entity uses soft_delete.
|
|
29
|
+
// BaseService.delete() issues UPDATE … SET deleted_at = now(), not DELETE, so Postgres
|
|
30
|
+
// cascade rules never fire for a soft-deleted parent. This FK constraint only applies on
|
|
31
|
+
// hard-delete (e.g. admin purge). See ADR-021: docs/adrs/ADR-021-on-delete-semantics.md
|
|
32
|
+
<%_ } _%>
|
|
33
|
+
<%= rel.camelField %>: uuid('<%= rel.field %>')<%= rel.nullable ? '' : '.notNull()' %>.references(() => <%= rel.relatedTable %>.id, { onDelete: '<%= rel.onDelete %>' }),
|
|
26
34
|
<%_ }) _%>
|
|
27
35
|
<%_ clpProcessedFields.forEach(field => { _%>
|
|
28
36
|
<%= field.camelName %>: <%- field.drizzleChain %>,
|
|
29
37
|
<%_ }) _%>
|
|
38
|
+
<%_ if (hasExternalIdTracking) { _%>
|
|
39
|
+
// external_id_tracking behavior
|
|
40
|
+
externalId: varchar('external_id'),
|
|
41
|
+
provider: varchar('provider'),
|
|
42
|
+
providerMetadata: jsonb('provider_metadata'),
|
|
43
|
+
<%_ } _%>
|
|
30
44
|
<%_ if (hasTimestamps) { _%>
|
|
31
45
|
createdAt: timestamp('created_at').notNull().defaultNow(),
|
|
32
46
|
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
|
@@ -40,7 +54,7 @@ export const <%= entityNamePlural %> = pgTable(
|
|
|
40
54
|
|
|
41
55
|
export const <%= entityNamePlural %>Relations = relations(<%= entityNamePlural %>, ({ one }) => ({
|
|
42
56
|
<%_ clpBelongsTo.forEach(rel => { _%>
|
|
43
|
-
<%= rel.
|
|
57
|
+
<%= rel.relationKey %>: one(<%= rel.relatedTable %>, {
|
|
44
58
|
fields: [<%= entityNamePlural %>.<%= rel.camelField %>],
|
|
45
59
|
references: [<%= rel.relatedTable %>.id],
|
|
46
60
|
}),
|