@memberjunction/metadata-sync 2.52.0 → 2.53.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,7 @@ 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}`,
398
481
  });
399
482
  }
400
483
  }
@@ -411,21 +494,41 @@ class ValidationService {
411
494
  field: fieldName,
412
495
  file: sourceFile,
413
496
  message: `Lookup entity "${parsed.entity}" not found`,
414
- suggestion: 'Check entity name spelling and case'
497
+ suggestion: 'Check entity name spelling and case',
415
498
  });
416
499
  return;
417
500
  }
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
- });
501
+ // For multi-field lookups, validate all fields
502
+ if (parsed.fields && parsed.fields.length > 0) {
503
+ for (const { field } of parsed.fields) {
504
+ const lookupField = lookupEntity.Fields.find((f) => f.Name === field);
505
+ if (!lookupField) {
506
+ this.addError({
507
+ type: 'reference',
508
+ severity: 'error',
509
+ entity: entityName,
510
+ field: fieldName,
511
+ file: sourceFile,
512
+ message: `Lookup field "${field}" not found on entity "${parsed.entity}"`,
513
+ suggestion: `Available fields: ${lookupEntity.Fields.map((f) => f.Name).join(', ')}`,
514
+ });
515
+ }
516
+ }
517
+ }
518
+ else if (parsed.field) {
519
+ // Fallback for single field lookup (backward compatibility)
520
+ const lookupField = lookupEntity.Fields.find((f) => f.Name === parsed.field);
521
+ if (!lookupField) {
522
+ this.addError({
523
+ type: 'reference',
524
+ severity: 'error',
525
+ entity: entityName,
526
+ field: fieldName,
527
+ file: sourceFile,
528
+ message: `Lookup field "${parsed.field}" not found on entity "${parsed.entity}"`,
529
+ suggestion: `Available fields: ${lookupEntity.Fields.map((f) => f.Name).join(', ')}`,
530
+ });
531
+ }
429
532
  }
430
533
  // Track dependency
431
534
  this.addEntityDependency(entityName, parsed.entity);
@@ -444,7 +547,7 @@ class ValidationService {
444
547
  field: fieldName,
445
548
  file: sourceFile,
446
549
  message: `Template file not found: "${templatePath}"`,
447
- suggestion: `Create template at: ${resolvedPath}`
550
+ suggestion: `Create template at: ${resolvedPath}`,
448
551
  });
449
552
  return;
450
553
  }
@@ -460,14 +563,14 @@ class ValidationService {
460
563
  field: fieldName,
461
564
  file: sourceFile,
462
565
  message: `Template file is not valid JSON: "${templatePath}"`,
463
- details: error instanceof Error ? error.message : String(error)
566
+ details: error instanceof Error ? error.message : String(error),
464
567
  });
465
568
  }
466
569
  }
467
570
  /**
468
571
  * Validates @parent: references
469
572
  */
470
- validateParentReference(fieldName, parentContext, sourceFile, entityName, currentFieldName) {
573
+ validateParentReference(_fieldName, parentContext, sourceFile, entityName, currentFieldName) {
471
574
  if (!parentContext) {
472
575
  this.addError({
473
576
  type: 'reference',
@@ -476,14 +579,14 @@ class ValidationService {
476
579
  field: currentFieldName,
477
580
  file: sourceFile,
478
581
  message: `@parent: reference used but no parent context exists`,
479
- suggestion: '@parent: can only be used in nested/related entities'
582
+ suggestion: '@parent: can only be used in nested/related entities',
480
583
  });
481
584
  }
482
585
  }
483
586
  /**
484
587
  * Validates @root: references
485
588
  */
486
- validateRootReference(fieldName, parentContext, sourceFile, entityName, currentFieldName) {
589
+ validateRootReference(_fieldName, parentContext, sourceFile, entityName, currentFieldName) {
487
590
  if (!parentContext) {
488
591
  this.addError({
489
592
  type: 'reference',
@@ -492,7 +595,7 @@ class ValidationService {
492
595
  field: currentFieldName,
493
596
  file: sourceFile,
494
597
  message: `@root: reference used but no root context exists`,
495
- suggestion: '@root: can only be used in nested/related entities'
598
+ suggestion: '@root: can only be used in nested/related entities',
496
599
  });
497
600
  }
498
601
  }
@@ -504,7 +607,7 @@ class ValidationService {
504
607
  this.entityDependencies.set(entityName, {
505
608
  entityName,
506
609
  dependsOn: new Set(),
507
- file: filePath
610
+ file: filePath,
508
611
  });
509
612
  }
510
613
  // Track dependencies from lookups in fields
@@ -523,11 +626,15 @@ class ValidationService {
523
626
  * Add an entity dependency
524
627
  */
525
628
  addEntityDependency(from, to) {
629
+ // Don't add self-references as dependencies (e.g., ParentID in hierarchical structures)
630
+ if (from === to) {
631
+ return;
632
+ }
526
633
  if (!this.entityDependencies.has(from)) {
527
634
  this.entityDependencies.set(from, {
528
635
  entityName: from,
529
636
  dependsOn: new Set(),
530
- file: ''
637
+ file: '',
531
638
  });
532
639
  }
533
640
  this.entityDependencies.get(from).dependsOn.add(to);
@@ -546,7 +653,7 @@ class ValidationService {
546
653
  // Check for circular dependencies
547
654
  const visited = new Set();
548
655
  const recursionStack = new Set();
549
- for (const [entity, deps] of this.entityDependencies) {
656
+ for (const [entity] of this.entityDependencies) {
550
657
  if (!visited.has(entity)) {
551
658
  this.checkCircularDependency(entity, visited, recursionStack);
552
659
  }
@@ -563,7 +670,7 @@ class ValidationService {
563
670
  entity: violation.entity,
564
671
  file: violation.file,
565
672
  message: `Entity '${violation.entity}' depends on '${violation.dependency}' but is processed before it`,
566
- suggestion: `Reorder directories to: [${suggestedOrder.join(', ')}]`
673
+ suggestion: `Reorder directories to: [${suggestedOrder.join(', ')}]`,
567
674
  });
568
675
  }
569
676
  }
@@ -594,7 +701,7 @@ class ValidationService {
594
701
  entity: entity,
595
702
  file: deps.file,
596
703
  message: `Circular dependency detected: ${cyclePath}`,
597
- suggestion: 'Restructure your entities to avoid circular references'
704
+ suggestion: 'Restructure your entities to avoid circular references',
598
705
  });
599
706
  return true;
600
707
  }
@@ -607,12 +714,13 @@ class ValidationService {
607
714
  * Get directories in order based on config
608
715
  */
609
716
  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('.'));
717
+ const allDirs = fs
718
+ .readdirSync(rootDir)
719
+ .filter((f) => fs.statSync(path.join(rootDir, f)).isDirectory())
720
+ .filter((d) => !d.startsWith('.'));
613
721
  if (config.directoryOrder && Array.isArray(config.directoryOrder)) {
614
722
  const ordered = config.directoryOrder.filter((d) => allDirs.includes(d));
615
- const remaining = allDirs.filter(d => !ordered.includes(d)).sort();
723
+ const remaining = allDirs.filter((d) => !ordered.includes(d)).sort();
616
724
  return [...ordered, ...remaining];
617
725
  }
618
726
  return allDirs.sort();
@@ -621,14 +729,13 @@ class ValidationService {
621
729
  * Get files matching pattern
622
730
  */
623
731
  async getMatchingFiles(dir, pattern) {
624
- const files = fs.readdirSync(dir)
625
- .filter(f => fs.statSync(path.join(dir, f)).isFile());
732
+ const files = fs.readdirSync(dir).filter((f) => fs.statSync(path.join(dir, f)).isFile());
626
733
  // Simple glob pattern matching
627
734
  if (pattern === '*.json') {
628
- return files.filter(f => f.endsWith('.json') && !f.startsWith('.mj-'));
735
+ return files.filter((f) => f.endsWith('.json') && !f.startsWith('.mj-'));
629
736
  }
630
737
  else if (pattern === '.*.json') {
631
- return files.filter(f => f.startsWith('.') && f.endsWith('.json') && !f.startsWith('.mj-'));
738
+ return files.filter((f) => f.startsWith('.') && f.endsWith('.json') && !f.startsWith('.mj-'));
632
739
  }
633
740
  return files;
634
741
  }
@@ -660,7 +767,7 @@ class ValidationService {
660
767
  violations.push({
661
768
  entity: entityName,
662
769
  dependency: dep,
663
- file: deps.file
770
+ file: deps.file,
664
771
  });
665
772
  }
666
773
  }
@@ -714,6 +821,7 @@ class ValidationService {
714
821
  this.warnings = [];
715
822
  this.entityDependencies.clear();
716
823
  this.processedEntities.clear();
824
+ this.userRoleCache.clear();
717
825
  }
718
826
  /**
719
827
  * Get validation result
@@ -728,10 +836,100 @@ class ValidationService {
728
836
  totalEntities: 0,
729
837
  totalErrors: this.errors.length,
730
838
  totalWarnings: this.warnings.length,
731
- fileResults: new Map()
732
- }
839
+ fileResults: new Map(),
840
+ },
733
841
  };
734
842
  }
843
+ /**
844
+ * Load user roles from the database into cache
845
+ */
846
+ async loadUserRoles() {
847
+ try {
848
+ const rv = new core_1.RunView();
849
+ const systemUser = (0, provider_utils_1.getSystemUser)();
850
+ // Load all user roles with role names
851
+ const result = await rv.RunView({
852
+ EntityName: 'User Roles',
853
+ ExtraFilter: '',
854
+ OrderBy: 'UserID',
855
+ MaxRows: 10000,
856
+ }, systemUser);
857
+ if (!result.Success) {
858
+ this.addWarning({
859
+ type: 'validation',
860
+ severity: 'warning',
861
+ file: 'system',
862
+ message: 'Failed to load user roles for validation',
863
+ details: result.ErrorMessage,
864
+ });
865
+ return;
866
+ }
867
+ // Group roles by UserID
868
+ for (const userRole of result.Results || []) {
869
+ const userId = userRole.UserID;
870
+ const roleName = userRole.Role;
871
+ if (!this.userRoleCache.has(userId)) {
872
+ this.userRoleCache.set(userId, []);
873
+ }
874
+ this.userRoleCache.get(userId).push(roleName);
875
+ }
876
+ if (this.options.verbose) {
877
+ console.log(`Loaded roles for ${this.userRoleCache.size} users`);
878
+ }
879
+ }
880
+ catch (error) {
881
+ this.addWarning({
882
+ type: 'validation',
883
+ severity: 'warning',
884
+ file: 'system',
885
+ message: 'Error loading user roles for validation',
886
+ details: error instanceof Error ? error.message : String(error),
887
+ });
888
+ }
889
+ }
890
+ /**
891
+ * Validate a UserID field value against allowed roles
892
+ */
893
+ async validateUserRole(userId, entityName, fieldName, filePath, config) {
894
+ // Skip if user role validation is not enabled
895
+ if (!config.userRoleValidation?.enabled) {
896
+ return;
897
+ }
898
+ const userRoles = this.userRoleCache.get(userId);
899
+ const allowedRoles = config.userRoleValidation.allowedRoles || [];
900
+ const allowUsersWithoutRoles = config.userRoleValidation.allowUsersWithoutRoles || false;
901
+ if (!userRoles || userRoles.length === 0) {
902
+ if (!allowUsersWithoutRoles) {
903
+ this.addError({
904
+ type: 'validation',
905
+ severity: 'error',
906
+ entity: entityName,
907
+ field: fieldName,
908
+ file: filePath,
909
+ message: `UserID '${userId}' does not have any assigned roles`,
910
+ suggestion: allowedRoles.length > 0
911
+ ? `User must have one of these roles: ${allowedRoles.join(', ')}`
912
+ : 'Assign appropriate roles to this user or set allowUsersWithoutRoles: true',
913
+ });
914
+ }
915
+ return;
916
+ }
917
+ // Check if user has at least one allowed role
918
+ if (allowedRoles.length > 0) {
919
+ const hasAllowedRole = userRoles.some((role) => allowedRoles.includes(role));
920
+ if (!hasAllowedRole) {
921
+ this.addError({
922
+ type: 'validation',
923
+ severity: 'error',
924
+ entity: entityName,
925
+ field: fieldName,
926
+ file: filePath,
927
+ message: `UserID '${userId}' has roles [${userRoles.join(', ')}] but none are in allowed list`,
928
+ suggestion: `Allowed roles: ${allowedRoles.join(', ')}`,
929
+ });
930
+ }
931
+ }
932
+ }
735
933
  }
736
934
  exports.ValidationService = ValidationService;
737
935
  //# sourceMappingURL=ValidationService.js.map