@memberjunction/metadata-sync 2.52.0 → 2.54.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.
@@ -27,6 +27,7 @@ exports.ValidationService = void 0;
27
27
  const core_1 = require("@memberjunction/core");
28
28
  const fs = __importStar(require("fs"));
29
29
  const path = __importStar(require("path"));
30
+ const provider_utils_1 = require("../lib/provider-utils");
30
31
  class ValidationService {
31
32
  metadata;
32
33
  errors = [];
@@ -34,6 +35,7 @@ class ValidationService {
34
35
  entityDependencies = new Map();
35
36
  processedEntities = new Set();
36
37
  options;
38
+ userRoleCache = new Map();
37
39
  constructor(options = {}) {
38
40
  this.metadata = new core_1.Metadata();
39
41
  this.options = {
@@ -41,7 +43,7 @@ class ValidationService {
41
43
  outputFormat: 'human',
42
44
  maxNestingDepth: 10,
43
45
  checkBestPractices: true,
44
- ...options
46
+ ...options,
45
47
  };
46
48
  }
47
49
  /**
@@ -55,11 +57,15 @@ class ValidationService {
55
57
  type: 'entity',
56
58
  severity: 'error',
57
59
  file: dir,
58
- message: 'No .mj-sync.json configuration file found in directory'
60
+ message: 'No .mj-sync.json configuration file found in directory',
59
61
  });
60
62
  return this.getResult();
61
63
  }
62
64
  const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
65
+ // Load user role configuration and cache if enabled
66
+ if (config.userRoleValidation?.enabled) {
67
+ await this.loadUserRoles();
68
+ }
63
69
  const directories = await this.getDirectoriesInOrder(dir, config);
64
70
  let totalFiles = 0;
65
71
  let totalEntities = 0;
@@ -86,26 +92,56 @@ class ValidationService {
86
92
  totalEntities,
87
93
  totalErrors: this.errors.length,
88
94
  totalWarnings: this.warnings.length,
89
- fileResults
90
- }
95
+ fileResults,
96
+ },
91
97
  };
92
98
  }
93
99
  /**
94
100
  * Validates a single entity directory
95
101
  */
96
102
  async validateEntityDirectory(dir) {
97
- const configPath = path.join(dir, '.mj-sync.json');
98
- if (!fs.existsSync(configPath)) {
103
+ // Check for .mj-folder.json first (new format)
104
+ let configPath = path.join(dir, '.mj-folder.json');
105
+ let config;
106
+ if (fs.existsSync(configPath)) {
107
+ config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
108
+ // .mj-folder.json uses entityName field
109
+ if (!config.entityName) {
110
+ this.addError({
111
+ type: 'validation',
112
+ severity: 'error',
113
+ file: configPath,
114
+ message: 'Missing entityName field in .mj-folder.json',
115
+ });
116
+ return null;
117
+ }
118
+ config.entity = config.entityName; // Normalize to entity field
119
+ }
120
+ else {
121
+ // Fall back to .mj-sync.json (legacy format)
122
+ configPath = path.join(dir, '.mj-sync.json');
123
+ if (!fs.existsSync(configPath)) {
124
+ return null;
125
+ }
126
+ config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
127
+ }
128
+ // Validate entity name exists
129
+ if (!config.entity || config.entity.trim() === '') {
130
+ this.addError({
131
+ type: 'validation',
132
+ severity: 'error',
133
+ file: configPath,
134
+ message: 'Entity name is empty or missing',
135
+ });
99
136
  return null;
100
137
  }
101
- const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
102
138
  const entityInfo = this.metadata.EntityByName(config.entity);
103
139
  if (!entityInfo) {
104
140
  this.addError({
105
141
  type: 'entity',
106
142
  severity: 'error',
107
143
  file: configPath,
108
- message: `Entity "${config.entity}" not found in metadata`
144
+ message: `Entity "${config.entity}" not found in metadata`,
109
145
  });
110
146
  return null;
111
147
  }
@@ -125,7 +161,6 @@ class ValidationService {
125
161
  */
126
162
  async validateFile(filePath, entityInfo, config) {
127
163
  const fileErrors = [];
128
- const fileWarnings = [];
129
164
  let entityCount = 0;
130
165
  try {
131
166
  const content = fs.readFileSync(filePath, 'utf8');
@@ -141,17 +176,17 @@ class ValidationService {
141
176
  type: 'entity',
142
177
  severity: 'error',
143
178
  file: filePath,
144
- message: `Failed to parse JSON: ${error instanceof Error ? error.message : String(error)}`
179
+ message: `Failed to parse JSON: ${error instanceof Error ? error.message : String(error)}`,
145
180
  });
146
181
  }
147
182
  // 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);
183
+ const currentFileErrors = this.errors.filter((e) => e.file === filePath);
184
+ const currentFileWarnings = this.warnings.filter((w) => w.file === filePath);
150
185
  return {
151
186
  file: filePath,
152
187
  entityCount,
153
188
  errors: currentFileErrors,
154
- warnings: currentFileWarnings
189
+ warnings: currentFileWarnings,
155
190
  };
156
191
  }
157
192
  /**
@@ -166,7 +201,7 @@ class ValidationService {
166
201
  entity: entityInfo.Name,
167
202
  file: filePath,
168
203
  message: `Nesting depth ${depth} exceeds recommended maximum of ${this.options.maxNestingDepth}`,
169
- suggestion: 'Consider flattening the data structure or increasing maxNestingDepth'
204
+ suggestion: 'Consider flattening the data structure or increasing maxNestingDepth',
170
205
  });
171
206
  }
172
207
  // Validate fields
@@ -185,7 +220,7 @@ class ValidationService {
185
220
  severity: 'error',
186
221
  entity: entityInfo.Name,
187
222
  file: filePath,
188
- message: `Related entity "${relatedEntityName}" not found in metadata`
223
+ message: `Related entity "${relatedEntityName}" not found in metadata`,
189
224
  });
190
225
  continue;
191
226
  }
@@ -216,7 +251,7 @@ class ValidationService {
216
251
  entity: entityInfo.Name,
217
252
  field: fieldName,
218
253
  file: filePath,
219
- message: `Field "${fieldName}" does not exist on entity "${entityInfo.Name}"`
254
+ message: `Field "${fieldName}" does not exist on entity "${entityInfo.Name}"`,
220
255
  });
221
256
  continue;
222
257
  }
@@ -232,7 +267,7 @@ class ValidationService {
232
267
  entity: entityInfo.Name,
233
268
  field: fieldName,
234
269
  file: filePath,
235
- message: `Field "${fieldName}" does not exist on entity "${entityInfo.Name}"`
270
+ message: `Field "${fieldName}" does not exist on entity "${entityInfo.Name}"`,
236
271
  });
237
272
  continue;
238
273
  }
@@ -246,7 +281,7 @@ class ValidationService {
246
281
  field: fieldName,
247
282
  file: filePath,
248
283
  message: `Field "${fieldName}" is a system field and cannot be set`,
249
- suggestion: 'Remove this field from your metadata file'
284
+ suggestion: 'Remove this field from your metadata file',
250
285
  });
251
286
  continue;
252
287
  }
@@ -294,7 +329,7 @@ class ValidationService {
294
329
  field: field.Name,
295
330
  file: filePath,
296
331
  message: `Required field "${field.Name}" is missing`,
297
- suggestion: `Add "${field.Name}" to the fields object`
332
+ suggestion: `Add "${field.Name}" to the fields object`,
298
333
  });
299
334
  }
300
335
  }
@@ -306,7 +341,34 @@ class ValidationService {
306
341
  if (typeof value === 'string' && value.startsWith('@')) {
307
342
  await this.validateReference(value, fieldInfo, entityInfo, filePath, parentContext);
308
343
  }
309
- // Add type validation here if needed
344
+ // Validate UserID fields against allowed roles
345
+ if (fieldInfo.Name === 'UserID' && typeof value === 'string' && value.length > 0) {
346
+ // Get the sync config from the file's directory
347
+ const dir = path.dirname(filePath);
348
+ // Walk up to find the root sync config with userRoleValidation
349
+ let currentDir = dir;
350
+ let config = null;
351
+ while (currentDir && currentDir !== path.parse(currentDir).root) {
352
+ const currentConfigPath = path.join(currentDir, '.mj-sync.json');
353
+ if (fs.existsSync(currentConfigPath)) {
354
+ try {
355
+ const currentConfig = JSON.parse(fs.readFileSync(currentConfigPath, 'utf8'));
356
+ if (currentConfig.userRoleValidation) {
357
+ config = currentConfig;
358
+ break;
359
+ }
360
+ }
361
+ catch {
362
+ // Ignore parse errors
363
+ }
364
+ }
365
+ currentDir = path.dirname(currentDir);
366
+ }
367
+ if (config?.userRoleValidation?.enabled) {
368
+ await this.validateUserRole(value, entityInfo.Name, fieldInfo.Name, filePath, config);
369
+ }
370
+ }
371
+ // Add other type validation here if needed
310
372
  }
311
373
  /**
312
374
  * Validates special references (@file:, @lookup:, etc.)
@@ -320,7 +382,7 @@ class ValidationService {
320
382
  entity: entityInfo.Name,
321
383
  field: fieldInfo.Name,
322
384
  file: filePath,
323
- message: `Invalid reference format: "${reference}"`
385
+ message: `Invalid reference format: "${reference}"`,
324
386
  });
325
387
  return;
326
388
  }
@@ -348,32 +410,53 @@ class ValidationService {
348
410
  parseReference(reference) {
349
411
  const patterns = [
350
412
  ['@file:', /^@file:(.+)$/],
351
- ['@lookup:', /^@lookup:([^.]+)\.([^=]+)=(.+)$/],
413
+ ['@lookup:', /^@lookup:([^.]+)\.(.+)$/],
352
414
  ['@template:', /^@template:(.+)$/],
353
415
  ['@parent:', /^@parent:(.+)$/],
354
416
  ['@root:', /^@root:(.+)$/],
355
- ['@env:', /^@env:(.+)$/]
417
+ ['@env:', /^@env:(.+)$/],
356
418
  ];
357
419
  for (const [type, pattern] of patterns) {
358
420
  const match = reference.match(pattern);
359
421
  if (match) {
360
422
  if (type === '@lookup:') {
361
- const [, entity, field, valueAndOptions] = match;
362
- const [value, ...options] = valueAndOptions.split('?');
363
- const createIfMissing = options.includes('create');
423
+ const [, entity, remaining] = match;
424
+ // Check if this has ?create syntax
425
+ const hasCreate = remaining.includes('?create');
426
+ const lookupPart = hasCreate ? remaining.split('?')[0] : remaining;
427
+ // Parse all lookup fields (can be multiple with &)
428
+ const lookupPairs = lookupPart.split('&');
429
+ const fields = [];
430
+ for (const pair of lookupPairs) {
431
+ const fieldMatch = pair.match(/^(.+?)=(.+)$/);
432
+ if (fieldMatch) {
433
+ const [, field, value] = fieldMatch;
434
+ fields.push({ field: field.trim(), value: value.trim() });
435
+ }
436
+ }
437
+ // For backward compatibility, use the first field as primary
438
+ const primaryField = fields.length > 0 ? fields[0] : { field: '', value: '' };
439
+ // Parse additional fields for creation if ?create is present
364
440
  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
- }
441
+ if (hasCreate && remaining.includes('?create&')) {
442
+ const createPart = remaining.split('?create&')[1];
443
+ const pairs = createPart.split('&');
444
+ for (const pair of pairs) {
445
+ const [key, val] = pair.split('=');
446
+ if (key && val) {
447
+ additionalFields[key] = decodeURIComponent(val);
373
448
  }
374
449
  }
375
450
  }
376
- return { type, value, entity, field, createIfMissing, additionalFields };
451
+ return {
452
+ type,
453
+ value: primaryField.value,
454
+ entity,
455
+ field: primaryField.field,
456
+ fields, // Include all fields for validation
457
+ createIfMissing: hasCreate,
458
+ additionalFields
459
+ };
377
460
  }
378
461
  return { type, value: match[1] };
379
462
  }
@@ -394,7 +477,24 @@ class ValidationService {
394
477
  field: fieldName,
395
478
  file: sourceFile,
396
479
  message: `File reference not found: "${filePath}"`,
397
- suggestion: `Create file at: ${resolvedPath}`
480
+ suggestion: `Create file at: ${resolvedPath}`,
481
+ });
482
+ return;
483
+ }
484
+ // Read the file and check for {@include} references
485
+ try {
486
+ const content = fs.readFileSync(resolvedPath, 'utf-8');
487
+ await this.validateIncludeReferences(content, resolvedPath, new Set([resolvedPath]));
488
+ }
489
+ catch (error) {
490
+ this.addError({
491
+ type: 'reference',
492
+ severity: 'error',
493
+ entity: entityName,
494
+ field: fieldName,
495
+ file: sourceFile,
496
+ message: `Failed to read file reference: "${filePath}"`,
497
+ details: error instanceof Error ? error.message : String(error),
398
498
  });
399
499
  }
400
500
  }
@@ -411,21 +511,41 @@ class ValidationService {
411
511
  field: fieldName,
412
512
  file: sourceFile,
413
513
  message: `Lookup entity "${parsed.entity}" not found`,
414
- suggestion: 'Check entity name spelling and case'
514
+ suggestion: 'Check entity name spelling and case',
415
515
  });
416
516
  return;
417
517
  }
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
- });
518
+ // For multi-field lookups, validate all fields
519
+ if (parsed.fields && parsed.fields.length > 0) {
520
+ for (const { field } of parsed.fields) {
521
+ const lookupField = lookupEntity.Fields.find((f) => f.Name === field);
522
+ if (!lookupField) {
523
+ this.addError({
524
+ type: 'reference',
525
+ severity: 'error',
526
+ entity: entityName,
527
+ field: fieldName,
528
+ file: sourceFile,
529
+ message: `Lookup field "${field}" not found on entity "${parsed.entity}"`,
530
+ suggestion: `Available fields: ${lookupEntity.Fields.map((f) => f.Name).join(', ')}`,
531
+ });
532
+ }
533
+ }
534
+ }
535
+ else if (parsed.field) {
536
+ // Fallback for single field lookup (backward compatibility)
537
+ const lookupField = lookupEntity.Fields.find((f) => f.Name === parsed.field);
538
+ if (!lookupField) {
539
+ this.addError({
540
+ type: 'reference',
541
+ severity: 'error',
542
+ entity: entityName,
543
+ field: fieldName,
544
+ file: sourceFile,
545
+ message: `Lookup field "${parsed.field}" not found on entity "${parsed.entity}"`,
546
+ suggestion: `Available fields: ${lookupEntity.Fields.map((f) => f.Name).join(', ')}`,
547
+ });
548
+ }
429
549
  }
430
550
  // Track dependency
431
551
  this.addEntityDependency(entityName, parsed.entity);
@@ -444,7 +564,7 @@ class ValidationService {
444
564
  field: fieldName,
445
565
  file: sourceFile,
446
566
  message: `Template file not found: "${templatePath}"`,
447
- suggestion: `Create template at: ${resolvedPath}`
567
+ suggestion: `Create template at: ${resolvedPath}`,
448
568
  });
449
569
  return;
450
570
  }
@@ -460,14 +580,14 @@ class ValidationService {
460
580
  field: fieldName,
461
581
  file: sourceFile,
462
582
  message: `Template file is not valid JSON: "${templatePath}"`,
463
- details: error instanceof Error ? error.message : String(error)
583
+ details: error instanceof Error ? error.message : String(error),
464
584
  });
465
585
  }
466
586
  }
467
587
  /**
468
588
  * Validates @parent: references
469
589
  */
470
- validateParentReference(fieldName, parentContext, sourceFile, entityName, currentFieldName) {
590
+ validateParentReference(_fieldName, parentContext, sourceFile, entityName, currentFieldName) {
471
591
  if (!parentContext) {
472
592
  this.addError({
473
593
  type: 'reference',
@@ -476,14 +596,14 @@ class ValidationService {
476
596
  field: currentFieldName,
477
597
  file: sourceFile,
478
598
  message: `@parent: reference used but no parent context exists`,
479
- suggestion: '@parent: can only be used in nested/related entities'
599
+ suggestion: '@parent: can only be used in nested/related entities',
480
600
  });
481
601
  }
482
602
  }
483
603
  /**
484
604
  * Validates @root: references
485
605
  */
486
- validateRootReference(fieldName, parentContext, sourceFile, entityName, currentFieldName) {
606
+ validateRootReference(_fieldName, parentContext, sourceFile, entityName, currentFieldName) {
487
607
  if (!parentContext) {
488
608
  this.addError({
489
609
  type: 'reference',
@@ -492,7 +612,7 @@ class ValidationService {
492
612
  field: currentFieldName,
493
613
  file: sourceFile,
494
614
  message: `@root: reference used but no root context exists`,
495
- suggestion: '@root: can only be used in nested/related entities'
615
+ suggestion: '@root: can only be used in nested/related entities',
496
616
  });
497
617
  }
498
618
  }
@@ -504,7 +624,7 @@ class ValidationService {
504
624
  this.entityDependencies.set(entityName, {
505
625
  entityName,
506
626
  dependsOn: new Set(),
507
- file: filePath
627
+ file: filePath,
508
628
  });
509
629
  }
510
630
  // Track dependencies from lookups in fields
@@ -523,11 +643,15 @@ class ValidationService {
523
643
  * Add an entity dependency
524
644
  */
525
645
  addEntityDependency(from, to) {
646
+ // Don't add self-references as dependencies (e.g., ParentID in hierarchical structures)
647
+ if (from === to) {
648
+ return;
649
+ }
526
650
  if (!this.entityDependencies.has(from)) {
527
651
  this.entityDependencies.set(from, {
528
652
  entityName: from,
529
653
  dependsOn: new Set(),
530
- file: ''
654
+ file: '',
531
655
  });
532
656
  }
533
657
  this.entityDependencies.get(from).dependsOn.add(to);
@@ -546,7 +670,7 @@ class ValidationService {
546
670
  // Check for circular dependencies
547
671
  const visited = new Set();
548
672
  const recursionStack = new Set();
549
- for (const [entity, deps] of this.entityDependencies) {
673
+ for (const [entity] of this.entityDependencies) {
550
674
  if (!visited.has(entity)) {
551
675
  this.checkCircularDependency(entity, visited, recursionStack);
552
676
  }
@@ -563,7 +687,7 @@ class ValidationService {
563
687
  entity: violation.entity,
564
688
  file: violation.file,
565
689
  message: `Entity '${violation.entity}' depends on '${violation.dependency}' but is processed before it`,
566
- suggestion: `Reorder directories to: [${suggestedOrder.join(', ')}]`
690
+ suggestion: `Reorder directories to: [${suggestedOrder.join(', ')}]`,
567
691
  });
568
692
  }
569
693
  }
@@ -594,7 +718,7 @@ class ValidationService {
594
718
  entity: entity,
595
719
  file: deps.file,
596
720
  message: `Circular dependency detected: ${cyclePath}`,
597
- suggestion: 'Restructure your entities to avoid circular references'
721
+ suggestion: 'Restructure your entities to avoid circular references',
598
722
  });
599
723
  return true;
600
724
  }
@@ -607,12 +731,13 @@ class ValidationService {
607
731
  * Get directories in order based on config
608
732
  */
609
733
  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('.'));
734
+ const allDirs = fs
735
+ .readdirSync(rootDir)
736
+ .filter((f) => fs.statSync(path.join(rootDir, f)).isDirectory())
737
+ .filter((d) => !d.startsWith('.'));
613
738
  if (config.directoryOrder && Array.isArray(config.directoryOrder)) {
614
739
  const ordered = config.directoryOrder.filter((d) => allDirs.includes(d));
615
- const remaining = allDirs.filter(d => !ordered.includes(d)).sort();
740
+ const remaining = allDirs.filter((d) => !ordered.includes(d)).sort();
616
741
  return [...ordered, ...remaining];
617
742
  }
618
743
  return allDirs.sort();
@@ -621,14 +746,13 @@ class ValidationService {
621
746
  * Get files matching pattern
622
747
  */
623
748
  async getMatchingFiles(dir, pattern) {
624
- const files = fs.readdirSync(dir)
625
- .filter(f => fs.statSync(path.join(dir, f)).isFile());
749
+ const files = fs.readdirSync(dir).filter((f) => fs.statSync(path.join(dir, f)).isFile());
626
750
  // Simple glob pattern matching
627
751
  if (pattern === '*.json') {
628
- return files.filter(f => f.endsWith('.json') && !f.startsWith('.mj-'));
752
+ return files.filter((f) => f.endsWith('.json') && !f.startsWith('.mj-'));
629
753
  }
630
754
  else if (pattern === '.*.json') {
631
- return files.filter(f => f.startsWith('.') && f.endsWith('.json') && !f.startsWith('.mj-'));
755
+ return files.filter((f) => f.startsWith('.') && f.endsWith('.json') && !f.startsWith('.mj-'));
632
756
  }
633
757
  return files;
634
758
  }
@@ -660,7 +784,7 @@ class ValidationService {
660
784
  violations.push({
661
785
  entity: entityName,
662
786
  dependency: dep,
663
- file: deps.file
787
+ file: deps.file,
664
788
  });
665
789
  }
666
790
  }
@@ -714,6 +838,7 @@ class ValidationService {
714
838
  this.warnings = [];
715
839
  this.entityDependencies.clear();
716
840
  this.processedEntities.clear();
841
+ this.userRoleCache.clear();
717
842
  }
718
843
  /**
719
844
  * Get validation result
@@ -728,10 +853,163 @@ class ValidationService {
728
853
  totalEntities: 0,
729
854
  totalErrors: this.errors.length,
730
855
  totalWarnings: this.warnings.length,
731
- fileResults: new Map()
732
- }
856
+ fileResults: new Map(),
857
+ },
733
858
  };
734
859
  }
860
+ /**
861
+ * Load user roles from the database into cache
862
+ */
863
+ async loadUserRoles() {
864
+ try {
865
+ const rv = new core_1.RunView();
866
+ const systemUser = (0, provider_utils_1.getSystemUser)();
867
+ // Load all user roles with role names
868
+ const result = await rv.RunView({
869
+ EntityName: 'User Roles',
870
+ ExtraFilter: '',
871
+ OrderBy: 'UserID',
872
+ MaxRows: 10000,
873
+ }, systemUser);
874
+ if (!result.Success) {
875
+ this.addWarning({
876
+ type: 'validation',
877
+ severity: 'warning',
878
+ file: 'system',
879
+ message: 'Failed to load user roles for validation',
880
+ details: result.ErrorMessage,
881
+ });
882
+ return;
883
+ }
884
+ // Group roles by UserID
885
+ for (const userRole of result.Results || []) {
886
+ const userId = userRole.UserID;
887
+ const roleName = userRole.Role;
888
+ if (!this.userRoleCache.has(userId)) {
889
+ this.userRoleCache.set(userId, []);
890
+ }
891
+ this.userRoleCache.get(userId).push(roleName);
892
+ }
893
+ if (this.options.verbose) {
894
+ console.log(`Loaded roles for ${this.userRoleCache.size} users`);
895
+ }
896
+ }
897
+ catch (error) {
898
+ this.addWarning({
899
+ type: 'validation',
900
+ severity: 'warning',
901
+ file: 'system',
902
+ message: 'Error loading user roles for validation',
903
+ details: error instanceof Error ? error.message : String(error),
904
+ });
905
+ }
906
+ }
907
+ /**
908
+ * Validate a UserID field value against allowed roles
909
+ */
910
+ async validateUserRole(userId, entityName, fieldName, filePath, config) {
911
+ // Skip if user role validation is not enabled
912
+ if (!config.userRoleValidation?.enabled) {
913
+ return;
914
+ }
915
+ const userRoles = this.userRoleCache.get(userId);
916
+ const allowedRoles = config.userRoleValidation.allowedRoles || [];
917
+ const allowUsersWithoutRoles = config.userRoleValidation.allowUsersWithoutRoles || false;
918
+ if (!userRoles || userRoles.length === 0) {
919
+ if (!allowUsersWithoutRoles) {
920
+ this.addError({
921
+ type: 'validation',
922
+ severity: 'error',
923
+ entity: entityName,
924
+ field: fieldName,
925
+ file: filePath,
926
+ message: `UserID '${userId}' does not have any assigned roles`,
927
+ suggestion: allowedRoles.length > 0
928
+ ? `User must have one of these roles: ${allowedRoles.join(', ')}`
929
+ : 'Assign appropriate roles to this user or set allowUsersWithoutRoles: true',
930
+ });
931
+ }
932
+ return;
933
+ }
934
+ // Check if user has at least one allowed role
935
+ if (allowedRoles.length > 0) {
936
+ const hasAllowedRole = userRoles.some((role) => allowedRoles.includes(role));
937
+ if (!hasAllowedRole) {
938
+ this.addError({
939
+ type: 'validation',
940
+ severity: 'error',
941
+ entity: entityName,
942
+ field: fieldName,
943
+ file: filePath,
944
+ message: `UserID '${userId}' has roles [${userRoles.join(', ')}] but none are in allowed list`,
945
+ suggestion: `Allowed roles: ${allowedRoles.join(', ')}`,
946
+ });
947
+ }
948
+ }
949
+ }
950
+ /**
951
+ * Validates {@include} references within file content
952
+ *
953
+ * Recursively checks all {@include path} references in file content to ensure:
954
+ * - Referenced files exist
955
+ * - No circular references occur
956
+ * - Include paths are valid
957
+ *
958
+ * @param content - The file content to validate
959
+ * @param filePath - Path of the file being validated
960
+ * @param visitedPaths - Set of already visited paths for circular reference detection
961
+ */
962
+ async validateIncludeReferences(content, filePath, visitedPaths) {
963
+ // Pattern to match {@include path} references
964
+ const includePattern = /\{@include\s+([^\}]+)\s*\}/g;
965
+ let match;
966
+ while ((match = includePattern.exec(content)) !== null) {
967
+ const [fullMatch, includePath] = match;
968
+ const trimmedPath = includePath.trim();
969
+ // Resolve the include path relative to the current file's directory
970
+ const currentDir = path.dirname(filePath);
971
+ const resolvedPath = path.resolve(currentDir, trimmedPath);
972
+ // Check for circular reference
973
+ if (visitedPaths.has(resolvedPath)) {
974
+ this.addError({
975
+ type: 'reference',
976
+ severity: 'error',
977
+ file: filePath,
978
+ message: `Circular {@include} reference detected: "${trimmedPath}"`,
979
+ details: `Path ${resolvedPath} is already being processed`,
980
+ suggestion: 'Restructure your includes to avoid circular references',
981
+ });
982
+ continue;
983
+ }
984
+ // Check if the included file exists
985
+ if (!fs.existsSync(resolvedPath)) {
986
+ this.addError({
987
+ type: 'reference',
988
+ severity: 'error',
989
+ file: filePath,
990
+ message: `{@include} file not found: "${trimmedPath}"`,
991
+ suggestion: `Create file at: ${resolvedPath}`,
992
+ });
993
+ continue;
994
+ }
995
+ // Recursively validate the included file
996
+ try {
997
+ const includedContent = fs.readFileSync(resolvedPath, 'utf-8');
998
+ const newVisitedPaths = new Set(visitedPaths);
999
+ newVisitedPaths.add(resolvedPath);
1000
+ await this.validateIncludeReferences(includedContent, resolvedPath, newVisitedPaths);
1001
+ }
1002
+ catch (error) {
1003
+ this.addError({
1004
+ type: 'reference',
1005
+ severity: 'error',
1006
+ file: filePath,
1007
+ message: `Failed to read {@include} file: "${trimmedPath}"`,
1008
+ details: error instanceof Error ? error.message : String(error),
1009
+ });
1010
+ }
1011
+ }
1012
+ }
735
1013
  }
736
1014
  exports.ValidationService = ValidationService;
737
1015
  //# sourceMappingURL=ValidationService.js.map