@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,492 @@
1
+ /**
2
+ * Hygen prompt.js — Loads junction YAML and prepares template locals
3
+ *
4
+ * Usage: bunx hygen junction new --yaml junctions/opportunity_contact.yaml
5
+ *
6
+ * Mirrors templates/relationship/new/prompt.js but adapted for junction
7
+ * definitions (two endpoints, role enum, BaseJunctionFields, composite PK,
8
+ * no controller/DTOs/use-cases).
9
+ *
10
+ * Architecture-aware output paths: reads `generate.architecture` from
11
+ * codegen.config.yaml and computes output paths for both 'clean' and
12
+ * 'clean-lite-ps' pipelines. Relationship's prompt.js hardcodes clean-lite-ps
13
+ * paths — this prompt does NOT inherit that limitation.
14
+ */
15
+
16
+ import fs from "node:fs";
17
+ import path from "node:path";
18
+ import yaml from "yaml";
19
+ import pluralizePkg from "pluralize";
20
+
21
+ // ============================================================================
22
+ // Naming Helpers (inlined to avoid import issues with Hygen)
23
+ // ============================================================================
24
+
25
+ const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
26
+ const camelCase = (s) => s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
27
+ const pascalCase = (s) => capitalize(camelCase(s));
28
+ const pluralize = (s) => pluralizePkg.plural(s);
29
+ const kebabCase = (s) => s.replace(/_/g, "-");
30
+
31
+ // ============================================================================
32
+ // Config Loading Helpers
33
+ // ============================================================================
34
+
35
+ /**
36
+ * Find and load codegen.config.yaml from cwd upward. Returns null when absent
37
+ * (safe fallback: assume clean-lite-ps layout with srcRoot = 'src').
38
+ */
39
+ function loadCodegenConfig(cwd) {
40
+ const candidates = [
41
+ path.join(cwd, "codegen.config.yaml"),
42
+ path.join(cwd, "codegen.config.yml"),
43
+ ];
44
+ for (const p of candidates) {
45
+ if (fs.existsSync(p)) {
46
+ try {
47
+ return yaml.parse(fs.readFileSync(p, "utf-8"));
48
+ } catch {
49
+ // Fall through
50
+ }
51
+ }
52
+ }
53
+ return null;
54
+ }
55
+
56
+ function resolveArchitecture(config) {
57
+ return config?.generate?.architecture === "clean" ? "clean" : "clean-lite-ps";
58
+ }
59
+
60
+ function resolveSrcRoot(config, architecture) {
61
+ // paths.backend_src from config; fallback by architecture
62
+ const fromConfig = config?.paths?.backend_src;
63
+ if (typeof fromConfig === "string" && fromConfig.length > 0) return fromConfig;
64
+ return architecture === "clean" ? "app/backend/src" : "src";
65
+ }
66
+
67
+ // ============================================================================
68
+ // Name Derivation
69
+ // ============================================================================
70
+
71
+ function deriveJunctionName(config) {
72
+ // Q8 resolution: insertion order — between: [opportunity, contact] → opportunity_contact
73
+ // Explicit `name:` on the YAML overrides the derivation.
74
+ return config.name ?? `${config.between[0]}_${config.between[1]}`;
75
+ }
76
+
77
+ function deriveTableName(config, junctionName) {
78
+ return config.table ?? pluralize(junctionName);
79
+ }
80
+
81
+ // ============================================================================
82
+ // On-Delete Action Mapping
83
+ // ============================================================================
84
+
85
+ const ON_DELETE_MAP = {
86
+ restrict: "restrict",
87
+ cascade: "cascade",
88
+ set_null: "set null",
89
+ no_action: "no action",
90
+ };
91
+
92
+ // ============================================================================
93
+ // Drizzle Import Set
94
+ // ============================================================================
95
+
96
+ function buildDrizzleImports(hasRole, temporal, sourced, hasCustomFields, processedCustomFields) {
97
+ const needed = new Set(["pgTable", "primaryKey", "uuid", "timestamp", "boolean", "numeric", "text"]);
98
+
99
+ if (hasRole) needed.add("pgEnum");
100
+ if (temporal) {
101
+ // started_at / ended_at already covered by "timestamp" above
102
+ }
103
+ if (sourced) {
104
+ // sourced_from: text, confidence: numeric, matched_at: timestamp — already in set
105
+ }
106
+ if (hasCustomFields) {
107
+ for (const f of processedCustomFields) {
108
+ if (f.hasChoices) needed.add("pgEnum");
109
+ if (f.drizzleType && f.drizzleType !== "text" && f.drizzleType !== "timestamp") {
110
+ needed.add(f.drizzleType);
111
+ }
112
+ }
113
+ }
114
+
115
+ // relations() is needed for the extension-path const
116
+ needed.add("relations");
117
+
118
+ return Array.from(needed).sort();
119
+ }
120
+
121
+ // ============================================================================
122
+ // Custom Field Processing
123
+ // ============================================================================
124
+
125
+ const DRIZZLE_TYPE_MAP = {
126
+ string: "text",
127
+ integer: "integer",
128
+ decimal: "numeric",
129
+ boolean: "boolean",
130
+ uuid: "uuid",
131
+ date: "date",
132
+ datetime: "timestamp",
133
+ json: "jsonb",
134
+ enum: "text", // overridden below when hasChoices
135
+ };
136
+
137
+ function processCustomFields(fields, junctionName) {
138
+ const processed = [];
139
+ for (const [fieldName, field] of Object.entries(fields ?? {})) {
140
+ const type = field.type || "string";
141
+ const choices = field.choices;
142
+ const hasChoices = Array.isArray(choices) && choices.length > 0;
143
+ const drizzleType = hasChoices ? "text" : (DRIZZLE_TYPE_MAP[type] ?? "text");
144
+ const enumName = hasChoices ? `${camelCase(junctionName)}${pascalCase(fieldName)}Enum` : null;
145
+
146
+ processed.push({
147
+ name: fieldName,
148
+ camelName: camelCase(fieldName),
149
+ type,
150
+ drizzleType,
151
+ nullable: field.nullable ?? true,
152
+ required: field.required ?? false,
153
+ choices: choices ?? [],
154
+ hasChoices,
155
+ enumName,
156
+ });
157
+ }
158
+ return processed;
159
+ }
160
+
161
+ // ============================================================================
162
+ // Output Path Resolution (architecture-aware)
163
+ // ============================================================================
164
+
165
+ function resolveOutputPaths(name, plural, architecture, srcRoot) {
166
+ if (architecture === "clean-lite-ps") {
167
+ const prefix = srcRoot && srcRoot !== "." ? `${srcRoot}/` : "";
168
+ return {
169
+ entity: `${prefix}modules/${plural}/${name}.entity.ts`,
170
+ repository: `${prefix}modules/${plural}/${name}.repository.ts`,
171
+ service: `${prefix}modules/${plural}/${name}.service.ts`,
172
+ module: `${prefix}modules/${plural}/${plural}.module.ts`,
173
+ index: `${prefix}modules/${plural}/index.ts`,
174
+ };
175
+ }
176
+
177
+ // 'clean' — full Clean Architecture. Mirrors entityFilePaths() in barrel-generator.ts.
178
+ const pluralKebab = kebabCase(plural);
179
+ return {
180
+ entity: `${srcRoot}/domain/${plural}/${name}.entity.ts`,
181
+ repository: `${srcRoot}/infrastructure/persistence/drizzle/${pluralKebab}.repository.ts`,
182
+ service: `${srcRoot}/application/${plural}/${name}.service.ts`,
183
+ module: `${srcRoot}/infrastructure/modules/${pluralKebab}.module.ts`,
184
+ index: `${srcRoot}/domain/${plural}/index.ts`,
185
+ };
186
+ }
187
+
188
+ // ============================================================================
189
+ // Main Export
190
+ // ============================================================================
191
+
192
+ export default {
193
+ prompt: async ({ args }) => {
194
+ const yamlPath = args.yaml;
195
+ if (!yamlPath) {
196
+ throw new Error(
197
+ "Missing --yaml argument. Usage: bunx hygen junction new --yaml junctions/opportunity_contact.yaml"
198
+ );
199
+ }
200
+
201
+ // Load and parse junction YAML
202
+ const cwd = process.cwd();
203
+ const fullPath = path.resolve(cwd, yamlPath);
204
+ if (!fs.existsSync(fullPath)) {
205
+ throw new Error(`File not found: ${fullPath}`);
206
+ }
207
+
208
+ const content = fs.readFileSync(fullPath, "utf-8");
209
+ const definition = yaml.parse(content);
210
+
211
+ if (definition.pattern !== "Junction") {
212
+ throw new Error(
213
+ `Not a junction definition — expected top-level 'pattern: Junction' in ${yamlPath}. ` +
214
+ `Got: pattern=${definition.pattern ?? "(missing)"}`
215
+ );
216
+ }
217
+
218
+ const config = definition;
219
+ const fields = definition.fields ?? {};
220
+
221
+ // Warn if queries: block present (v1 ignores it — Q2 resolution)
222
+ if (definition.queries && Array.isArray(definition.queries) && definition.queries.length > 0) {
223
+ console.warn(
224
+ `[junction/new] WARNING: 'queries:' block in ${yamlPath} is ignored in v1. ` +
225
+ "Declarative queries on junctions land in a future leaf."
226
+ );
227
+ }
228
+
229
+ // ======================================================================
230
+ // Derive junction identity
231
+ // ======================================================================
232
+
233
+ const junctionName = deriveJunctionName(config);
234
+ const tableName = deriveTableName(config, junctionName);
235
+ const entityNamePascal = pascalCase(junctionName);
236
+ const entityNameCamel = camelCase(junctionName);
237
+ const entityNamePlural = tableName;
238
+ const tableVarName = camelCase(entityNamePlural);
239
+ const entityNamePluralPascal = pascalCase(entityNamePlural);
240
+ const entityNameKebab = kebabCase(junctionName);
241
+ const entityNamePluralKebab = kebabCase(entityNamePlural);
242
+
243
+ // ======================================================================
244
+ // Pairing endpoints
245
+ // ======================================================================
246
+
247
+ const leftEntity = config.between[0]; // e.g. 'opportunity'
248
+ const rightEntity = config.between[1]; // e.g. 'contact'
249
+ const leftEntityPascal = pascalCase(leftEntity);
250
+ const rightEntityPascal = pascalCase(rightEntity);
251
+ const leftEntityPlural = pluralize(leftEntity);
252
+ const rightEntityPlural = pluralize(rightEntity);
253
+
254
+ // FK column names (same derivation as relationship — no self-referential
255
+ // prefix needed since between[] endpoints must be distinct per schema)
256
+ const leftColumn = `${leftEntity}_id`;
257
+ const rightColumn = `${rightEntity}_id`;
258
+ const leftColumnCamel = camelCase(leftColumn);
259
+ const rightColumnCamel = camelCase(rightColumn);
260
+
261
+ // Drizzle variable names for the parent tables (used in FK .references())
262
+ const leftTable = leftEntityPlural; // e.g. 'opportunities'
263
+ const rightTable = rightEntityPlural; // e.g. 'contacts'
264
+
265
+ // ======================================================================
266
+ // Role enum
267
+ // ======================================================================
268
+
269
+ const roleField = fields.role;
270
+ const roleChoices = roleField?.choices;
271
+ const hasRole = Array.isArray(roleChoices) && roleChoices.length > 0;
272
+ const roleEnumName = hasRole ? `${entityNameCamel}RoleEnum` : null;
273
+ const roleEnumValues = hasRole ? roleChoices : [];
274
+
275
+ // ======================================================================
276
+ // BaseJunctionFields gating (opt-outs per Q4 / #58 resolution)
277
+ // ======================================================================
278
+
279
+ const temporal = config.temporal !== false; // default true
280
+ const sourced = config.sourced !== false; // default true
281
+
282
+ // ======================================================================
283
+ // On-delete actions
284
+ // ======================================================================
285
+
286
+ const onDeleteLeftRaw = config.on_delete_left ?? "restrict";
287
+ const onDeleteRightRaw = config.on_delete_right ?? "restrict";
288
+ const onDeleteLeft = ON_DELETE_MAP[onDeleteLeftRaw] ?? "restrict";
289
+ const onDeleteRight = ON_DELETE_MAP[onDeleteRightRaw] ?? "restrict";
290
+
291
+ // ======================================================================
292
+ // Custom fields (fields other than `role`)
293
+ // ======================================================================
294
+
295
+ const otherFields = { ...fields };
296
+ delete otherFields.role; // role is handled separately as the role enum
297
+ const processedCustomFields = processCustomFields(otherFields, junctionName);
298
+ const hasCustomFields = processedCustomFields.length > 0;
299
+
300
+ // ======================================================================
301
+ // Drizzle imports
302
+ // ======================================================================
303
+
304
+ const drizzleImports = buildDrizzleImports(
305
+ hasRole, temporal, sourced, hasCustomFields, processedCustomFields
306
+ );
307
+
308
+ // ======================================================================
309
+ // Architecture-aware output paths
310
+ // ======================================================================
311
+
312
+ const config_ = loadCodegenConfig(cwd);
313
+ const architecture = resolveArchitecture(config_);
314
+ const srcRoot = resolveSrcRoot(config_, architecture);
315
+ const outputPaths = resolveOutputPaths(junctionName, entityNamePlural, architecture, srcRoot);
316
+
317
+ // ======================================================================
318
+ // CGP-60 — parent-side paths + fan-out locals
319
+ // ======================================================================
320
+ // Parent service / module file paths — anchored on each endpoint.
321
+ // The parent's own `entity new` pipeline previously wrote these files
322
+ // with `force: true`; the junction inject templates target them.
323
+ const leftParentPaths = resolveOutputPaths(leftEntity, leftEntityPlural, architecture, srcRoot);
324
+ const rightParentPaths = resolveOutputPaths(rightEntity, rightEntityPlural, architecture, srcRoot);
325
+ const parentServicePathLeft = leftParentPaths.service;
326
+ const parentServicePathRight = rightParentPaths.service;
327
+ const parentModulePathLeft = leftParentPaths.module;
328
+ const parentModulePathRight = rightParentPaths.module;
329
+
330
+ // Opt-out — defaults to { left: true, right: true }. Schema fills the
331
+ // defaults when omitted, but tolerate raw YAML that bypasses Zod
332
+ // (e.g. tests / direct Hygen invocation).
333
+ const exposeRaw = config.expose_on_parent ?? {};
334
+ const exposeOnParent = {
335
+ left: exposeRaw.left !== false, // default true
336
+ right: exposeRaw.right !== false, // default true
337
+ };
338
+
339
+ // Per-junction unique inject markers (Risk (a) in spec — generic
340
+ // markers silently skip second-junction emission on the same parent).
341
+ const injectionMarkerLeft = `// junction:${junctionName}:left-fan-out`;
342
+ const injectionMarkerRight = `// junction:${junctionName}:right-fan-out`;
343
+
344
+ // Import path for the junction service from each parent's perspective.
345
+ // clean-lite-ps layout: parent service lives at
346
+ // `src/modules/<parentPlural>/<parent>.service.ts`; junction service at
347
+ // `src/modules/<junctionPlural>/<junction>.service.ts`. Relative import
348
+ // is `../<junctionPlural>/<junction>.service`.
349
+ // 'clean' layout: parents live under application/<plural>, junction
350
+ // under application/<junctionPlural>. Same `../` relative form works.
351
+ const junctionServiceImportFromLeft = `../${entityNamePlural}/${junctionName}.service`;
352
+ const junctionServiceImportFromRight = `../${entityNamePlural}/${junctionName}.service`;
353
+ const junctionModuleImportFromLeft = `../${entityNamePlural}/${entityNamePlural}.module`;
354
+ const junctionModuleImportFromRight = `../${entityNamePlural}/${entityNamePlural}.module`;
355
+
356
+ // Left/right repo + module import paths from the junction service's
357
+ // perspective (used by service.ejs.t to import target repos and by
358
+ // module.ejs.t to import the parent modules).
359
+ const leftRepoImportFromJunction = `../${leftEntityPlural}/${leftEntity}.repository`;
360
+ const rightRepoImportFromJunction = `../${rightEntityPlural}/${rightEntity}.repository`;
361
+ const leftEntityImportFromJunction = `../${leftEntityPlural}/${leftEntity}.entity`;
362
+ const rightEntityImportFromJunction = `../${rightEntityPlural}/${rightEntity}.entity`;
363
+ const leftModuleImportFromJunction = `../${leftEntityPlural}/${leftEntityPlural}.module`;
364
+ const rightModuleImportFromJunction = `../${rightEntityPlural}/${rightEntityPlural}.module`;
365
+
366
+ // Parent module / service class names + repo class names.
367
+ const leftRepositoryClass = `${leftEntityPascal}Repository`;
368
+ const rightRepositoryClass = `${rightEntityPascal}Repository`;
369
+ const leftModuleClass = `${pascalCase(leftEntityPlural)}Module`;
370
+ const rightModuleClass = `${pascalCase(rightEntityPlural)}Module`;
371
+ const leftServiceClass = `${leftEntityPascal}Service`;
372
+ const rightServiceClass = `${rightEntityPascal}Service`;
373
+
374
+ // Camel forms of left/right entity names for use in method signatures
375
+ // (e.g. attachContact -> opportunityId, contactId).
376
+ const leftEntityCamel = camelCase(leftEntity);
377
+ const rightEntityCamel = camelCase(rightEntity);
378
+
379
+ // ======================================================================
380
+ // Class names
381
+ // ======================================================================
382
+
383
+ const classNames = {
384
+ entity: entityNamePascal, // OpportunityContact
385
+ repository: `${entityNamePascal}Repository`, // OpportunityContactRepository
386
+ service: `${entityNamePascal}Service`, // OpportunityContactService
387
+ module: `${entityNamePluralPascal}Module`, // OpportunityContactsModule
388
+ };
389
+
390
+ // ======================================================================
391
+ // Return all template locals
392
+ // ======================================================================
393
+
394
+ return {
395
+ // Identity
396
+ name: junctionName,
397
+ entityNamePascal,
398
+ entityNameCamel,
399
+ entityNamePlural,
400
+ entityNamePluralPascal,
401
+ entityNameKebab,
402
+ entityNamePluralKebab,
403
+ tableName,
404
+ tableVarName,
405
+
406
+ // Pairing endpoints
407
+ between: config.between,
408
+ leftEntity,
409
+ rightEntity,
410
+ leftEntityPascal,
411
+ rightEntityPascal,
412
+ leftEntityPlural,
413
+ rightEntityPlural,
414
+ leftColumn,
415
+ rightColumn,
416
+ leftColumnCamel,
417
+ rightColumnCamel,
418
+ selfReferential: false, // always false per schema refinement (endpoints must be distinct)
419
+
420
+ // Role enum
421
+ hasRole,
422
+ roleEnumName,
423
+ roleEnumValues,
424
+
425
+ // BaseJunctionFields gating
426
+ temporal,
427
+ sourced,
428
+
429
+ // On-delete
430
+ onDeleteLeft,
431
+ onDeleteRight,
432
+
433
+ // Custom fields
434
+ processedCustomFields,
435
+ hasCustomFields,
436
+
437
+ // Drizzle
438
+ drizzleImports,
439
+
440
+ // Output paths
441
+ outputPaths,
442
+
443
+ // Class names
444
+ classNames,
445
+
446
+ // Source root + architecture
447
+ srcRoot,
448
+ architecture,
449
+
450
+ // Parent table Drizzle var names (for FK .references())
451
+ leftTable,
452
+ rightTable,
453
+
454
+ // ──────────────────────────────────────────────────────────────────
455
+ // CGP-60 — fan-out locals
456
+ // ──────────────────────────────────────────────────────────────────
457
+ // Camel-case forms of endpoint names (used in method param names).
458
+ leftEntityCamel,
459
+ rightEntityCamel,
460
+ // Parent service / module target paths for inject templates.
461
+ parentServicePathLeft,
462
+ parentServicePathRight,
463
+ parentModulePathLeft,
464
+ parentModulePathRight,
465
+ // Opt-out toggles (default { left: true, right: true }).
466
+ exposeOnParent,
467
+ // Per-junction unique inject markers (skip_if idempotency).
468
+ injectionMarkerLeft,
469
+ injectionMarkerRight,
470
+ // Junction service import paths from each parent's perspective.
471
+ junctionServiceImportFromLeft,
472
+ junctionServiceImportFromRight,
473
+ junctionModuleImportFromLeft,
474
+ junctionModuleImportFromRight,
475
+ // Parent-side repo + entity + module import paths from the junction's
476
+ // perspective (used by junction service.ejs.t + module.ejs.t).
477
+ leftRepoImportFromJunction,
478
+ rightRepoImportFromJunction,
479
+ leftEntityImportFromJunction,
480
+ rightEntityImportFromJunction,
481
+ leftModuleImportFromJunction,
482
+ rightModuleImportFromJunction,
483
+ // Class names used by the inject + service + module templates.
484
+ leftRepositoryClass,
485
+ rightRepositoryClass,
486
+ leftModuleClass,
487
+ rightModuleClass,
488
+ leftServiceClass,
489
+ rightServiceClass,
490
+ };
491
+ },
492
+ };
@@ -0,0 +1,67 @@
1
+ ---
2
+ to: "<%= outputPaths.repository %>"
3
+ force: true
4
+ ---
5
+ import { Injectable, Inject } from '@nestjs/common';
6
+ import { eq } from 'drizzle-orm';
7
+ import { DRIZZLE } from '@shared/constants/tokens';
8
+ import type { DrizzleClient } from '@shared/types/drizzle';
9
+ import { BaseRepository } from '@shared/base-classes/base-repository';
10
+ import { <%= tableVarName %>, type <%= classNames.entity %> } from './<%= name %>.entity';
11
+
12
+ @Injectable()
13
+ export class <%= classNames.repository %> extends BaseRepository<<%= classNames.entity %>> {
14
+ readonly table = <%= tableVarName %>;
15
+
16
+ // Junctions track temporal validity via started_at / ended_at, NOT via
17
+ // deleted_at. is_primary flips replace soft-delete semantics (Q5 resolution).
18
+ protected override readonly behaviors = {
19
+ timestamps: true,
20
+ softDelete: false,
21
+ userTracking: false,
22
+ };
23
+
24
+ constructor(@Inject(DRIZZLE) db: DrizzleClient) {
25
+ super(db);
26
+ }
27
+
28
+ // ═══════════════════════════════════════════════════════════════════════════
29
+ // Pairing-aware finders — hardcoded in v1 (Q2 resolution: no declarative
30
+ // queries block on junctions; every junction needs exactly these two methods).
31
+ // ═══════════════════════════════════════════════════════════════════════════
32
+
33
+ /**
34
+ * Fetch all junction rows where <%= leftColumn %> matches.
35
+ *
36
+ * Pagination shape: { cursor?, limit? } — canonical per cgp-62 r4.
37
+ * FIXME: align with codegen-patterns#358 pagination shape if it diverges.
38
+ */
39
+ async findBy<%= leftEntityPascal %>Id(
40
+ <%= leftColumnCamel %>: string,
41
+ opts?: { cursor?: string; limit?: number },
42
+ ): Promise<<%= classNames.entity %>[]> {
43
+ const rows = await this.baseQuery()
44
+ .where(eq(this.table.<%= leftColumnCamel %>, <%= leftColumnCamel %>))
45
+ .limit(opts?.limit ?? 100);
46
+ return rows as <%= classNames.entity %>[];
47
+ }
48
+
49
+ /**
50
+ * Fetch all junction rows where <%= rightColumn %> matches.
51
+ *
52
+ * Pagination shape: { cursor?, limit? } — canonical per cgp-62 r4.
53
+ * FIXME: align with codegen-patterns#358 pagination shape if it diverges.
54
+ */
55
+ async findBy<%= rightEntityPascal %>Id(
56
+ <%= rightColumnCamel %>: string,
57
+ opts?: { cursor?: string; limit?: number },
58
+ ): Promise<<%= classNames.entity %>[]> {
59
+ const rows = await this.baseQuery()
60
+ .where(eq(this.table.<%= rightColumnCamel %>, <%= rightColumnCamel %>))
61
+ .limit(opts?.limit ?? 100);
62
+ return rows as <%= classNames.entity %>[];
63
+ }
64
+
65
+ // Inherited from BaseRepository:
66
+ // findById, findByIds, list, count, exists, create, update, delete, upsertMany
67
+ }