@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.
- package/README.md +138 -1
- package/dist/commands/push/index.d.ts +4 -0
- package/dist/commands/push/index.js +100 -10
- package/dist/commands/push/index.js.map +1 -1
- package/dist/config.d.ts +23 -0
- package/dist/config.js.map +1 -1
- 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 +49 -16
- package/dist/lib/sync-engine.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 +25 -25
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -718,14 +718,27 @@ Examples:
|
|
|
718
718
|
### @lookup: References (ENHANCED)
|
|
719
719
|
Enable entity relationships using human-readable values:
|
|
720
720
|
- Basic syntax: `@lookup:EntityName.FieldName=Value`
|
|
721
|
+
- Multi-field syntax: `@lookup:EntityName.Field1=Value1&Field2=Value2`
|
|
721
722
|
- Auto-create syntax: `@lookup:EntityName.FieldName=Value?create`
|
|
722
723
|
- With additional fields: `@lookup:EntityName.FieldName=Value?create&Field2=Value2`
|
|
723
724
|
|
|
724
725
|
Examples:
|
|
725
|
-
- `@lookup:AI Prompt Types.Name=Chat` -
|
|
726
|
+
- `@lookup:AI Prompt Types.Name=Chat` - Single field lookup, fails if not found
|
|
727
|
+
- `@lookup:Users.Email=john@example.com&Department=Sales` - Multi-field lookup for precise matching
|
|
726
728
|
- `@lookup:AI Prompt Categories.Name=Examples?create` - Creates if missing
|
|
727
729
|
- `@lookup:AI Prompt Categories.Name=Examples?create&Description=Example prompts` - Creates with description
|
|
728
730
|
|
|
731
|
+
#### Multi-Field Lookups (NEW)
|
|
732
|
+
When you need to match records based on multiple criteria, use the multi-field syntax:
|
|
733
|
+
```json
|
|
734
|
+
{
|
|
735
|
+
"CategoryID": "@lookup:AI Prompt Categories.Name=Actions&Status=Active",
|
|
736
|
+
"ManagerID": "@lookup:Users.Email=manager@company.com&Department=Engineering&Status=Active"
|
|
737
|
+
}
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
This ensures you get the exact record you want when multiple records might have the same value in a single field.
|
|
741
|
+
|
|
729
742
|
### @parent: References (NEW)
|
|
730
743
|
Reference fields from the immediate parent entity in embedded collections:
|
|
731
744
|
- `@parent:ID` - Get the parent's ID field
|
|
@@ -818,6 +831,9 @@ mj-sync validate --verbose
|
|
|
818
831
|
# Validate with JSON output for CI/CD
|
|
819
832
|
mj-sync validate --format=json
|
|
820
833
|
|
|
834
|
+
# Save validation report to markdown file
|
|
835
|
+
mj-sync validate --save-report
|
|
836
|
+
|
|
821
837
|
# Initialize a directory for metadata sync
|
|
822
838
|
mj-sync init
|
|
823
839
|
|
|
@@ -898,6 +914,53 @@ Directory order is configured in the root-level `.mj-sync.json` file only (not i
|
|
|
898
914
|
2. **Categories → Items**: Create category records before items that reference them
|
|
899
915
|
3. **Parent → Child**: Process parent entities before child entities with foreign key dependencies
|
|
900
916
|
|
|
917
|
+
### Ignore Directories
|
|
918
|
+
|
|
919
|
+
The MetadataSync tool supports ignoring specific directories during push/pull operations. This is useful for:
|
|
920
|
+
- Excluding output or example directories from processing
|
|
921
|
+
- Skipping temporary or build directories
|
|
922
|
+
- Organizing support files without them being processed as metadata
|
|
923
|
+
|
|
924
|
+
#### Configuration
|
|
925
|
+
|
|
926
|
+
Ignore directories are configured in `.mj-sync.json` files and are **cumulative** through the directory hierarchy:
|
|
927
|
+
|
|
928
|
+
```json
|
|
929
|
+
{
|
|
930
|
+
"version": "1.0.0",
|
|
931
|
+
"ignoreDirectories": [
|
|
932
|
+
"output",
|
|
933
|
+
"examples",
|
|
934
|
+
"templates"
|
|
935
|
+
]
|
|
936
|
+
}
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
#### How It Works
|
|
940
|
+
|
|
941
|
+
- **Cumulative Inheritance**: Each directory inherits ignore patterns from its parent directories
|
|
942
|
+
- **Relative Paths**: Directory names are relative to the location of the `.mj-sync.json` file
|
|
943
|
+
- **Simple Patterns**: Supports exact directory names (e.g., "output", "temp")
|
|
944
|
+
- **Additive**: Child directories can add their own ignore patterns to parent patterns
|
|
945
|
+
|
|
946
|
+
#### Example
|
|
947
|
+
|
|
948
|
+
```
|
|
949
|
+
metadata/.mj-sync.json → ignoreDirectories: ["output", "temp"]
|
|
950
|
+
├── prompts/.mj-sync.json → ignoreDirectories: ["examples"]
|
|
951
|
+
│ ├── output/ → IGNORED (from root)
|
|
952
|
+
│ ├── examples/ → IGNORED (from prompts)
|
|
953
|
+
│ └── production/.mj-sync.json → ignoreDirectories: ["drafts"]
|
|
954
|
+
│ ├── drafts/ → IGNORED (from production)
|
|
955
|
+
│ └── output/ → IGNORED (inherited from root)
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
In this example:
|
|
959
|
+
- Root level ignores "output" and "temp" everywhere
|
|
960
|
+
- Prompts directory adds "examples" to the ignore list
|
|
961
|
+
- Production subdirectory further adds "drafts"
|
|
962
|
+
- All patterns are cumulative, so production inherits all parent ignores
|
|
963
|
+
|
|
901
964
|
### SQL Logging (NEW)
|
|
902
965
|
|
|
903
966
|
The MetadataSync tool now supports SQL logging for capturing all database operations during push commands. This feature is useful for:
|
|
@@ -973,6 +1036,75 @@ Migration files include:
|
|
|
973
1036
|
|
|
974
1037
|
The SQL logging runs in parallel with the actual database operations, ensuring minimal performance impact while capturing all SQL statements for review and potential migration use.
|
|
975
1038
|
|
|
1039
|
+
### User Role Validation (NEW)
|
|
1040
|
+
|
|
1041
|
+
MetadataSync now supports validating UserID fields against specific roles in the MemberJunction system. This ensures that only users with appropriate roles can be referenced in metadata files.
|
|
1042
|
+
|
|
1043
|
+
#### Configuration
|
|
1044
|
+
|
|
1045
|
+
Add the `userRoleValidation` configuration to your root `.mj-sync.json` file:
|
|
1046
|
+
|
|
1047
|
+
```json
|
|
1048
|
+
{
|
|
1049
|
+
"version": "1.0.0",
|
|
1050
|
+
"userRoleValidation": {
|
|
1051
|
+
"enabled": true,
|
|
1052
|
+
"allowedRoles": [
|
|
1053
|
+
"Administrator",
|
|
1054
|
+
"Developer",
|
|
1055
|
+
"Content Manager"
|
|
1056
|
+
],
|
|
1057
|
+
"allowUsersWithoutRoles": false
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
```
|
|
1061
|
+
|
|
1062
|
+
#### Configuration Options
|
|
1063
|
+
|
|
1064
|
+
| Option | Type | Default | Description |
|
|
1065
|
+
|--------|------|---------|-------------|
|
|
1066
|
+
| `enabled` | boolean | false | Enable user role validation for UserID fields |
|
|
1067
|
+
| `allowedRoles` | string[] | [] | List of role names that are allowed |
|
|
1068
|
+
| `allowUsersWithoutRoles` | boolean | false | Allow users without any assigned roles |
|
|
1069
|
+
|
|
1070
|
+
#### How It Works
|
|
1071
|
+
|
|
1072
|
+
1. During validation, all user roles are loaded from the database and cached
|
|
1073
|
+
2. For each UserID field in metadata files, the validator checks:
|
|
1074
|
+
- If the user exists and has roles assigned
|
|
1075
|
+
- If the user has at least one of the allowed roles
|
|
1076
|
+
3. Validation fails if:
|
|
1077
|
+
- A UserID references a user without any roles (unless `allowUsersWithoutRoles` is true)
|
|
1078
|
+
- A UserID references a user whose roles are not in the `allowedRoles` list
|
|
1079
|
+
|
|
1080
|
+
#### Example
|
|
1081
|
+
|
|
1082
|
+
Given a metadata file with a UserID field:
|
|
1083
|
+
|
|
1084
|
+
```json
|
|
1085
|
+
{
|
|
1086
|
+
"fields": {
|
|
1087
|
+
"Name": "Admin Action",
|
|
1088
|
+
"UserID": "user-123"
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
```
|
|
1092
|
+
|
|
1093
|
+
The validation will:
|
|
1094
|
+
1. Check if user-123 exists in the system
|
|
1095
|
+
2. Verify that user-123 has one of the allowed roles
|
|
1096
|
+
3. Report an error if the user doesn't have appropriate roles
|
|
1097
|
+
|
|
1098
|
+
#### Error Messages
|
|
1099
|
+
|
|
1100
|
+
```
|
|
1101
|
+
✗ UserID 'user-123' does not have any assigned roles
|
|
1102
|
+
Suggestion: User must have one of these roles: Administrator, Developer
|
|
1103
|
+
|
|
1104
|
+
✗ UserID 'user-456' has roles [Viewer] but none are in allowed list
|
|
1105
|
+
Suggestion: Allowed roles: Administrator, Developer, Content Manager
|
|
1106
|
+
```
|
|
1107
|
+
|
|
976
1108
|
### Root Configuration (metadata/.mj-sync.json)
|
|
977
1109
|
```json
|
|
978
1110
|
{
|
|
@@ -990,6 +1122,11 @@ The SQL logging runs in parallel with the actual database operations, ensuring m
|
|
|
990
1122
|
"outputDirectory": "./sql_logging",
|
|
991
1123
|
"formatAsMigration": false
|
|
992
1124
|
},
|
|
1125
|
+
"userRoleValidation": {
|
|
1126
|
+
"enabled": true,
|
|
1127
|
+
"allowedRoles": ["Administrator", "Developer"],
|
|
1128
|
+
"allowUsersWithoutRoles": false
|
|
1129
|
+
},
|
|
993
1130
|
"watch": {
|
|
994
1131
|
"debounceMs": 1000,
|
|
995
1132
|
"ignorePatterns": ["*.tmp", "*.bak"]
|
|
@@ -26,6 +26,10 @@ export default class Push extends Command {
|
|
|
26
26
|
* Check if a record has already been processed and warn if duplicate
|
|
27
27
|
*/
|
|
28
28
|
private checkAndTrackRecord;
|
|
29
|
+
/**
|
|
30
|
+
* Format field value for console display
|
|
31
|
+
*/
|
|
32
|
+
private formatFieldValue;
|
|
29
33
|
/**
|
|
30
34
|
* Parse JSON file and track line numbers for array elements
|
|
31
35
|
*/
|
|
@@ -137,7 +137,7 @@ class Push extends core_1.Command {
|
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
// Find entity directories to process
|
|
140
|
-
const entityDirs = (0, provider_utils_1.findEntityDirectories)(config_manager_1.configManager.getOriginalCwd(), flags.dir, syncConfig?.directoryOrder);
|
|
140
|
+
const entityDirs = (0, provider_utils_1.findEntityDirectories)(config_manager_1.configManager.getOriginalCwd(), flags.dir, syncConfig?.directoryOrder, syncConfig?.ignoreDirectories);
|
|
141
141
|
if (entityDirs.length === 0) {
|
|
142
142
|
this.error('No entity directories found');
|
|
143
143
|
}
|
|
@@ -168,7 +168,9 @@ class Push extends core_1.Command {
|
|
|
168
168
|
default: false
|
|
169
169
|
});
|
|
170
170
|
if (!shouldContinue) {
|
|
171
|
-
this.
|
|
171
|
+
this.log(chalk_1.default.yellow('\n⚠️ Push cancelled due to validation errors.'));
|
|
172
|
+
// Exit cleanly without throwing an error
|
|
173
|
+
return;
|
|
172
174
|
}
|
|
173
175
|
}
|
|
174
176
|
}
|
|
@@ -249,7 +251,12 @@ class Push extends core_1.Command {
|
|
|
249
251
|
if (flags.verbose) {
|
|
250
252
|
this.log(`\nProcessing ${entityConfig.entity} in ${entityDir}`);
|
|
251
253
|
}
|
|
252
|
-
|
|
254
|
+
// Combine root ignoreDirectories with entity-level ignoreDirectories
|
|
255
|
+
const initialIgnoreDirectories = [
|
|
256
|
+
...(syncConfig?.ignoreDirectories || []),
|
|
257
|
+
...(entityConfig?.ignoreDirectories || [])
|
|
258
|
+
];
|
|
259
|
+
const result = await this.processEntityDirectory(entityDir, entityConfig, syncEngine, flags, syncConfig, fileBackupManager, initialIgnoreDirectories);
|
|
253
260
|
// Show per-directory summary
|
|
254
261
|
const dirName = path_1.default.relative(process.cwd(), entityDir) || '.';
|
|
255
262
|
const dirTotal = result.created + result.updated + result.unchanged;
|
|
@@ -444,7 +451,7 @@ class Push extends core_1.Command {
|
|
|
444
451
|
process.exit(0);
|
|
445
452
|
}
|
|
446
453
|
}
|
|
447
|
-
async processEntityDirectory(entityDir, entityConfig, syncEngine, flags, syncConfig, fileBackupManager) {
|
|
454
|
+
async processEntityDirectory(entityDir, entityConfig, syncEngine, flags, syncConfig, fileBackupManager, parentIgnoreDirectories) {
|
|
448
455
|
const result = { created: 0, updated: 0, unchanged: 0, errors: 0 };
|
|
449
456
|
// Find files matching the configured pattern
|
|
450
457
|
const pattern = entityConfig.filePattern || '*.json';
|
|
@@ -454,6 +461,56 @@ class Push extends core_1.Command {
|
|
|
454
461
|
dot: true, // Include dotfiles (files starting with .)
|
|
455
462
|
onlyFiles: true
|
|
456
463
|
});
|
|
464
|
+
// Check if no JSON files were found
|
|
465
|
+
if (jsonFiles.length === 0) {
|
|
466
|
+
const relativePath = path_1.default.relative(process.cwd(), entityDir) || '.';
|
|
467
|
+
const parentPath = path_1.default.dirname(entityDir);
|
|
468
|
+
const dirName = path_1.default.basename(entityDir);
|
|
469
|
+
// Check if this is a subdirectory (not a top-level entity directory)
|
|
470
|
+
const isSubdirectory = parentPath !== path_1.default.resolve(config_manager_1.configManager.getOriginalCwd(), flags.dir || '.');
|
|
471
|
+
if (isSubdirectory) {
|
|
472
|
+
// For subdirectories, make it a warning instead of an error
|
|
473
|
+
let warningMessage = `No JSON files found in ${relativePath} matching pattern: ${pattern}`;
|
|
474
|
+
// Try to be more helpful by checking what files do exist
|
|
475
|
+
const allFiles = await (0, fast_glob_1.default)('*', {
|
|
476
|
+
cwd: entityDir,
|
|
477
|
+
onlyFiles: true,
|
|
478
|
+
dot: true
|
|
479
|
+
});
|
|
480
|
+
if (allFiles.length > 0) {
|
|
481
|
+
warningMessage += `\n Files found: ${allFiles.slice(0, 3).join(', ')}`;
|
|
482
|
+
if (allFiles.length > 3) {
|
|
483
|
+
warningMessage += ` (and ${allFiles.length - 3} more)`;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
const rootConfigPath = path_1.default.join(config_manager_1.configManager.getOriginalCwd(), flags.dir || '.', '.mj-sync.json');
|
|
487
|
+
warningMessage += `\n 💡 If this directory should be ignored, add "${dirName}" to the "ignoreDirectories" array in:\n ${rootConfigPath}`;
|
|
488
|
+
this.warn(warningMessage);
|
|
489
|
+
return result; // Return early without processing further
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
// For top-level entity directories, this is still an error
|
|
493
|
+
const configFile = path_1.default.join(entityDir, '.mj-sync.json');
|
|
494
|
+
let errorMessage = `No JSON files found in ${relativePath} matching pattern: ${pattern}\n`;
|
|
495
|
+
errorMessage += `\nPlease check:\n`;
|
|
496
|
+
errorMessage += ` 1. Files exist with the expected extension (.json)\n`;
|
|
497
|
+
errorMessage += ` 2. The filePattern in ${configFile} matches your files\n`;
|
|
498
|
+
errorMessage += ` 3. Files are not in ignored patterns: .mj-sync.json, .mj-folder.json, *.backup\n`;
|
|
499
|
+
// Try to be more helpful by checking what files do exist
|
|
500
|
+
const allFiles = await (0, fast_glob_1.default)('*', {
|
|
501
|
+
cwd: entityDir,
|
|
502
|
+
onlyFiles: true,
|
|
503
|
+
dot: true
|
|
504
|
+
});
|
|
505
|
+
if (allFiles.length > 0) {
|
|
506
|
+
errorMessage += `\nFiles found in directory: ${allFiles.slice(0, 5).join(', ')}`;
|
|
507
|
+
if (allFiles.length > 5) {
|
|
508
|
+
errorMessage += ` (and ${allFiles.length - 5} more)`;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
throw new Error(errorMessage);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
457
514
|
if (flags.verbose) {
|
|
458
515
|
this.log(`Processing ${jsonFiles.length} records in ${path_1.default.relative(process.cwd(), entityDir) || '.'}`);
|
|
459
516
|
}
|
|
@@ -463,6 +520,24 @@ class Push extends core_1.Command {
|
|
|
463
520
|
const entries = await fs_extra_1.default.readdir(entityDir, { withFileTypes: true });
|
|
464
521
|
for (const entry of entries) {
|
|
465
522
|
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
523
|
+
// Build cumulative ignore list: parent + current directory's ignores
|
|
524
|
+
const currentDirConfig = await (0, config_1.loadSyncConfig)(entityDir);
|
|
525
|
+
const currentEntityConfig = await (0, config_1.loadEntityConfig)(entityDir);
|
|
526
|
+
const cumulativeIgnoreDirectories = [
|
|
527
|
+
...(parentIgnoreDirectories || []),
|
|
528
|
+
...(currentDirConfig?.ignoreDirectories || []),
|
|
529
|
+
...(currentEntityConfig?.ignoreDirectories || [])
|
|
530
|
+
];
|
|
531
|
+
// Check if this directory should be ignored
|
|
532
|
+
if (cumulativeIgnoreDirectories.some((pattern) => {
|
|
533
|
+
// Simple pattern matching: exact name or ends with pattern
|
|
534
|
+
return entry.name === pattern || entry.name.endsWith(pattern);
|
|
535
|
+
})) {
|
|
536
|
+
if (flags.verbose) {
|
|
537
|
+
this.log(` Ignoring directory: ${entry.name} (matched ignore pattern)`);
|
|
538
|
+
}
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
466
541
|
const subDir = path_1.default.join(entityDir, entry.name);
|
|
467
542
|
// Load subdirectory config and merge with parent config
|
|
468
543
|
let subEntityConfig = { ...entityConfig };
|
|
@@ -483,8 +558,8 @@ class Push extends core_1.Command {
|
|
|
483
558
|
}
|
|
484
559
|
};
|
|
485
560
|
}
|
|
486
|
-
// Process subdirectory with merged config
|
|
487
|
-
const subResult = await this.processEntityDirectory(subDir, subEntityConfig, syncEngine, flags, syncConfig, fileBackupManager);
|
|
561
|
+
// Process subdirectory with merged config and cumulative ignore directories
|
|
562
|
+
const subResult = await this.processEntityDirectory(subDir, subEntityConfig, syncEngine, flags, syncConfig, fileBackupManager, cumulativeIgnoreDirectories);
|
|
488
563
|
result.created += subResult.created;
|
|
489
564
|
result.updated += subResult.updated;
|
|
490
565
|
result.unchanged += subResult.unchanged;
|
|
@@ -558,6 +633,7 @@ class Push extends core_1.Command {
|
|
|
558
633
|
const fullErrorMessage = `Failed to process ${file}: ${errorMessage}`;
|
|
559
634
|
this.errors.push(fullErrorMessage);
|
|
560
635
|
this.error(fullErrorMessage, { exit: false });
|
|
636
|
+
this.log(' ⚠️ This error will cause all changes to be rolled back at the end of processing');
|
|
561
637
|
}
|
|
562
638
|
}
|
|
563
639
|
if (flags.verbose) {
|
|
@@ -640,7 +716,7 @@ class Push extends core_1.Command {
|
|
|
640
716
|
try {
|
|
641
717
|
const processedValue = await syncEngine.processFieldValue(value, baseDir, null, null);
|
|
642
718
|
if (verbose) {
|
|
643
|
-
this.log(` Setting ${field}: ${
|
|
719
|
+
this.log(` Setting ${field}: ${this.formatFieldValue(value)} -> ${this.formatFieldValue(processedValue)}`);
|
|
644
720
|
}
|
|
645
721
|
entity[field] = processedValue;
|
|
646
722
|
}
|
|
@@ -688,7 +764,7 @@ class Push extends core_1.Command {
|
|
|
688
764
|
const field = entity.GetFieldByName(fieldName);
|
|
689
765
|
const oldValue = field ? field.OldValue : undefined;
|
|
690
766
|
const newValue = changes[fieldName];
|
|
691
|
-
this.log(` ${fieldName}: ${oldValue} → ${newValue}`);
|
|
767
|
+
this.log(` ${fieldName}: ${this.formatFieldValue(oldValue)} → ${this.formatFieldValue(newValue)}`);
|
|
692
768
|
}
|
|
693
769
|
}
|
|
694
770
|
}
|
|
@@ -814,7 +890,7 @@ class Push extends core_1.Command {
|
|
|
814
890
|
try {
|
|
815
891
|
const processedValue = await syncEngine.processFieldValue(value, baseDir, parentEntity, rootEntity);
|
|
816
892
|
if (verbose) {
|
|
817
|
-
this.log(`${indent} Setting ${field}: ${
|
|
893
|
+
this.log(`${indent} Setting ${field}: ${this.formatFieldValue(value)} -> ${this.formatFieldValue(processedValue)}`);
|
|
818
894
|
}
|
|
819
895
|
entity[field] = processedValue;
|
|
820
896
|
}
|
|
@@ -859,7 +935,7 @@ class Push extends core_1.Command {
|
|
|
859
935
|
const field = entity.GetFieldByName(fieldName);
|
|
860
936
|
const oldValue = field ? field.OldValue : undefined;
|
|
861
937
|
const newValue = changes[fieldName];
|
|
862
|
-
this.log(`${indent} ${fieldName}: ${oldValue} → ${newValue}`);
|
|
938
|
+
this.log(`${indent} ${fieldName}: ${this.formatFieldValue(oldValue)} → ${this.formatFieldValue(newValue)}`);
|
|
863
939
|
}
|
|
864
940
|
}
|
|
865
941
|
}
|
|
@@ -973,6 +1049,20 @@ class Push extends core_1.Command {
|
|
|
973
1049
|
});
|
|
974
1050
|
return false; // not duplicate
|
|
975
1051
|
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Format field value for console display
|
|
1054
|
+
*/
|
|
1055
|
+
formatFieldValue(value, maxLength = 50) {
|
|
1056
|
+
// Convert value to string representation
|
|
1057
|
+
let strValue = JSON.stringify(value);
|
|
1058
|
+
// Trim the string
|
|
1059
|
+
strValue = strValue.trim();
|
|
1060
|
+
// If it's longer than maxLength, truncate and add ellipsis
|
|
1061
|
+
if (strValue.length > maxLength) {
|
|
1062
|
+
return strValue.substring(0, maxLength) + '...';
|
|
1063
|
+
}
|
|
1064
|
+
return strValue;
|
|
1065
|
+
}
|
|
976
1066
|
/**
|
|
977
1067
|
* Parse JSON file and track line numbers for array elements
|
|
978
1068
|
*/
|