@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,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
|
+
};
|