@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.
@@ -0,0 +1,690 @@
1
+ /**
2
+ * Centralized path configuration for codegen
3
+ *
4
+ * All generated file paths are defined here to make it easy to update
5
+ * the architecture structure without hunting through multiple files.
6
+ *
7
+ * NEW: For path + import alias pairs, see ./locations.mjs
8
+ * The LOCATIONS export provides both filesystem paths and TypeScript import aliases.
9
+ *
10
+ * NEW: For configurable naming conventions, see ./naming-config.mjs
11
+ * The naming config supports fileCase, suffixStyle, entityInclusion, and terminology options.
12
+ *
13
+ * Usage:
14
+ * import { paths, getPath, LOCATIONS } from '../config/paths.js';
15
+ */
16
+
17
+ import path from 'node:path';
18
+ import { projectConfig } from './config-loader.mjs';
19
+ import { getNamingConfig } from './naming-config.mjs';
20
+ import { applyCase, toPascalCase, getCaseSeparator } from './case-converters.mjs';
21
+ import { FILE_TYPE_SUFFIXES } from '../schema/naming-config.schema.mjs';
22
+ import { LOCATIONS } from './locations.mjs';
23
+
24
+ // Re-export LOCATIONS for unified path + import configuration
25
+ export { LOCATIONS, getLocation, getLocationPath, getLocationImport } from './locations.mjs';
26
+
27
+ // ============================================================================
28
+ // Layout Options
29
+ // ============================================================================
30
+
31
+ /**
32
+ * Folder structure options - controls directory nesting
33
+ */
34
+ export const FOLDER_STRUCTURES = {
35
+ nested: "nested", // domain/opportunity/opportunity.entity.ts
36
+ flat: "flat", // domain/opportunity.entity.ts
37
+ };
38
+
39
+ /**
40
+ * File grouping options - controls how related code is organized into files
41
+ * This is orthogonal to folder_structure (layout vs content organization)
42
+ */
43
+ export const FILE_GROUPINGS = {
44
+ separate: "separate", // Each concern in its own file (entity.ts, repository.interface.ts)
45
+ grouped: "grouped", // Related concerns combined (index.ts with entity + interface)
46
+ };
47
+
48
+ /**
49
+ * Default layout configuration
50
+ */
51
+ export const DEFAULT_LAYOUT = {
52
+ folderStructure: FOLDER_STRUCTURES.nested,
53
+ fileGrouping: FILE_GROUPINGS.separate,
54
+ };
55
+
56
+ // ============================================================================
57
+ // Path Configuration
58
+ // ============================================================================
59
+
60
+ /**
61
+ * Base paths relative to project root
62
+ * Can be overridden by codegen.config.yaml
63
+ */
64
+ export const BASE_PATHS = {
65
+ // Backend base
66
+ backendSrc: projectConfig?.paths?.backend_src ?? "app/backend/src",
67
+
68
+ // Frontend base
69
+ frontendSrc: projectConfig?.paths?.frontend_src ?? "app/frontend/src",
70
+
71
+ // Shared packages
72
+ packages: projectConfig?.paths?.packages ?? "packages",
73
+
74
+ // Schema directory (relative to backendSrc)
75
+ schemaDir: projectConfig?.paths?.schema_dir ?? "infrastructure/persistence/drizzle",
76
+
77
+ // Entity definitions directory
78
+ entitiesDir: projectConfig?.paths?.entities_dir ?? "entities",
79
+
80
+ // Manifest output directory
81
+ manifestDir: projectConfig?.paths?.manifest_dir ?? ".codegen",
82
+
83
+ // Orchestration emission root (ADR-032 Phase 3-2, O-6).
84
+ // Default sits under backendSrc so it co-locates with src/modules/ and
85
+ // src/subsystems/ in the consumer's tree; override via paths.orchestration_src.
86
+ orchestrationSrc:
87
+ projectConfig?.paths?.orchestration_src ??
88
+ `${projectConfig?.paths?.backend_src ?? "app/backend/src"}/orchestration`,
89
+ };
90
+
91
+ const posixPath = path.posix;
92
+
93
+ function normalizeRelativePath(value) {
94
+ return value === "" ? "." : value;
95
+ }
96
+
97
+ function joinPath(...parts) {
98
+ return posixPath.join(...parts.filter((part) => part !== "" && part != null));
99
+ }
100
+
101
+ function relativeImport(fromDir, toPath) {
102
+ const from = fromDir && fromDir !== "" ? fromDir : ".";
103
+ const rel = posixPath.relative(from, toPath);
104
+ if (!rel) return ".";
105
+ return rel.startsWith(".") ? rel : `./${rel}`;
106
+ }
107
+
108
+ function toBackendLayer(locationKey) {
109
+ const location = LOCATIONS[locationKey];
110
+ if (!location?.path) return ".";
111
+ const rel = posixPath.relative(BASE_PATHS.backendSrc, location.path);
112
+ return normalizeRelativePath(rel);
113
+ }
114
+
115
+ /**
116
+ * Layer paths within backend (relative to backendSrc)
117
+ * Following Clean Architecture principles
118
+ */
119
+ export const BACKEND_LAYERS = {
120
+ // Domain layer - pure business logic, no framework deps
121
+ domain: toBackendLayer("backendDomain"),
122
+
123
+ // Application layer - use cases, commands, queries
124
+ application: normalizeRelativePath(posixPath.dirname(toBackendLayer("backendCommands"))),
125
+ commands: toBackendLayer("backendCommands"),
126
+ queries: toBackendLayer("backendQueries"),
127
+ schemas: toBackendLayer("backendSchemas"),
128
+
129
+ // Infrastructure layer - external integrations
130
+ infrastructure: normalizeRelativePath(
131
+ posixPath.dirname(posixPath.dirname(toBackendLayer("backendDrizzle")))
132
+ ),
133
+ persistence: normalizeRelativePath(posixPath.dirname(toBackendLayer("backendDrizzle"))),
134
+ drizzle: toBackendLayer("backendDrizzle"),
135
+ repositories: toBackendLayer("backendRepositories"),
136
+
137
+ // Presentation layer - REST controllers, GraphQL resolvers
138
+ presentation: normalizeRelativePath(posixPath.dirname(toBackendLayer("backendControllers"))),
139
+ controllers: toBackendLayer("backendControllers"),
140
+
141
+ // Modules - NestJS DI configuration
142
+ modules: toBackendLayer("backendModules"),
143
+
144
+ // Constants
145
+ constants: toBackendLayer("backendConstants"),
146
+ };
147
+
148
+ /**
149
+ * Frontend paths (relative to frontendSrc)
150
+ */
151
+ export const FRONTEND_LAYERS = {
152
+ lib: "lib",
153
+ collections: "lib/collections",
154
+ store: "lib/store",
155
+ entities: "lib/entities",
156
+ generated: "generated",
157
+ entityMetadata: "generated/entity-metadata",
158
+ };
159
+
160
+ /**
161
+ * Shared package paths
162
+ */
163
+ export const PACKAGE_PATHS = {
164
+ db: "packages/db/src",
165
+ dbEntities: "packages/db/src/entities",
166
+ };
167
+
168
+ /**
169
+ * Get full path from project root
170
+ */
171
+ export function getBackendPath(layer, subpath = "") {
172
+ const basePath = joinPath(BASE_PATHS.backendSrc, BACKEND_LAYERS[layer]);
173
+ return subpath ? joinPath(basePath, subpath) : basePath;
174
+ }
175
+
176
+ /**
177
+ * Get the orchestration emission directory (ADR-032 Phase 3-2).
178
+ *
179
+ * Honors `paths.orchestration_src`; defaults to `${backend_src}/orchestration`.
180
+ * Returns a relative path (from project root). When `slug` is provided, joins
181
+ * the per-pattern subdirectory (e.g. `crm-ports`).
182
+ */
183
+ export function getOrchestrationPath(slug = "") {
184
+ return slug
185
+ ? joinPath(BASE_PATHS.orchestrationSrc, slug)
186
+ : BASE_PATHS.orchestrationSrc;
187
+ }
188
+
189
+ export function getFrontendPath(layer, subpath = "") {
190
+ const basePath = joinPath(BASE_PATHS.frontendSrc, FRONTEND_LAYERS[layer]);
191
+ return subpath ? joinPath(basePath, subpath) : basePath;
192
+ }
193
+
194
+ /**
195
+ * File naming conventions (legacy constant for backward compatibility)
196
+ *
197
+ * @deprecated Use computeFileNaming() or computeFileName() for config-driven naming
198
+ */
199
+ export const FILE_NAMING = {
200
+ // File suffixes (dotted style - default)
201
+ entity: ".entity.ts",
202
+ repositoryInterface: ".repository.interface.ts",
203
+ repository: ".repository.ts",
204
+ command: ".command.ts",
205
+ query: ".query.ts",
206
+ dto: ".dto.ts",
207
+ controller: ".controller.ts",
208
+ module: ".module.ts",
209
+ schema: ".schema.ts",
210
+ };
211
+
212
+ // ============================================================================
213
+ // Config-Driven File Naming
214
+ // ============================================================================
215
+
216
+ /**
217
+ * Compute file suffix based on file type and suffix style
218
+ *
219
+ * @param {string} fileType - File type from FILE_TYPE_SUFFIXES
220
+ * @param {'dotted' | 'suffixed' | 'worded'} suffixStyle - How to apply suffix
221
+ * @param {'kebab-case' | 'snake_case' | 'camelCase' | 'PascalCase'} fileCase - For worded style separator
222
+ * @returns {string} Suffix without .ts extension (e.g., ".entity" or "Entity" or "-entity")
223
+ */
224
+ function computeSuffix(fileType, suffixStyle, fileCase) {
225
+ const suffixInfo = FILE_TYPE_SUFFIXES[fileType];
226
+ if (!suffixInfo) {
227
+ console.warn(`Unknown file type: ${fileType}, using .${fileType}`);
228
+ return `.${fileType}`;
229
+ }
230
+
231
+ switch (suffixStyle) {
232
+ case 'dotted':
233
+ return suffixInfo.dotted;
234
+ case 'suffixed':
235
+ return suffixInfo.suffixed;
236
+ case 'worded':
237
+ // Use the case-appropriate separator
238
+ const separator = getCaseSeparator(fileCase);
239
+ if (separator) {
240
+ return `${separator}${suffixInfo.word}`;
241
+ }
242
+ // For camelCase/PascalCase, capitalize the suffix word
243
+ return toPascalCase(suffixInfo.word);
244
+ default:
245
+ return suffixInfo.dotted;
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Compute a single file name from entity name, type, and config
251
+ *
252
+ * @param {string} entityName - Entity name (typically snake_case from YAML)
253
+ * @param {string} fileType - File type key (entity, repository, command, etc.)
254
+ * @param {Object} namingConfig - Naming configuration
255
+ * @param {Object} options - Additional options
256
+ * @param {boolean} options.isNested - Whether using nested folder structure
257
+ * @param {string} options.plural - Plural form of entity name
258
+ * @param {string} options.action - Action prefix for commands/queries (create, update, delete, etc.)
259
+ * @returns {string} Complete file name with .ts extension
260
+ */
261
+ export function computeFileName(entityName, fileType, namingConfig = null, options = {}) {
262
+ const config = namingConfig || getNamingConfig();
263
+ const { fileCase, suffixStyle, entityInclusion, terminology } = config;
264
+ const { isNested = true, plural, action } = options;
265
+
266
+ // Determine the base name
267
+ let baseName = entityName;
268
+
269
+ // Compute the suffix based on style and terminology
270
+ // For commands/queries with use-case terminology, override the default suffix
271
+ const useUseCaseSuffix =
272
+ (fileType === 'command' && terminology.command === 'use-case') ||
273
+ (fileType === 'query' && terminology.query === 'use-case');
274
+
275
+ let suffix;
276
+ if (useUseCaseSuffix) {
277
+ // Use-case terminology: generate .use-case / UseCase / -use-case suffix
278
+ const separator = getCaseSeparator(fileCase);
279
+ suffix = suffixStyle === 'dotted' ? '.use-case' :
280
+ suffixStyle === 'suffixed' ? 'UseCase' :
281
+ separator ? `${separator}use-case` : 'UseCase';
282
+ } else {
283
+ suffix = computeSuffix(fileType, suffixStyle, fileCase);
284
+ }
285
+
286
+ // For command/query files with action prefix
287
+ if (action) {
288
+ // Determine if entity name should be included
289
+ const includeEntity =
290
+ entityInclusion === 'always' ||
291
+ (entityInclusion === 'flat-only' && !isNested);
292
+
293
+ if (includeEntity) {
294
+ // Use natural action patterns (Dealbrain-style):
295
+ // - get-{entity}-by-id (not get-by-id-{entity})
296
+ // - get-all-{plural} (not list-{plural})
297
+ // - {action}-{entity} for others (create-user, update-user, delete-user)
298
+ if (action === 'get-by-id') {
299
+ baseName = `get-${entityName}-by-id`;
300
+ } else if (action === 'list' && plural) {
301
+ baseName = `get-all-${plural}`;
302
+ } else {
303
+ baseName = `${action}-${entityName}`;
304
+ }
305
+ } else {
306
+ // Exclude entity: create.command.ts
307
+ baseName = action;
308
+ }
309
+ }
310
+
311
+ // For controller/module, use plural form
312
+ if ((fileType === 'controller' || fileType === 'module') && plural) {
313
+ baseName = plural;
314
+ }
315
+
316
+ // Apply case transformation to base name
317
+ const casedName = applyCase(baseName, fileCase);
318
+
319
+ // Build final file name
320
+ if (suffixStyle === 'suffixed') {
321
+ // For suffixed style, base should also be PascalCase
322
+ const pascalBase = toPascalCase(baseName);
323
+ return `${pascalBase}${suffix}.ts`;
324
+ }
325
+
326
+ return `${casedName}${suffix}.ts`;
327
+ }
328
+
329
+ /**
330
+ * Compute FILE_NAMING map from configuration
331
+ *
332
+ * Returns same shape as legacy FILE_NAMING constant but computed from config.
333
+ * Useful for backward compatibility with existing code.
334
+ *
335
+ * @param {Object} namingConfig - Optional naming configuration (uses default if not provided)
336
+ * @returns {Object} FILE_NAMING-compatible object
337
+ */
338
+ export function computeFileNaming(namingConfig = null) {
339
+ const config = namingConfig || getNamingConfig();
340
+ const { suffixStyle, fileCase } = config;
341
+
342
+ const result = {};
343
+ const fileTypes = ['entity', 'repositoryInterface', 'repository', 'command', 'query', 'dto', 'controller', 'module', 'schema'];
344
+
345
+ for (const type of fileTypes) {
346
+ const suffix = computeSuffix(type, suffixStyle, fileCase);
347
+ result[type] = `${suffix}.ts`;
348
+ }
349
+
350
+ return result;
351
+ }
352
+
353
+ /**
354
+ * Get dynamic paths based on entity configuration
355
+ *
356
+ * @param {Object} options
357
+ * @param {string} options.name - Entity name (snake_case)
358
+ * @param {string} options.plural - Plural form of entity name
359
+ * @param {boolean} options.isNested - Whether to use nested folder structure
360
+ * @param {boolean} options.isGrouped - Whether to group related files together
361
+ * @returns {Object} Computed paths for this entity
362
+ */
363
+ export function getEntityPaths({ name, plural, isNested = true, isGrouped = false }) {
364
+ return {
365
+ // Domain paths
366
+ domain: isNested ? joinPath(BACKEND_LAYERS.domain, name) : BACKEND_LAYERS.domain,
367
+
368
+ // Application paths
369
+ commands: isNested
370
+ ? joinPath(BACKEND_LAYERS.commands, name)
371
+ : BACKEND_LAYERS.commands,
372
+ queries: isNested
373
+ ? joinPath(BACKEND_LAYERS.queries, name)
374
+ : BACKEND_LAYERS.queries,
375
+
376
+ // These are flat (single file per entity type)
377
+ schemas: BACKEND_LAYERS.schemas,
378
+ drizzle: BACKEND_LAYERS.drizzle,
379
+ repositories: BACKEND_LAYERS.repositories,
380
+ controllers: BACKEND_LAYERS.controllers,
381
+ modules: BACKEND_LAYERS.modules,
382
+ constants: BACKEND_LAYERS.constants,
383
+ };
384
+ }
385
+
386
+ /**
387
+ * Get file names based on entity configuration
388
+ *
389
+ * Layout options:
390
+ * - isNested: controls folder nesting (domain/opportunity/ vs domain/)
391
+ * - isGrouped: controls file grouping (index.ts vs separate files)
392
+ * - namingConfig: optional naming configuration (uses default if not provided)
393
+ *
394
+ * @param {Object} options
395
+ * @param {string} options.name - Entity name (snake_case)
396
+ * @param {string} options.plural - Plural form of entity name
397
+ * @param {boolean} options.isNested - Whether to use nested folder structure
398
+ * @param {boolean} options.isGrouped - Whether to group related files together
399
+ * @param {Object} options.namingConfig - Optional naming configuration
400
+ * @returns {Object} Computed file names for this entity
401
+ */
402
+ export function getEntityFileNames({ name, plural, isNested = true, isGrouped = false, namingConfig = null }) {
403
+ // Use config-driven naming if available, otherwise use legacy hardcoded values
404
+ const config = namingConfig || getNamingConfig();
405
+ const opts = { isNested, plural };
406
+
407
+ // Base file names (always computed for import path generation)
408
+ const baseNames = {
409
+ entity: computeFileName(name, 'entity', config, opts),
410
+ repositoryInterface: computeFileName(name, 'repositoryInterface', config, opts),
411
+ repository: computeFileName(name, 'repository', config, opts),
412
+ createCommand: computeFileName(name, 'command', config, { ...opts, action: 'create' }),
413
+ updateCommand: computeFileName(name, 'command', config, { ...opts, action: 'update' }),
414
+ deleteCommand: computeFileName(name, 'command', config, { ...opts, action: 'delete' }),
415
+ getByIdQuery: computeFileName(name, 'query', config, { ...opts, action: 'get-by-id' }),
416
+ listQuery: computeFileName(plural, 'query', config, { ...opts, action: 'list' }),
417
+ dto: computeFileName(name, 'dto', config, opts),
418
+ controller: computeFileName(name, 'controller', config, { ...opts, plural }),
419
+ module: computeFileName(name, 'module', config, { ...opts, plural }),
420
+ schema: computeFileName(name, 'schema', config, opts),
421
+ };
422
+
423
+ // When grouped, add index file names for combined output
424
+ if (isGrouped) {
425
+ return {
426
+ ...baseNames,
427
+ // Grouped output files (used by grouped-index.ejs.t templates)
428
+ domainIndex: "index.ts", // Contains entity + repository interface
429
+ commandsIndex: "index.ts", // Contains all commands
430
+ queriesIndex: "index.ts", // Contains all queries
431
+ // Flag for templates
432
+ isGrouped: true,
433
+ };
434
+ }
435
+
436
+ // Separate mode - just the base names
437
+ return {
438
+ ...baseNames,
439
+ isGrouped: false,
440
+ };
441
+ }
442
+
443
+ /**
444
+ * Get layout configuration from entity definition
445
+ *
446
+ * @param {Object} entity - Entity definition from YAML
447
+ * @returns {Object} Layout configuration
448
+ */
449
+ export function getLayoutConfig(entity) {
450
+ const folderStructure = entity.folder_structure || DEFAULT_LAYOUT.folderStructure;
451
+ const fileGrouping = entity.file_grouping || DEFAULT_LAYOUT.fileGrouping;
452
+
453
+ return {
454
+ folderStructure,
455
+ fileGrouping,
456
+ isNested: folderStructure === FOLDER_STRUCTURES.nested,
457
+ isGrouped: fileGrouping === FILE_GROUPINGS.grouped,
458
+ };
459
+ }
460
+
461
+ /**
462
+ * Import path helpers
463
+ * Calculate relative import paths from one location to another
464
+ */
465
+ export function getImportPaths({ isNested }) {
466
+ const commandDir = (name) => joinPath(BACKEND_LAYERS.commands, isNested ? name : "");
467
+ const queryDir = (name) => joinPath(BACKEND_LAYERS.queries, isNested ? name : "");
468
+ const controllerDir = BACKEND_LAYERS.controllers;
469
+ const moduleDir = BACKEND_LAYERS.modules;
470
+ const repositoriesDir = BACKEND_LAYERS.repositories;
471
+ const appModuleDir = ".";
472
+ // Database module location - use locations config if available, otherwise fall back to sibling of drizzle
473
+ const databaseModuleBase = LOCATIONS.backendDatabaseModule?.path
474
+ ? posixPath.relative(BASE_PATHS.backendSrc, LOCATIONS.backendDatabaseModule.path)
475
+ : posixPath.dirname(BACKEND_LAYERS.drizzle);
476
+ const databaseModulePath = joinPath(databaseModuleBase, "database.module");
477
+ // Constants token path (not barrel export)
478
+ const constantsTokenPath = joinPath(BACKEND_LAYERS.constants, "tokens");
479
+
480
+ return {
481
+ // From commands/queries to other locations
482
+ constants: (name) => relativeImport(commandDir(name), BACKEND_LAYERS.constants),
483
+ domain: (name) => relativeImport(commandDir(name), BACKEND_LAYERS.domain),
484
+ schemas: (name) => relativeImport(commandDir(name), BACKEND_LAYERS.schemas),
485
+
486
+ // From domain to other domain files
487
+ domainEntity: (name) => `./${name}.entity`,
488
+
489
+ // From module to commands/queries
490
+ moduleToQuery: (name, queryFile) =>
491
+ relativeImport(moduleDir, joinPath(BACKEND_LAYERS.queries, isNested ? name : "", queryFile)),
492
+ moduleToCommand: (name, commandFile) =>
493
+ relativeImport(moduleDir, joinPath(BACKEND_LAYERS.commands, isNested ? name : "", commandFile)),
494
+
495
+ // From module to repositories/constants/database module
496
+ moduleToRepository: (repositoryFile) =>
497
+ relativeImport(moduleDir, joinPath(BACKEND_LAYERS.repositories, repositoryFile)),
498
+ moduleToConstants: () => relativeImport(moduleDir, constantsTokenPath),
499
+ moduleToDomain: () => relativeImport(moduleDir, BACKEND_LAYERS.domain),
500
+ moduleToDatabaseModule: () => relativeImport(moduleDir, databaseModulePath),
501
+ moduleToController: (controllerFile) =>
502
+ relativeImport(moduleDir, joinPath(BACKEND_LAYERS.controllers, controllerFile)),
503
+ // From module to the entity's DTO file (application/schemas/<entity>.dto).
504
+ // Consumed by OPENAPI-2 — generated modules register each DTO's Zod
505
+ // schema with the shared OpenApiRegistry at onModuleInit.
506
+ moduleToDto: (dtoFile) =>
507
+ relativeImport(moduleDir, joinPath(BACKEND_LAYERS.schemas, dtoFile)),
508
+
509
+ // From repository to constants (relative path)
510
+ repositoryToConstants: () => relativeImport(repositoriesDir, constantsTokenPath),
511
+
512
+ // From controller to commands/queries
513
+ controllerToQuery: (name, queryFile) =>
514
+ relativeImport(controllerDir, joinPath(BACKEND_LAYERS.queries, isNested ? name : "", queryFile)),
515
+ controllerToCommand: (name, commandFile) =>
516
+ relativeImport(controllerDir, joinPath(BACKEND_LAYERS.commands, isNested ? name : "", commandFile)),
517
+ controllerToSchemas: () => relativeImport(controllerDir, BACKEND_LAYERS.schemas),
518
+ controllerToDomain: () => relativeImport(controllerDir, BACKEND_LAYERS.domain),
519
+
520
+ // From app.module.ts (backend root) to modules
521
+ appModuleToModule: (moduleFile) =>
522
+ relativeImport(appModuleDir, joinPath(BACKEND_LAYERS.modules, moduleFile)),
523
+ appModuleToTrpcModule: (moduleFile) =>
524
+ relativeImport(appModuleDir, joinPath(BACKEND_LAYERS.modules, moduleFile)),
525
+
526
+ // From controller to Electric-related imports
527
+ controllerToAuthGuard: () => {
528
+ const authGuardPath = LOCATIONS.backendAuthGuard?.path
529
+ ? posixPath.relative(BASE_PATHS.backendSrc, LOCATIONS.backendAuthGuard.path)
530
+ : 'core/guards';
531
+ return relativeImport(controllerDir, authGuardPath);
532
+ },
533
+ controllerToCurrentUser: () => {
534
+ const currentUserPath = LOCATIONS.backendCurrentUserDecorator?.path
535
+ ? posixPath.relative(BASE_PATHS.backendSrc, LOCATIONS.backendCurrentUserDecorator.path)
536
+ : 'core/decorators';
537
+ return relativeImport(controllerDir, currentUserPath);
538
+ },
539
+ controllerToElectricService: () => {
540
+ const electricServicePath = LOCATIONS.backendElectricService?.path
541
+ ? posixPath.relative(BASE_PATHS.backendSrc, LOCATIONS.backendElectricService.path)
542
+ : 'core/services';
543
+ return relativeImport(controllerDir, electricServicePath);
544
+ },
545
+
546
+ // From module to Electric module
547
+ moduleToElectricModule: () => {
548
+ const electricModulePath = LOCATIONS.backendElectricModule?.path
549
+ ? posixPath.relative(BASE_PATHS.backendSrc, LOCATIONS.backendElectricModule.path)
550
+ : 'infrastructure/electric';
551
+ return relativeImport(moduleDir, electricModulePath);
552
+ },
553
+ };
554
+ }
555
+
556
+ /**
557
+ * Test configuration - paths for test runner
558
+ */
559
+ export const TEST_OUTPUT_PATHS = [
560
+ `${BASE_PATHS.backendSrc}/${BACKEND_LAYERS.domain}`,
561
+ `${BASE_PATHS.backendSrc}/${BACKEND_LAYERS.application}`,
562
+ `${BASE_PATHS.backendSrc}/${BACKEND_LAYERS.drizzle}`,
563
+ `${BASE_PATHS.backendSrc}/${BACKEND_LAYERS.repositories}`,
564
+ `${BASE_PATHS.backendSrc}/${BACKEND_LAYERS.modules}`,
565
+ `${BASE_PATHS.backendSrc}/${BACKEND_LAYERS.controllers}`,
566
+ `${BASE_PATHS.backendSrc}/${BACKEND_LAYERS.constants}/tokens.ts`,
567
+ `${BASE_PATHS.backendSrc}/app.module.ts`,
568
+ `${BASE_PATHS.frontendSrc}/${FRONTEND_LAYERS.collections}`,
569
+ `${BASE_PATHS.frontendSrc}/${FRONTEND_LAYERS.store}`,
570
+ `${BASE_PATHS.frontendSrc}/${FRONTEND_LAYERS.entityMetadata}`,
571
+ PACKAGE_PATHS.dbEntities,
572
+ ];
573
+
574
+ export const INJECTABLE_FILES = [
575
+ `${BASE_PATHS.backendSrc}/${BACKEND_LAYERS.domain}/index.ts`,
576
+ `${BASE_PATHS.backendSrc}/${BACKEND_LAYERS.schemas}/index.ts`,
577
+ `${BASE_PATHS.backendSrc}/${BACKEND_LAYERS.drizzle}/index.ts`,
578
+ `${BASE_PATHS.backendSrc}/${BACKEND_LAYERS.constants}/tokens.ts`,
579
+ `${BASE_PATHS.backendSrc}/app.module.ts`,
580
+ `${BASE_PATHS.frontendSrc}/${FRONTEND_LAYERS.collections}/index.ts`,
581
+ `${BASE_PATHS.frontendSrc}/${FRONTEND_LAYERS.collections}/collections.ts`,
582
+ `${BASE_PATHS.frontendSrc}/${FRONTEND_LAYERS.store}/index.ts`,
583
+ `${LOCATIONS.dbSchemaServer.path}`,
584
+ `${LOCATIONS.dbSchemaClient.path}`,
585
+ `${PACKAGE_PATHS.dbEntities}/index.ts`,
586
+ ];
587
+
588
+ // ============================================================================
589
+ // Database Configuration
590
+ // ============================================================================
591
+
592
+ /**
593
+ * Database configuration
594
+ * Default to postgres for backward compatibility
595
+ */
596
+ export const DATABASE_CONFIG = {
597
+ dialect: projectConfig?.database?.dialect ?? 'postgres',
598
+ };
599
+
600
+ /**
601
+ * Get database dialect from config
602
+ */
603
+ export function getDatabaseDialect() {
604
+ return DATABASE_CONFIG.dialect;
605
+ }
606
+
607
+ /**
608
+ * Get project configuration (useful for template access)
609
+ */
610
+ export function getProjectConfig() {
611
+ return projectConfig;
612
+ }
613
+
614
+ /**
615
+ * Get pipelines configuration from project config.
616
+ * Returns the pipelines block (or an empty object if not configured).
617
+ *
618
+ * Usage:
619
+ * const pipelines = getPipelinesConfig();
620
+ * const arch = pipelines?.backend?.architecture ?? 'clean';
621
+ */
622
+ export function getPipelinesConfig() {
623
+ return projectConfig?.pipelines ?? {};
624
+ }
625
+
626
+ /**
627
+ * Resolve the directory codegen writes barrel files to (modules.ts, schema.ts).
628
+ *
629
+ * Honors `paths.generated` from codegen.config.yaml; defaults to 'src/generated'.
630
+ * Returns a relative path (from project root) — callers are responsible for
631
+ * resolving against a cwd as needed.
632
+ */
633
+ export function getGeneratedDir() {
634
+ const fromConfig = projectConfig?.paths?.generated;
635
+ return typeof fromConfig === 'string' && fromConfig.length > 0
636
+ ? fromConfig
637
+ : 'src/generated';
638
+ }
639
+
640
+ /**
641
+ * Get the `generate` block with defaults applied for the two top-level
642
+ * pipeline switches validated by GenerateConfigSchema.
643
+ *
644
+ * Returns an object that always includes `architecture` and `frontend`
645
+ * alongside any other user-supplied toggles.
646
+ */
647
+ export function getGenerateConfig() {
648
+ const raw = projectConfig?.generate ?? {};
649
+ return {
650
+ ...raw,
651
+ architecture: raw.architecture ?? 'clean',
652
+ frontend: raw.frontend ?? false,
653
+ };
654
+ }
655
+
656
+ // Default export for convenience
657
+ export default {
658
+ // Layout options
659
+ FOLDER_STRUCTURES,
660
+ FILE_GROUPINGS,
661
+ DEFAULT_LAYOUT,
662
+ // Path configuration
663
+ BASE_PATHS,
664
+ BACKEND_LAYERS,
665
+ FRONTEND_LAYERS,
666
+ PACKAGE_PATHS,
667
+ FILE_NAMING,
668
+ TEST_OUTPUT_PATHS,
669
+ INJECTABLE_FILES,
670
+ // NEW: Unified locations (path + import)
671
+ LOCATIONS,
672
+ // Database configuration
673
+ DATABASE_CONFIG,
674
+ // Helper functions
675
+ getBackendPath,
676
+ getOrchestrationPath,
677
+ getFrontendPath,
678
+ getEntityPaths,
679
+ getEntityFileNames,
680
+ getImportPaths,
681
+ getLayoutConfig,
682
+ getDatabaseDialect,
683
+ getProjectConfig,
684
+ getPipelinesConfig,
685
+ getGenerateConfig,
686
+ getGeneratedDir,
687
+ // NEW: Config-driven naming functions
688
+ computeFileName,
689
+ computeFileNaming,
690
+ };