@pattern-stack/codegen 0.6.8 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/src/cli/index.js +516 -73
  3. package/dist/src/cli/index.js.map +1 -1
  4. package/dist/src/index.d.ts +208 -1
  5. package/dist/src/index.js +147 -0
  6. package/dist/src/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/patterns/library/base-junction-fields.ts +32 -0
  9. package/src/patterns/library/index.ts +7 -0
  10. package/src/patterns/library/junction.pattern.ts +41 -0
  11. package/templates/entity/new/backend/application/queries/get-by-id.ejs.t +3 -3
  12. package/templates/entity/new/backend/application/queries/grouped-index.ejs.t +5 -5
  13. package/templates/entity/new/backend/application/queries/index.ejs.t +3 -0
  14. package/templates/entity/new/backend/application/queries/list.ejs.t +3 -3
  15. package/templates/entity/new/backend/application/queries/relationships.queries.ejs.t +147 -0
  16. package/templates/entity/new/backend/database/repository.ejs.t +36 -176
  17. package/templates/entity/new/backend/domain/entity.ejs.t +0 -44
  18. package/templates/entity/new/backend/domain/grouped-index.ejs.t +4 -60
  19. package/templates/entity/new/backend/domain/index.ejs.t +2 -2
  20. package/templates/entity/new/backend/domain/repository-interface.ejs.t +16 -17
  21. package/templates/entity/new/backend/modules/core/module.ejs.t +10 -0
  22. package/templates/entity/new/backend/presentation/controller.ejs.t +2 -34
  23. package/templates/entity/new/clean-lite-ps/entity.ejs.t +15 -2
  24. package/templates/entity/new/clean-lite-ps/module.ejs.t +27 -2
  25. package/templates/entity/new/clean-lite-ps/prompt-extension.js +72 -5
  26. package/templates/entity/new/clean-lite-ps/repository.ejs.t +33 -1
  27. package/templates/entity/new/clean-lite-ps/service.ejs.t +79 -0
  28. package/templates/entity/new/prompt.js +1 -0
  29. package/templates/junction/new/_inject-parent-module-clp-left.ejs.t +8 -0
  30. package/templates/junction/new/_inject-parent-module-clp-right.ejs.t +8 -0
  31. package/templates/junction/new/_inject-parent-module-forwardref-clp-left.ejs.t +8 -0
  32. package/templates/junction/new/_inject-parent-module-forwardref-clp-right.ejs.t +8 -0
  33. package/templates/junction/new/_inject-parent-module-import-clp-left.ejs.t +8 -0
  34. package/templates/junction/new/_inject-parent-module-import-clp-right.ejs.t +8 -0
  35. package/templates/junction/new/_inject-parent-service-clp-left.ejs.t +51 -0
  36. package/templates/junction/new/_inject-parent-service-clp-right.ejs.t +48 -0
  37. package/templates/junction/new/_inject-parent-service-counterparty-clp-left.ejs.t +7 -0
  38. package/templates/junction/new/_inject-parent-service-counterparty-clp-right.ejs.t +7 -0
  39. package/templates/junction/new/_inject-parent-service-forwardref-clp-left.ejs.t +8 -0
  40. package/templates/junction/new/_inject-parent-service-forwardref-clp-right.ejs.t +8 -0
  41. package/templates/junction/new/_inject-parent-service-import-clp-left.ejs.t +9 -0
  42. package/templates/junction/new/_inject-parent-service-import-clp-right.ejs.t +9 -0
  43. package/templates/junction/new/entity.ejs.t +111 -0
  44. package/templates/junction/new/index.ejs.t +15 -0
  45. package/templates/junction/new/module.ejs.t +37 -0
  46. package/templates/junction/new/prompt.js +492 -0
  47. package/templates/junction/new/repository.ejs.t +67 -0
  48. 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
+ }