@pattern-stack/codegen 0.6.8 → 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.
Files changed (41) hide show
  1. package/dist/src/cli/index.js +516 -73
  2. package/dist/src/cli/index.js.map +1 -1
  3. package/dist/src/index.d.ts +208 -1
  4. package/dist/src/index.js +147 -0
  5. package/dist/src/index.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/patterns/library/base-junction-fields.ts +32 -0
  8. package/src/patterns/library/index.ts +7 -0
  9. package/src/patterns/library/junction.pattern.ts +41 -0
  10. package/templates/entity/new/backend/application/queries/get-by-id.ejs.t +3 -3
  11. package/templates/entity/new/backend/application/queries/grouped-index.ejs.t +5 -5
  12. package/templates/entity/new/backend/application/queries/index.ejs.t +3 -0
  13. package/templates/entity/new/backend/application/queries/list.ejs.t +3 -3
  14. package/templates/entity/new/backend/application/queries/relationships.queries.ejs.t +147 -0
  15. package/templates/entity/new/backend/database/repository.ejs.t +36 -176
  16. package/templates/entity/new/backend/domain/entity.ejs.t +0 -44
  17. package/templates/entity/new/backend/domain/grouped-index.ejs.t +4 -60
  18. package/templates/entity/new/backend/domain/index.ejs.t +2 -2
  19. package/templates/entity/new/backend/domain/repository-interface.ejs.t +16 -17
  20. package/templates/entity/new/backend/modules/core/module.ejs.t +10 -0
  21. package/templates/entity/new/backend/presentation/controller.ejs.t +2 -34
  22. package/templates/entity/new/clean-lite-ps/entity.ejs.t +15 -2
  23. package/templates/entity/new/clean-lite-ps/module.ejs.t +27 -2
  24. package/templates/entity/new/clean-lite-ps/prompt-extension.js +72 -5
  25. package/templates/entity/new/clean-lite-ps/repository.ejs.t +33 -1
  26. package/templates/entity/new/clean-lite-ps/service.ejs.t +79 -0
  27. package/templates/entity/new/prompt.js +1 -0
  28. package/templates/junction/new/_inject-parent-module-clp-left.ejs.t +8 -0
  29. package/templates/junction/new/_inject-parent-module-clp-right.ejs.t +8 -0
  30. package/templates/junction/new/_inject-parent-module-import-clp-left.ejs.t +9 -0
  31. package/templates/junction/new/_inject-parent-module-import-clp-right.ejs.t +9 -0
  32. package/templates/junction/new/_inject-parent-service-clp-left.ejs.t +51 -0
  33. package/templates/junction/new/_inject-parent-service-clp-right.ejs.t +48 -0
  34. package/templates/junction/new/_inject-parent-service-import-clp-left.ejs.t +11 -0
  35. package/templates/junction/new/_inject-parent-service-import-clp-right.ejs.t +11 -0
  36. package/templates/junction/new/entity.ejs.t +111 -0
  37. package/templates/junction/new/index.ejs.t +15 -0
  38. package/templates/junction/new/module.ejs.t +37 -0
  39. package/templates/junction/new/prompt.js +492 -0
  40. package/templates/junction/new/repository.ejs.t +67 -0
  41. package/templates/junction/new/service.ejs.t +174 -0
@@ -17,6 +17,22 @@ import { toEavRows, mergeEavRows } from '@shared/eav-helpers';
17
17
  import type { DrizzleTx } from '@shared/types/drizzle';
18
18
  import { <%= eavDefinitionPascal %>Repository } from '../<%= eavDefinitionEntityPlural %>/<%= eavDefinitionEntity %>.repository';
19
19
  <% } -%>
20
+ <%_ /* CGP-358b — service-layer composition: import target repos for belongs_to relationships */ _%>
21
+ <%_ if (typeof clpBelongsTo !== 'undefined') { _%>
22
+ <%_ const uniqueBelongsToTargets = [...new Map(clpBelongsTo.filter(r => !r.isSelfFk).map(r => [r.relatedEntity, r])).values()]; _%>
23
+ <%_ uniqueBelongsToTargets.forEach(rel => { _%>
24
+ import { <%= rel.relatedEntityPascal %>Repository } from '../<%= rel.relatedPlural %>/<%= rel.relatedEntity %>.repository';
25
+ import type { <%= rel.relatedEntityPascal %> } from '../<%= rel.relatedPlural %>/<%= rel.relatedEntity %>.entity';
26
+ <%_ }) _%>
27
+ <%_ } _%>
28
+ <%_ /* CGP-358b — import target repos for has_many relationships */ _%>
29
+ <%_ if (typeof clpExistingHasMany !== 'undefined') { _%>
30
+ <%_ const uniqueHasManyTargets = [...new Map(clpExistingHasMany.filter(r => !r.isSelfRef).map(r => [r.target, r])).values()]; _%>
31
+ <%_ uniqueHasManyTargets.forEach(rel => { _%>
32
+ import { <%= rel.targetClass %>Repository } from '../<%= rel.targetPlural %>/<%= rel.target %>.repository';
33
+ import type { <%= rel.targetClass %> } from '../<%= rel.targetPlural %>/<%= rel.target %>.entity';
34
+ <%_ }) _%>
35
+ <%_ } _%>
20
36
 
21
37
  @Injectable()
22
38
  export class <%= classNames.service %> extends WithAnalytics(
@@ -43,6 +59,20 @@ export class <%= classNames.service %> extends WithAnalytics(
43
59
  <% if (eavValueTable) { -%>
44
60
  private readonly definitionRepo: <%= eavDefinitionPascal %>Repository,
45
61
  <% } -%>
62
+ <%_ /* CGP-358b — inject target repos for belongs_to (non-self-ref) */ _%>
63
+ <%_ if (typeof clpBelongsTo !== 'undefined') { _%>
64
+ <%_ const uniqueBelongsToTargets2 = [...new Map(clpBelongsTo.filter(r => !r.isSelfFk).map(r => [r.relatedEntity, r])).values()]; _%>
65
+ <%_ uniqueBelongsToTargets2.forEach(rel => { _%>
66
+ private readonly <%= rel.relatedEntity.replace(/_([a-z])/g, (_, c) => c.toUpperCase()) %>Repo: <%= rel.relatedEntityPascal %>Repository,
67
+ <%_ }) _%>
68
+ <%_ } _%>
69
+ <%_ /* CGP-358b — inject target repos for has_many (non-self-ref) */ _%>
70
+ <%_ if (typeof clpExistingHasMany !== 'undefined') { _%>
71
+ <%_ const uniqueHasManyTargets2 = [...new Map(clpExistingHasMany.filter(r => !r.isSelfRef).map(r => [r.target, r])).values()]; _%>
72
+ <%_ uniqueHasManyTargets2.forEach(rel => { _%>
73
+ private readonly <%= rel.target.replace(/_([a-z])/g, (_, c) => c.toUpperCase()) %>Repo: <%= rel.targetClass %>Repository,
74
+ <%_ }) _%>
75
+ <%_ } _%>
46
76
  ) {
47
77
  super(repository);
48
78
  }
@@ -67,6 +97,55 @@ export class <%= classNames.service %> extends WithAnalytics(
67
97
  }
68
98
  <%_ }) _%>
69
99
  <% } %>
100
+ <%_ /* CGP-358b — service-layer composition methods for relationships */ _%>
101
+ <%_ const hasBelongsToComposition = typeof clpBelongsTo !== 'undefined' && clpBelongsTo.length > 0; _%>
102
+ <%_ const hasHasManyComposition = typeof clpExistingHasMany !== 'undefined' && clpExistingHasMany.length > 0; _%>
103
+ <%_ if (hasBelongsToComposition || hasHasManyComposition) { _%>
104
+ // ═══════════════════════════════════════════════════════════════════════
105
+ // Relationship composition methods (CGP-358b / CGP-62)
106
+ // Two queries, no SQL JOIN. Core-contract path; relations() const stays
107
+ // as opt-in extension for hand-written Drizzle queries.
108
+ // ═══════════════════════════════════════════════════════════════════════
109
+ <%_ } _%>
110
+ <%_ if (hasBelongsToComposition) { _%>
111
+ <%_ clpBelongsTo.forEach(rel => { _%>
112
+ <%_ const relCamel = rel.relatedEntity.replace(/_([a-z])/g, (_, c) => c.toUpperCase()); _%>
113
+ <%_ const entityCamel = entityName.replace(/_([a-z])/g, (_, c) => c.toUpperCase()); _%>
114
+
115
+ /**
116
+ * Fetch the <%= rel.relatedEntityPascal %> parent for this <%= entityNamePascal %>.
117
+ * Two repo calls: find self by id → find target by FK.
118
+ */
119
+ async <%= rel.relationKey %>(<%- entityCamel %>Id: string): Promise<<%= rel.relatedEntityPascal %> | null> {
120
+ const entity = await this.repository.findById(<%- entityCamel %>Id);
121
+ if (!entity) return null;
122
+ <%_ if (rel.isSelfFk) { _%>
123
+ return entity.<%= rel.camelField %> ? this.repository.findById(entity.<%= rel.camelField %>) : null;
124
+ <%_ } else { _%>
125
+ return entity.<%= rel.camelField %> ? this.<%= relCamel %>Repo.findById(entity.<%= rel.camelField %>) : null;
126
+ <%_ } _%>
127
+ }
128
+ <%_ }) _%>
129
+ <%_ } _%>
130
+ <%_ if (hasHasManyComposition) { _%>
131
+ <%_ clpExistingHasMany.forEach(rel => { _%>
132
+ <%_ const relCamel = rel.target.replace(/_([a-z])/g, (_, c) => c.toUpperCase()); _%>
133
+ <%_ const entityCamel = entityName.replace(/_([a-z])/g, (_, c) => c.toUpperCase()); _%>
134
+ <%_ const fkPascal = rel.inverseForeignKeyPascal; _%>
135
+
136
+ /**
137
+ * Fetch <%= rel.name %> for this <%= entityNamePascal %> by FK traversal.
138
+ * Single repo call with optional cursor/limit pagination.
139
+ */
140
+ async <%= rel.name %>(<%- entityCamel %>Id: string, opts?: { cursor?: string; limit?: number }): Promise<<%= rel.targetClass %>[]> {
141
+ <%_ if (rel.isSelfRef) { _%>
142
+ return this.repository.findBy<%= fkPascal %>(<%- entityCamel %>Id, opts);
143
+ <%_ } else { _%>
144
+ return this.<%= relCamel %>Repo.findBy<%= fkPascal %>(<%- entityCamel %>Id, opts);
145
+ <%_ } _%>
146
+ }
147
+ <%_ }) _%>
148
+ <%_ } _%>
70
149
  <% if (eavEnabled) { %>
71
150
  /**
72
151
  * EAV paired read (ADR-13): fetch the entity and merge dynamic `field_values`
@@ -568,6 +568,7 @@ export default {
568
568
  moduleToGetByIdQuery: importHelpers.moduleToQuery(name, fileNames.getByIdQuery.replace('.ts', '')),
569
569
  moduleToListQuery: importHelpers.moduleToQuery(name, fileNames.listQuery.replace('.ts', '')),
570
570
  moduleToDeclarativeQueries: importHelpers.moduleToQuery(name, 'declarative-queries'),
571
+ moduleToRelationshipQueries: importHelpers.moduleToQuery(name, 'relationships.queries'),
571
572
  moduleToCreateCommand: importHelpers.moduleToCommand(name, fileNames.createCommand.replace('.ts', '')),
572
573
  moduleToUpdateCommand: importHelpers.moduleToCommand(name, fileNames.updateCommand.replace('.ts', '')),
573
574
  moduleToDeleteCommand: importHelpers.moduleToCommand(name, fileNames.deleteCommand.replace('.ts', '')),
@@ -0,0 +1,8 @@
1
+ ---
2
+ to: "<%= architecture === 'clean-lite-ps' && exposeOnParent.left ? parentModulePathLeft : '' %>"
3
+ inject: true
4
+ after: " DatabaseModule,"
5
+ skip_if: "<%= classNames.module %>"
6
+ ---
7
+ // CGP-60 — junction module (forwardRef breaks the parent↔junction module cycle)
8
+ forwardRef(() => <%= classNames.module %>),
@@ -0,0 +1,8 @@
1
+ ---
2
+ to: "<%= architecture === 'clean-lite-ps' && exposeOnParent.right ? parentModulePathRight : '' %>"
3
+ inject: true
4
+ after: " DatabaseModule,"
5
+ skip_if: "<%= classNames.module %>"
6
+ ---
7
+ // CGP-60 — junction module (forwardRef breaks the parent↔junction module cycle)
8
+ forwardRef(() => <%= classNames.module %>),
@@ -0,0 +1,9 @@
1
+ ---
2
+ to: "<%= architecture === 'clean-lite-ps' && exposeOnParent.left ? parentModulePathLeft : '' %>"
3
+ inject: true
4
+ after: "from '@nestjs/common';"
5
+ skip_if: "<%= classNames.module %> }"
6
+ ---
7
+ // CGP-60 — junction module wiring
8
+ import { forwardRef } from '@nestjs/common';
9
+ import { <%= classNames.module %> } from '<%= junctionModuleImportFromLeft %>';
@@ -0,0 +1,9 @@
1
+ ---
2
+ to: "<%= architecture === 'clean-lite-ps' && exposeOnParent.right ? parentModulePathRight : '' %>"
3
+ inject: true
4
+ after: "from '@nestjs/common';"
5
+ skip_if: "<%= classNames.module %> }"
6
+ ---
7
+ // CGP-60 — junction module wiring
8
+ import { forwardRef } from '@nestjs/common';
9
+ import { <%= classNames.module %> } from '<%= junctionModuleImportFromRight %>';
@@ -0,0 +1,51 @@
1
+ ---
2
+ to: "<%= architecture === 'clean-lite-ps' && exposeOnParent.left ? parentServicePathLeft : '' %>"
3
+ inject: true
4
+ before: "// Inherited from"
5
+ skip_if: "<%= injectionMarkerLeft %>"
6
+ ---
7
+
8
+ // ═══════════════════════════════════════════════════════════════════════
9
+ // CGP-60 — fan-out to <%= rightEntityPascal %> (junction: <%= name %>)
10
+ // Delegates to <%= classNames.service %>. Per-junction marker keeps
11
+ // idempotency; multiple junctions on the same parent each emit their own
12
+ // block. `forwardRef` resolves the circular module import (parent module
13
+ // imports junction module; junction module imports parent modules for
14
+ // repo DI).
15
+ // ═══════════════════════════════════════════════════════════════════════
16
+ <%= injectionMarkerLeft %>
17
+
18
+ @Inject(forwardRef(() => <%= classNames.service %>))
19
+ private readonly <%= entityNameCamel %>Service!: <%= classNames.service %>;
20
+
21
+ async attach<%= rightEntityPascal %>(
22
+ <%= leftEntityCamel %>Id: string,
23
+ <%= rightEntityCamel %>Id: string,
24
+ link?: <%= entityNamePascal %>LinkInput,
25
+ ): Promise<<%= entityNamePascal %>> {
26
+ return this.<%= entityNameCamel %>Service.attach(<%= leftEntityCamel %>Id, <%= rightEntityCamel %>Id, link);
27
+ }
28
+
29
+ async detach<%= rightEntityPascal %>(
30
+ <%= leftEntityCamel %>Id: string,
31
+ <%= rightEntityCamel %>Id: string,
32
+ ): Promise<void> {
33
+ return this.<%= entityNameCamel %>Service.detach(<%= leftEntityCamel %>Id, <%= rightEntityCamel %>Id);
34
+ }
35
+
36
+ async <%= rightEntityPlural %>List(
37
+ <%= leftEntityCamel %>Id: string,
38
+ opts?: { cursor?: string; limit?: number },
39
+ ): Promise<Array<{ entity: <%= rightEntityPascal %>; link: <%= entityNamePascal %> }>> {
40
+ return this.<%= entityNameCamel %>Service.listAssoc('left', <%= leftEntityCamel %>Id, opts) as Promise<
41
+ Array<{ entity: <%= rightEntityPascal %>; link: <%= entityNamePascal %> }>
42
+ >;
43
+ }
44
+
45
+ async <%= rightEntityPlural %>SetPrimary(
46
+ <%= leftEntityCamel %>Id: string,
47
+ <%= rightEntityCamel %>Id: string,
48
+ ): Promise<void> {
49
+ return this.<%= entityNameCamel %>Service.setPrimary(<%= leftEntityCamel %>Id, <%= rightEntityCamel %>Id);
50
+ }
51
+
@@ -0,0 +1,48 @@
1
+ ---
2
+ to: "<%= architecture === 'clean-lite-ps' && exposeOnParent.right ? parentServicePathRight : '' %>"
3
+ inject: true
4
+ before: "// Inherited from"
5
+ skip_if: "<%= injectionMarkerRight %>"
6
+ ---
7
+
8
+ // ═══════════════════════════════════════════════════════════════════════
9
+ // CGP-60 — fan-out to <%= leftEntityPascal %> (junction: <%= name %>)
10
+ // Delegates to <%= classNames.service %>. See left-side block for the
11
+ // forwardRef + per-junction marker rationale.
12
+ // ═══════════════════════════════════════════════════════════════════════
13
+ <%= injectionMarkerRight %>
14
+
15
+ @Inject(forwardRef(() => <%= classNames.service %>))
16
+ private readonly <%= entityNameCamel %>Service!: <%= classNames.service %>;
17
+
18
+ async addTo<%= leftEntityPascal %>(
19
+ <%= rightEntityCamel %>Id: string,
20
+ <%= leftEntityCamel %>Id: string,
21
+ link?: <%= entityNamePascal %>LinkInput,
22
+ ): Promise<<%= entityNamePascal %>> {
23
+ return this.<%= entityNameCamel %>Service.attach(<%= leftEntityCamel %>Id, <%= rightEntityCamel %>Id, link);
24
+ }
25
+
26
+ async removeFrom<%= leftEntityPascal %>(
27
+ <%= rightEntityCamel %>Id: string,
28
+ <%= leftEntityCamel %>Id: string,
29
+ ): Promise<void> {
30
+ return this.<%= entityNameCamel %>Service.detach(<%= leftEntityCamel %>Id, <%= rightEntityCamel %>Id);
31
+ }
32
+
33
+ async <%= leftEntityPlural %>List(
34
+ <%= rightEntityCamel %>Id: string,
35
+ opts?: { cursor?: string; limit?: number },
36
+ ): Promise<Array<{ entity: <%= leftEntityPascal %>; link: <%= entityNamePascal %> }>> {
37
+ return this.<%= entityNameCamel %>Service.listAssoc('right', <%= rightEntityCamel %>Id, opts) as Promise<
38
+ Array<{ entity: <%= leftEntityPascal %>; link: <%= entityNamePascal %> }>
39
+ >;
40
+ }
41
+
42
+ async <%= leftEntityPlural %>SetPrimary(
43
+ <%= rightEntityCamel %>Id: string,
44
+ <%= leftEntityCamel %>Id: string,
45
+ ): Promise<void> {
46
+ return this.<%= entityNameCamel %>Service.setPrimary(<%= leftEntityCamel %>Id, <%= rightEntityCamel %>Id);
47
+ }
48
+
@@ -0,0 +1,11 @@
1
+ ---
2
+ to: "<%= architecture === 'clean-lite-ps' && exposeOnParent.left ? parentServicePathLeft : '' %>"
3
+ inject: true
4
+ after: "from '@nestjs/common';"
5
+ skip_if: "<%= classNames.service %> }"
6
+ ---
7
+ // CGP-60 — junction service + types (forwardRef resolves circular module dep)
8
+ import { forwardRef } from '@nestjs/common';
9
+ import { <%= classNames.service %>, <%= entityNamePascal %>LinkInput } from '<%= junctionServiceImportFromLeft %>';
10
+ import type { <%= entityNamePascal %> } from '../<%= entityNamePlural %>/<%= name %>.entity';
11
+ import type { <%= rightEntityPascal %> } from '<%= rightEntityImportFromJunction %>';
@@ -0,0 +1,11 @@
1
+ ---
2
+ to: "<%= architecture === 'clean-lite-ps' && exposeOnParent.right ? parentServicePathRight : '' %>"
3
+ inject: true
4
+ after: "from '@nestjs/common';"
5
+ skip_if: "<%= classNames.service %> }"
6
+ ---
7
+ // CGP-60 — junction service + types (forwardRef resolves circular module dep)
8
+ import { forwardRef } from '@nestjs/common';
9
+ import { <%= classNames.service %>, <%= entityNamePascal %>LinkInput } from '<%= junctionServiceImportFromRight %>';
10
+ import type { <%= entityNamePascal %> } from '../<%= entityNamePlural %>/<%= name %>.entity';
11
+ import type { <%= leftEntityPascal %> } from '<%= leftEntityImportFromJunction %>';
@@ -0,0 +1,111 @@
1
+ ---
2
+ to: "<%= outputPaths.entity %>"
3
+ force: true
4
+ ---
5
+ import {
6
+ <%_ drizzleImports.filter(i => i !== 'relations').forEach(i => { _%>
7
+ <%= i %>,
8
+ <%_ }) _%>
9
+ } from 'drizzle-orm/pg-core';
10
+ import { relations, type InferSelectModel } from 'drizzle-orm';
11
+ import { <%= leftTable %> } from '../<%= leftTable %>/<%= leftEntity %>.entity';
12
+ <%_ if (leftEntity !== rightEntity) { _%>
13
+ import { <%= rightTable %> } from '../<%= rightTable %>/<%= rightEntity %>.entity';
14
+ <%_ } _%>
15
+
16
+ // ============================================================================
17
+ // Enums
18
+ // ============================================================================
19
+ <%_ if (hasRole) { _%>
20
+
21
+ export const <%= roleEnumName %> = pgEnum('<%= name %>_role', [
22
+ <%_ roleEnumValues.forEach(v => { _%>
23
+ '<%= v %>',
24
+ <%_ }) _%>
25
+ ]);
26
+ <%_ } _%>
27
+ <%_ processedCustomFields.filter(f => f.hasChoices).forEach(field => { _%>
28
+
29
+ export const <%= field.enumName %> = pgEnum('<%= name %>_<%= field.name %>', [
30
+ <%_ field.choices.forEach(c => { _%>
31
+ '<%= c %>',
32
+ <%_ }) _%>
33
+ ]);
34
+ <%_ }) _%>
35
+
36
+ // ============================================================================
37
+ // Table
38
+ // ============================================================================
39
+
40
+ export const <%= tableVarName %> = pgTable(
41
+ '<%= tableName %>',
42
+ {
43
+ // FK columns — composite primary key (no surrogate id: Q4 resolution)
44
+ <%= leftColumnCamel %>: uuid('<%= leftColumn %>').notNull().references(() => <%= leftTable %>.id, { onDelete: '<%= onDeleteLeft %>' }),
45
+ <%= rightColumnCamel %>: uuid('<%= rightColumn %>').notNull().references(() => <%= rightTable %>.id, { onDelete: '<%= onDeleteRight %>' }),
46
+ <%_ if (hasRole) { _%>
47
+
48
+ // Role enum (per-pairing; declared in junction YAML's fields.role.choices)
49
+ role: <%= roleEnumName %>('role'),
50
+ <%_ } _%>
51
+
52
+ // BaseJunctionFields — is_primary is always emitted
53
+ isPrimary: boolean('is_primary').notNull().default(false),
54
+ <%_ if (temporal) { _%>
55
+
56
+ // Temporal window (temporal: true, default)
57
+ startedAt: timestamp('started_at'),
58
+ endedAt: timestamp('ended_at'),
59
+ <%_ } _%>
60
+ <%_ if (sourced) { _%>
61
+
62
+ // Provenance (sourced: true, default)
63
+ sourcedFrom: text('sourced_from'),
64
+ confidence: numeric('confidence', { precision: 5, scale: 4 }),
65
+ matchedAt: timestamp('matched_at'),
66
+ <%_ } _%>
67
+ <%_ if (hasCustomFields) { _%>
68
+
69
+ // Custom fields
70
+ <%_ processedCustomFields.forEach(field => { _%>
71
+ <%_ if (field.hasChoices) { _%>
72
+ <%= field.camelName %>: <%= field.enumName %>('<%= field.name %>'),
73
+ <%_ } else if (field.drizzleType === 'uuid') { _%>
74
+ <%= field.camelName %>: uuid('<%= field.name %>'),
75
+ <%_ } else { _%>
76
+ <%= field.camelName %>: <%= field.drizzleType %>('<%= field.name %>'),
77
+ <%_ } _%>
78
+ <%_ }) _%>
79
+ <%_ } _%>
80
+
81
+ // Timestamps
82
+ createdAt: timestamp('created_at').notNull().defaultNow(),
83
+ updatedAt: timestamp('updated_at').notNull().defaultNow(),
84
+ },
85
+ (table) => [
86
+ // Composite primary key on the two FK columns (Q4 resolution: no surrogate id)
87
+ primaryKey({ columns: [table.<%= leftColumnCamel %>, table.<%= rightColumnCamel %>] }),
88
+ ],
89
+ );
90
+
91
+ export type <%= classNames.entity %> = InferSelectModel<typeof <%= tableVarName %>>;
92
+ export type <%= classNames.entity %>Insert = typeof <%= tableVarName %>.$inferInsert;
93
+
94
+ // ============================================================================
95
+ // Relations — extension-path metadata for db.query.X.findMany({ with: ... })
96
+ // Generated code does NOT consume these; they exist for hand-written admin
97
+ // queries and for #60's fan-out methods once they land.
98
+ // ============================================================================
99
+
100
+ export const <%= tableVarName %>Relations = relations(<%= tableVarName %>, ({ one }) => ({
101
+ <%= leftEntity %>: one(<%= leftTable %>, {
102
+ fields: [<%= tableVarName %>.<%= leftColumnCamel %>],
103
+ references: [<%= leftTable %>.id],
104
+ }),
105
+ <%_ if (leftEntity !== rightEntity) { _%>
106
+ <%= rightEntity %>: one(<%= rightTable %>, {
107
+ fields: [<%= tableVarName %>.<%= rightColumnCamel %>],
108
+ references: [<%= rightTable %>.id],
109
+ }),
110
+ <%_ } _%>
111
+ }));
@@ -0,0 +1,15 @@
1
+ ---
2
+ to: "<%= outputPaths.index %>"
3
+ force: true
4
+ ---
5
+ /**
6
+ * <%= classNames.entity %> module barrel export
7
+ * Generated by junction codegen — do not edit directly
8
+ */
9
+
10
+ // Value exports (module, service)
11
+ export { <%= classNames.module %> } from './<%= entityNamePlural %>.module';
12
+ export { <%= classNames.service %> } from './<%= name %>.service';
13
+
14
+ // Type-only exports (entity)
15
+ export type { <%= classNames.entity %> } from './<%= name %>.entity';
@@ -0,0 +1,37 @@
1
+ ---
2
+ to: "<%= outputPaths.module %>"
3
+ force: true
4
+ ---
5
+ import { Module, forwardRef } from '@nestjs/common';
6
+ import { DatabaseModule } from '@shared/database/database.module';
7
+
8
+ import { <%= classNames.repository %> } from './<%= name %>.repository';
9
+ import { <%= classNames.service %> } from './<%= name %>.service';
10
+ import { <%= leftModuleClass %> } from '<%= leftModuleImportFromJunction %>';
11
+ import { <%= rightModuleClass %> } from '<%= rightModuleImportFromJunction %>';
12
+
13
+ // Note: No controller — junctions are not directly addressable HTTP resources
14
+ // in v1 (Q1 resolution). They are accessed via the canonical port of one of
15
+ // the two parent entities. Add a controller in a follow-up if a consumer
16
+ // surfaces a need for direct HTTP access to junction rows.
17
+
18
+ @Module({
19
+ imports: [
20
+ DatabaseModule,
21
+ // CGP-60 — parent modules provide the left/right repositories that the
22
+ // junction service injects for the canonical `list` composition path.
23
+ // forwardRef resolves the parent↔junction module cycle (parent modules
24
+ // also import this module to wire fan-out).
25
+ forwardRef(() => <%= leftModuleClass %>),
26
+ forwardRef(() => <%= rightModuleClass %>),
27
+ // TODO: Add subsystem modules as needed (EventsSubsystemModule, etc.)
28
+ ],
29
+ controllers: [],
30
+ providers: [
31
+ <%= classNames.repository %>,
32
+ <%= classNames.service %>,
33
+ // TODO: Register hand-written use cases here
34
+ ],
35
+ exports: [<%= classNames.service %>], // Only service is exported (ADR-002)
36
+ })
37
+ export class <%= classNames.module %> {}