@memberjunction/metadata-sync 2.51.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.
- package/README.md +138 -1
- package/dist/commands/file-reset/index.d.ts +15 -0
- package/dist/commands/file-reset/index.js +221 -0
- package/dist/commands/file-reset/index.js.map +1 -0
- package/dist/commands/push/index.d.ts +21 -0
- package/dist/commands/push/index.js +502 -42
- package/dist/commands/push/index.js.map +1 -1
- package/dist/commands/watch/index.js +39 -1
- package/dist/commands/watch/index.js.map +1 -1
- package/dist/config.d.ts +30 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/file-backup-manager.d.ts +90 -0
- package/dist/lib/file-backup-manager.js +186 -0
- package/dist/lib/file-backup-manager.js.map +1 -0
- package/dist/lib/provider-utils.d.ts +4 -2
- package/dist/lib/provider-utils.js +26 -4
- package/dist/lib/provider-utils.js.map +1 -1
- package/dist/lib/sync-engine.d.ts +4 -1
- package/dist/lib/sync-engine.js +50 -16
- package/dist/lib/sync-engine.js.map +1 -1
- package/dist/services/FormattingService.d.ts +1 -0
- package/dist/services/FormattingService.js +4 -1
- package/dist/services/FormattingService.js.map +1 -1
- package/dist/services/ValidationService.d.ts +9 -0
- package/dist/services/ValidationService.js +268 -70
- package/dist/services/ValidationService.js.map +1 -1
- package/dist/types/validation.d.ts +6 -2
- package/dist/types/validation.js.map +1 -1
- package/oclif.manifest.json +114 -45
- package/package.json +7 -7
|
@@ -6,6 +6,7 @@ export declare class ValidationService {
|
|
|
6
6
|
private entityDependencies;
|
|
7
7
|
private processedEntities;
|
|
8
8
|
private options;
|
|
9
|
+
private userRoleCache;
|
|
9
10
|
constructor(options?: Partial<ValidationOptions>);
|
|
10
11
|
/**
|
|
11
12
|
* Validates all metadata files in the specified directory
|
|
@@ -107,4 +108,12 @@ export declare class ValidationService {
|
|
|
107
108
|
* Get validation result
|
|
108
109
|
*/
|
|
109
110
|
private getResult;
|
|
111
|
+
/**
|
|
112
|
+
* Load user roles from the database into cache
|
|
113
|
+
*/
|
|
114
|
+
private loadUserRoles;
|
|
115
|
+
/**
|
|
116
|
+
* Validate a UserID field value against allowed roles
|
|
117
|
+
*/
|
|
118
|
+
private validateUserRole;
|
|
110
119
|
}
|
|
@@ -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
|
-
|
|
98
|
-
|
|
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
|
-
//
|
|
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,
|
|
362
|
-
|
|
363
|
-
const
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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 {
|
|
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
|
-
|
|
419
|
-
if (
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
611
|
-
.
|
|
612
|
-
.filter(
|
|
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
|