@pattern-stack/codegen 0.6.0 → 0.6.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 +12 -0
- package/package.json +6 -1
- package/src/config/case-converters.mjs +181 -0
- package/src/config/config-loader.mjs +34 -0
- package/src/config/locations.mjs +298 -0
- package/src/config/naming-config.mjs +173 -0
- package/src/config/paths.mjs +690 -0
- package/src/patterns/library/activity.pattern.ts +32 -0
- package/src/patterns/library/base.pattern.ts +28 -0
- package/src/patterns/library/index.ts +30 -0
- package/src/patterns/library/knowledge.pattern.ts +31 -0
- package/src/patterns/library/metadata.pattern.ts +31 -0
- package/src/patterns/library/synced.pattern.ts +34 -0
- package/src/patterns/pattern-definition.ts +280 -0
- package/src/patterns/registry.ts +365 -0
- package/src/schema/naming-config.schema.mjs +119 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern Registry — library + app pattern storage and discovery.
|
|
3
|
+
*
|
|
4
|
+
* Three stores keyed by pattern name:
|
|
5
|
+
* - `LIBRARY_PATTERNS` — seeded by the codegen package itself when the
|
|
6
|
+
* `src/patterns/library/*` barrel imports execute. Consumers never
|
|
7
|
+
* list these in `codegen.config.yaml patterns:`. Domain only.
|
|
8
|
+
* - `APP_PATTERNS` — populated by `loadAppPatterns()` from a
|
|
9
|
+
* consumer-supplied glob set (default `src/patterns/*.pattern.ts`).
|
|
10
|
+
* Domain only.
|
|
11
|
+
* - `ORCHESTRATION_APP_PATTERNS` — populated by the same loader,
|
|
12
|
+
* routed by `kind: 'orchestration'` (ADR-032). No library
|
|
13
|
+
* orchestration patterns ship in Phase 3-1.
|
|
14
|
+
*
|
|
15
|
+
* `getPattern()` checks app patterns first so a consumer could, in
|
|
16
|
+
* principle, shadow a library pattern by using the same `name`. That's
|
|
17
|
+
* not a documented feature, but nothing in the API prevents it.
|
|
18
|
+
*
|
|
19
|
+
* The Hygen subprocess (`src/cli/shared/hygen.ts:64`) reloads this module
|
|
20
|
+
* independently — it has no shared memory with the CLI process. Both
|
|
21
|
+
* loads are deterministic, side-effect-free reads of the same files, so
|
|
22
|
+
* the registry contents are identical across processes. The registry
|
|
23
|
+
* test suite asserts this determinism explicitly.
|
|
24
|
+
*
|
|
25
|
+
* See `docs/adrs/ADR-031-app-defined-patterns.md` §"Decision 5" and
|
|
26
|
+
* `docs/specs/app-defined-patterns-implementation.md` §3.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { glob } from 'glob';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
import { pathToFileURL } from 'node:url';
|
|
32
|
+
import {
|
|
33
|
+
isOrchestrationPattern,
|
|
34
|
+
isPatternDefinition,
|
|
35
|
+
type AnyPatternDefinition,
|
|
36
|
+
type OrchestrationPatternDefinition,
|
|
37
|
+
type PatternDefinition,
|
|
38
|
+
} from './pattern-definition.js';
|
|
39
|
+
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// Stores
|
|
42
|
+
// ============================================================================
|
|
43
|
+
|
|
44
|
+
const LIBRARY_PATTERNS: Map<string, PatternDefinition> = new Map();
|
|
45
|
+
const APP_PATTERNS: Map<string, PatternDefinition> = new Map();
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Orchestration patterns (ADR-032). Library never ships orchestration
|
|
49
|
+
* patterns in Phase 3-1 — only the app-pattern map exists for this kind.
|
|
50
|
+
* If a library-shipped orchestration pattern ever lands, add a parallel
|
|
51
|
+
* `LIBRARY_ORCHESTRATION_PATTERNS` map; for now keep storage minimal.
|
|
52
|
+
*/
|
|
53
|
+
const ORCHESTRATION_APP_PATTERNS: Map<string, OrchestrationPatternDefinition> =
|
|
54
|
+
new Map();
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Every pattern must contribute *something* — either at least one column
|
|
58
|
+
* or at least one of the two class references. A pattern that contributes
|
|
59
|
+
* nothing would generate no useful output and almost certainly indicates
|
|
60
|
+
* a typo or an unfinished definition.
|
|
61
|
+
*/
|
|
62
|
+
function assertHasContribution(def: PatternDefinition): void {
|
|
63
|
+
const hasColumns = Array.isArray(def.columns) && def.columns.length > 0;
|
|
64
|
+
const hasRepo =
|
|
65
|
+
typeof def.repositoryClass === 'string' && def.repositoryClass.length > 0;
|
|
66
|
+
const hasService =
|
|
67
|
+
typeof def.serviceClass === 'string' && def.serviceClass.length > 0;
|
|
68
|
+
|
|
69
|
+
if (!hasColumns && !hasRepo && !hasService) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Pattern '${def.name}' contributes nothing — at least one of ` +
|
|
72
|
+
'`columns`, `repositoryClass`, or `serviceClass` is required.',
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Orchestration counterpart to `assertHasContribution`. An orchestration
|
|
79
|
+
* pattern's minimum contribution is one registry with at least one entry —
|
|
80
|
+
* a registry with zero entries would emit a token + module that nothing
|
|
81
|
+
* resolves to, almost certainly a typo. Detailed entry validation
|
|
82
|
+
* (duplicate keys, malformed entries, co-keyed mismatches) lives in the
|
|
83
|
+
* project-level validator so loader behaviour stays symmetrical with the
|
|
84
|
+
* domain side: load is non-throwing for content-level issues, validator
|
|
85
|
+
* is the single authoritative reporter.
|
|
86
|
+
*/
|
|
87
|
+
function assertOrchestrationContribution(
|
|
88
|
+
def: OrchestrationPatternDefinition,
|
|
89
|
+
): void {
|
|
90
|
+
if (!def.registry || typeof def.registry !== 'object') {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Orchestration pattern '${def.name}' is missing a 'registry' field.`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (
|
|
96
|
+
typeof def.registry.keyType !== 'string' ||
|
|
97
|
+
def.registry.keyType.length === 0
|
|
98
|
+
) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`Orchestration pattern '${def.name}' registry.keyType must be a non-empty string.`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
if (
|
|
104
|
+
typeof def.registry.valueType !== 'string' ||
|
|
105
|
+
def.registry.valueType.length === 0
|
|
106
|
+
) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Orchestration pattern '${def.name}' registry.valueType must be a non-empty string.`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
if (
|
|
112
|
+
!Array.isArray(def.registry.entries) ||
|
|
113
|
+
def.registry.entries.length === 0
|
|
114
|
+
) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`Orchestration pattern '${def.name}' registry.entries must contain at least one entry.`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ============================================================================
|
|
122
|
+
// Library pattern registration
|
|
123
|
+
// ============================================================================
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Insert a library pattern into the registry. Called once by each
|
|
127
|
+
* `src/patterns/library/*.pattern.ts` file via the barrel. Re-registering
|
|
128
|
+
* the same name overwrites the previous value silently; this is
|
|
129
|
+
* intentional for hot-reload scenarios but should not happen in normal
|
|
130
|
+
* use.
|
|
131
|
+
*/
|
|
132
|
+
export function registerLibraryPattern(def: PatternDefinition): void {
|
|
133
|
+
assertHasContribution(def);
|
|
134
|
+
LIBRARY_PATTERNS.set(def.name, def);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ============================================================================
|
|
138
|
+
// Lookup
|
|
139
|
+
// ============================================================================
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Resolve a **domain** pattern by name. App patterns shadow library
|
|
143
|
+
* patterns with the same name — useful in principle but not a documented
|
|
144
|
+
* feature.
|
|
145
|
+
*
|
|
146
|
+
* Orchestration patterns live in a disjoint store; use
|
|
147
|
+
* `getOrchestrationPattern()` to look those up. The two surfaces are
|
|
148
|
+
* intentionally separate (ADR-032 Decision 8) so callers don't have to
|
|
149
|
+
* narrow the result on every callsite.
|
|
150
|
+
*/
|
|
151
|
+
export function getPattern(name: string): PatternDefinition | undefined {
|
|
152
|
+
return APP_PATTERNS.get(name) ?? LIBRARY_PATTERNS.get(name);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Return every registered domain pattern name (library + app), sorted for
|
|
157
|
+
* deterministic output. The two-process determinism test relies on this
|
|
158
|
+
* ordering being stable across processes. Orchestration names are NOT
|
|
159
|
+
* included — see `getOrchestrationPatternNames()`.
|
|
160
|
+
*/
|
|
161
|
+
export function getAllPatternNames(): string[] {
|
|
162
|
+
const set = new Set<string>([
|
|
163
|
+
...LIBRARY_PATTERNS.keys(),
|
|
164
|
+
...APP_PATTERNS.keys(),
|
|
165
|
+
]);
|
|
166
|
+
return [...set].sort();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Library-only view — mainly for debugging and tests. */
|
|
170
|
+
export function getLibraryPatternNames(): string[] {
|
|
171
|
+
return [...LIBRARY_PATTERNS.keys()].sort();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** App-only view — mainly for debugging and tests. */
|
|
175
|
+
export function getAppPatternNames(): string[] {
|
|
176
|
+
return [...APP_PATTERNS.keys()].sort();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ============================================================================
|
|
180
|
+
// Orchestration accessors (ADR-032)
|
|
181
|
+
// ============================================================================
|
|
182
|
+
|
|
183
|
+
/** Resolve an orchestration pattern by name. */
|
|
184
|
+
export function getOrchestrationPattern(
|
|
185
|
+
name: string,
|
|
186
|
+
): OrchestrationPatternDefinition | undefined {
|
|
187
|
+
return ORCHESTRATION_APP_PATTERNS.get(name);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Sorted list of orchestration pattern names. */
|
|
191
|
+
export function getOrchestrationPatternNames(): string[] {
|
|
192
|
+
return [...ORCHESTRATION_APP_PATTERNS.keys()].sort();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Every registered orchestration pattern, sorted by name. The
|
|
197
|
+
* project-level validator iterates this list in one place so issue
|
|
198
|
+
* ordering is stable across processes.
|
|
199
|
+
*/
|
|
200
|
+
export function getAllOrchestrationPatterns(): OrchestrationPatternDefinition[] {
|
|
201
|
+
return getOrchestrationPatternNames().map(
|
|
202
|
+
(n) => ORCHESTRATION_APP_PATTERNS.get(n)!,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ============================================================================
|
|
207
|
+
// App pattern discovery
|
|
208
|
+
// ============================================================================
|
|
209
|
+
|
|
210
|
+
export interface LoadAppPatternsResult {
|
|
211
|
+
/** Pattern names that were successfully registered, sorted */
|
|
212
|
+
loaded: string[];
|
|
213
|
+
/** One human-readable error per failed file import */
|
|
214
|
+
errors: string[];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Expand every glob in `manifestPaths` relative to `cwd`, dynamic-import
|
|
219
|
+
* each matching file, and register every exported value that passes
|
|
220
|
+
* `isPatternDefinition()`. Exports whose name ends in `Pattern` and
|
|
221
|
+
* pass the shape check are registered; other exports are ignored so
|
|
222
|
+
* that files can export helper values alongside their pattern.
|
|
223
|
+
*
|
|
224
|
+
* Import failures are non-fatal — the error is collected and returned
|
|
225
|
+
* so the CLI can surface it without breaking generation of unrelated
|
|
226
|
+
* entities. A pattern that fails the "at-least-one-contribution" check
|
|
227
|
+
* surfaces here as an error too.
|
|
228
|
+
*
|
|
229
|
+
* Idempotent: calling twice with the same arguments leaves `APP_PATTERNS`
|
|
230
|
+
* in the same state as calling once.
|
|
231
|
+
*/
|
|
232
|
+
export async function loadAppPatterns(
|
|
233
|
+
manifestPaths: string[],
|
|
234
|
+
cwd: string,
|
|
235
|
+
): Promise<LoadAppPatternsResult> {
|
|
236
|
+
const loaded = new Set<string>();
|
|
237
|
+
const errors: string[] = [];
|
|
238
|
+
|
|
239
|
+
// Collect + dedupe absolute file paths across every glob pattern so
|
|
240
|
+
// a file matched by two globs is imported once.
|
|
241
|
+
const files = new Set<string>();
|
|
242
|
+
for (const raw of manifestPaths) {
|
|
243
|
+
try {
|
|
244
|
+
const expanded = await glob(raw, { cwd, absolute: true, nodir: true });
|
|
245
|
+
for (const filePath of expanded) {
|
|
246
|
+
files.add(filePath);
|
|
247
|
+
}
|
|
248
|
+
} catch (err) {
|
|
249
|
+
errors.push(
|
|
250
|
+
`Failed to expand pattern glob '${raw}': ${stringifyError(err)}`,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Sort so dynamic-import order is deterministic across processes —
|
|
256
|
+
// the Hygen subprocess relies on this to produce the same registry
|
|
257
|
+
// as the CLI.
|
|
258
|
+
const sortedFiles = [...files].sort();
|
|
259
|
+
|
|
260
|
+
for (const filePath of sortedFiles) {
|
|
261
|
+
try {
|
|
262
|
+
// `pathToFileURL` is required for absolute-path dynamic imports on
|
|
263
|
+
// Windows and makes the behavior identical on macOS/Linux.
|
|
264
|
+
const mod = (await import(pathToFileURL(filePath).href)) as Record<
|
|
265
|
+
string,
|
|
266
|
+
unknown
|
|
267
|
+
>;
|
|
268
|
+
for (const [key, val] of Object.entries(mod)) {
|
|
269
|
+
if (!key.endsWith('Pattern')) continue;
|
|
270
|
+
if (!isPatternDefinition(val)) continue;
|
|
271
|
+
|
|
272
|
+
// Route on `kind`. Domain (default) and orchestration land in
|
|
273
|
+
// disjoint maps; same-name collisions within either map are
|
|
274
|
+
// load-time errors (silent overwrite was wrong by CLAUDE.md
|
|
275
|
+
// "architectural correctness" — see ADR-032 §Composition rules
|
|
276
|
+
// row 1).
|
|
277
|
+
if (isOrchestrationPattern(val as unknown as AnyPatternDefinition)) {
|
|
278
|
+
const orch = val as unknown as OrchestrationPatternDefinition;
|
|
279
|
+
try {
|
|
280
|
+
assertOrchestrationContribution(orch);
|
|
281
|
+
} catch (assertErr) {
|
|
282
|
+
errors.push(
|
|
283
|
+
`Orchestration pattern '${orch.name}' in ${relPath(filePath, cwd)} is invalid: ${stringifyError(assertErr)}`,
|
|
284
|
+
);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const existingOrch = ORCHESTRATION_APP_PATTERNS.get(orch.name);
|
|
288
|
+
if (existingOrch && existingOrch !== orch) {
|
|
289
|
+
errors.push(
|
|
290
|
+
`Orchestration pattern '${orch.name}' in ${relPath(filePath, cwd)} duplicates a previously loaded orchestration pattern. Pattern names must be unique.`,
|
|
291
|
+
);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
ORCHESTRATION_APP_PATTERNS.set(orch.name, orch);
|
|
295
|
+
loaded.add(orch.name);
|
|
296
|
+
} else {
|
|
297
|
+
try {
|
|
298
|
+
assertHasContribution(val);
|
|
299
|
+
} catch (assertErr) {
|
|
300
|
+
errors.push(
|
|
301
|
+
`Pattern '${val.name}' in ${relPath(filePath, cwd)} is invalid: ${stringifyError(assertErr)}`,
|
|
302
|
+
);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
const existingDom = APP_PATTERNS.get(val.name);
|
|
306
|
+
if (existingDom && existingDom !== val) {
|
|
307
|
+
errors.push(
|
|
308
|
+
`Pattern '${val.name}' in ${relPath(filePath, cwd)} duplicates a previously loaded app pattern. Pattern names must be unique.`,
|
|
309
|
+
);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
APP_PATTERNS.set(val.name, val);
|
|
313
|
+
loaded.add(val.name);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
} catch (err) {
|
|
317
|
+
errors.push(
|
|
318
|
+
`Failed to load pattern file '${relPath(filePath, cwd)}': ${stringifyError(err)}`,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
loaded: [...loaded].sort(),
|
|
325
|
+
errors,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ============================================================================
|
|
330
|
+
// Test-only reset
|
|
331
|
+
// ============================================================================
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Clear every registered app pattern and, optionally, library patterns too.
|
|
335
|
+
*
|
|
336
|
+
* Intended for unit tests that build isolated scenarios on top of a clean
|
|
337
|
+
* registry. Not exported from the barrel — tests import it directly from
|
|
338
|
+
* `./registry.js`.
|
|
339
|
+
*/
|
|
340
|
+
export function _resetRegistryForTests(
|
|
341
|
+
opts: { includeLibrary?: boolean } = {},
|
|
342
|
+
): void {
|
|
343
|
+
APP_PATTERNS.clear();
|
|
344
|
+
ORCHESTRATION_APP_PATTERNS.clear();
|
|
345
|
+
if (opts.includeLibrary) {
|
|
346
|
+
LIBRARY_PATTERNS.clear();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ============================================================================
|
|
351
|
+
// Helpers
|
|
352
|
+
// ============================================================================
|
|
353
|
+
|
|
354
|
+
function stringifyError(err: unknown): string {
|
|
355
|
+
if (err instanceof Error) return err.message;
|
|
356
|
+
return String(err);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function relPath(abs: string, cwd: string): string {
|
|
360
|
+
try {
|
|
361
|
+
return path.relative(cwd, abs) || abs;
|
|
362
|
+
} catch {
|
|
363
|
+
return abs;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* naming-config.schema.mjs
|
|
3
|
+
*
|
|
4
|
+
* Pure-JS mirror of naming-config.schema.ts for use in hygen (Node.js) context.
|
|
5
|
+
* No Zod, no TypeScript — only plain-object constants and functions.
|
|
6
|
+
*
|
|
7
|
+
* Keep in sync with naming-config.schema.ts.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Default Configuration
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_BACKEND_NAMING = {
|
|
15
|
+
fileCase: 'kebab-case',
|
|
16
|
+
suffixStyle: 'dotted',
|
|
17
|
+
entityInclusion: 'flat-only',
|
|
18
|
+
terminology: {
|
|
19
|
+
command: 'command',
|
|
20
|
+
query: 'query',
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Validation (plain JS — no Zod)
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
const VALID_FILE_CASES = ['kebab-case', 'camelCase', 'snake_case', 'PascalCase'];
|
|
29
|
+
const VALID_SUFFIX_STYLES = ['dotted', 'suffixed', 'worded'];
|
|
30
|
+
const VALID_ENTITY_INCLUSIONS = ['always', 'never', 'flat-only'];
|
|
31
|
+
const VALID_COMMAND_TERMS = ['command', 'use-case'];
|
|
32
|
+
const VALID_QUERY_TERMS = ['query', 'use-case'];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Validate and parse a backend naming config object.
|
|
36
|
+
* Applies defaults for missing fields.
|
|
37
|
+
* Throws on invalid values.
|
|
38
|
+
*/
|
|
39
|
+
export const BackendNamingConfigSchema = {
|
|
40
|
+
parse(data) {
|
|
41
|
+
const fc = data?.fileCase ?? DEFAULT_BACKEND_NAMING.fileCase;
|
|
42
|
+
const ss = data?.suffixStyle ?? DEFAULT_BACKEND_NAMING.suffixStyle;
|
|
43
|
+
const ei = data?.entityInclusion ?? DEFAULT_BACKEND_NAMING.entityInclusion;
|
|
44
|
+
const tc = data?.terminology?.command ?? DEFAULT_BACKEND_NAMING.terminology.command;
|
|
45
|
+
const tq = data?.terminology?.query ?? DEFAULT_BACKEND_NAMING.terminology.query;
|
|
46
|
+
|
|
47
|
+
if (!VALID_FILE_CASES.includes(fc)) {
|
|
48
|
+
throw new Error(`Invalid fileCase: ${fc}. Must be one of: ${VALID_FILE_CASES.join(', ')}`);
|
|
49
|
+
}
|
|
50
|
+
if (!VALID_SUFFIX_STYLES.includes(ss)) {
|
|
51
|
+
throw new Error(`Invalid suffixStyle: ${ss}. Must be one of: ${VALID_SUFFIX_STYLES.join(', ')}`);
|
|
52
|
+
}
|
|
53
|
+
if (!VALID_ENTITY_INCLUSIONS.includes(ei)) {
|
|
54
|
+
throw new Error(`Invalid entityInclusion: ${ei}. Must be one of: ${VALID_ENTITY_INCLUSIONS.join(', ')}`);
|
|
55
|
+
}
|
|
56
|
+
if (!VALID_COMMAND_TERMS.includes(tc)) {
|
|
57
|
+
throw new Error(`Invalid terminology.command: ${tc}. Must be one of: ${VALID_COMMAND_TERMS.join(', ')}`);
|
|
58
|
+
}
|
|
59
|
+
if (!VALID_QUERY_TERMS.includes(tq)) {
|
|
60
|
+
throw new Error(`Invalid terminology.query: ${tq}. Must be one of: ${VALID_QUERY_TERMS.join(', ')}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
fileCase: fc,
|
|
65
|
+
suffixStyle: ss,
|
|
66
|
+
entityInclusion: ei,
|
|
67
|
+
terminology: { command: tc, query: tq },
|
|
68
|
+
layers: data?.layers ?? undefined,
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// Resolution Helper
|
|
75
|
+
// ============================================================================
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Resolve effective naming config for a specific layer.
|
|
79
|
+
* Merges layer-specific overrides with global defaults.
|
|
80
|
+
*/
|
|
81
|
+
export function resolveLayerNaming(config, layer) {
|
|
82
|
+
const layerConfig = config.layers?.[layer];
|
|
83
|
+
return {
|
|
84
|
+
fileCase: layerConfig?.fileCase ?? config.fileCase,
|
|
85
|
+
suffixStyle: layerConfig?.suffixStyle ?? config.suffixStyle,
|
|
86
|
+
entityInclusion: layerConfig?.entityInclusion ?? config.entityInclusion,
|
|
87
|
+
terminology: {
|
|
88
|
+
command: layerConfig?.terminology?.command ?? config.terminology.command,
|
|
89
|
+
query: layerConfig?.terminology?.query ?? config.terminology.query,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ============================================================================
|
|
95
|
+
// File Type Suffixes
|
|
96
|
+
// ============================================================================
|
|
97
|
+
|
|
98
|
+
export const FILE_TYPE_SUFFIXES = {
|
|
99
|
+
entity: { dotted: '.entity', suffixed: 'Entity', word: 'entity' },
|
|
100
|
+
repositoryInterface: {
|
|
101
|
+
dotted: '.repository.interface',
|
|
102
|
+
suffixed: 'RepositoryInterface',
|
|
103
|
+
word: 'repository-interface',
|
|
104
|
+
},
|
|
105
|
+
repository: { dotted: '.repository', suffixed: 'Repository', word: 'repository' },
|
|
106
|
+
command: { dotted: '.command', suffixed: 'Command', word: 'command' },
|
|
107
|
+
query: { dotted: '.query', suffixed: 'Query', word: 'query' },
|
|
108
|
+
dto: { dotted: '.dto', suffixed: 'Dto', word: 'dto' },
|
|
109
|
+
controller: { dotted: '.controller', suffixed: 'Controller', word: 'controller' },
|
|
110
|
+
module: { dotted: '.module', suffixed: 'Module', word: 'module' },
|
|
111
|
+
schema: { dotted: '.schema', suffixed: 'Schema', word: 'schema' },
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export default {
|
|
115
|
+
DEFAULT_BACKEND_NAMING,
|
|
116
|
+
BackendNamingConfigSchema,
|
|
117
|
+
resolveLayerNaming,
|
|
118
|
+
FILE_TYPE_SUFFIXES,
|
|
119
|
+
};
|