@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 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` - Fails if not found
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"]
@@ -0,0 +1,15 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class FileReset extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ sections: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
7
+ 'dry-run': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
8
+ 'no-backup': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
9
+ yes: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
10
+ verbose: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
11
+ };
12
+ run(): Promise<void>;
13
+ private countSections;
14
+ private removeSections;
15
+ }
@@ -0,0 +1,221 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const core_1 = require("@oclif/core");
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const prompts_1 = require("@inquirer/prompts");
10
+ const ora_classic_1 = __importDefault(require("ora-classic"));
11
+ const fast_glob_1 = __importDefault(require("fast-glob"));
12
+ const chalk_1 = __importDefault(require("chalk"));
13
+ const config_1 = require("../../config");
14
+ const config_manager_1 = require("../../lib/config-manager");
15
+ class FileReset extends core_1.Command {
16
+ static description = 'Remove primaryKey and sync sections from metadata JSON files';
17
+ static examples = [
18
+ `<%= config.bin %> <%= command.id %>`,
19
+ `<%= config.bin %> <%= command.id %> --sections=primaryKey`,
20
+ `<%= config.bin %> <%= command.id %> --sections=sync`,
21
+ `<%= config.bin %> <%= command.id %> --dry-run`,
22
+ `<%= config.bin %> <%= command.id %> --no-backup`,
23
+ `<%= config.bin %> <%= command.id %> --yes`,
24
+ ];
25
+ static flags = {
26
+ sections: core_1.Flags.string({
27
+ description: 'Which sections to remove',
28
+ options: ['both', 'primaryKey', 'sync'],
29
+ default: 'both',
30
+ }),
31
+ 'dry-run': core_1.Flags.boolean({
32
+ description: 'Show what would be removed without actually removing'
33
+ }),
34
+ 'no-backup': core_1.Flags.boolean({
35
+ description: 'Skip creating backup files'
36
+ }),
37
+ yes: core_1.Flags.boolean({
38
+ char: 'y',
39
+ description: 'Skip confirmation prompt'
40
+ }),
41
+ verbose: core_1.Flags.boolean({
42
+ char: 'v',
43
+ description: 'Show detailed output'
44
+ }),
45
+ };
46
+ async run() {
47
+ const { flags } = await this.parse(FileReset);
48
+ const spinner = (0, ora_classic_1.default)();
49
+ try {
50
+ // Load sync config
51
+ const syncConfig = await (0, config_1.loadSyncConfig)(config_manager_1.configManager.getOriginalCwd());
52
+ if (!syncConfig) {
53
+ this.error('No .mj-sync.json found in current directory');
54
+ }
55
+ // Find all metadata JSON files
56
+ spinner.start('Finding metadata files');
57
+ const pattern = syncConfig.filePattern || '.*.json';
58
+ const files = await (0, fast_glob_1.default)(pattern, {
59
+ cwd: config_manager_1.configManager.getOriginalCwd(),
60
+ absolute: true,
61
+ ignore: ['.mj-sync.json', '.mj-folder.json'],
62
+ });
63
+ spinner.stop();
64
+ if (files.length === 0) {
65
+ this.log('No metadata files found');
66
+ return;
67
+ }
68
+ this.log(`Found ${files.length} metadata file${files.length === 1 ? '' : 's'}`);
69
+ // Count what will be removed
70
+ let filesWithPrimaryKey = 0;
71
+ let filesWithSync = 0;
72
+ let totalPrimaryKeys = 0;
73
+ let totalSyncs = 0;
74
+ for (const file of files) {
75
+ const content = await fs_extra_1.default.readJson(file);
76
+ const stats = this.countSections(content);
77
+ if (stats.primaryKeyCount > 0) {
78
+ filesWithPrimaryKey++;
79
+ totalPrimaryKeys += stats.primaryKeyCount;
80
+ }
81
+ if (stats.syncCount > 0) {
82
+ filesWithSync++;
83
+ totalSyncs += stats.syncCount;
84
+ }
85
+ }
86
+ // Show what will be removed
87
+ this.log('');
88
+ if (flags.sections === 'both' || flags.sections === 'primaryKey') {
89
+ this.log(`Will remove ${chalk_1.default.yellow(totalPrimaryKeys)} primaryKey section${totalPrimaryKeys === 1 ? '' : 's'} from ${chalk_1.default.yellow(filesWithPrimaryKey)} file${filesWithPrimaryKey === 1 ? '' : 's'}`);
90
+ }
91
+ if (flags.sections === 'both' || flags.sections === 'sync') {
92
+ this.log(`Will remove ${chalk_1.default.yellow(totalSyncs)} sync section${totalSyncs === 1 ? '' : 's'} from ${chalk_1.default.yellow(filesWithSync)} file${filesWithSync === 1 ? '' : 's'}`);
93
+ }
94
+ if (flags['dry-run']) {
95
+ this.log('');
96
+ this.log(chalk_1.default.cyan('Dry run mode - no files will be modified'));
97
+ if (flags.verbose) {
98
+ this.log('');
99
+ for (const file of files) {
100
+ const content = await fs_extra_1.default.readJson(file);
101
+ const stats = this.countSections(content);
102
+ if (stats.primaryKeyCount > 0 || stats.syncCount > 0) {
103
+ this.log(`${path_1.default.relative(config_manager_1.configManager.getOriginalCwd(), file)}:`);
104
+ if (stats.primaryKeyCount > 0) {
105
+ this.log(` - ${stats.primaryKeyCount} primaryKey section${stats.primaryKeyCount === 1 ? '' : 's'}`);
106
+ }
107
+ if (stats.syncCount > 0) {
108
+ this.log(` - ${stats.syncCount} sync section${stats.syncCount === 1 ? '' : 's'}`);
109
+ }
110
+ }
111
+ }
112
+ }
113
+ return;
114
+ }
115
+ // Confirm before proceeding
116
+ if (!flags.yes) {
117
+ const confirmed = await (0, prompts_1.confirm)({
118
+ message: 'Do you want to proceed?',
119
+ default: false,
120
+ });
121
+ if (!confirmed) {
122
+ this.log('Operation cancelled');
123
+ return;
124
+ }
125
+ }
126
+ // Process files
127
+ spinner.start('Processing files');
128
+ let processedFiles = 0;
129
+ let modifiedFiles = 0;
130
+ for (const file of files) {
131
+ processedFiles++;
132
+ const content = await fs_extra_1.default.readJson(file);
133
+ const originalContent = JSON.stringify(content);
134
+ // Remove sections
135
+ const cleanedContent = this.removeSections(content, flags.sections);
136
+ // Only write if content changed
137
+ if (JSON.stringify(cleanedContent) !== originalContent) {
138
+ // Create backup if requested
139
+ if (!flags['no-backup']) {
140
+ const backupPath = `${file}.backup`;
141
+ await fs_extra_1.default.writeJson(backupPath, content, { spaces: 2 });
142
+ }
143
+ // Write cleaned content
144
+ await fs_extra_1.default.writeJson(file, cleanedContent, { spaces: 2 });
145
+ modifiedFiles++;
146
+ if (flags.verbose) {
147
+ spinner.stop();
148
+ this.log(`✓ ${path_1.default.relative(config_manager_1.configManager.getOriginalCwd(), file)}`);
149
+ spinner.start('Processing files');
150
+ }
151
+ }
152
+ }
153
+ spinner.stop();
154
+ // Show summary
155
+ this.log('');
156
+ this.log(chalk_1.default.green(`✓ Reset complete`));
157
+ this.log(` Processed: ${processedFiles} file${processedFiles === 1 ? '' : 's'}`);
158
+ this.log(` Modified: ${modifiedFiles} file${modifiedFiles === 1 ? '' : 's'}`);
159
+ if (!flags['no-backup'] && modifiedFiles > 0) {
160
+ this.log(` Backups created: ${modifiedFiles}`);
161
+ }
162
+ }
163
+ catch (error) {
164
+ spinner.stop();
165
+ this.error(error instanceof Error ? error.message : String(error));
166
+ }
167
+ }
168
+ countSections(data) {
169
+ let primaryKeyCount = 0;
170
+ let syncCount = 0;
171
+ if (Array.isArray(data)) {
172
+ for (const item of data) {
173
+ const stats = this.countSections(item);
174
+ primaryKeyCount += stats.primaryKeyCount;
175
+ syncCount += stats.syncCount;
176
+ }
177
+ }
178
+ else if (data && typeof data === 'object') {
179
+ if ('primaryKey' in data)
180
+ primaryKeyCount++;
181
+ if ('sync' in data)
182
+ syncCount++;
183
+ // Check related entities
184
+ if (data.relatedEntities) {
185
+ for (const entityData of Object.values(data.relatedEntities)) {
186
+ const stats = this.countSections(entityData);
187
+ primaryKeyCount += stats.primaryKeyCount;
188
+ syncCount += stats.syncCount;
189
+ }
190
+ }
191
+ }
192
+ return { primaryKeyCount, syncCount };
193
+ }
194
+ removeSections(data, sections) {
195
+ if (Array.isArray(data)) {
196
+ return data.map(item => this.removeSections(item, sections));
197
+ }
198
+ else if (data && typeof data === 'object') {
199
+ const cleaned = { ...data };
200
+ // Remove specified sections
201
+ if (sections === 'both' || sections === 'primaryKey') {
202
+ delete cleaned.primaryKey;
203
+ }
204
+ if (sections === 'both' || sections === 'sync') {
205
+ delete cleaned.sync;
206
+ }
207
+ // Process related entities
208
+ if (cleaned.relatedEntities) {
209
+ const cleanedRelated = {};
210
+ for (const [entityName, entityData] of Object.entries(cleaned.relatedEntities)) {
211
+ cleanedRelated[entityName] = this.removeSections(entityData, sections);
212
+ }
213
+ cleaned.relatedEntities = cleanedRelated;
214
+ }
215
+ return cleaned;
216
+ }
217
+ return data;
218
+ }
219
+ }
220
+ exports.default = FileReset;
221
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/commands/file-reset/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { Command, Flags } from '@oclif/core';\nimport fs from 'fs-extra';\nimport path from 'path';\nimport { confirm } from '@inquirer/prompts';\nimport ora from 'ora-classic';\nimport fastGlob from 'fast-glob';\nimport chalk from 'chalk';\nimport { loadSyncConfig } from '../../config';\nimport { configManager } from '../../lib/config-manager';\n\nexport default class FileReset extends Command {\n static description = 'Remove primaryKey and sync sections from metadata JSON files';\n \n static examples = [\n `<%= config.bin %> <%= command.id %>`,\n `<%= config.bin %> <%= command.id %> --sections=primaryKey`,\n `<%= config.bin %> <%= command.id %> --sections=sync`,\n `<%= config.bin %> <%= command.id %> --dry-run`,\n `<%= config.bin %> <%= command.id %> --no-backup`,\n `<%= config.bin %> <%= command.id %> --yes`,\n ];\n \n static flags = {\n sections: Flags.string({\n description: 'Which sections to remove',\n options: ['both', 'primaryKey', 'sync'],\n default: 'both',\n }),\n 'dry-run': Flags.boolean({ \n description: 'Show what would be removed without actually removing' \n }),\n 'no-backup': Flags.boolean({ \n description: 'Skip creating backup files' \n }),\n yes: Flags.boolean({ \n char: 'y', \n description: 'Skip confirmation prompt' \n }),\n verbose: Flags.boolean({ \n char: 'v', \n description: 'Show detailed output' \n }),\n };\n \n async run(): Promise<void> {\n const { flags } = await this.parse(FileReset);\n const spinner = ora();\n \n try {\n // Load sync config\n const syncConfig = await loadSyncConfig(configManager.getOriginalCwd());\n if (!syncConfig) {\n this.error('No .mj-sync.json found in current directory');\n }\n \n // Find all metadata JSON files\n spinner.start('Finding metadata files');\n const pattern = syncConfig.filePattern || '.*.json';\n const files = await fastGlob(pattern, {\n cwd: configManager.getOriginalCwd(),\n absolute: true,\n ignore: ['.mj-sync.json', '.mj-folder.json'],\n });\n \n spinner.stop();\n \n if (files.length === 0) {\n this.log('No metadata files found');\n return;\n }\n \n this.log(`Found ${files.length} metadata file${files.length === 1 ? '' : 's'}`);\n \n // Count what will be removed\n let filesWithPrimaryKey = 0;\n let filesWithSync = 0;\n let totalPrimaryKeys = 0;\n let totalSyncs = 0;\n \n for (const file of files) {\n const content = await fs.readJson(file);\n const stats = this.countSections(content);\n if (stats.primaryKeyCount > 0) {\n filesWithPrimaryKey++;\n totalPrimaryKeys += stats.primaryKeyCount;\n }\n if (stats.syncCount > 0) {\n filesWithSync++;\n totalSyncs += stats.syncCount;\n }\n }\n \n // Show what will be removed\n this.log('');\n if (flags.sections === 'both' || flags.sections === 'primaryKey') {\n this.log(`Will remove ${chalk.yellow(totalPrimaryKeys)} primaryKey section${totalPrimaryKeys === 1 ? '' : 's'} from ${chalk.yellow(filesWithPrimaryKey)} file${filesWithPrimaryKey === 1 ? '' : 's'}`);\n }\n if (flags.sections === 'both' || flags.sections === 'sync') {\n this.log(`Will remove ${chalk.yellow(totalSyncs)} sync section${totalSyncs === 1 ? '' : 's'} from ${chalk.yellow(filesWithSync)} file${filesWithSync === 1 ? '' : 's'}`);\n }\n \n if (flags['dry-run']) {\n this.log('');\n this.log(chalk.cyan('Dry run mode - no files will be modified'));\n \n if (flags.verbose) {\n this.log('');\n for (const file of files) {\n const content = await fs.readJson(file);\n const stats = this.countSections(content);\n if (stats.primaryKeyCount > 0 || stats.syncCount > 0) {\n this.log(`${path.relative(configManager.getOriginalCwd(), file)}:`);\n if (stats.primaryKeyCount > 0) {\n this.log(` - ${stats.primaryKeyCount} primaryKey section${stats.primaryKeyCount === 1 ? '' : 's'}`);\n }\n if (stats.syncCount > 0) {\n this.log(` - ${stats.syncCount} sync section${stats.syncCount === 1 ? '' : 's'}`);\n }\n }\n }\n }\n return;\n }\n \n // Confirm before proceeding\n if (!flags.yes) {\n const confirmed = await confirm({\n message: 'Do you want to proceed?',\n default: false,\n });\n \n if (!confirmed) {\n this.log('Operation cancelled');\n return;\n }\n }\n \n // Process files\n spinner.start('Processing files');\n let processedFiles = 0;\n let modifiedFiles = 0;\n \n for (const file of files) {\n processedFiles++;\n const content = await fs.readJson(file);\n const originalContent = JSON.stringify(content);\n \n // Remove sections\n const cleanedContent = this.removeSections(content, flags.sections);\n \n // Only write if content changed\n if (JSON.stringify(cleanedContent) !== originalContent) {\n // Create backup if requested\n if (!flags['no-backup']) {\n const backupPath = `${file}.backup`;\n await fs.writeJson(backupPath, content, { spaces: 2 });\n }\n \n // Write cleaned content\n await fs.writeJson(file, cleanedContent, { spaces: 2 });\n modifiedFiles++;\n \n if (flags.verbose) {\n spinner.stop();\n this.log(`✓ ${path.relative(configManager.getOriginalCwd(), file)}`);\n spinner.start('Processing files');\n }\n }\n }\n \n spinner.stop();\n \n // Show summary\n this.log('');\n this.log(chalk.green(`✓ Reset complete`));\n this.log(` Processed: ${processedFiles} file${processedFiles === 1 ? '' : 's'}`);\n this.log(` Modified: ${modifiedFiles} file${modifiedFiles === 1 ? '' : 's'}`);\n if (!flags['no-backup'] && modifiedFiles > 0) {\n this.log(` Backups created: ${modifiedFiles}`);\n }\n \n } catch (error) {\n spinner.stop();\n this.error(error instanceof Error ? error.message : String(error));\n }\n }\n \n private countSections(data: any): { primaryKeyCount: number; syncCount: number } {\n let primaryKeyCount = 0;\n let syncCount = 0;\n \n if (Array.isArray(data)) {\n for (const item of data) {\n const stats = this.countSections(item);\n primaryKeyCount += stats.primaryKeyCount;\n syncCount += stats.syncCount;\n }\n } else if (data && typeof data === 'object') {\n if ('primaryKey' in data) primaryKeyCount++;\n if ('sync' in data) syncCount++;\n \n // Check related entities\n if (data.relatedEntities) {\n for (const entityData of Object.values(data.relatedEntities)) {\n const stats = this.countSections(entityData);\n primaryKeyCount += stats.primaryKeyCount;\n syncCount += stats.syncCount;\n }\n }\n }\n \n return { primaryKeyCount, syncCount };\n }\n \n private removeSections(data: any, sections: string): any {\n if (Array.isArray(data)) {\n return data.map(item => this.removeSections(item, sections));\n } else if (data && typeof data === 'object') {\n const cleaned = { ...data };\n \n // Remove specified sections\n if (sections === 'both' || sections === 'primaryKey') {\n delete cleaned.primaryKey;\n }\n if (sections === 'both' || sections === 'sync') {\n delete cleaned.sync;\n }\n \n // Process related entities\n if (cleaned.relatedEntities) {\n const cleanedRelated: any = {};\n for (const [entityName, entityData] of Object.entries(cleaned.relatedEntities)) {\n cleanedRelated[entityName] = this.removeSections(entityData, sections);\n }\n cleaned.relatedEntities = cleanedRelated;\n }\n \n return cleaned;\n }\n \n return data;\n }\n}"]}
@@ -3,6 +3,7 @@ export default class Push extends Command {
3
3
  static description: string;
4
4
  private warnings;
5
5
  private errors;
6
+ private processedRecords;
6
7
  static examples: string[];
7
8
  static flags: {
8
9
  dir: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
@@ -17,4 +18,24 @@ export default class Push extends Command {
17
18
  private processJsonFiles;
18
19
  private pushRecord;
19
20
  private processRelatedEntities;
21
+ /**
22
+ * Generate a unique tracking key for a record based on entity name and primary key values
23
+ */
24
+ private generateRecordKey;
25
+ /**
26
+ * Check if a record has already been processed and warn if duplicate
27
+ */
28
+ private checkAndTrackRecord;
29
+ /**
30
+ * Format field value for console display
31
+ */
32
+ private formatFieldValue;
33
+ /**
34
+ * Parse JSON file and track line numbers for array elements
35
+ */
36
+ private parseJsonWithLineNumbers;
37
+ /**
38
+ * Format file location with clickable link for VSCode
39
+ */
40
+ private formatFileLocation;
20
41
  }