@pattern-stack/codegen 0.6.7 → 0.7.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/CHANGELOG.md +12 -0
- package/dist/src/cli/index.js +516 -73
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/index.d.ts +208 -1
- package/dist/src/index.js +147 -0
- package/dist/src/index.js.map +1 -1
- package/package.json +1 -1
- package/src/patterns/library/base-junction-fields.ts +32 -0
- package/src/patterns/library/index.ts +7 -0
- package/src/patterns/library/junction.pattern.ts +41 -0
- package/templates/entity/new/backend/application/queries/get-by-id.ejs.t +3 -3
- package/templates/entity/new/backend/application/queries/grouped-index.ejs.t +5 -5
- package/templates/entity/new/backend/application/queries/index.ejs.t +3 -0
- package/templates/entity/new/backend/application/queries/list.ejs.t +3 -3
- package/templates/entity/new/backend/application/queries/relationships.queries.ejs.t +147 -0
- package/templates/entity/new/backend/database/repository.ejs.t +36 -176
- package/templates/entity/new/backend/domain/entity.ejs.t +0 -44
- package/templates/entity/new/backend/domain/grouped-index.ejs.t +4 -60
- package/templates/entity/new/backend/domain/index.ejs.t +2 -2
- package/templates/entity/new/backend/domain/repository-interface.ejs.t +16 -17
- package/templates/entity/new/backend/modules/core/module.ejs.t +10 -0
- package/templates/entity/new/backend/presentation/controller.ejs.t +2 -34
- package/templates/entity/new/clean-lite-ps/entity.ejs.t +21 -2
- package/templates/entity/new/clean-lite-ps/module.ejs.t +27 -2
- package/templates/entity/new/clean-lite-ps/prompt-extension.js +108 -9
- package/templates/entity/new/clean-lite-ps/repository.ejs.t +33 -1
- package/templates/entity/new/clean-lite-ps/service.ejs.t +79 -0
- package/templates/entity/new/prompt.js +1 -0
- package/templates/junction/new/_inject-parent-module-clp-left.ejs.t +8 -0
- package/templates/junction/new/_inject-parent-module-clp-right.ejs.t +8 -0
- package/templates/junction/new/_inject-parent-module-import-clp-left.ejs.t +9 -0
- package/templates/junction/new/_inject-parent-module-import-clp-right.ejs.t +9 -0
- package/templates/junction/new/_inject-parent-service-clp-left.ejs.t +51 -0
- package/templates/junction/new/_inject-parent-service-clp-right.ejs.t +48 -0
- package/templates/junction/new/_inject-parent-service-import-clp-left.ejs.t +11 -0
- package/templates/junction/new/_inject-parent-service-import-clp-right.ejs.t +11 -0
- package/templates/junction/new/entity.ejs.t +111 -0
- package/templates/junction/new/index.ejs.t +15 -0
- package/templates/junction/new/module.ejs.t +37 -0
- package/templates/junction/new/prompt.js +492 -0
- package/templates/junction/new/repository.ejs.t +67 -0
- package/templates/junction/new/service.ejs.t +174 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: "<%= outputPaths.service %>"
|
|
3
|
+
force: true
|
|
4
|
+
---
|
|
5
|
+
import { Injectable, Inject, Optional } from '@nestjs/common';
|
|
6
|
+
import { WithAnalytics } from '@shared/base-classes/with-analytics';
|
|
7
|
+
import { EVENT_BUS } from '@shared/constants/tokens';
|
|
8
|
+
import { BaseService } from '@shared/base-classes/base-service';
|
|
9
|
+
import { <%= classNames.repository %> } from './<%= name %>.repository';
|
|
10
|
+
import type { <%= classNames.entity %> } from './<%= name %>.entity';
|
|
11
|
+
import { <%= leftRepositoryClass %> } from '<%= leftRepoImportFromJunction %>';
|
|
12
|
+
import type { <%= leftEntityPascal %> } from '<%= leftEntityImportFromJunction %>';
|
|
13
|
+
import { <%= rightRepositoryClass %> } from '<%= rightRepoImportFromJunction %>';
|
|
14
|
+
import type { <%= rightEntityPascal %> } from '<%= rightEntityImportFromJunction %>';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Pick of the link-side mutable fields that callers may supply when
|
|
18
|
+
* attaching. Subset of `<%= classNames.entity %>` minus the two FK columns
|
|
19
|
+
* (those come from the method args).
|
|
20
|
+
*/
|
|
21
|
+
export type <%= entityNamePascal %>LinkInput = Partial<
|
|
22
|
+
Pick<<%= classNames.entity %>,
|
|
23
|
+
'isPrimary'<% if (temporal) { %> | 'startedAt' | 'endedAt'<% } %><% if (sourced) { %> | 'sourcedFrom' | 'confidence' | 'matchedAt'<% } %><% if (hasRole) { %> | 'role'<% } %>
|
|
24
|
+
>
|
|
25
|
+
>;
|
|
26
|
+
|
|
27
|
+
@Injectable()
|
|
28
|
+
export class <%= classNames.service %> extends WithAnalytics(
|
|
29
|
+
BaseService<<%= classNames.repository %>, <%= classNames.entity %>>,
|
|
30
|
+
) {
|
|
31
|
+
protected override readonly entityName = '<%= name %>';
|
|
32
|
+
|
|
33
|
+
/** Injected by NestJS when EventsModule is registered. */
|
|
34
|
+
@Optional() @Inject(EVENT_BUS)
|
|
35
|
+
protected override eventBus: any = undefined;
|
|
36
|
+
|
|
37
|
+
constructor(
|
|
38
|
+
protected override readonly repository: <%= classNames.repository %>,
|
|
39
|
+
private readonly <%= leftEntityCamel %>Repo: <%= leftRepositoryClass %>,
|
|
40
|
+
private readonly <%= rightEntityCamel %>Repo: <%= rightRepositoryClass %>,
|
|
41
|
+
) {
|
|
42
|
+
super(repository);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Pairing-aware pass-throughs — mirror the repo's two finders so use-cases
|
|
46
|
+
// and #60's fan-out methods both delegate through the service layer, keeping
|
|
47
|
+
// analytics/events instrumentation uniform (per relationship's service.ejs.t).
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Fetch all junction rows for a given <%= leftColumn %>.
|
|
51
|
+
*/
|
|
52
|
+
async findBy<%= leftEntityPascal %>Id(
|
|
53
|
+
<%= leftColumnCamel %>: string,
|
|
54
|
+
opts?: { cursor?: string; limit?: number },
|
|
55
|
+
): Promise<<%= classNames.entity %>[]> {
|
|
56
|
+
return this.repository.findBy<%= leftEntityPascal %>Id(<%= leftColumnCamel %>, opts);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Fetch all junction rows for a given <%= rightColumn %>.
|
|
61
|
+
*/
|
|
62
|
+
async findBy<%= rightEntityPascal %>Id(
|
|
63
|
+
<%= rightColumnCamel %>: string,
|
|
64
|
+
opts?: { cursor?: string; limit?: number },
|
|
65
|
+
): Promise<<%= classNames.entity %>[]> {
|
|
66
|
+
return this.repository.findBy<%= rightEntityPascal %>Id(<%= rightColumnCamel %>, opts);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
70
|
+
// CGP-60 — canonical fan-out methods
|
|
71
|
+
// Mirrored, paginated, composed `{ entity, link }` shape. Always emitted
|
|
72
|
+
// on the junction service; parent-side `attach<Right>` / `addTo<Left>` /
|
|
73
|
+
// etc. inject templates delegate here. `list` is implemented with two
|
|
74
|
+
// single-table queries (no Drizzle `with:`).
|
|
75
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create a junction row linking a <%= leftEntity %> and a <%= rightEntity %>.
|
|
79
|
+
* Returns the persisted row.
|
|
80
|
+
*
|
|
81
|
+
* **Idempotency:** NOT idempotent at the service layer in v1. A duplicate
|
|
82
|
+
* pair raises the DB-level composite-PK unique-constraint error from the
|
|
83
|
+
* underlying repository's `create`. Callers requiring idempotency should
|
|
84
|
+
* either check existence via `findBy<%= leftEntityPascal %>Id` + filter,
|
|
85
|
+
* or wrap the call in try/catch on the unique-violation error. A future
|
|
86
|
+
* leaf may add a transactional check-then-create here if a consumer
|
|
87
|
+
* surfaces the need (track as a follow-up if so).
|
|
88
|
+
*/
|
|
89
|
+
async attach(
|
|
90
|
+
<%= leftEntityCamel %>Id: string,
|
|
91
|
+
<%= rightEntityCamel %>Id: string,
|
|
92
|
+
link?: <%= entityNamePascal %>LinkInput,
|
|
93
|
+
): Promise<<%= classNames.entity %>> {
|
|
94
|
+
return this.create({
|
|
95
|
+
<%= leftColumnCamel %>: <%= leftEntityCamel %>Id,
|
|
96
|
+
<%= rightColumnCamel %>: <%= rightEntityCamel %>Id,
|
|
97
|
+
...(link ?? {}),
|
|
98
|
+
} as unknown as Partial<<%= classNames.entity %>>);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Remove the junction row linking `<%= leftEntityCamel %>Id` and
|
|
103
|
+
* `<%= rightEntityCamel %>Id`. No-op if no row exists.
|
|
104
|
+
*/
|
|
105
|
+
async detach(
|
|
106
|
+
<%= leftEntityCamel %>Id: string,
|
|
107
|
+
<%= rightEntityCamel %>Id: string,
|
|
108
|
+
): Promise<void> {
|
|
109
|
+
const links = await this.repository.findBy<%= leftEntityPascal %>Id(<%= leftEntityCamel %>Id);
|
|
110
|
+
const match = links.find(
|
|
111
|
+
(l) => (l as any).<%= rightColumnCamel %> === <%= rightEntityCamel %>Id,
|
|
112
|
+
);
|
|
113
|
+
if (match) {
|
|
114
|
+
await this.delete((match as any).id ?? `${<%= leftEntityCamel %>Id}:${<%= rightEntityCamel %>Id}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* List the targets associated with one side of the junction, composed as
|
|
120
|
+
* `{ entity, link }`. Implementation: one repo call for the links, one
|
|
121
|
+
* `findByIds` call for the targets — no SQL JOIN. Cursor pagination by
|
|
122
|
+
* right-entity `id` (matches CGP-358 has_many shape; time-ordered cursor
|
|
123
|
+
* is deferred per spec Open Q3).
|
|
124
|
+
*/
|
|
125
|
+
async listAssoc(
|
|
126
|
+
side: 'left' | 'right',
|
|
127
|
+
anchorId: string,
|
|
128
|
+
opts?: { cursor?: string; limit?: number },
|
|
129
|
+
): Promise<Array<{ entity: <%= leftEntityPascal %> | <%= rightEntityPascal %>; link: <%= classNames.entity %> }>> {
|
|
130
|
+
if (side === 'left') {
|
|
131
|
+
const links = await this.repository.findBy<%= leftEntityPascal %>Id(anchorId, opts);
|
|
132
|
+
const targetIds = links.map((l) => (l as any).<%= rightColumnCamel %> as string);
|
|
133
|
+
const targets = await this.<%= rightEntityCamel %>Repo.findByIds(targetIds);
|
|
134
|
+
const byId = new Map(targets.map((t) => [(t as any).id, t]));
|
|
135
|
+
return links.map((link) => ({
|
|
136
|
+
entity: byId.get((link as any).<%= rightColumnCamel %>)! as <%= rightEntityPascal %>,
|
|
137
|
+
link,
|
|
138
|
+
}));
|
|
139
|
+
} else {
|
|
140
|
+
const links = await this.repository.findBy<%= rightEntityPascal %>Id(anchorId, opts);
|
|
141
|
+
const targetIds = links.map((l) => (l as any).<%= leftColumnCamel %> as string);
|
|
142
|
+
const targets = await this.<%= leftEntityCamel %>Repo.findByIds(targetIds);
|
|
143
|
+
const byId = new Map(targets.map((t) => [(t as any).id, t]));
|
|
144
|
+
return links.map((link) => ({
|
|
145
|
+
entity: byId.get((link as any).<%= leftColumnCamel %>)! as <%= leftEntityPascal %>,
|
|
146
|
+
link,
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Mark the (`<%= leftEntityCamel %>Id`, `<%= rightEntityCamel %>Id`) row
|
|
153
|
+
* as `is_primary: true`. Demoting other rows on the same side is the
|
|
154
|
+
* caller's concern in v1; future leaves may add transactional demotion.
|
|
155
|
+
*/
|
|
156
|
+
async setPrimary(
|
|
157
|
+
<%= leftEntityCamel %>Id: string,
|
|
158
|
+
<%= rightEntityCamel %>Id: string,
|
|
159
|
+
): Promise<void> {
|
|
160
|
+
const links = await this.repository.findBy<%= leftEntityPascal %>Id(<%= leftEntityCamel %>Id);
|
|
161
|
+
const match = links.find(
|
|
162
|
+
(l) => (l as any).<%= rightColumnCamel %> === <%= rightEntityCamel %>Id,
|
|
163
|
+
);
|
|
164
|
+
if (match) {
|
|
165
|
+
await this.update(
|
|
166
|
+
(match as any).id ?? `${<%= leftEntityCamel %>Id}:${<%= rightEntityCamel %>Id}`,
|
|
167
|
+
{ isPrimary: true } as unknown as Partial<<%= classNames.entity %>>,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Inherited from BaseService:
|
|
173
|
+
// findById, findByIds, list, count, exists, create, update, delete
|
|
174
|
+
}
|