@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
|
@@ -7,10 +7,13 @@ import { Injectable, Inject } from '@nestjs/common';
|
|
|
7
7
|
<% if (hasDeclarativeQueries) { -%>
|
|
8
8
|
import { eq<%= hasMultiFieldQuery ? ', and' : '' %><%= hasOrderedQuery ? ', desc, asc' : '' %> } from 'drizzle-orm';
|
|
9
9
|
<% } -%>
|
|
10
|
+
<% if (eavValueTable) { -%>
|
|
11
|
+
import { sql } from 'drizzle-orm';
|
|
12
|
+
<% } -%>
|
|
10
13
|
import { DRIZZLE } from '@shared/constants/tokens';
|
|
11
|
-
import type { DrizzleClient } from '@shared/types/drizzle';
|
|
14
|
+
import type { DrizzleClient<% if (eavValueTable) { %>, DrizzleTx<% } %> } from '@shared/types/drizzle';
|
|
12
15
|
import { <%= repositoryBaseClass %> } from '<%= repositoryBaseImport %>';
|
|
13
|
-
<% if (hasTimestamps || hasSoftDelete) { -%>
|
|
16
|
+
<% if (hasTimestamps || hasSoftDelete || hasUserTracking) { -%>
|
|
14
17
|
import type { BehaviorConfig } from '@shared/base-classes/base-repository';
|
|
15
18
|
<% } -%>
|
|
16
19
|
import { <%= entityNamePlural %>, type <%= classNames.entity %> } from './<%= entityName %>.entity';
|
|
@@ -18,15 +21,23 @@ import { <%= entityNamePlural %>, type <%= classNames.entity %> } from './<%= en
|
|
|
18
21
|
@Injectable()
|
|
19
22
|
export class <%= classNames.repository %> extends <%= repositoryBaseClass %><<%= classNames.entity %>> {
|
|
20
23
|
readonly table = <%= entityNamePlural %>;
|
|
21
|
-
<% if (hasTimestamps || hasSoftDelete) { -%>
|
|
24
|
+
<% if (hasTimestamps || hasSoftDelete || hasUserTracking) { -%>
|
|
22
25
|
|
|
23
26
|
// Behaviors declared in YAML -> generated as config object
|
|
24
27
|
protected override readonly behaviors: BehaviorConfig = {
|
|
25
28
|
timestamps: <%= !!hasTimestamps %>,
|
|
26
29
|
softDelete: <%= !!hasSoftDelete %>,
|
|
27
|
-
userTracking:
|
|
30
|
+
userTracking: <%= !!hasUserTracking %>,
|
|
28
31
|
};
|
|
29
32
|
<% } -%>
|
|
33
|
+
<% if (hasPatternConfig) { -%>
|
|
34
|
+
|
|
35
|
+
// Per-entity `<%= patternName %>` pattern config (from YAML `config:` block).
|
|
36
|
+
// The pattern's base class declares `protected readonly patternConfig: TConfig`
|
|
37
|
+
// typed via its `configSchema`; this concrete record is read by the base at
|
|
38
|
+
// runtime (identical shape to `behaviors: BehaviorConfig`).
|
|
39
|
+
protected override readonly patternConfig = <%- renderPatternConfigLiteral(patternConfig, ' ', ' ') %> as const;
|
|
40
|
+
<% } -%>
|
|
30
41
|
|
|
31
42
|
constructor(@Inject(DRIZZLE) db: DrizzleClient) {
|
|
32
43
|
super(db);
|
|
@@ -55,6 +66,48 @@ export class <%= classNames.repository %> extends <%= repositoryBaseClass %><<%=
|
|
|
55
66
|
|
|
56
67
|
// TODO: Add entity-specific query methods here.
|
|
57
68
|
<% } -%>
|
|
69
|
+
<% if (eavValueTable) { -%>
|
|
70
|
+
|
|
71
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
72
|
+
// EAV compound writes (task #23) — generated from eav_value_table: true
|
|
73
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
74
|
+
/**
|
|
75
|
+
* Upsert "current" value rows keyed by the composite unique
|
|
76
|
+
* (entity_type, entity_id, field_definition_id). Used by the service's
|
|
77
|
+
* upsertFieldsTransactional to dual-write a bag of dynamic fields
|
|
78
|
+
* atomically with the owning entity. Inherited upsertMany only
|
|
79
|
+
* supports single-column conflict targets — this override uses
|
|
80
|
+
* Drizzle's `onConflictDoUpdate` with an explicit column list.
|
|
81
|
+
*/
|
|
82
|
+
async upsertCurrentValues(
|
|
83
|
+
inputs: Array<Partial<<%= classNames.entity %>>>,
|
|
84
|
+
tx?: DrizzleTx,
|
|
85
|
+
): Promise<<%= classNames.entity %>[]> {
|
|
86
|
+
if (inputs.length === 0) return [];
|
|
87
|
+
const data = inputs.map((input) =>
|
|
88
|
+
this.withTimestamps(input as Record<string, unknown>, 'create'),
|
|
89
|
+
);
|
|
90
|
+
const runner = this.runner(tx);
|
|
91
|
+
const rows = await runner
|
|
92
|
+
.insert(this.table)
|
|
93
|
+
.values(data as any) // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
94
|
+
.onConflictDoUpdate({
|
|
95
|
+
target: [
|
|
96
|
+
this.table['entityType'],
|
|
97
|
+
this.table['entityId'],
|
|
98
|
+
this.table['fieldDefinitionId'],
|
|
99
|
+
],
|
|
100
|
+
set: {
|
|
101
|
+
value: sql`excluded.value`,
|
|
102
|
+
userId: sql`excluded.user_id`,
|
|
103
|
+
updatedAt: sql`excluded.updated_at`,
|
|
104
|
+
} as any, // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
105
|
+
})
|
|
106
|
+
.returning();
|
|
107
|
+
return rows as <%= classNames.entity %>[];
|
|
108
|
+
}
|
|
109
|
+
<% } -%>
|
|
110
|
+
|
|
58
111
|
// Inherited from <%= repositoryBaseClass %>:
|
|
59
112
|
<%_ repositoryInheritedMethods.forEach(line => { _%>
|
|
60
113
|
// <%= line %>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: "<%= typeof clpOutputPaths !== 'undefined' ? clpOutputPaths.searchController : null %>"
|
|
3
|
+
skip_if: "<%= typeof clpOutputPaths === 'undefined' || !clpOutputPaths.searchController %>"
|
|
4
|
+
force: true
|
|
5
|
+
---
|
|
6
|
+
<% if (hasSearchQuery) { -%>
|
|
7
|
+
import { BadRequestException, Controller, Get, Query } from '@nestjs/common';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { PaginationSchema } from '@shared/http/pagination';
|
|
10
|
+
import { <%= searchQuery.useCaseClassName %> } from './use-cases/search-<%= entityNamePlural %>.use-case';
|
|
11
|
+
|
|
12
|
+
const <%= searchQuery.filtersSchemaName %> = z.object({
|
|
13
|
+
<% searchQuery.filters.forEach((f) => { -%>
|
|
14
|
+
<% if (f.isUuid) { -%>
|
|
15
|
+
<%= f.camelName %>: z.string().uuid().optional(),
|
|
16
|
+
<% } else if (f.hasChoices) { -%>
|
|
17
|
+
<%= f.camelName %>: z.enum([<%- f.choices.map((c) => `'${c}'`).join(', ') %>]).optional(),
|
|
18
|
+
<% } else if (f.isBoolean) { -%>
|
|
19
|
+
<%= f.camelName %>: z.coerce.boolean().optional(),
|
|
20
|
+
<% } else if (f.isNumber) { -%>
|
|
21
|
+
<%= f.camelName %>: z.coerce.number().optional(),
|
|
22
|
+
<% } else { -%>
|
|
23
|
+
<%= f.camelName %>: z.string().optional(),
|
|
24
|
+
<% } -%>
|
|
25
|
+
<% }) -%>
|
|
26
|
+
<% if (searchQuery.searchField) { -%>
|
|
27
|
+
search: z.string().optional(),
|
|
28
|
+
<% } -%>
|
|
29
|
+
}).merge(PaginationSchema);
|
|
30
|
+
|
|
31
|
+
function parseOrThrow<S extends z.ZodTypeAny>(schema: S, input: unknown): z.infer<S> {
|
|
32
|
+
const result = schema.safeParse(input);
|
|
33
|
+
if (!result.success) throw new BadRequestException(result.error.flatten());
|
|
34
|
+
return result.data;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Filtered search controller (task #16) — generated from queries:
|
|
39
|
+
* block in <%= entityName %>.yaml.
|
|
40
|
+
*/
|
|
41
|
+
@Controller('<%= entityNamePlural %>')
|
|
42
|
+
export class <%= classNames.searchController %> {
|
|
43
|
+
constructor(private readonly searchUseCase: <%= searchQuery.useCaseClassName %>) {}
|
|
44
|
+
|
|
45
|
+
@Get('search')
|
|
46
|
+
async search(@Query() query: Record<string, unknown>) {
|
|
47
|
+
return this.searchUseCase.execute(parseOrThrow(<%= searchQuery.filtersSchemaName %>, query));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
<% } -%>
|
|
@@ -9,18 +9,41 @@ import { EVENT_BUS } from '@shared/constants/tokens';
|
|
|
9
9
|
import { <%= serviceBaseClass %> } from '<%= serviceBaseImport %>';
|
|
10
10
|
import { <%= classNames.repository %> } from './<%= entityName %>.repository';
|
|
11
11
|
import type { <%= classNames.entity %> } from './<%= entityName %>.entity';
|
|
12
|
+
<% if (eavEnabled) { -%>
|
|
13
|
+
import { FieldValueService } from '../field_values/field_value.service';
|
|
14
|
+
<% } -%>
|
|
15
|
+
<% if (eavValueTable) { -%>
|
|
16
|
+
import { toEavRows, mergeEavRows } from '@shared/eav-helpers';
|
|
17
|
+
import type { DrizzleTx } from '@shared/types/drizzle';
|
|
18
|
+
import { <%= eavDefinitionPascal %>Repository } from '../<%= eavDefinitionEntityPlural %>/<%= eavDefinitionEntity %>.repository';
|
|
19
|
+
<% } -%>
|
|
12
20
|
|
|
13
21
|
@Injectable()
|
|
14
22
|
export class <%= classNames.service %> extends WithAnalytics(
|
|
15
23
|
<%= serviceBaseClass %><<%= classNames.repository %>, <%= classNames.entity %>>,
|
|
16
24
|
) {
|
|
17
25
|
protected override readonly entityName = '<%= entityName %>';
|
|
26
|
+
<% if (hasPatternConfig) { -%>
|
|
27
|
+
|
|
28
|
+
// Per-entity `<%= patternName %>` pattern config (from YAML `config:` block).
|
|
29
|
+
// Mirrors the repository-side emission; the pattern's base service reads
|
|
30
|
+
// `this.patternConfig` directly.
|
|
31
|
+
protected override readonly patternConfig = <%- renderPatternConfigLiteral(patternConfig, ' ', ' ') %> as const;
|
|
32
|
+
<% } -%>
|
|
18
33
|
|
|
19
34
|
/** Injected by NestJS when EventsModule is registered. */
|
|
20
35
|
@Optional() @Inject(EVENT_BUS)
|
|
21
36
|
protected override eventBus: any = undefined;
|
|
22
37
|
|
|
23
|
-
constructor(
|
|
38
|
+
constructor(
|
|
39
|
+
protected override readonly repository: <%= classNames.repository %>,
|
|
40
|
+
<% if (eavEnabled) { -%>
|
|
41
|
+
private readonly fieldValues: FieldValueService,
|
|
42
|
+
<% } -%>
|
|
43
|
+
<% if (eavValueTable) { -%>
|
|
44
|
+
private readonly definitionRepo: <%= eavDefinitionPascal %>Repository,
|
|
45
|
+
<% } -%>
|
|
46
|
+
) {
|
|
24
47
|
super(repository);
|
|
25
48
|
}
|
|
26
49
|
|
|
@@ -31,4 +54,78 @@ export class <%= classNames.service %> extends WithAnalytics(
|
|
|
31
54
|
<%_ serviceInheritedMethods.forEach(line => { _%>
|
|
32
55
|
// <%= line %>
|
|
33
56
|
<%_ }) _%>
|
|
57
|
+
<% if (eavEnabled) { %>
|
|
58
|
+
/**
|
|
59
|
+
* EAV paired read (ADR-13): fetch the entity and merge dynamic `field_values`
|
|
60
|
+
* into a single `fields` bag. FieldValueService owns the FieldDefinition
|
|
61
|
+
* lookup internally. Use this for frontend detail views, LLM context,
|
|
62
|
+
* exports.
|
|
63
|
+
*/
|
|
64
|
+
async findByIdWithFields(
|
|
65
|
+
id: string,
|
|
66
|
+
): Promise<(<%= classNames.entity %> & { fields: Record<string, unknown> }) | null> {
|
|
67
|
+
const entity = await this.repository.findById(id);
|
|
68
|
+
if (!entity) return null;
|
|
69
|
+
const fields = await this.fieldValues.findMergedByEntity('<%= entityName %>', id);
|
|
70
|
+
return { ...entity, fields };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* EAV paired read (ADR-13): list variant. Fetches all entities then merges
|
|
75
|
+
* each one's EAV fields via FieldValueService. Acceptable for modest result
|
|
76
|
+
* sets; page externally for large collections.
|
|
77
|
+
*/
|
|
78
|
+
async listWithFields(): Promise<Array<<%= classNames.entity %> & { fields: Record<string, unknown> }>> {
|
|
79
|
+
const entities = await this.repository.list();
|
|
80
|
+
if (entities.length === 0) return [];
|
|
81
|
+
return Promise.all(
|
|
82
|
+
entities.map(async (entity) => {
|
|
83
|
+
const fields = await this.fieldValues.findMergedByEntity('<%= entityName %>', entity.id);
|
|
84
|
+
return { ...entity, fields };
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
<% } %>
|
|
89
|
+
<% if (eavValueTable) { %>
|
|
90
|
+
/**
|
|
91
|
+
* EAV compound write (task #23) — upserts a bag of dynamic fields onto
|
|
92
|
+
* an owning entity in a single transaction. Resolves field keys to
|
|
93
|
+
* definition ids internally (by reading <%= eavDefinitionPascal %>Repository),
|
|
94
|
+
* so use-cases inject only this service. Unknown keys are skipped; the
|
|
95
|
+
* caller is expected to have created the definitions first (auto-create
|
|
96
|
+
* is a later step).
|
|
97
|
+
*/
|
|
98
|
+
async upsertFieldsTransactional(
|
|
99
|
+
entityType: string,
|
|
100
|
+
entityId: string,
|
|
101
|
+
userId: string,
|
|
102
|
+
fields: Record<string, unknown>,
|
|
103
|
+
tx?: DrizzleTx,
|
|
104
|
+
): Promise<void> {
|
|
105
|
+
if (!fields || Object.keys(fields).length === 0) return;
|
|
106
|
+
const allDefs = await this.definitionRepo.list();
|
|
107
|
+
const defs = allDefs.filter((d) => (d as any).entityType === entityType);
|
|
108
|
+
const defIdByKey = new Map(defs.map((d) => [d.key, d.id]));
|
|
109
|
+
const rows = toEavRows(entityId, entityType, userId, fields, defIdByKey);
|
|
110
|
+
if (rows.length === 0) return;
|
|
111
|
+
await this.repository.upsertCurrentValues(rows as Array<Partial<<%= classNames.entity %>>>, tx);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* EAV paired read (task #23) — returns the current merged `{ key: value }`
|
|
116
|
+
* bag for one owning entity. Resolves definition ids to keys internally.
|
|
117
|
+
*/
|
|
118
|
+
async findMergedByEntity(
|
|
119
|
+
entityType: string,
|
|
120
|
+
entityId: string,
|
|
121
|
+
): Promise<Record<string, unknown>> {
|
|
122
|
+
const [rows, allDefs] = await Promise.all([
|
|
123
|
+
this.repository.findByEntityIdAndType(entityId, entityType),
|
|
124
|
+
this.definitionRepo.list(),
|
|
125
|
+
]);
|
|
126
|
+
const defs = allDefs.filter((d) => (d as any).entityType === entityType);
|
|
127
|
+
const defsById = new Map(defs.map((d) => [d.id, { key: d.key }]));
|
|
128
|
+
return mergeEavRows(rows as any, defsById);
|
|
129
|
+
}
|
|
130
|
+
<% } %>
|
|
34
131
|
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: "<%= typeof clpOutputPaths !== 'undefined' ? clpOutputPaths.createUseCase : null %>"
|
|
3
|
+
skip_if: "<%= typeof clpOutputPaths === 'undefined' || !clpOutputPaths.createUseCase %>"
|
|
4
|
+
force: true
|
|
5
|
+
---
|
|
6
|
+
<% if (eavEnabled) { -%>
|
|
7
|
+
import { Injectable, Inject } from '@nestjs/common';
|
|
8
|
+
import { DRIZZLE } from '<%= drizzleTokenImport %>';
|
|
9
|
+
import type { DrizzleClient } from '<%= drizzleTypeImport %>';
|
|
10
|
+
<% if (hasEmits && createEventType) { -%>
|
|
11
|
+
import { TYPED_EVENT_BUS, TypedEventBus } from '<%= eventsTokenImport %>';
|
|
12
|
+
<% } -%>
|
|
13
|
+
import { FieldValueService } from '../../field_values/field_value.service';
|
|
14
|
+
import { <%= classNames.service %> } from '../<%= entityName %>.service';
|
|
15
|
+
import type { <%= classNames.createDto %> } from '../dto/create-<%= entityName %>.dto';
|
|
16
|
+
import type { <%= classNames.entity %> } from '../<%= entityName %>.entity';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* EAV compound-write use case (ADR-13).
|
|
20
|
+
*
|
|
21
|
+
* Splits `{ fields, ...core }` from the DTO and persists both halves in a
|
|
22
|
+
* single transaction: core columns go to the <%= entityName %> table via
|
|
23
|
+
* <%= classNames.service %>, dynamic `fields` go to `field_values` via
|
|
24
|
+
* FieldValueService.upsertFieldsTransactional (which owns the
|
|
25
|
+
* FieldDefinition lookup internally). Atomicity comes from the shared tx;
|
|
26
|
+
* each service still only writes its own domain.
|
|
27
|
+
<% if (hasEmits && createEventType) { -%>
|
|
28
|
+
*
|
|
29
|
+
* EXTENSION POINT (EVT-7): verify payload mapping against
|
|
30
|
+
* events/<%= createEventType.type %>.yaml before shipping.
|
|
31
|
+
<% } -%>
|
|
32
|
+
*/
|
|
33
|
+
@Injectable()
|
|
34
|
+
export class <%= classNames.createUseCase %> {
|
|
35
|
+
constructor(
|
|
36
|
+
private readonly <%= entityNamePlural %>: <%= classNames.service %>,
|
|
37
|
+
private readonly fields: FieldValueService,
|
|
38
|
+
@Inject(DRIZZLE) private readonly db: DrizzleClient,
|
|
39
|
+
<% if (hasEmits && createEventType) { -%>
|
|
40
|
+
@Inject(TYPED_EVENT_BUS) private readonly typedEvents: TypedEventBus,
|
|
41
|
+
<% } -%>
|
|
42
|
+
) {}
|
|
43
|
+
|
|
44
|
+
async execute(
|
|
45
|
+
dto: <%= classNames.createDto %> & { fields?: Record<string, unknown> },
|
|
46
|
+
<%= hasEmits && createEventType ? 'opts' : '_opts' %>?: { actor?: { tenantId?: string | null; userId?: string } },
|
|
47
|
+
): Promise<<%= classNames.entity %>> {
|
|
48
|
+
return this.db.transaction(async (tx) => {
|
|
49
|
+
const { fields, ...core } = dto;
|
|
50
|
+
const entity = await this.<%= entityNamePlural %>.create(core as <%= classNames.createDto %>, tx);
|
|
51
|
+
if (fields && Object.keys(fields).length > 0) {
|
|
52
|
+
await this.fields.upsertFieldsTransactional(
|
|
53
|
+
'<%= entityName %>',
|
|
54
|
+
entity.id,
|
|
55
|
+
core.userId,
|
|
56
|
+
fields,
|
|
57
|
+
tx,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
<% if (hasEmits && createEventType) { -%>
|
|
61
|
+
// TODO: verify payload mapping against events/<%= createEventType.type %>.yaml
|
|
62
|
+
await this.typedEvents.publish(
|
|
63
|
+
'<%= createEventType.type %>',
|
|
64
|
+
entity.id,
|
|
65
|
+
{
|
|
66
|
+
<% createEventType.payloadMap.forEach((p) => { -%>
|
|
67
|
+
<%= p.camelKey %>: <%- p.expression %>,<% if (p.todo) { %> // TODO: <%= p.todo %><% } %>
|
|
68
|
+
|
|
69
|
+
<% }) -%>
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
tx,
|
|
73
|
+
metadata: opts?.actor
|
|
74
|
+
? { tenantId: opts.actor.tenantId, userId: opts.actor.userId }
|
|
75
|
+
: undefined,
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
<% } -%>
|
|
79
|
+
return entity;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
<% } else { -%>
|
|
84
|
+
<% if (hasEmits && createEventType) { -%>
|
|
85
|
+
import { Injectable, Inject } from '@nestjs/common';
|
|
86
|
+
import { DRIZZLE } from '<%= drizzleTokenImport %>';
|
|
87
|
+
import type { DrizzleClient } from '<%= drizzleTypeImport %>';
|
|
88
|
+
import { TYPED_EVENT_BUS, TypedEventBus } from '<%= eventsTokenImport %>';
|
|
89
|
+
import { <%= classNames.service %> } from '../<%= entityName %>.service';
|
|
90
|
+
import type { <%= classNames.createDto %> } from '../dto/create-<%= entityName %>.dto';
|
|
91
|
+
import type { <%= classNames.entity %> } from '../<%= entityName %>.entity';
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* EXTENSION POINT (EVT-7): verify payload mapping against
|
|
95
|
+
* events/<%= createEventType.type %>.yaml before shipping.
|
|
96
|
+
*/
|
|
97
|
+
@Injectable()
|
|
98
|
+
export class <%= classNames.createUseCase %> {
|
|
99
|
+
constructor(
|
|
100
|
+
private readonly service: <%= classNames.service %>,
|
|
101
|
+
@Inject(DRIZZLE) private readonly db: DrizzleClient,
|
|
102
|
+
@Inject(TYPED_EVENT_BUS) private readonly typedEvents: TypedEventBus,
|
|
103
|
+
) {}
|
|
104
|
+
|
|
105
|
+
async execute(
|
|
106
|
+
dto: <%= classNames.createDto %>,
|
|
107
|
+
opts?: { actor?: { tenantId?: string | null; userId?: string } },
|
|
108
|
+
): Promise<<%= classNames.entity %>> {
|
|
109
|
+
return this.db.transaction(async (tx) => {
|
|
110
|
+
const entity = await this.service.create(dto, tx);
|
|
111
|
+
// TODO: verify payload mapping against events/<%= createEventType.type %>.yaml
|
|
112
|
+
await this.typedEvents.publish(
|
|
113
|
+
'<%= createEventType.type %>',
|
|
114
|
+
entity.id,
|
|
115
|
+
{
|
|
116
|
+
<% createEventType.payloadMap.forEach((p) => { -%>
|
|
117
|
+
<%= p.camelKey %>: <%- p.expression %>,<% if (p.todo) { %> // TODO: <%= p.todo %><% } %>
|
|
118
|
+
|
|
119
|
+
<% }) -%>
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
tx,
|
|
123
|
+
metadata: opts?.actor
|
|
124
|
+
? { tenantId: opts.actor.tenantId, userId: opts.actor.userId }
|
|
125
|
+
: undefined,
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
return entity;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
<% } else { -%>
|
|
133
|
+
import { Injectable } from '@nestjs/common';
|
|
134
|
+
import { <%= classNames.service %> } from '../<%= entityName %>.service';
|
|
135
|
+
import type { <%= classNames.createDto %> } from '../dto/create-<%= entityName %>.dto';
|
|
136
|
+
import type { <%= classNames.entity %> } from '../<%= entityName %>.entity';
|
|
137
|
+
|
|
138
|
+
@Injectable()
|
|
139
|
+
export class <%= classNames.createUseCase %> {
|
|
140
|
+
constructor(private readonly service: <%= classNames.service %>) {}
|
|
141
|
+
|
|
142
|
+
async execute(
|
|
143
|
+
dto: <%= classNames.createDto %>,
|
|
144
|
+
_opts?: { actor?: { tenantId?: string | null; userId?: string } },
|
|
145
|
+
): Promise<<%= classNames.entity %>> {
|
|
146
|
+
return this.service.create(dto);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
<% } -%>
|
|
150
|
+
<% } -%>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: "<%= typeof clpOutputPaths !== 'undefined' ? clpOutputPaths.deleteUseCase : null %>"
|
|
3
|
+
skip_if: "<%= typeof clpOutputPaths === 'undefined' || !clpOutputPaths.deleteUseCase %>"
|
|
4
|
+
force: true
|
|
5
|
+
---
|
|
6
|
+
<% if (hasEmits && deleteEventType) { -%>
|
|
7
|
+
import { Injectable, Inject, NotFoundException } from '@nestjs/common';
|
|
8
|
+
import { DRIZZLE } from '<%= drizzleTokenImport %>';
|
|
9
|
+
import type { DrizzleClient } from '<%= drizzleTypeImport %>';
|
|
10
|
+
import { TYPED_EVENT_BUS, TypedEventBus } from '<%= eventsTokenImport %>';
|
|
11
|
+
import { <%= classNames.service %> } from '../<%= entityName %>.service';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* EXTENSION POINT (EVT-7): verify payload mapping against
|
|
15
|
+
* events/<%= deleteEventType.type %>.yaml before shipping.
|
|
16
|
+
*/
|
|
17
|
+
@Injectable()
|
|
18
|
+
export class <%= classNames.deleteUseCase %> {
|
|
19
|
+
constructor(
|
|
20
|
+
private readonly service: <%= classNames.service %>,
|
|
21
|
+
@Inject(DRIZZLE) private readonly db: DrizzleClient,
|
|
22
|
+
@Inject(TYPED_EVENT_BUS) private readonly typedEvents: TypedEventBus,
|
|
23
|
+
) {}
|
|
24
|
+
|
|
25
|
+
async execute(
|
|
26
|
+
id: string,
|
|
27
|
+
opts?: { actor?: { tenantId?: string | null; userId?: string } },
|
|
28
|
+
): Promise<void> {
|
|
29
|
+
return this.db.transaction(async (tx) => {
|
|
30
|
+
const entity = await this.service.findById(id);
|
|
31
|
+
if (!entity) {
|
|
32
|
+
throw new NotFoundException(`<%= classNames.entity %> with id ${id} not found`);
|
|
33
|
+
}
|
|
34
|
+
await this.service.delete(id, tx);
|
|
35
|
+
// TODO: verify payload mapping against events/<%= deleteEventType.type %>.yaml
|
|
36
|
+
await this.typedEvents.publish(
|
|
37
|
+
'<%= deleteEventType.type %>',
|
|
38
|
+
entity.id,
|
|
39
|
+
{
|
|
40
|
+
<% deleteEventType.payloadMap.forEach((p) => { -%>
|
|
41
|
+
<%= p.camelKey %>: <%- p.expression %>,<% if (p.todo) { %> // TODO: <%= p.todo %><% } %>
|
|
42
|
+
|
|
43
|
+
<% }) -%>
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
tx,
|
|
47
|
+
metadata: opts?.actor
|
|
48
|
+
? { tenantId: opts.actor.tenantId, userId: opts.actor.userId }
|
|
49
|
+
: undefined,
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
<% } else { -%>
|
|
56
|
+
import { Injectable } from '@nestjs/common';
|
|
57
|
+
import { <%= classNames.service %> } from '../<%= entityName %>.service';
|
|
58
|
+
|
|
59
|
+
@Injectable()
|
|
60
|
+
export class <%= classNames.deleteUseCase %> {
|
|
61
|
+
constructor(private readonly service: <%= classNames.service %>) {}
|
|
62
|
+
|
|
63
|
+
async execute(
|
|
64
|
+
id: string,
|
|
65
|
+
_opts?: { actor?: { tenantId?: string | null; userId?: string } },
|
|
66
|
+
): Promise<void> {
|
|
67
|
+
return this.service.delete(id);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
<% } -%>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: "<%= typeof clpOutputPaths !== 'undefined' ? clpOutputPaths.findByIdWithFieldsUseCase : null %>"
|
|
3
|
+
skip_if: "<%= typeof clpOutputPaths === 'undefined' || !clpOutputPaths.findByIdWithFieldsUseCase %>"
|
|
4
|
+
force: true
|
|
5
|
+
---
|
|
6
|
+
import { Injectable } from '@nestjs/common';
|
|
7
|
+
import { <%= classNames.service %> } from '../<%= entityName %>.service';
|
|
8
|
+
import type { <%= classNames.entity %> } from '../<%= entityName %>.entity';
|
|
9
|
+
|
|
10
|
+
@Injectable()
|
|
11
|
+
export class <%= classNames.findByIdWithFieldsUseCase %> {
|
|
12
|
+
constructor(private readonly service: <%= classNames.service %>) {}
|
|
13
|
+
|
|
14
|
+
async execute(
|
|
15
|
+
id: string,
|
|
16
|
+
): Promise<(<%= classNames.entity %> & { fields: Record<string, unknown> }) | null> {
|
|
17
|
+
return this.service.findByIdWithFields(id);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
to: "<%= typeof clpOutputPaths !== 'undefined' ? clpOutputPaths.findByIdUseCase : null %>"
|
|
3
3
|
force: true
|
|
4
4
|
---
|
|
5
|
-
import { Injectable } from '@nestjs/common';
|
|
5
|
+
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
6
6
|
import { <%= classNames.service %> } from '../<%= entityName %>.service';
|
|
7
7
|
import type { <%= classNames.entity %> } from '../<%= entityName %>.entity';
|
|
8
8
|
|
|
@@ -10,7 +10,11 @@ import type { <%= classNames.entity %> } from '../<%= entityName %>.entity';
|
|
|
10
10
|
export class <%= classNames.findByIdUseCase %> {
|
|
11
11
|
constructor(private readonly service: <%= classNames.service %>) {}
|
|
12
12
|
|
|
13
|
-
async execute(id: string): Promise<<%= classNames.entity
|
|
14
|
-
|
|
13
|
+
async execute(id: string): Promise<<%= classNames.entity %>> {
|
|
14
|
+
const entity = await this.service.findById(id);
|
|
15
|
+
if (entity === null || entity === undefined) {
|
|
16
|
+
throw new NotFoundException(`<%= classNames.entity %> not found: ${id}`);
|
|
17
|
+
}
|
|
18
|
+
return entity;
|
|
15
19
|
}
|
|
16
20
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: "<%= typeof clpOutputPaths !== 'undefined' ? clpOutputPaths.listWithFieldsUseCase : null %>"
|
|
3
|
+
skip_if: "<%= typeof clpOutputPaths === 'undefined' || !clpOutputPaths.listWithFieldsUseCase %>"
|
|
4
|
+
force: true
|
|
5
|
+
---
|
|
6
|
+
import { Injectable } from '@nestjs/common';
|
|
7
|
+
import { <%= classNames.service %> } from '../<%= entityName %>.service';
|
|
8
|
+
import type { <%= classNames.entity %> } from '../<%= entityName %>.entity';
|
|
9
|
+
|
|
10
|
+
@Injectable()
|
|
11
|
+
export class <%= classNames.listWithFieldsUseCase %> {
|
|
12
|
+
constructor(private readonly service: <%= classNames.service %>) {}
|
|
13
|
+
|
|
14
|
+
async execute(): Promise<Array<<%= classNames.entity %> & { fields: Record<string, unknown> }>> {
|
|
15
|
+
return this.service.listWithFields();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: "<%= typeof clpOutputPaths !== 'undefined' ? clpOutputPaths.searchUseCase : null %>"
|
|
3
|
+
skip_if: "<%= typeof clpOutputPaths === 'undefined' || !clpOutputPaths.searchUseCase %>"
|
|
4
|
+
force: true
|
|
5
|
+
---
|
|
6
|
+
<% if (hasSearchQuery) { -%>
|
|
7
|
+
import { Injectable } from '@nestjs/common';
|
|
8
|
+
import { and, asc, eq<% if (searchQuery.searchField) { %>, ilike<% } %>, type SQL } from 'drizzle-orm';
|
|
9
|
+
import type { Page } from '@shared/http/pagination';
|
|
10
|
+
import { <%= classNames.service %> } from '../<%= entityName %>.service';
|
|
11
|
+
import { <%= entityNamePlural %>, type <%= classNames.entity %> } from '../<%= entityName %>.entity';
|
|
12
|
+
|
|
13
|
+
export interface <%= searchQuery.inputTypeName %> {
|
|
14
|
+
<% searchQuery.filters.forEach((f) => { -%>
|
|
15
|
+
<%= f.camelName %>?: <%- f.hasChoices ? f.choices.map((c) => `'${c}'`).join(' | ') : f.tsType %>;
|
|
16
|
+
<% }) -%>
|
|
17
|
+
<% if (searchQuery.searchField) { -%>
|
|
18
|
+
search?: string;
|
|
19
|
+
<% } -%>
|
|
20
|
+
<% if (searchQuery.paginate) { -%>
|
|
21
|
+
limit: number;
|
|
22
|
+
offset: number;
|
|
23
|
+
<% } -%>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Filtered search use case (task #16).
|
|
28
|
+
*
|
|
29
|
+
* Composes the entity service's `list` + `count` with filter-AND and
|
|
30
|
+
* an optional ilike search on `<%= searchQuery.searchField ?? 'N/A' %>`.
|
|
31
|
+
* Pagination is enforced at the Zod layer in the controller.
|
|
32
|
+
*/
|
|
33
|
+
@Injectable()
|
|
34
|
+
export class <%= searchQuery.useCaseClassName %> {
|
|
35
|
+
constructor(private readonly service: <%= classNames.service %>) {}
|
|
36
|
+
|
|
37
|
+
async execute(input: <%= searchQuery.inputTypeName %>): Promise<Page<<%= classNames.entity %>>> {
|
|
38
|
+
const conditions: SQL[] = [];
|
|
39
|
+
<% searchQuery.filters.forEach((f) => { -%>
|
|
40
|
+
<% if (f.isBoolean) { -%>
|
|
41
|
+
if (input.<%= f.camelName %> !== undefined) conditions.push(eq(<%= entityNamePlural %>.<%= f.camelName %>, input.<%= f.camelName %>));
|
|
42
|
+
<% } else { -%>
|
|
43
|
+
if (input.<%= f.camelName %>) conditions.push(eq(<%= entityNamePlural %>.<%= f.camelName %>, input.<%= f.camelName %>));
|
|
44
|
+
<% } -%>
|
|
45
|
+
<% }) -%>
|
|
46
|
+
<% if (searchQuery.searchField) { -%>
|
|
47
|
+
if (input.search) conditions.push(ilike(<%= entityNamePlural %>.<%= searchQuery.searchFieldCamel %>, `%${input.search}%`));
|
|
48
|
+
<% } -%>
|
|
49
|
+
|
|
50
|
+
const where =
|
|
51
|
+
conditions.length === 0 ? undefined :
|
|
52
|
+
conditions.length === 1 ? conditions[0] :
|
|
53
|
+
and(...conditions);
|
|
54
|
+
|
|
55
|
+
const [items, total] = await Promise.all([
|
|
56
|
+
this.service.list({ where, limit: input.limit, offset: input.offset, orderBy: asc(<%= entityNamePlural %>.createdAt) }),
|
|
57
|
+
this.service.count(where),
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
return { items, total, limit: input.limit, offset: input.offset };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
<% } -%>
|