@memberjunction/metadata-sync 2.55.0 ā 2.57.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 +92 -51
- package/dist/index.d.ts +21 -1
- package/dist/index.js +43 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/file-backup-manager.js +2 -2
- package/dist/lib/file-backup-manager.js.map +1 -1
- package/dist/lib/provider-utils.d.ts +2 -2
- package/dist/lib/provider-utils.js.map +1 -1
- package/dist/lib/sql-logger.d.ts +44 -0
- package/dist/lib/sql-logger.js +140 -0
- package/dist/lib/sql-logger.js.map +1 -0
- package/dist/lib/sync-engine.d.ts +25 -0
- package/dist/lib/sync-engine.js +72 -2
- package/dist/lib/sync-engine.js.map +1 -1
- package/dist/lib/transaction-manager.d.ts +35 -0
- package/dist/lib/transaction-manager.js +100 -0
- package/dist/lib/transaction-manager.js.map +1 -0
- package/dist/services/FileResetService.d.ts +30 -0
- package/dist/services/FileResetService.js +183 -0
- package/dist/services/FileResetService.js.map +1 -0
- package/dist/services/InitService.d.ts +17 -0
- package/dist/services/InitService.js +118 -0
- package/dist/services/InitService.js.map +1 -0
- package/dist/services/PullService.d.ts +45 -0
- package/dist/services/PullService.js +564 -0
- package/dist/services/PullService.js.map +1 -0
- package/dist/services/PushService.d.ts +47 -0
- package/dist/services/PushService.js +558 -0
- package/dist/services/PushService.js.map +1 -0
- package/dist/services/StatusService.d.ts +32 -0
- package/dist/services/StatusService.js +138 -0
- package/dist/services/StatusService.js.map +1 -0
- package/dist/services/WatchService.d.ts +34 -0
- package/dist/services/WatchService.js +296 -0
- package/dist/services/WatchService.js.map +1 -0
- package/dist/services/index.d.ts +16 -0
- package/dist/services/index.js +28 -0
- package/dist/services/index.js.map +1 -0
- package/package.json +14 -45
- package/bin/debug.js +0 -7
- package/bin/run +0 -17
- package/bin/run.js +0 -6
- package/dist/commands/file-reset/index.d.ts +0 -15
- package/dist/commands/file-reset/index.js +0 -221
- package/dist/commands/file-reset/index.js.map +0 -1
- package/dist/commands/init/index.d.ts +0 -7
- package/dist/commands/init/index.js +0 -155
- package/dist/commands/init/index.js.map +0 -1
- package/dist/commands/pull/index.d.ts +0 -246
- package/dist/commands/pull/index.js +0 -1448
- package/dist/commands/pull/index.js.map +0 -1
- package/dist/commands/push/index.d.ts +0 -41
- package/dist/commands/push/index.js +0 -1129
- package/dist/commands/push/index.js.map +0 -1
- package/dist/commands/status/index.d.ts +0 -10
- package/dist/commands/status/index.js +0 -199
- package/dist/commands/status/index.js.map +0 -1
- package/dist/commands/validate/index.d.ts +0 -15
- package/dist/commands/validate/index.js +0 -149
- package/dist/commands/validate/index.js.map +0 -1
- package/dist/commands/watch/index.d.ts +0 -15
- package/dist/commands/watch/index.js +0 -300
- package/dist/commands/watch/index.js.map +0 -1
- package/dist/hooks/init.d.ts +0 -3
- package/dist/hooks/init.js +0 -59
- package/dist/hooks/init.js.map +0 -1
- package/oclif.manifest.json +0 -376
|
@@ -0,0 +1,558 @@
|
|
|
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
|
+
exports.PushService = void 0;
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
10
|
+
const core_1 = require("@memberjunction/core");
|
|
11
|
+
const config_1 = require("../config");
|
|
12
|
+
const file_backup_manager_1 = require("../lib/file-backup-manager");
|
|
13
|
+
const config_manager_1 = require("../lib/config-manager");
|
|
14
|
+
const sql_logger_1 = require("../lib/sql-logger");
|
|
15
|
+
const transaction_manager_1 = require("../lib/transaction-manager");
|
|
16
|
+
class PushService {
|
|
17
|
+
syncEngine;
|
|
18
|
+
contextUser;
|
|
19
|
+
warnings = [];
|
|
20
|
+
processedRecords = new Map();
|
|
21
|
+
syncConfig;
|
|
22
|
+
constructor(syncEngine, contextUser) {
|
|
23
|
+
this.syncEngine = syncEngine;
|
|
24
|
+
this.contextUser = contextUser;
|
|
25
|
+
}
|
|
26
|
+
async push(options, callbacks) {
|
|
27
|
+
this.warnings = [];
|
|
28
|
+
this.processedRecords.clear();
|
|
29
|
+
const fileBackupManager = new file_backup_manager_1.FileBackupManager();
|
|
30
|
+
// Load sync config for SQL logging settings and autoCreateMissingRecords flag
|
|
31
|
+
// If dir option is specified, load from that directory, otherwise use original CWD
|
|
32
|
+
const configDir = options.dir ? path_1.default.resolve(config_manager_1.configManager.getOriginalCwd(), options.dir) : config_manager_1.configManager.getOriginalCwd();
|
|
33
|
+
this.syncConfig = await (0, config_1.loadSyncConfig)(configDir);
|
|
34
|
+
if (options.verbose) {
|
|
35
|
+
callbacks?.onLog?.(`Original working directory: ${config_manager_1.configManager.getOriginalCwd()}`);
|
|
36
|
+
callbacks?.onLog?.(`Config directory (with dir option): ${configDir}`);
|
|
37
|
+
callbacks?.onLog?.(`Config file path: ${path_1.default.join(configDir, '.mj-sync.json')}`);
|
|
38
|
+
callbacks?.onLog?.(`Full sync config loaded: ${JSON.stringify(this.syncConfig, null, 2)}`);
|
|
39
|
+
callbacks?.onLog?.(`SQL logging config: ${JSON.stringify(this.syncConfig?.sqlLogging)}`);
|
|
40
|
+
}
|
|
41
|
+
const sqlLogger = new sql_logger_1.SQLLogger(this.syncConfig);
|
|
42
|
+
const transactionManager = new transaction_manager_1.TransactionManager(sqlLogger);
|
|
43
|
+
if (options.verbose) {
|
|
44
|
+
callbacks?.onLog?.(`SQLLogger enabled status: ${sqlLogger.enabled}`);
|
|
45
|
+
}
|
|
46
|
+
// Setup SQL logging session with the provider if enabled
|
|
47
|
+
let sqlLoggingSession = null;
|
|
48
|
+
try {
|
|
49
|
+
// Initialize SQL logger if enabled and not dry-run
|
|
50
|
+
if (sqlLogger.enabled && !options.dryRun) {
|
|
51
|
+
const provider = core_1.Metadata.Provider;
|
|
52
|
+
if (options.verbose) {
|
|
53
|
+
callbacks?.onLog?.(`SQL logging enabled: ${sqlLogger.enabled}`);
|
|
54
|
+
callbacks?.onLog?.(`Provider type: ${provider?.constructor?.name || 'Unknown'}`);
|
|
55
|
+
callbacks?.onLog?.(`Has CreateSqlLogger: ${typeof provider?.CreateSqlLogger === 'function'}`);
|
|
56
|
+
}
|
|
57
|
+
if (provider && typeof provider.CreateSqlLogger === 'function') {
|
|
58
|
+
// Generate filename with timestamp
|
|
59
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
60
|
+
const filename = this.syncConfig.sqlLogging?.formatAsMigration
|
|
61
|
+
? `MetadataSync_Push_${timestamp}.sql`
|
|
62
|
+
: `push_${timestamp}.sql`;
|
|
63
|
+
// Use .sql-log-push directory in the config directory (where sync was initiated)
|
|
64
|
+
const outputDir = path_1.default.join(configDir, this.syncConfig?.sqlLogging?.outputDirectory || './sql-log-push');
|
|
65
|
+
const filepath = path_1.default.join(outputDir, filename);
|
|
66
|
+
// Ensure the directory exists
|
|
67
|
+
await fs_extra_1.default.ensureDir(path_1.default.dirname(filepath));
|
|
68
|
+
// Create the SQL logging session
|
|
69
|
+
sqlLoggingSession = await provider.CreateSqlLogger(filepath, {
|
|
70
|
+
formatAsMigration: this.syncConfig.sqlLogging?.formatAsMigration || false,
|
|
71
|
+
description: 'MetadataSync push operation',
|
|
72
|
+
statementTypes: "mutations",
|
|
73
|
+
prettyPrint: true,
|
|
74
|
+
});
|
|
75
|
+
if (options.verbose) {
|
|
76
|
+
callbacks?.onLog?.(`š SQL logging enabled: ${filepath}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
if (options.verbose) {
|
|
81
|
+
callbacks?.onWarn?.('SQL logging requested but provider does not support it');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Find entity directories to process
|
|
86
|
+
const entityDirs = this.findEntityDirectories(configDir);
|
|
87
|
+
if (entityDirs.length === 0) {
|
|
88
|
+
throw new Error('No entity directories found');
|
|
89
|
+
}
|
|
90
|
+
if (options.verbose) {
|
|
91
|
+
callbacks?.onLog?.(`Found ${entityDirs.length} entity ${entityDirs.length === 1 ? 'directory' : 'directories'} to process`);
|
|
92
|
+
}
|
|
93
|
+
// Initialize file backup manager (unless in dry-run mode)
|
|
94
|
+
if (!options.dryRun) {
|
|
95
|
+
await fileBackupManager.initialize();
|
|
96
|
+
if (options.verbose) {
|
|
97
|
+
callbacks?.onLog?.('š File backup manager initialized');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Process each entity directory
|
|
101
|
+
let totalCreated = 0;
|
|
102
|
+
let totalUpdated = 0;
|
|
103
|
+
let totalUnchanged = 0;
|
|
104
|
+
let totalErrors = 0;
|
|
105
|
+
// Begin transaction if not in dry-run mode
|
|
106
|
+
if (!options.dryRun) {
|
|
107
|
+
await transactionManager.beginTransaction();
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
for (const entityDir of entityDirs) {
|
|
111
|
+
const entityConfig = await (0, config_1.loadEntityConfig)(entityDir);
|
|
112
|
+
if (!entityConfig) {
|
|
113
|
+
const warning = `Skipping ${entityDir} - no valid entity configuration`;
|
|
114
|
+
this.warnings.push(warning);
|
|
115
|
+
callbacks?.onWarn?.(warning);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (options.verbose) {
|
|
119
|
+
callbacks?.onLog?.(`\nProcessing ${entityConfig.entity} in ${entityDir}`);
|
|
120
|
+
}
|
|
121
|
+
const result = await this.processEntityDirectory(entityDir, entityConfig, options, fileBackupManager, callbacks, sqlLogger);
|
|
122
|
+
// Show per-directory summary
|
|
123
|
+
const dirName = path_1.default.relative(process.cwd(), entityDir) || '.';
|
|
124
|
+
const dirTotal = result.created + result.updated + result.unchanged;
|
|
125
|
+
if (dirTotal > 0 || result.errors > 0) {
|
|
126
|
+
callbacks?.onLog?.(`\nš ${dirName}:`);
|
|
127
|
+
callbacks?.onLog?.(` Total processed: ${dirTotal} unique records`);
|
|
128
|
+
if (result.created > 0) {
|
|
129
|
+
callbacks?.onLog?.(` ā Created: ${result.created}`);
|
|
130
|
+
}
|
|
131
|
+
if (result.updated > 0) {
|
|
132
|
+
callbacks?.onLog?.(` ā Updated: ${result.updated}`);
|
|
133
|
+
}
|
|
134
|
+
if (result.unchanged > 0) {
|
|
135
|
+
callbacks?.onLog?.(` - Unchanged: ${result.unchanged}`);
|
|
136
|
+
}
|
|
137
|
+
if (result.errors > 0) {
|
|
138
|
+
callbacks?.onLog?.(` ā Errors: ${result.errors}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
totalCreated += result.created;
|
|
142
|
+
totalUpdated += result.updated;
|
|
143
|
+
totalUnchanged += result.unchanged;
|
|
144
|
+
totalErrors += result.errors;
|
|
145
|
+
}
|
|
146
|
+
// Commit transaction if successful
|
|
147
|
+
if (!options.dryRun && totalErrors === 0) {
|
|
148
|
+
await transactionManager.commitTransaction();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
// Rollback transaction on error
|
|
153
|
+
if (!options.dryRun) {
|
|
154
|
+
await transactionManager.rollbackTransaction();
|
|
155
|
+
}
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
// Commit file backups if successful and not in dry-run mode
|
|
159
|
+
if (!options.dryRun && totalErrors === 0) {
|
|
160
|
+
await fileBackupManager.cleanup();
|
|
161
|
+
if (options.verbose) {
|
|
162
|
+
callbacks?.onLog?.('ā
File backups committed');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Close SQL logging session if it was created
|
|
166
|
+
let sqlLogPath;
|
|
167
|
+
if (sqlLoggingSession) {
|
|
168
|
+
sqlLogPath = sqlLoggingSession.filePath;
|
|
169
|
+
await sqlLoggingSession.dispose();
|
|
170
|
+
if (options.verbose) {
|
|
171
|
+
callbacks?.onLog?.(`š SQL log written to: ${sqlLogPath}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
created: totalCreated,
|
|
176
|
+
updated: totalUpdated,
|
|
177
|
+
unchanged: totalUnchanged,
|
|
178
|
+
errors: totalErrors,
|
|
179
|
+
warnings: this.warnings,
|
|
180
|
+
sqlLogPath
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
// Rollback file backups on error
|
|
185
|
+
if (!options.dryRun) {
|
|
186
|
+
try {
|
|
187
|
+
await fileBackupManager.rollback();
|
|
188
|
+
callbacks?.onWarn?.('File backups rolled back due to error');
|
|
189
|
+
}
|
|
190
|
+
catch (rollbackError) {
|
|
191
|
+
callbacks?.onWarn?.(`Failed to rollback file backups: ${rollbackError}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// Close SQL logging session on error
|
|
195
|
+
if (sqlLoggingSession) {
|
|
196
|
+
try {
|
|
197
|
+
await sqlLoggingSession.dispose();
|
|
198
|
+
}
|
|
199
|
+
catch (disposeError) {
|
|
200
|
+
callbacks?.onWarn?.(`Failed to close SQL logging session: ${disposeError}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async processEntityDirectory(entityDir, entityConfig, options, fileBackupManager, callbacks, sqlLogger) {
|
|
207
|
+
let created = 0;
|
|
208
|
+
let updated = 0;
|
|
209
|
+
let unchanged = 0;
|
|
210
|
+
let errors = 0;
|
|
211
|
+
// Find all JSON files in the directory
|
|
212
|
+
const pattern = entityConfig.filePattern || '*.json';
|
|
213
|
+
const files = await (0, fast_glob_1.default)(pattern, {
|
|
214
|
+
cwd: entityDir,
|
|
215
|
+
absolute: true,
|
|
216
|
+
onlyFiles: true,
|
|
217
|
+
dot: true,
|
|
218
|
+
ignore: ['**/node_modules/**', '**/.mj-*.json']
|
|
219
|
+
});
|
|
220
|
+
if (options.verbose) {
|
|
221
|
+
callbacks?.onLog?.(`Found ${files.length} files to process`);
|
|
222
|
+
}
|
|
223
|
+
// Process each file
|
|
224
|
+
for (const filePath of files) {
|
|
225
|
+
try {
|
|
226
|
+
// Backup the file before any modifications (unless dry-run)
|
|
227
|
+
if (!options.dryRun) {
|
|
228
|
+
await fileBackupManager.backupFile(filePath);
|
|
229
|
+
}
|
|
230
|
+
const fileData = await fs_extra_1.default.readJson(filePath);
|
|
231
|
+
const records = Array.isArray(fileData) ? fileData : [fileData];
|
|
232
|
+
const isArray = Array.isArray(fileData);
|
|
233
|
+
for (let i = 0; i < records.length; i++) {
|
|
234
|
+
const recordData = records[i];
|
|
235
|
+
if (!this.isValidRecordData(recordData)) {
|
|
236
|
+
callbacks?.onWarn?.(`Invalid record format in ${filePath}${isArray ? ` at index ${i}` : ''}`);
|
|
237
|
+
errors++;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
// For arrays, work with a deep copy to avoid modifying the original
|
|
242
|
+
const recordToProcess = isArray ? JSON.parse(JSON.stringify(recordData)) : recordData;
|
|
243
|
+
const result = await this.processRecord(recordToProcess, entityConfig, entityDir, options, callbacks, filePath, isArray ? i : undefined);
|
|
244
|
+
if (result === 'created')
|
|
245
|
+
created++;
|
|
246
|
+
else if (result === 'updated')
|
|
247
|
+
updated++;
|
|
248
|
+
else if (result === 'unchanged')
|
|
249
|
+
unchanged++;
|
|
250
|
+
// For arrays, update the original record's primaryKey and sync only
|
|
251
|
+
if (isArray) {
|
|
252
|
+
// Update primaryKey if it exists (for new records)
|
|
253
|
+
if (recordToProcess.primaryKey) {
|
|
254
|
+
records[i].primaryKey = recordToProcess.primaryKey;
|
|
255
|
+
}
|
|
256
|
+
// Update sync metadata only if it was updated (dirty records only)
|
|
257
|
+
if (recordToProcess.sync) {
|
|
258
|
+
records[i].sync = recordToProcess.sync;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// Track processed record
|
|
262
|
+
const recordKey = this.getRecordKey(recordData, entityConfig.entity);
|
|
263
|
+
this.processedRecords.set(recordKey, {
|
|
264
|
+
filePath,
|
|
265
|
+
arrayIndex: isArray ? i : undefined,
|
|
266
|
+
lineNumber: i + 1 // Simple line number approximation
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
catch (recordError) {
|
|
270
|
+
const errorMsg = `Error processing record in ${filePath}${isArray ? ` at index ${i}` : ''}: ${recordError}`;
|
|
271
|
+
callbacks?.onError?.(errorMsg);
|
|
272
|
+
errors++;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// Write back the entire file if it's an array (after processing all records)
|
|
276
|
+
if (isArray && !options.dryRun) {
|
|
277
|
+
await fs_extra_1.default.writeJson(filePath, records, { spaces: 2 });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
catch (fileError) {
|
|
281
|
+
const errorMsg = `Error reading file ${filePath}: ${fileError}`;
|
|
282
|
+
callbacks?.onError?.(errorMsg);
|
|
283
|
+
errors++;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return { created, updated, unchanged, errors };
|
|
287
|
+
}
|
|
288
|
+
async processRecord(recordData, entityConfig, entityDir, options, callbacks, filePath, arrayIndex) {
|
|
289
|
+
const metadata = new core_1.Metadata();
|
|
290
|
+
// Get or create entity instance
|
|
291
|
+
let entity = await metadata.GetEntityObject(entityConfig.entity, this.contextUser);
|
|
292
|
+
if (!entity) {
|
|
293
|
+
throw new Error(`Failed to create entity object for ${entityConfig.entity}`);
|
|
294
|
+
}
|
|
295
|
+
// Apply defaults from configuration
|
|
296
|
+
const defaults = { ...entityConfig.defaults };
|
|
297
|
+
// Build full record data - keep original values for file writing
|
|
298
|
+
const originalFields = { ...recordData.fields };
|
|
299
|
+
const fullData = {
|
|
300
|
+
...defaults,
|
|
301
|
+
...recordData.fields
|
|
302
|
+
};
|
|
303
|
+
// Process field values for database operations
|
|
304
|
+
const processedData = {};
|
|
305
|
+
for (const [fieldName, fieldValue] of Object.entries(fullData)) {
|
|
306
|
+
const processedValue = await this.syncEngine.processFieldValue(fieldValue, entityDir, null, // parentRecord
|
|
307
|
+
null // rootRecord
|
|
308
|
+
);
|
|
309
|
+
processedData[fieldName] = processedValue;
|
|
310
|
+
}
|
|
311
|
+
// Check if record exists
|
|
312
|
+
const primaryKey = recordData.primaryKey;
|
|
313
|
+
let exists = false;
|
|
314
|
+
let isNew = false;
|
|
315
|
+
if (primaryKey && Object.keys(primaryKey).length > 0) {
|
|
316
|
+
// Try to load existing record
|
|
317
|
+
const compositeKey = new core_1.CompositeKey();
|
|
318
|
+
compositeKey.LoadFromSimpleObject(primaryKey);
|
|
319
|
+
exists = await entity.InnerLoad(compositeKey);
|
|
320
|
+
// Check autoCreateMissingRecords flag if record not found
|
|
321
|
+
if (!exists) {
|
|
322
|
+
const autoCreate = this.syncConfig?.push?.autoCreateMissingRecords ?? false;
|
|
323
|
+
const pkDisplay = Object.entries(primaryKey)
|
|
324
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
325
|
+
.join(', ');
|
|
326
|
+
if (!autoCreate) {
|
|
327
|
+
const warning = `Record not found: ${entityConfig.entity} with primaryKey {${pkDisplay}}. To auto-create missing records, set push.autoCreateMissingRecords=true in .mj-sync.json`;
|
|
328
|
+
this.warnings.push(warning);
|
|
329
|
+
callbacks?.onWarn?.(warning);
|
|
330
|
+
return 'error';
|
|
331
|
+
}
|
|
332
|
+
else if (options.verbose) {
|
|
333
|
+
callbacks?.onLog?.(`Auto-creating missing ${entityConfig.entity} record with primaryKey {${pkDisplay}}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (options.dryRun) {
|
|
338
|
+
if (exists) {
|
|
339
|
+
callbacks?.onLog?.(`[DRY RUN] Would update ${entityConfig.entity} record`);
|
|
340
|
+
return 'updated';
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
callbacks?.onLog?.(`[DRY RUN] Would create ${entityConfig.entity} record`);
|
|
344
|
+
return 'created';
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (!exists) {
|
|
348
|
+
entity.NewRecord(); // make sure our record starts out fresh
|
|
349
|
+
isNew = true;
|
|
350
|
+
// Set primary key values for new records if provided, this is important for the auto-create logic
|
|
351
|
+
if (primaryKey) {
|
|
352
|
+
for (const [pkField, pkValue] of Object.entries(primaryKey)) {
|
|
353
|
+
entity.Set(pkField, pkValue);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// Set field values
|
|
358
|
+
for (const [fieldName, fieldValue] of Object.entries(processedData)) {
|
|
359
|
+
entity.Set(fieldName, fieldValue);
|
|
360
|
+
}
|
|
361
|
+
// Handle related entities
|
|
362
|
+
if (recordData.relatedEntities) {
|
|
363
|
+
// Store related entities to process after parent save
|
|
364
|
+
entity.__pendingRelatedEntities = recordData.relatedEntities;
|
|
365
|
+
}
|
|
366
|
+
// Check if the record is actually dirty before considering it changed
|
|
367
|
+
let isDirty = entity.Dirty;
|
|
368
|
+
// Also check if file content has changed (for @file references)
|
|
369
|
+
if (!isDirty && !isNew && recordData.sync) {
|
|
370
|
+
const currentChecksum = await this.syncEngine.calculateChecksumWithFileContent(originalFields, entityDir);
|
|
371
|
+
if (currentChecksum !== recordData.sync.checksum) {
|
|
372
|
+
isDirty = true;
|
|
373
|
+
if (options.verbose) {
|
|
374
|
+
callbacks?.onLog?.(`š File content changed for ${entityConfig.entity} record (checksum mismatch)`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
// If updating an existing record that's dirty, show what changed
|
|
379
|
+
if (!isNew && isDirty) {
|
|
380
|
+
const changes = entity.GetChangesSinceLastSave();
|
|
381
|
+
const changeKeys = Object.keys(changes);
|
|
382
|
+
if (changeKeys.length > 0) {
|
|
383
|
+
// Get primary key info for display
|
|
384
|
+
const entityInfo = this.syncEngine.getEntityInfo(entityConfig.entity);
|
|
385
|
+
const primaryKeyDisplay = [];
|
|
386
|
+
if (entityInfo) {
|
|
387
|
+
for (const pk of entityInfo.PrimaryKeys) {
|
|
388
|
+
primaryKeyDisplay.push(`${pk.Name}: ${entity.Get(pk.Name)}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
callbacks?.onLog?.(`š Updating ${entityConfig.entity} record:`);
|
|
392
|
+
if (primaryKeyDisplay.length > 0) {
|
|
393
|
+
callbacks?.onLog?.(` Primary Key: ${primaryKeyDisplay.join(', ')}`);
|
|
394
|
+
}
|
|
395
|
+
callbacks?.onLog?.(` Changes:`);
|
|
396
|
+
for (const fieldName of changeKeys) {
|
|
397
|
+
const field = entity.GetFieldByName(fieldName);
|
|
398
|
+
const oldValue = field ? field.OldValue : undefined;
|
|
399
|
+
const newValue = changes[fieldName];
|
|
400
|
+
callbacks?.onLog?.(` ${fieldName}: ${this.formatFieldValue(oldValue)} ā ${this.formatFieldValue(newValue)}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
// Save the record (always call Save, but track if it was actually dirty)
|
|
405
|
+
const saveResult = await entity.Save();
|
|
406
|
+
if (!saveResult) {
|
|
407
|
+
throw new Error(`Failed to save ${entityConfig.entity} record: ${entity.LatestResult?.Message || 'Unknown error'}`);
|
|
408
|
+
}
|
|
409
|
+
// Update primaryKey for new records
|
|
410
|
+
if (isNew) {
|
|
411
|
+
const entityInfo = this.syncEngine.getEntityInfo(entityConfig.entity);
|
|
412
|
+
if (entityInfo) {
|
|
413
|
+
const newPrimaryKey = {};
|
|
414
|
+
for (const pk of entityInfo.PrimaryKeys) {
|
|
415
|
+
newPrimaryKey[pk.Name] = entity.Get(pk.Name);
|
|
416
|
+
}
|
|
417
|
+
recordData.primaryKey = newPrimaryKey;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// Only update sync metadata if the record was actually dirty (changed)
|
|
421
|
+
if (isNew || isDirty) {
|
|
422
|
+
recordData.sync = {
|
|
423
|
+
lastModified: new Date().toISOString(),
|
|
424
|
+
checksum: await this.syncEngine.calculateChecksumWithFileContent(originalFields, entityDir)
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
// Restore original field values to preserve @ references
|
|
428
|
+
recordData.fields = originalFields;
|
|
429
|
+
// Write back to file only if it's a single record (not part of an array)
|
|
430
|
+
if (filePath && arrayIndex === undefined && !options.dryRun) {
|
|
431
|
+
await fs_extra_1.default.writeJson(filePath, recordData, { spaces: 2 });
|
|
432
|
+
}
|
|
433
|
+
// Process related entities after parent save
|
|
434
|
+
if (recordData.relatedEntities) {
|
|
435
|
+
await this.processRelatedEntities(entity, recordData.relatedEntities, entityDir, options, callbacks);
|
|
436
|
+
}
|
|
437
|
+
// Return the actual status based on whether the record was dirty
|
|
438
|
+
if (isNew) {
|
|
439
|
+
return 'created';
|
|
440
|
+
}
|
|
441
|
+
else if (isDirty) {
|
|
442
|
+
return 'updated';
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
return 'unchanged';
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
async processRelatedEntities(parentEntity, relatedEntities, entityDir, options, callbacks) {
|
|
449
|
+
// TODO: Complete implementation for processing related entities
|
|
450
|
+
// This is a simplified version - full implementation would:
|
|
451
|
+
// 1. Create entity objects for each related entity type
|
|
452
|
+
// 2. Apply field values with proper parent/root references
|
|
453
|
+
// 3. Save related entities with proper error handling
|
|
454
|
+
// 4. Support nested related entities recursively
|
|
455
|
+
for (const [key, records] of Object.entries(relatedEntities)) {
|
|
456
|
+
for (const relatedRecord of records) {
|
|
457
|
+
// Process @parent references but DON'T modify the original fields
|
|
458
|
+
const processedFields = {};
|
|
459
|
+
for (const [fieldName, fieldValue] of Object.entries(relatedRecord.fields)) {
|
|
460
|
+
if (typeof fieldValue === 'string' && fieldValue.startsWith('@parent:')) {
|
|
461
|
+
const parentField = fieldValue.substring(8);
|
|
462
|
+
processedFields[fieldName] = parentEntity.Get(parentField);
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
processedFields[fieldName] = await this.syncEngine.processFieldValue(fieldValue, entityDir, parentEntity, null);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
// TODO: Actually save the related entity with processedFields
|
|
469
|
+
// For now, we're just processing the values but not saving
|
|
470
|
+
// This needs to be implemented to actually create/update the related entities
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
isValidRecordData(data) {
|
|
475
|
+
return data &&
|
|
476
|
+
typeof data === 'object' &&
|
|
477
|
+
'fields' in data &&
|
|
478
|
+
typeof data.fields === 'object';
|
|
479
|
+
}
|
|
480
|
+
getRecordKey(recordData, entityName) {
|
|
481
|
+
if (recordData.primaryKey) {
|
|
482
|
+
const keys = Object.entries(recordData.primaryKey)
|
|
483
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
484
|
+
.map(([k, v]) => `${k}:${v}`)
|
|
485
|
+
.join('|');
|
|
486
|
+
return `${entityName}|${keys}`;
|
|
487
|
+
}
|
|
488
|
+
// Generate a key from fields if no primary key
|
|
489
|
+
const fieldKeys = Object.keys(recordData.fields).sort().join(',');
|
|
490
|
+
return `${entityName}|fields:${fieldKeys}`;
|
|
491
|
+
}
|
|
492
|
+
formatFieldValue(value, maxLength = 50) {
|
|
493
|
+
let strValue = JSON.stringify(value);
|
|
494
|
+
strValue = strValue.trim();
|
|
495
|
+
if (strValue.length > maxLength) {
|
|
496
|
+
return strValue.substring(0, maxLength) + '...';
|
|
497
|
+
}
|
|
498
|
+
return strValue;
|
|
499
|
+
}
|
|
500
|
+
findEntityDirectories(baseDir, specificDir) {
|
|
501
|
+
const dirs = [];
|
|
502
|
+
if (specificDir) {
|
|
503
|
+
// Process specific directory
|
|
504
|
+
const fullPath = path_1.default.resolve(baseDir, specificDir);
|
|
505
|
+
if (fs_extra_1.default.existsSync(fullPath) && fs_extra_1.default.statSync(fullPath).isDirectory()) {
|
|
506
|
+
// Check if this directory has an entity configuration
|
|
507
|
+
const configPath = path_1.default.join(fullPath, '.mj-sync.json');
|
|
508
|
+
if (fs_extra_1.default.existsSync(configPath)) {
|
|
509
|
+
try {
|
|
510
|
+
const config = fs_extra_1.default.readJsonSync(configPath);
|
|
511
|
+
if (config.entity) {
|
|
512
|
+
// It's an entity directory, add it
|
|
513
|
+
dirs.push(fullPath);
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
// It's a container directory, search its subdirectories
|
|
517
|
+
this.findEntityDirectoriesRecursive(fullPath, dirs);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
// Invalid config, skip
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
// Find all entity directories
|
|
528
|
+
this.findEntityDirectoriesRecursive(baseDir, dirs);
|
|
529
|
+
}
|
|
530
|
+
return dirs;
|
|
531
|
+
}
|
|
532
|
+
findEntityDirectoriesRecursive(dir, dirs) {
|
|
533
|
+
const entries = fs_extra_1.default.readdirSync(dir, { withFileTypes: true });
|
|
534
|
+
for (const entry of entries) {
|
|
535
|
+
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
|
536
|
+
const fullPath = path_1.default.join(dir, entry.name);
|
|
537
|
+
const configPath = path_1.default.join(fullPath, '.mj-sync.json');
|
|
538
|
+
if (fs_extra_1.default.existsSync(configPath)) {
|
|
539
|
+
try {
|
|
540
|
+
const config = fs_extra_1.default.readJsonSync(configPath);
|
|
541
|
+
if (config.entity) {
|
|
542
|
+
dirs.push(fullPath);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
catch {
|
|
546
|
+
// Skip invalid config files
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
// Recurse into subdirectories
|
|
551
|
+
this.findEntityDirectoriesRecursive(fullPath, dirs);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
exports.PushService = PushService;
|
|
558
|
+
//# sourceMappingURL=PushService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PushService.js","sourceRoot":"","sources":["../../src/services/PushService.ts"],"names":[],"mappings":"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","sourcesContent":["import fs from 'fs-extra';\nimport path from 'path';\nimport fastGlob from 'fast-glob';\nimport { BaseEntity, LogStatus, Metadata, UserInfo, CompositeKey } from '@memberjunction/core';\nimport { SyncEngine, RecordData } from '../lib/sync-engine';\nimport { loadEntityConfig, loadSyncConfig } from '../config';\nimport { FileBackupManager } from '../lib/file-backup-manager';\nimport { configManager } from '../lib/config-manager';\nimport { SQLLogger } from '../lib/sql-logger';\nimport { TransactionManager } from '../lib/transaction-manager';\nimport type { SqlLoggingSession, SQLServerDataProvider } from '@memberjunction/sqlserver-dataprovider';\n\nexport interface PushOptions {\n dir?: string;\n dryRun?: boolean;\n verbose?: boolean;\n noValidate?: boolean;\n}\n\nexport interface PushCallbacks {\n onProgress?: (message: string) => void;\n onSuccess?: (message: string) => void;\n onError?: (message: string) => void;\n onWarn?: (message: string) => void;\n onLog?: (message: string) => void;\n onConfirm?: (message: string) => Promise<boolean>;\n}\n\nexport interface PushResult {\n created: number;\n updated: number;\n unchanged: number;\n errors: number;\n warnings: string[];\n sqlLogPath?: string;\n}\n\nexport interface EntityPushResult {\n created: number;\n updated: number;\n unchanged: number;\n errors: number;\n}\n\nexport class PushService {\n private syncEngine: SyncEngine;\n private contextUser: UserInfo;\n private warnings: string[] = [];\n private processedRecords: Map<string, { filePath: string; arrayIndex?: number; lineNumber?: number }> = new Map();\n private syncConfig: any;\n \n constructor(syncEngine: SyncEngine, contextUser: UserInfo) {\n this.syncEngine = syncEngine;\n this.contextUser = contextUser;\n }\n \n async push(options: PushOptions, callbacks?: PushCallbacks): Promise<PushResult> {\n this.warnings = [];\n this.processedRecords.clear();\n \n const fileBackupManager = new FileBackupManager();\n \n // Load sync config for SQL logging settings and autoCreateMissingRecords flag\n // If dir option is specified, load from that directory, otherwise use original CWD\n const configDir = options.dir ? path.resolve(configManager.getOriginalCwd(), options.dir) : configManager.getOriginalCwd();\n this.syncConfig = await loadSyncConfig(configDir);\n \n if (options.verbose) {\n callbacks?.onLog?.(`Original working directory: ${configManager.getOriginalCwd()}`);\n callbacks?.onLog?.(`Config directory (with dir option): ${configDir}`);\n callbacks?.onLog?.(`Config file path: ${path.join(configDir, '.mj-sync.json')}`);\n callbacks?.onLog?.(`Full sync config loaded: ${JSON.stringify(this.syncConfig, null, 2)}`);\n callbacks?.onLog?.(`SQL logging config: ${JSON.stringify(this.syncConfig?.sqlLogging)}`);\n }\n \n const sqlLogger = new SQLLogger(this.syncConfig);\n const transactionManager = new TransactionManager(sqlLogger);\n \n if (options.verbose) {\n callbacks?.onLog?.(`SQLLogger enabled status: ${sqlLogger.enabled}`);\n }\n \n // Setup SQL logging session with the provider if enabled\n let sqlLoggingSession: SqlLoggingSession | null = null;\n \n try {\n // Initialize SQL logger if enabled and not dry-run\n if (sqlLogger.enabled && !options.dryRun) {\n const provider = Metadata.Provider as SQLServerDataProvider;\n \n if (options.verbose) {\n callbacks?.onLog?.(`SQL logging enabled: ${sqlLogger.enabled}`);\n callbacks?.onLog?.(`Provider type: ${provider?.constructor?.name || 'Unknown'}`);\n callbacks?.onLog?.(`Has CreateSqlLogger: ${typeof provider?.CreateSqlLogger === 'function'}`);\n }\n \n if (provider && typeof provider.CreateSqlLogger === 'function') {\n // Generate filename with timestamp\n const timestamp = new Date().toISOString().replace(/[:.]/g, '-');\n const filename = this.syncConfig.sqlLogging?.formatAsMigration \n ? `MetadataSync_Push_${timestamp}.sql`\n : `push_${timestamp}.sql`;\n \n // Use .sql-log-push directory in the config directory (where sync was initiated)\n const outputDir = path.join(configDir, this.syncConfig?.sqlLogging?.outputDirectory || './sql-log-push');\n const filepath = path.join(outputDir, filename);\n \n // Ensure the directory exists\n await fs.ensureDir(path.dirname(filepath));\n \n // Create the SQL logging session\n sqlLoggingSession = await provider.CreateSqlLogger(filepath, {\n formatAsMigration: this.syncConfig.sqlLogging?.formatAsMigration || false,\n description: 'MetadataSync push operation',\n statementTypes: \"mutations\",\n prettyPrint: true, \n });\n \n if (options.verbose) {\n callbacks?.onLog?.(`š SQL logging enabled: ${filepath}`);\n }\n } else {\n if (options.verbose) {\n callbacks?.onWarn?.('SQL logging requested but provider does not support it');\n }\n }\n }\n \n // Find entity directories to process\n const entityDirs = this.findEntityDirectories(configDir);\n \n if (entityDirs.length === 0) {\n throw new Error('No entity directories found');\n }\n \n if (options.verbose) {\n callbacks?.onLog?.(`Found ${entityDirs.length} entity ${entityDirs.length === 1 ? 'directory' : 'directories'} to process`);\n }\n \n // Initialize file backup manager (unless in dry-run mode)\n if (!options.dryRun) {\n await fileBackupManager.initialize();\n if (options.verbose) {\n callbacks?.onLog?.('š File backup manager initialized');\n }\n }\n \n // Process each entity directory\n let totalCreated = 0;\n let totalUpdated = 0;\n let totalUnchanged = 0;\n let totalErrors = 0;\n \n // Begin transaction if not in dry-run mode\n if (!options.dryRun) {\n await transactionManager.beginTransaction();\n }\n \n try {\n for (const entityDir of entityDirs) {\n const entityConfig = await loadEntityConfig(entityDir);\n if (!entityConfig) {\n const warning = `Skipping ${entityDir} - no valid entity configuration`;\n this.warnings.push(warning);\n callbacks?.onWarn?.(warning);\n continue;\n }\n \n if (options.verbose) {\n callbacks?.onLog?.(`\\nProcessing ${entityConfig.entity} in ${entityDir}`);\n }\n \n const result = await this.processEntityDirectory(\n entityDir,\n entityConfig,\n options,\n fileBackupManager,\n callbacks,\n sqlLogger\n );\n \n // Show per-directory summary\n const dirName = path.relative(process.cwd(), entityDir) || '.';\n const dirTotal = result.created + result.updated + result.unchanged;\n if (dirTotal > 0 || result.errors > 0) {\n callbacks?.onLog?.(`\\nš ${dirName}:`);\n callbacks?.onLog?.(` Total processed: ${dirTotal} unique records`);\n if (result.created > 0) {\n callbacks?.onLog?.(` ā Created: ${result.created}`);\n }\n if (result.updated > 0) {\n callbacks?.onLog?.(` ā Updated: ${result.updated}`);\n }\n if (result.unchanged > 0) {\n callbacks?.onLog?.(` - Unchanged: ${result.unchanged}`);\n }\n if (result.errors > 0) {\n callbacks?.onLog?.(` ā Errors: ${result.errors}`);\n }\n }\n \n totalCreated += result.created;\n totalUpdated += result.updated;\n totalUnchanged += result.unchanged;\n totalErrors += result.errors;\n }\n \n // Commit transaction if successful\n if (!options.dryRun && totalErrors === 0) {\n await transactionManager.commitTransaction();\n }\n } catch (error) {\n // Rollback transaction on error\n if (!options.dryRun) {\n await transactionManager.rollbackTransaction();\n }\n throw error;\n }\n \n // Commit file backups if successful and not in dry-run mode\n if (!options.dryRun && totalErrors === 0) {\n await fileBackupManager.cleanup();\n if (options.verbose) {\n callbacks?.onLog?.('ā
File backups committed');\n }\n }\n \n // Close SQL logging session if it was created\n let sqlLogPath: string | undefined;\n if (sqlLoggingSession) {\n sqlLogPath = sqlLoggingSession.filePath;\n await sqlLoggingSession.dispose();\n if (options.verbose) {\n callbacks?.onLog?.(`š SQL log written to: ${sqlLogPath}`);\n }\n }\n \n return {\n created: totalCreated,\n updated: totalUpdated,\n unchanged: totalUnchanged,\n errors: totalErrors,\n warnings: this.warnings,\n sqlLogPath\n };\n \n } catch (error) {\n // Rollback file backups on error\n if (!options.dryRun) {\n try {\n await fileBackupManager.rollback();\n callbacks?.onWarn?.('File backups rolled back due to error');\n } catch (rollbackError) {\n callbacks?.onWarn?.(`Failed to rollback file backups: ${rollbackError}`);\n }\n }\n \n // Close SQL logging session on error\n if (sqlLoggingSession) {\n try {\n await sqlLoggingSession.dispose();\n } catch (disposeError) {\n callbacks?.onWarn?.(`Failed to close SQL logging session: ${disposeError}`);\n }\n }\n \n throw error;\n }\n }\n \n private async processEntityDirectory(\n entityDir: string,\n entityConfig: any,\n options: PushOptions,\n fileBackupManager: FileBackupManager,\n callbacks?: PushCallbacks,\n sqlLogger?: SQLLogger\n ): Promise<EntityPushResult> {\n let created = 0;\n let updated = 0;\n let unchanged = 0;\n let errors = 0;\n \n // Find all JSON files in the directory\n const pattern = entityConfig.filePattern || '*.json';\n const files = await fastGlob(pattern, {\n cwd: entityDir,\n absolute: true,\n onlyFiles: true,\n dot: true,\n ignore: ['**/node_modules/**', '**/.mj-*.json']\n });\n \n if (options.verbose) {\n callbacks?.onLog?.(`Found ${files.length} files to process`);\n }\n \n // Process each file\n for (const filePath of files) {\n try {\n // Backup the file before any modifications (unless dry-run)\n if (!options.dryRun) {\n await fileBackupManager.backupFile(filePath);\n }\n \n const fileData = await fs.readJson(filePath);\n const records = Array.isArray(fileData) ? fileData : [fileData];\n const isArray = Array.isArray(fileData);\n \n for (let i = 0; i < records.length; i++) {\n const recordData = records[i];\n \n if (!this.isValidRecordData(recordData)) {\n callbacks?.onWarn?.(`Invalid record format in ${filePath}${isArray ? ` at index ${i}` : ''}`);\n errors++;\n continue;\n }\n \n try {\n // For arrays, work with a deep copy to avoid modifying the original\n const recordToProcess = isArray ? JSON.parse(JSON.stringify(recordData)) : recordData;\n \n const result = await this.processRecord(\n recordToProcess,\n entityConfig,\n entityDir,\n options,\n callbacks,\n filePath,\n isArray ? i : undefined\n );\n \n if (result === 'created') created++;\n else if (result === 'updated') updated++;\n else if (result === 'unchanged') unchanged++;\n \n // For arrays, update the original record's primaryKey and sync only\n if (isArray) {\n // Update primaryKey if it exists (for new records)\n if (recordToProcess.primaryKey) {\n records[i].primaryKey = recordToProcess.primaryKey;\n }\n // Update sync metadata only if it was updated (dirty records only)\n if (recordToProcess.sync) {\n records[i].sync = recordToProcess.sync;\n }\n }\n \n // Track processed record\n const recordKey = this.getRecordKey(recordData, entityConfig.entity);\n this.processedRecords.set(recordKey, {\n filePath,\n arrayIndex: isArray ? i : undefined,\n lineNumber: i + 1 // Simple line number approximation\n });\n \n } catch (recordError) {\n const errorMsg = `Error processing record in ${filePath}${isArray ? ` at index ${i}` : ''}: ${recordError}`;\n callbacks?.onError?.(errorMsg);\n errors++;\n }\n }\n \n // Write back the entire file if it's an array (after processing all records)\n if (isArray && !options.dryRun) {\n await fs.writeJson(filePath, records, { spaces: 2 });\n }\n } catch (fileError) {\n const errorMsg = `Error reading file ${filePath}: ${fileError}`;\n callbacks?.onError?.(errorMsg);\n errors++;\n }\n }\n \n return { created, updated, unchanged, errors };\n }\n \n private async processRecord(\n recordData: RecordData,\n entityConfig: any,\n entityDir: string,\n options: PushOptions,\n callbacks?: PushCallbacks,\n filePath?: string,\n arrayIndex?: number\n ): Promise<'created' | 'updated' | 'unchanged' | 'error'> {\n const metadata = new Metadata();\n \n // Get or create entity instance\n let entity = await metadata.GetEntityObject(entityConfig.entity, this.contextUser);\n if (!entity) {\n throw new Error(`Failed to create entity object for ${entityConfig.entity}`);\n }\n \n // Apply defaults from configuration\n const defaults = { ...entityConfig.defaults };\n \n // Build full record data - keep original values for file writing\n const originalFields = { ...recordData.fields };\n const fullData = {\n ...defaults,\n ...recordData.fields\n };\n \n // Process field values for database operations\n const processedData: Record<string, any> = {};\n for (const [fieldName, fieldValue] of Object.entries(fullData)) {\n const processedValue = await this.syncEngine.processFieldValue(\n fieldValue,\n entityDir,\n null, // parentRecord\n null // rootRecord\n );\n processedData[fieldName] = processedValue;\n }\n \n // Check if record exists\n const primaryKey = recordData.primaryKey;\n let exists = false;\n let isNew = false;\n \n if (primaryKey && Object.keys(primaryKey).length > 0) {\n // Try to load existing record\n const compositeKey = new CompositeKey();\n compositeKey.LoadFromSimpleObject(primaryKey);\n exists = await entity.InnerLoad(compositeKey);\n \n // Check autoCreateMissingRecords flag if record not found\n if (!exists) {\n const autoCreate = this.syncConfig?.push?.autoCreateMissingRecords ?? false;\n const pkDisplay = Object.entries(primaryKey)\n .map(([key, value]) => `${key}=${value}`)\n .join(', ');\n \n if (!autoCreate) {\n const warning = `Record not found: ${entityConfig.entity} with primaryKey {${pkDisplay}}. To auto-create missing records, set push.autoCreateMissingRecords=true in .mj-sync.json`;\n this.warnings.push(warning);\n callbacks?.onWarn?.(warning);\n return 'error';\n } else if (options.verbose) {\n callbacks?.onLog?.(`Auto-creating missing ${entityConfig.entity} record with primaryKey {${pkDisplay}}`);\n }\n }\n }\n \n if (options.dryRun) {\n if (exists) {\n callbacks?.onLog?.(`[DRY RUN] Would update ${entityConfig.entity} record`);\n return 'updated';\n } else {\n callbacks?.onLog?.(`[DRY RUN] Would create ${entityConfig.entity} record`);\n return 'created';\n }\n }\n \n if (!exists) {\n entity.NewRecord(); // make sure our record starts out fresh\n isNew = true;\n // Set primary key values for new records if provided, this is important for the auto-create logic\n if (primaryKey) {\n for (const [pkField, pkValue] of Object.entries(primaryKey)) {\n entity.Set(pkField, pkValue);\n }\n }\n }\n \n // Set field values\n for (const [fieldName, fieldValue] of Object.entries(processedData)) {\n entity.Set(fieldName, fieldValue);\n }\n\n // Handle related entities\n if (recordData.relatedEntities) {\n // Store related entities to process after parent save\n (entity as any).__pendingRelatedEntities = recordData.relatedEntities;\n }\n \n // Check if the record is actually dirty before considering it changed\n let isDirty = entity.Dirty;\n \n // Also check if file content has changed (for @file references)\n if (!isDirty && !isNew && recordData.sync) {\n const currentChecksum = await this.syncEngine.calculateChecksumWithFileContent(originalFields, entityDir);\n if (currentChecksum !== recordData.sync.checksum) {\n isDirty = true;\n if (options.verbose) {\n callbacks?.onLog?.(`š File content changed for ${entityConfig.entity} record (checksum mismatch)`);\n }\n }\n }\n \n // If updating an existing record that's dirty, show what changed\n if (!isNew && isDirty) {\n const changes = entity.GetChangesSinceLastSave();\n const changeKeys = Object.keys(changes);\n if (changeKeys.length > 0) {\n // Get primary key info for display\n const entityInfo = this.syncEngine.getEntityInfo(entityConfig.entity);\n const primaryKeyDisplay: string[] = [];\n if (entityInfo) {\n for (const pk of entityInfo.PrimaryKeys) {\n primaryKeyDisplay.push(`${pk.Name}: ${entity.Get(pk.Name)}`);\n }\n }\n \n callbacks?.onLog?.(`š Updating ${entityConfig.entity} record:`);\n if (primaryKeyDisplay.length > 0) {\n callbacks?.onLog?.(` Primary Key: ${primaryKeyDisplay.join(', ')}`);\n }\n callbacks?.onLog?.(` Changes:`);\n for (const fieldName of changeKeys) {\n const field = entity.GetFieldByName(fieldName);\n const oldValue = field ? field.OldValue : undefined;\n const newValue = (changes as any)[fieldName];\n callbacks?.onLog?.(` ${fieldName}: ${this.formatFieldValue(oldValue)} ā ${this.formatFieldValue(newValue)}`);\n }\n }\n }\n \n // Save the record (always call Save, but track if it was actually dirty)\n const saveResult = await entity.Save();\n \n if (!saveResult) {\n throw new Error(`Failed to save ${entityConfig.entity} record: ${entity.LatestResult?.Message || 'Unknown error'}`);\n }\n \n // Update primaryKey for new records\n if (isNew) {\n const entityInfo = this.syncEngine.getEntityInfo(entityConfig.entity);\n if (entityInfo) {\n const newPrimaryKey: Record<string, any> = {};\n for (const pk of entityInfo.PrimaryKeys) {\n newPrimaryKey[pk.Name] = entity.Get(pk.Name);\n }\n recordData.primaryKey = newPrimaryKey;\n }\n }\n \n // Only update sync metadata if the record was actually dirty (changed)\n if (isNew || isDirty) {\n recordData.sync = {\n lastModified: new Date().toISOString(),\n checksum: await this.syncEngine.calculateChecksumWithFileContent(originalFields, entityDir)\n };\n }\n \n // Restore original field values to preserve @ references\n recordData.fields = originalFields;\n \n // Write back to file only if it's a single record (not part of an array)\n if (filePath && arrayIndex === undefined && !options.dryRun) {\n await fs.writeJson(filePath, recordData, { spaces: 2 });\n }\n \n // Process related entities after parent save\n if (recordData.relatedEntities) {\n await this.processRelatedEntities(\n entity,\n recordData.relatedEntities,\n entityDir,\n options,\n callbacks\n );\n }\n \n // Return the actual status based on whether the record was dirty\n if (isNew) {\n return 'created';\n } else if (isDirty) {\n return 'updated';\n } else {\n return 'unchanged';\n }\n }\n \n private async processRelatedEntities(\n parentEntity: BaseEntity,\n relatedEntities: Record<string, RecordData[]>,\n entityDir: string,\n options: PushOptions,\n callbacks?: PushCallbacks\n ): Promise<void> {\n // TODO: Complete implementation for processing related entities\n // This is a simplified version - full implementation would:\n // 1. Create entity objects for each related entity type\n // 2. Apply field values with proper parent/root references\n // 3. Save related entities with proper error handling\n // 4. Support nested related entities recursively\n for (const [key, records] of Object.entries(relatedEntities)) {\n for (const relatedRecord of records) {\n // Process @parent references but DON'T modify the original fields\n const processedFields: Record<string, any> = {};\n for (const [fieldName, fieldValue] of Object.entries(relatedRecord.fields)) {\n if (typeof fieldValue === 'string' && fieldValue.startsWith('@parent:')) {\n const parentField = fieldValue.substring(8);\n processedFields[fieldName] = parentEntity.Get(parentField);\n } else {\n processedFields[fieldName] = await this.syncEngine.processFieldValue(\n fieldValue,\n entityDir,\n parentEntity,\n null\n );\n }\n }\n \n // TODO: Actually save the related entity with processedFields\n // For now, we're just processing the values but not saving\n // This needs to be implemented to actually create/update the related entities\n }\n }\n }\n \n private isValidRecordData(data: any): data is RecordData {\n return data && \n typeof data === 'object' && \n 'fields' in data &&\n typeof data.fields === 'object';\n }\n \n private getRecordKey(recordData: RecordData, entityName: string): string {\n if (recordData.primaryKey) {\n const keys = Object.entries(recordData.primaryKey)\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([k, v]) => `${k}:${v}`)\n .join('|');\n return `${entityName}|${keys}`;\n }\n \n // Generate a key from fields if no primary key\n const fieldKeys = Object.keys(recordData.fields).sort().join(',');\n return `${entityName}|fields:${fieldKeys}`;\n }\n \n private formatFieldValue(value: any, maxLength: number = 50): string {\n let strValue = JSON.stringify(value);\n strValue = strValue.trim();\n\n if (strValue.length > maxLength) {\n return strValue.substring(0, maxLength) + '...';\n }\n\n return strValue;\n }\n \n private findEntityDirectories(baseDir: string, specificDir?: string): string[] {\n const dirs: string[] = [];\n \n if (specificDir) {\n // Process specific directory\n const fullPath = path.resolve(baseDir, specificDir);\n if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {\n // Check if this directory has an entity configuration\n const configPath = path.join(fullPath, '.mj-sync.json');\n if (fs.existsSync(configPath)) {\n try {\n const config = fs.readJsonSync(configPath);\n if (config.entity) {\n // It's an entity directory, add it\n dirs.push(fullPath);\n } else {\n // It's a container directory, search its subdirectories\n this.findEntityDirectoriesRecursive(fullPath, dirs);\n }\n } catch {\n // Invalid config, skip\n }\n }\n }\n } else {\n // Find all entity directories\n this.findEntityDirectoriesRecursive(baseDir, dirs);\n }\n \n return dirs;\n }\n\n private findEntityDirectoriesRecursive(dir: string, dirs: string[]): void {\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n \n for (const entry of entries) {\n if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {\n const fullPath = path.join(dir, entry.name);\n const configPath = path.join(fullPath, '.mj-sync.json');\n \n if (fs.existsSync(configPath)) {\n try {\n const config = fs.readJsonSync(configPath);\n if (config.entity) {\n dirs.push(fullPath);\n }\n } catch {\n // Skip invalid config files\n }\n } else {\n // Recurse into subdirectories\n this.findEntityDirectoriesRecursive(fullPath, dirs);\n }\n }\n }\n }\n}"]}
|