@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.
- package/CHANGELOG.md +16 -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 +15 -2
- package/templates/entity/new/clean-lite-ps/module.ejs.t +27 -2
- package/templates/entity/new/clean-lite-ps/prompt-extension.js +72 -5
- 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-forwardref-clp-left.ejs.t +8 -0
- package/templates/junction/new/_inject-parent-module-forwardref-clp-right.ejs.t +8 -0
- package/templates/junction/new/_inject-parent-module-import-clp-left.ejs.t +8 -0
- package/templates/junction/new/_inject-parent-module-import-clp-right.ejs.t +8 -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-counterparty-clp-left.ejs.t +7 -0
- package/templates/junction/new/_inject-parent-service-counterparty-clp-right.ejs.t +7 -0
- package/templates/junction/new/_inject-parent-service-forwardref-clp-left.ejs.t +8 -0
- package/templates/junction/new/_inject-parent-service-forwardref-clp-right.ejs.t +8 -0
- package/templates/junction/new/_inject-parent-service-import-clp-left.ejs.t +9 -0
- package/templates/junction/new/_inject-parent-service-import-clp-right.ejs.t +9 -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,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
|
+
}
|