@memberjunction/metadata-sync 2.50.0 → 2.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +423 -2
  2. package/dist/commands/file-reset/index.d.ts +15 -0
  3. package/dist/commands/file-reset/index.js +221 -0
  4. package/dist/commands/file-reset/index.js.map +1 -0
  5. package/dist/commands/pull/index.d.ts +1 -0
  6. package/dist/commands/pull/index.js +82 -10
  7. package/dist/commands/pull/index.js.map +1 -1
  8. package/dist/commands/push/index.d.ts +21 -0
  9. package/dist/commands/push/index.js +589 -45
  10. package/dist/commands/push/index.js.map +1 -1
  11. package/dist/commands/validate/index.d.ts +15 -0
  12. package/dist/commands/validate/index.js +149 -0
  13. package/dist/commands/validate/index.js.map +1 -0
  14. package/dist/commands/watch/index.js +39 -1
  15. package/dist/commands/watch/index.js.map +1 -1
  16. package/dist/config.d.ts +7 -0
  17. package/dist/config.js.map +1 -1
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.js +5 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/lib/file-backup-manager.d.ts +90 -0
  22. package/dist/lib/file-backup-manager.js +186 -0
  23. package/dist/lib/file-backup-manager.js.map +1 -0
  24. package/dist/lib/provider-utils.d.ts +2 -2
  25. package/dist/lib/provider-utils.js +3 -4
  26. package/dist/lib/provider-utils.js.map +1 -1
  27. package/dist/lib/sync-engine.js +29 -3
  28. package/dist/lib/sync-engine.js.map +1 -1
  29. package/dist/services/FormattingService.d.ts +45 -0
  30. package/dist/services/FormattingService.js +564 -0
  31. package/dist/services/FormattingService.js.map +1 -0
  32. package/dist/services/ValidationService.d.ts +110 -0
  33. package/dist/services/ValidationService.js +737 -0
  34. package/dist/services/ValidationService.js.map +1 -0
  35. package/dist/types/validation.d.ts +98 -0
  36. package/dist/types/validation.js +97 -0
  37. package/dist/types/validation.js.map +1 -0
  38. package/oclif.manifest.json +205 -39
  39. package/package.json +7 -7
@@ -0,0 +1,737 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.ValidationService = void 0;
27
+ const core_1 = require("@memberjunction/core");
28
+ const fs = __importStar(require("fs"));
29
+ const path = __importStar(require("path"));
30
+ class ValidationService {
31
+ metadata;
32
+ errors = [];
33
+ warnings = [];
34
+ entityDependencies = new Map();
35
+ processedEntities = new Set();
36
+ options;
37
+ constructor(options = {}) {
38
+ this.metadata = new core_1.Metadata();
39
+ this.options = {
40
+ verbose: false,
41
+ outputFormat: 'human',
42
+ maxNestingDepth: 10,
43
+ checkBestPractices: true,
44
+ ...options
45
+ };
46
+ }
47
+ /**
48
+ * Validates all metadata files in the specified directory
49
+ */
50
+ async validateDirectory(dir) {
51
+ this.reset();
52
+ const configPath = path.join(dir, '.mj-sync.json');
53
+ if (!fs.existsSync(configPath)) {
54
+ this.addError({
55
+ type: 'entity',
56
+ severity: 'error',
57
+ file: dir,
58
+ message: 'No .mj-sync.json configuration file found in directory'
59
+ });
60
+ return this.getResult();
61
+ }
62
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
63
+ const directories = await this.getDirectoriesInOrder(dir, config);
64
+ let totalFiles = 0;
65
+ let totalEntities = 0;
66
+ const fileResults = new Map();
67
+ for (const subDir of directories) {
68
+ const subDirPath = path.join(dir, subDir);
69
+ const result = await this.validateEntityDirectory(subDirPath);
70
+ if (result) {
71
+ totalFiles += result.files;
72
+ totalEntities += result.entities;
73
+ for (const [file, fileResult] of result.fileResults) {
74
+ fileResults.set(file, fileResult);
75
+ }
76
+ }
77
+ }
78
+ // Validate dependency order
79
+ await this.validateDependencyOrder(directories);
80
+ return {
81
+ isValid: this.errors.length === 0,
82
+ errors: this.errors,
83
+ warnings: this.warnings,
84
+ summary: {
85
+ totalFiles,
86
+ totalEntities,
87
+ totalErrors: this.errors.length,
88
+ totalWarnings: this.warnings.length,
89
+ fileResults
90
+ }
91
+ };
92
+ }
93
+ /**
94
+ * Validates a single entity directory
95
+ */
96
+ async validateEntityDirectory(dir) {
97
+ const configPath = path.join(dir, '.mj-sync.json');
98
+ if (!fs.existsSync(configPath)) {
99
+ return null;
100
+ }
101
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
102
+ const entityInfo = this.metadata.EntityByName(config.entity);
103
+ if (!entityInfo) {
104
+ this.addError({
105
+ type: 'entity',
106
+ severity: 'error',
107
+ file: configPath,
108
+ message: `Entity "${config.entity}" not found in metadata`
109
+ });
110
+ return null;
111
+ }
112
+ const files = await this.getMatchingFiles(dir, config.filePattern);
113
+ let totalEntities = 0;
114
+ const fileResults = new Map();
115
+ for (const file of files) {
116
+ const filePath = path.join(dir, file);
117
+ const result = await this.validateFile(filePath, entityInfo, config);
118
+ totalEntities += result.entityCount;
119
+ fileResults.set(filePath, result);
120
+ }
121
+ return { files: files.length, entities: totalEntities, fileResults };
122
+ }
123
+ /**
124
+ * Validates a single metadata file
125
+ */
126
+ async validateFile(filePath, entityInfo, config) {
127
+ const fileErrors = [];
128
+ const fileWarnings = [];
129
+ let entityCount = 0;
130
+ try {
131
+ const content = fs.readFileSync(filePath, 'utf8');
132
+ const data = JSON.parse(content);
133
+ const entities = Array.isArray(data) ? data : [data];
134
+ entityCount = entities.length;
135
+ for (const entityData of entities) {
136
+ await this.validateEntityData(entityData, entityInfo, filePath, config);
137
+ }
138
+ }
139
+ catch (error) {
140
+ fileErrors.push({
141
+ type: 'entity',
142
+ severity: 'error',
143
+ file: filePath,
144
+ message: `Failed to parse JSON: ${error instanceof Error ? error.message : String(error)}`
145
+ });
146
+ }
147
+ // Collect errors and warnings for this file
148
+ const currentFileErrors = this.errors.filter(e => e.file === filePath);
149
+ const currentFileWarnings = this.warnings.filter(w => w.file === filePath);
150
+ return {
151
+ file: filePath,
152
+ entityCount,
153
+ errors: currentFileErrors,
154
+ warnings: currentFileWarnings
155
+ };
156
+ }
157
+ /**
158
+ * Validates a single entity data object
159
+ */
160
+ async validateEntityData(entityData, entityInfo, filePath, config, parentContext, depth = 0) {
161
+ // Check nesting depth
162
+ if (depth > this.options.maxNestingDepth) {
163
+ this.addWarning({
164
+ type: 'nesting',
165
+ severity: 'warning',
166
+ entity: entityInfo.Name,
167
+ file: filePath,
168
+ message: `Nesting depth ${depth} exceeds recommended maximum of ${this.options.maxNestingDepth}`,
169
+ suggestion: 'Consider flattening the data structure or increasing maxNestingDepth'
170
+ });
171
+ }
172
+ // Validate fields
173
+ if (entityData.fields) {
174
+ await this.validateFields(entityData.fields, entityInfo, filePath, parentContext);
175
+ }
176
+ // Track dependencies
177
+ this.trackEntityDependencies(entityData, entityInfo.Name, filePath);
178
+ // Validate related entities
179
+ if (entityData.relatedEntities) {
180
+ for (const [relatedEntityName, relatedData] of Object.entries(entityData.relatedEntities)) {
181
+ const relatedEntityInfo = this.metadata.EntityByName(relatedEntityName);
182
+ if (!relatedEntityInfo) {
183
+ this.addError({
184
+ type: 'entity',
185
+ severity: 'error',
186
+ entity: entityInfo.Name,
187
+ file: filePath,
188
+ message: `Related entity "${relatedEntityName}" not found in metadata`
189
+ });
190
+ continue;
191
+ }
192
+ const relatedEntities = Array.isArray(relatedData) ? relatedData : [relatedData];
193
+ for (const relatedEntity of relatedEntities) {
194
+ await this.validateEntityData(relatedEntity, relatedEntityInfo, filePath, config, { entity: entityInfo.Name, field: relatedEntityName }, depth + 1);
195
+ }
196
+ }
197
+ }
198
+ }
199
+ /**
200
+ * Validates entity fields
201
+ */
202
+ async validateFields(fields, entityInfo, filePath, parentContext) {
203
+ const entityFields = entityInfo.Fields;
204
+ const fieldMap = new Map(entityFields.map((f) => [f.Name, f]));
205
+ for (const [fieldName, fieldValue] of Object.entries(fields)) {
206
+ const fieldInfo = fieldMap.get(fieldName);
207
+ if (!fieldInfo) {
208
+ // Check if this might be a virtual property (getter/setter)
209
+ try {
210
+ const entityInstance = await this.metadata.GetEntityObject(entityInfo.Name);
211
+ const hasProperty = fieldName in entityInstance;
212
+ if (!hasProperty) {
213
+ this.addError({
214
+ type: 'field',
215
+ severity: 'error',
216
+ entity: entityInfo.Name,
217
+ field: fieldName,
218
+ file: filePath,
219
+ message: `Field "${fieldName}" does not exist on entity "${entityInfo.Name}"`
220
+ });
221
+ continue;
222
+ }
223
+ // It's a virtual property, validate the value
224
+ await this.validateFieldValue(fieldValue, { Name: fieldName }, entityInfo, filePath, parentContext);
225
+ continue;
226
+ }
227
+ catch (error) {
228
+ // If we can't create an entity instance, fall back to error
229
+ this.addError({
230
+ type: 'field',
231
+ severity: 'error',
232
+ entity: entityInfo.Name,
233
+ field: fieldName,
234
+ file: filePath,
235
+ message: `Field "${fieldName}" does not exist on entity "${entityInfo.Name}"`
236
+ });
237
+ continue;
238
+ }
239
+ }
240
+ // Check if field is settable (not system field)
241
+ if (fieldInfo.IsSystemField || fieldName.startsWith('__mj_')) {
242
+ this.addError({
243
+ type: 'field',
244
+ severity: 'error',
245
+ entity: entityInfo.Name,
246
+ field: fieldName,
247
+ file: filePath,
248
+ message: `Field "${fieldName}" is a system field and cannot be set`,
249
+ suggestion: 'Remove this field from your metadata file'
250
+ });
251
+ continue;
252
+ }
253
+ // Validate field value and references
254
+ await this.validateFieldValue(fieldValue, fieldInfo, entityInfo, filePath, parentContext);
255
+ }
256
+ // Check for required fields
257
+ if (this.options.checkBestPractices) {
258
+ for (const field of entityFields) {
259
+ // Skip if field allows null or has a value already
260
+ if (field.AllowsNull || fields[field.Name]) {
261
+ continue;
262
+ }
263
+ // Skip if field has a default value
264
+ if (field.DefaultValue !== null && field.DefaultValue !== undefined) {
265
+ continue;
266
+ }
267
+ // Skip virtual/computed fields (foreign key reference fields)
268
+ // These are typically named without 'ID' suffix but have a corresponding FK field
269
+ const relatedEntityField = field.RelatedEntity;
270
+ const correspondingFKField = entityFields.find((f) => f.Name === field.Name + 'ID' && f.IsForeignKey);
271
+ if (relatedEntityField && correspondingFKField) {
272
+ continue;
273
+ }
274
+ // Skip fields that are marked as AutoUpdateOnly or ReadOnly
275
+ if (field.AutoUpdateOnly || field.ReadOnly) {
276
+ continue;
277
+ }
278
+ // Skip if this is a parent context and the field can be inherited
279
+ if (parentContext && (field.Name === parentContext.field || field.Name === parentContext.field + 'ID')) {
280
+ continue;
281
+ }
282
+ // Special case: Skip TemplateID if TemplateText is provided (virtual property)
283
+ if (field.Name === 'TemplateID' && fields['TemplateText']) {
284
+ continue;
285
+ }
286
+ // Skip Template field if TemplateText is provided
287
+ if (field.Name === 'Template' && fields['TemplateText']) {
288
+ continue;
289
+ }
290
+ this.addWarning({
291
+ type: 'bestpractice',
292
+ severity: 'warning',
293
+ entity: entityInfo.Name,
294
+ field: field.Name,
295
+ file: filePath,
296
+ message: `Required field "${field.Name}" is missing`,
297
+ suggestion: `Add "${field.Name}" to the fields object`
298
+ });
299
+ }
300
+ }
301
+ }
302
+ /**
303
+ * Validates field values and references
304
+ */
305
+ async validateFieldValue(value, fieldInfo, entityInfo, filePath, parentContext) {
306
+ if (typeof value === 'string' && value.startsWith('@')) {
307
+ await this.validateReference(value, fieldInfo, entityInfo, filePath, parentContext);
308
+ }
309
+ // Add type validation here if needed
310
+ }
311
+ /**
312
+ * Validates special references (@file:, @lookup:, etc.)
313
+ */
314
+ async validateReference(reference, fieldInfo, entityInfo, filePath, parentContext) {
315
+ const parsed = this.parseReference(reference);
316
+ if (!parsed) {
317
+ this.addError({
318
+ type: 'reference',
319
+ severity: 'error',
320
+ entity: entityInfo.Name,
321
+ field: fieldInfo.Name,
322
+ file: filePath,
323
+ message: `Invalid reference format: "${reference}"`
324
+ });
325
+ return;
326
+ }
327
+ switch (parsed.type) {
328
+ case '@file:':
329
+ await this.validateFileReference(parsed.value, filePath, entityInfo.Name, fieldInfo.Name);
330
+ break;
331
+ case '@lookup:':
332
+ await this.validateLookupReference(parsed, filePath, entityInfo.Name, fieldInfo.Name);
333
+ break;
334
+ case '@template:':
335
+ await this.validateTemplateReference(parsed.value, filePath, entityInfo.Name, fieldInfo.Name);
336
+ break;
337
+ case '@parent:':
338
+ this.validateParentReference(parsed.value, parentContext, filePath, entityInfo.Name, fieldInfo.Name);
339
+ break;
340
+ case '@root:':
341
+ this.validateRootReference(parsed.value, parentContext, filePath, entityInfo.Name, fieldInfo.Name);
342
+ break;
343
+ }
344
+ }
345
+ /**
346
+ * Parses a reference string
347
+ */
348
+ parseReference(reference) {
349
+ const patterns = [
350
+ ['@file:', /^@file:(.+)$/],
351
+ ['@lookup:', /^@lookup:([^.]+)\.([^=]+)=(.+)$/],
352
+ ['@template:', /^@template:(.+)$/],
353
+ ['@parent:', /^@parent:(.+)$/],
354
+ ['@root:', /^@root:(.+)$/],
355
+ ['@env:', /^@env:(.+)$/]
356
+ ];
357
+ for (const [type, pattern] of patterns) {
358
+ const match = reference.match(pattern);
359
+ if (match) {
360
+ if (type === '@lookup:') {
361
+ const [, entity, field, valueAndOptions] = match;
362
+ const [value, ...options] = valueAndOptions.split('?');
363
+ const createIfMissing = options.includes('create');
364
+ const additionalFields = {};
365
+ for (const option of options) {
366
+ if (option.includes('&')) {
367
+ const pairs = option.split('&');
368
+ for (const pair of pairs) {
369
+ const [key, val] = pair.split('=');
370
+ if (key && val && key !== 'create') {
371
+ additionalFields[key] = val;
372
+ }
373
+ }
374
+ }
375
+ }
376
+ return { type, value, entity, field, createIfMissing, additionalFields };
377
+ }
378
+ return { type, value: match[1] };
379
+ }
380
+ }
381
+ return null;
382
+ }
383
+ /**
384
+ * Validates @file: references
385
+ */
386
+ async validateFileReference(filePath, sourceFile, entityName, fieldName) {
387
+ const dir = path.dirname(sourceFile);
388
+ const resolvedPath = path.resolve(dir, filePath);
389
+ if (!fs.existsSync(resolvedPath)) {
390
+ this.addError({
391
+ type: 'reference',
392
+ severity: 'error',
393
+ entity: entityName,
394
+ field: fieldName,
395
+ file: sourceFile,
396
+ message: `File reference not found: "${filePath}"`,
397
+ suggestion: `Create file at: ${resolvedPath}`
398
+ });
399
+ }
400
+ }
401
+ /**
402
+ * Validates @lookup: references
403
+ */
404
+ async validateLookupReference(parsed, sourceFile, entityName, fieldName) {
405
+ const lookupEntity = this.metadata.EntityByName(parsed.entity);
406
+ if (!lookupEntity) {
407
+ this.addError({
408
+ type: 'reference',
409
+ severity: 'error',
410
+ entity: entityName,
411
+ field: fieldName,
412
+ file: sourceFile,
413
+ message: `Lookup entity "${parsed.entity}" not found`,
414
+ suggestion: 'Check entity name spelling and case'
415
+ });
416
+ return;
417
+ }
418
+ const lookupField = lookupEntity.Fields.find((f) => f.Name === parsed.field);
419
+ if (!lookupField) {
420
+ this.addError({
421
+ type: 'reference',
422
+ severity: 'error',
423
+ entity: entityName,
424
+ field: fieldName,
425
+ file: sourceFile,
426
+ message: `Lookup field "${parsed.field}" not found on entity "${parsed.entity}"`,
427
+ suggestion: `Available fields: ${lookupEntity.Fields.map((f) => f.Name).join(', ')}`
428
+ });
429
+ }
430
+ // Track dependency
431
+ this.addEntityDependency(entityName, parsed.entity);
432
+ }
433
+ /**
434
+ * Validates @template: references
435
+ */
436
+ async validateTemplateReference(templatePath, sourceFile, entityName, fieldName) {
437
+ const dir = path.dirname(sourceFile);
438
+ const resolvedPath = path.resolve(dir, templatePath);
439
+ if (!fs.existsSync(resolvedPath)) {
440
+ this.addError({
441
+ type: 'reference',
442
+ severity: 'error',
443
+ entity: entityName,
444
+ field: fieldName,
445
+ file: sourceFile,
446
+ message: `Template file not found: "${templatePath}"`,
447
+ suggestion: `Create template at: ${resolvedPath}`
448
+ });
449
+ return;
450
+ }
451
+ // Validate template is valid JSON
452
+ try {
453
+ JSON.parse(fs.readFileSync(resolvedPath, 'utf8'));
454
+ }
455
+ catch (error) {
456
+ this.addError({
457
+ type: 'reference',
458
+ severity: 'error',
459
+ entity: entityName,
460
+ field: fieldName,
461
+ file: sourceFile,
462
+ message: `Template file is not valid JSON: "${templatePath}"`,
463
+ details: error instanceof Error ? error.message : String(error)
464
+ });
465
+ }
466
+ }
467
+ /**
468
+ * Validates @parent: references
469
+ */
470
+ validateParentReference(fieldName, parentContext, sourceFile, entityName, currentFieldName) {
471
+ if (!parentContext) {
472
+ this.addError({
473
+ type: 'reference',
474
+ severity: 'error',
475
+ entity: entityName,
476
+ field: currentFieldName,
477
+ file: sourceFile,
478
+ message: `@parent: reference used but no parent context exists`,
479
+ suggestion: '@parent: can only be used in nested/related entities'
480
+ });
481
+ }
482
+ }
483
+ /**
484
+ * Validates @root: references
485
+ */
486
+ validateRootReference(fieldName, parentContext, sourceFile, entityName, currentFieldName) {
487
+ if (!parentContext) {
488
+ this.addError({
489
+ type: 'reference',
490
+ severity: 'error',
491
+ entity: entityName,
492
+ field: currentFieldName,
493
+ file: sourceFile,
494
+ message: `@root: reference used but no root context exists`,
495
+ suggestion: '@root: can only be used in nested/related entities'
496
+ });
497
+ }
498
+ }
499
+ /**
500
+ * Track entity dependencies
501
+ */
502
+ trackEntityDependencies(entityData, entityName, filePath) {
503
+ if (!this.entityDependencies.has(entityName)) {
504
+ this.entityDependencies.set(entityName, {
505
+ entityName,
506
+ dependsOn: new Set(),
507
+ file: filePath
508
+ });
509
+ }
510
+ // Track dependencies from lookups in fields
511
+ if (entityData.fields) {
512
+ for (const value of Object.values(entityData.fields)) {
513
+ if (typeof value === 'string' && value.startsWith('@lookup:')) {
514
+ const parsed = this.parseReference(value);
515
+ if (parsed?.entity) {
516
+ this.addEntityDependency(entityName, parsed.entity);
517
+ }
518
+ }
519
+ }
520
+ }
521
+ }
522
+ /**
523
+ * Add an entity dependency
524
+ */
525
+ addEntityDependency(from, to) {
526
+ if (!this.entityDependencies.has(from)) {
527
+ this.entityDependencies.set(from, {
528
+ entityName: from,
529
+ dependsOn: new Set(),
530
+ file: ''
531
+ });
532
+ }
533
+ this.entityDependencies.get(from).dependsOn.add(to);
534
+ }
535
+ /**
536
+ * Validates dependency order
537
+ */
538
+ async validateDependencyOrder(directoryOrder) {
539
+ // Build a map of entity to directory
540
+ const entityToDirectory = new Map();
541
+ for (const dir of directoryOrder) {
542
+ // This is simplified - in reality we'd need to read the .mj-sync.json
543
+ // to get the actual entity name
544
+ entityToDirectory.set(dir, dir);
545
+ }
546
+ // Check for circular dependencies
547
+ const visited = new Set();
548
+ const recursionStack = new Set();
549
+ for (const [entity, deps] of this.entityDependencies) {
550
+ if (!visited.has(entity)) {
551
+ this.checkCircularDependency(entity, visited, recursionStack);
552
+ }
553
+ }
554
+ // Check if current order satisfies dependencies
555
+ const orderViolations = this.checkDependencyOrder(directoryOrder);
556
+ if (orderViolations.length > 0) {
557
+ // Suggest a corrected order
558
+ const suggestedOrder = this.topologicalSort();
559
+ for (const violation of orderViolations) {
560
+ this.addError({
561
+ type: 'dependency',
562
+ severity: 'error',
563
+ entity: violation.entity,
564
+ file: violation.file,
565
+ message: `Entity '${violation.entity}' depends on '${violation.dependency}' but is processed before it`,
566
+ suggestion: `Reorder directories to: [${suggestedOrder.join(', ')}]`
567
+ });
568
+ }
569
+ }
570
+ }
571
+ /**
572
+ * Check for circular dependencies
573
+ */
574
+ checkCircularDependency(entity, visited, recursionStack, path = []) {
575
+ visited.add(entity);
576
+ recursionStack.add(entity);
577
+ path.push(entity);
578
+ const deps = this.entityDependencies.get(entity);
579
+ if (deps) {
580
+ for (const dep of deps.dependsOn) {
581
+ if (!visited.has(dep)) {
582
+ if (this.checkCircularDependency(dep, visited, recursionStack, [...path])) {
583
+ return true;
584
+ }
585
+ }
586
+ else if (recursionStack.has(dep)) {
587
+ // Found circular dependency
588
+ const cycle = [...path, dep];
589
+ const cycleStart = cycle.indexOf(dep);
590
+ const cyclePath = cycle.slice(cycleStart).join(' → ');
591
+ this.addError({
592
+ type: 'circular',
593
+ severity: 'error',
594
+ entity: entity,
595
+ file: deps.file,
596
+ message: `Circular dependency detected: ${cyclePath}`,
597
+ suggestion: 'Restructure your entities to avoid circular references'
598
+ });
599
+ return true;
600
+ }
601
+ }
602
+ }
603
+ recursionStack.delete(entity);
604
+ return false;
605
+ }
606
+ /**
607
+ * Get directories in order based on config
608
+ */
609
+ async getDirectoriesInOrder(rootDir, config) {
610
+ const allDirs = fs.readdirSync(rootDir)
611
+ .filter(f => fs.statSync(path.join(rootDir, f)).isDirectory())
612
+ .filter(d => !d.startsWith('.'));
613
+ if (config.directoryOrder && Array.isArray(config.directoryOrder)) {
614
+ const ordered = config.directoryOrder.filter((d) => allDirs.includes(d));
615
+ const remaining = allDirs.filter(d => !ordered.includes(d)).sort();
616
+ return [...ordered, ...remaining];
617
+ }
618
+ return allDirs.sort();
619
+ }
620
+ /**
621
+ * Get files matching pattern
622
+ */
623
+ async getMatchingFiles(dir, pattern) {
624
+ const files = fs.readdirSync(dir)
625
+ .filter(f => fs.statSync(path.join(dir, f)).isFile());
626
+ // Simple glob pattern matching
627
+ if (pattern === '*.json') {
628
+ return files.filter(f => f.endsWith('.json') && !f.startsWith('.mj-'));
629
+ }
630
+ else if (pattern === '.*.json') {
631
+ return files.filter(f => f.startsWith('.') && f.endsWith('.json') && !f.startsWith('.mj-'));
632
+ }
633
+ return files;
634
+ }
635
+ /**
636
+ * Add an error
637
+ */
638
+ addError(error) {
639
+ this.errors.push(error);
640
+ }
641
+ /**
642
+ * Add a warning
643
+ */
644
+ addWarning(warning) {
645
+ this.warnings.push(warning);
646
+ }
647
+ /**
648
+ * Check if current directory order satisfies dependencies
649
+ */
650
+ checkDependencyOrder(directoryOrder) {
651
+ const violations = [];
652
+ const processedEntities = new Set();
653
+ for (const dir of directoryOrder) {
654
+ // In real implementation, we'd read .mj-sync.json to get entity name
655
+ const entityName = dir; // Simplified for now
656
+ const deps = this.entityDependencies.get(entityName);
657
+ if (deps) {
658
+ for (const dep of deps.dependsOn) {
659
+ if (!processedEntities.has(dep) && directoryOrder.includes(dep)) {
660
+ violations.push({
661
+ entity: entityName,
662
+ dependency: dep,
663
+ file: deps.file
664
+ });
665
+ }
666
+ }
667
+ }
668
+ processedEntities.add(entityName);
669
+ }
670
+ return violations;
671
+ }
672
+ /**
673
+ * Perform topological sort on entity dependencies
674
+ */
675
+ topologicalSort() {
676
+ const result = [];
677
+ const visited = new Set();
678
+ const tempStack = new Set();
679
+ const visit = (entity) => {
680
+ if (tempStack.has(entity)) {
681
+ // Circular dependency, already handled by checkCircularDependency
682
+ return false;
683
+ }
684
+ if (visited.has(entity)) {
685
+ return true;
686
+ }
687
+ tempStack.add(entity);
688
+ const deps = this.entityDependencies.get(entity);
689
+ if (deps) {
690
+ for (const dep of deps.dependsOn) {
691
+ if (!visit(dep)) {
692
+ return false;
693
+ }
694
+ }
695
+ }
696
+ tempStack.delete(entity);
697
+ visited.add(entity);
698
+ result.push(entity);
699
+ return true;
700
+ };
701
+ // Visit all entities
702
+ for (const entity of this.entityDependencies.keys()) {
703
+ if (!visited.has(entity)) {
704
+ visit(entity);
705
+ }
706
+ }
707
+ return result;
708
+ }
709
+ /**
710
+ * Reset validation state
711
+ */
712
+ reset() {
713
+ this.errors = [];
714
+ this.warnings = [];
715
+ this.entityDependencies.clear();
716
+ this.processedEntities.clear();
717
+ }
718
+ /**
719
+ * Get validation result
720
+ */
721
+ getResult() {
722
+ return {
723
+ isValid: this.errors.length === 0,
724
+ errors: this.errors,
725
+ warnings: this.warnings,
726
+ summary: {
727
+ totalFiles: 0,
728
+ totalEntities: 0,
729
+ totalErrors: this.errors.length,
730
+ totalWarnings: this.warnings.length,
731
+ fileResults: new Map()
732
+ }
733
+ };
734
+ }
735
+ }
736
+ exports.ValidationService = ValidationService;
737
+ //# sourceMappingURL=ValidationService.js.map