@memberjunction/metadata-sync 2.67.0 → 2.68.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.
Files changed (34) hide show
  1. package/README.md +57 -0
  2. package/dist/config.d.ts +4 -0
  3. package/dist/config.js.map +1 -1
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.js +5 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/lib/EntityPropertyExtractor.d.ts +60 -0
  8. package/dist/lib/EntityPropertyExtractor.js +166 -0
  9. package/dist/lib/EntityPropertyExtractor.js.map +1 -0
  10. package/dist/lib/FieldExternalizer.d.ts +62 -0
  11. package/dist/lib/FieldExternalizer.js +177 -0
  12. package/dist/lib/FieldExternalizer.js.map +1 -0
  13. package/dist/lib/RecordProcessor.d.ts +82 -0
  14. package/dist/lib/RecordProcessor.js +309 -0
  15. package/dist/lib/RecordProcessor.js.map +1 -0
  16. package/dist/lib/RelatedEntityHandler.d.ts +75 -0
  17. package/dist/lib/RelatedEntityHandler.js +273 -0
  18. package/dist/lib/RelatedEntityHandler.js.map +1 -0
  19. package/dist/lib/file-write-batch.d.ts +61 -0
  20. package/dist/lib/file-write-batch.js +180 -0
  21. package/dist/lib/file-write-batch.js.map +1 -0
  22. package/dist/lib/json-write-helper.d.ts +39 -0
  23. package/dist/lib/json-write-helper.js +105 -0
  24. package/dist/lib/json-write-helper.js.map +1 -0
  25. package/dist/services/FileResetService.js +2 -1
  26. package/dist/services/FileResetService.js.map +1 -1
  27. package/dist/services/PullService.d.ts +22 -2
  28. package/dist/services/PullService.js +268 -173
  29. package/dist/services/PullService.js.map +1 -1
  30. package/dist/services/PushService.js +3 -2
  31. package/dist/services/PushService.js.map +1 -1
  32. package/dist/services/WatchService.js +3 -2
  33. package/dist/services/WatchService.js.map +1 -1
  34. package/package.json +7 -7
@@ -0,0 +1,75 @@
1
+ import { BaseEntity, UserInfo } from '@memberjunction/core';
2
+ import { SyncEngine, RecordData } from '../lib/sync-engine';
3
+ import { RelatedEntityConfig, EntityConfig } from '../config';
4
+ /**
5
+ * Handles loading and processing of related entities for records
6
+ */
7
+ export declare class RelatedEntityHandler {
8
+ private syncEngine;
9
+ private contextUser;
10
+ constructor(syncEngine: SyncEngine, contextUser: UserInfo);
11
+ /**
12
+ * Load related entities for a record
13
+ */
14
+ loadRelatedEntities(parentRecord: BaseEntity, relationConfig: RelatedEntityConfig, parentEntityConfig: EntityConfig, existingRelatedEntities: RecordData[], processRecordData: (record: BaseEntity, primaryKey: Record<string, any>, targetDir: string, entityConfig: EntityConfig, verbose?: boolean, isNewRecord?: boolean, existingRecordData?: RecordData, currentDepth?: number, ancestryPath?: Set<string>, fieldOverrides?: Record<string, any>) => Promise<RecordData>, currentDepth: number, ancestryPath: Set<string>, verbose?: boolean): Promise<RecordData[]>;
15
+ /**
16
+ * Queries the database for related entities
17
+ */
18
+ private queryRelatedEntities;
19
+ /**
20
+ * Builds the filter for querying related entities
21
+ */
22
+ private buildRelatedEntityFilter;
23
+ /**
24
+ * Creates entity config for related entity processing
25
+ */
26
+ private createRelatedEntityConfig;
27
+ /**
28
+ * Processes all related records (both existing and new)
29
+ */
30
+ private processRelatedRecords;
31
+ /**
32
+ * Builds a map of database records by primary key for efficient lookup
33
+ */
34
+ private buildDatabaseRecordMap;
35
+ /**
36
+ * Processes existing related entities
37
+ */
38
+ private processExistingRelatedEntities;
39
+ /**
40
+ * Processes a single existing related entity
41
+ */
42
+ private processExistingRelatedEntity;
43
+ /**
44
+ * Processes new related entities
45
+ */
46
+ private processNewRelatedEntities;
47
+ /**
48
+ * Processes a single new related entity
49
+ */
50
+ private processNewRelatedEntity;
51
+ /**
52
+ * Creates field overrides for @parent:ID replacement
53
+ */
54
+ private createFieldOverrides;
55
+ /**
56
+ * Builds primary key for a record
57
+ */
58
+ private buildPrimaryKeyForRecord;
59
+ /**
60
+ * Get the primary key value from a record
61
+ */
62
+ private getRecordPrimaryKey;
63
+ /**
64
+ * Get a field value from a record, handling both entity objects and plain objects
65
+ */
66
+ private getFieldValue;
67
+ /**
68
+ * Log warning message if verbose mode is enabled
69
+ */
70
+ private logWarning;
71
+ /**
72
+ * Log error message if verbose mode is enabled
73
+ */
74
+ private logError;
75
+ }
@@ -0,0 +1,273 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RelatedEntityHandler = void 0;
4
+ const core_1 = require("@memberjunction/core");
5
+ /**
6
+ * Handles loading and processing of related entities for records
7
+ */
8
+ class RelatedEntityHandler {
9
+ syncEngine;
10
+ contextUser;
11
+ constructor(syncEngine, contextUser) {
12
+ this.syncEngine = syncEngine;
13
+ this.contextUser = contextUser;
14
+ }
15
+ /**
16
+ * Load related entities for a record
17
+ */
18
+ async loadRelatedEntities(parentRecord, relationConfig, parentEntityConfig, existingRelatedEntities, processRecordData, currentDepth, ancestryPath, verbose) {
19
+ try {
20
+ const parentPrimaryKey = this.getRecordPrimaryKey(parentRecord);
21
+ if (!parentPrimaryKey) {
22
+ this.logWarning('Unable to determine primary key for parent record', verbose);
23
+ return [];
24
+ }
25
+ const relatedRecords = await this.queryRelatedEntities(parentPrimaryKey, relationConfig, verbose);
26
+ if (!relatedRecords) {
27
+ return [];
28
+ }
29
+ const relatedEntityConfig = this.createRelatedEntityConfig(relationConfig, parentEntityConfig);
30
+ return await this.processRelatedRecords(relatedRecords, relationConfig, relatedEntityConfig, existingRelatedEntities, processRecordData, currentDepth, ancestryPath, verbose);
31
+ }
32
+ catch (error) {
33
+ this.logError(`Error loading related entities for ${relationConfig.entity}`, error, verbose);
34
+ return [];
35
+ }
36
+ }
37
+ /**
38
+ * Queries the database for related entities
39
+ */
40
+ async queryRelatedEntities(parentPrimaryKey, relationConfig, verbose) {
41
+ const filter = this.buildRelatedEntityFilter(parentPrimaryKey, relationConfig);
42
+ if (verbose) {
43
+ console.log(`Loading related entities: ${relationConfig.entity} with filter: ${filter}`);
44
+ }
45
+ const rv = new core_1.RunView();
46
+ const result = await rv.RunView({
47
+ EntityName: relationConfig.entity,
48
+ ExtraFilter: filter,
49
+ ResultType: 'entity_object'
50
+ }, this.contextUser);
51
+ if (!result.Success) {
52
+ this.logWarning(`Failed to load related entities ${relationConfig.entity}: ${result.ErrorMessage}`, verbose);
53
+ return null;
54
+ }
55
+ if (verbose) {
56
+ console.log(`Found ${result.Results.length} related records for ${relationConfig.entity}`);
57
+ }
58
+ return result.Results;
59
+ }
60
+ /**
61
+ * Builds the filter for querying related entities
62
+ */
63
+ buildRelatedEntityFilter(parentPrimaryKey, relationConfig) {
64
+ let filter = `${relationConfig.foreignKey} = '${parentPrimaryKey}'`;
65
+ if (relationConfig.filter) {
66
+ filter += ` AND (${relationConfig.filter})`;
67
+ }
68
+ return filter;
69
+ }
70
+ /**
71
+ * Creates entity config for related entity processing
72
+ */
73
+ createRelatedEntityConfig(relationConfig, parentEntityConfig) {
74
+ return {
75
+ entity: relationConfig.entity,
76
+ pull: {
77
+ excludeFields: relationConfig.excludeFields || [],
78
+ lookupFields: relationConfig.lookupFields || {},
79
+ externalizeFields: relationConfig.externalizeFields || [],
80
+ relatedEntities: relationConfig.relatedEntities || {},
81
+ ignoreVirtualFields: parentEntityConfig.pull?.ignoreVirtualFields || false,
82
+ ignoreNullFields: parentEntityConfig.pull?.ignoreNullFields || false
83
+ }
84
+ };
85
+ }
86
+ /**
87
+ * Processes all related records (both existing and new)
88
+ */
89
+ async processRelatedRecords(dbRecords, relationConfig, relatedEntityConfig, existingRelatedEntities, processRecordData, currentDepth, ancestryPath, verbose) {
90
+ const dbRecordMap = this.buildDatabaseRecordMap(dbRecords);
91
+ const relatedRecords = [];
92
+ const processedIds = new Set();
93
+ // Process existing related entities first (preserving order)
94
+ await this.processExistingRelatedEntities(existingRelatedEntities, dbRecordMap, relationConfig, relatedEntityConfig, processRecordData, currentDepth, ancestryPath, relatedRecords, processedIds, verbose);
95
+ // Process new related entities (append to end)
96
+ await this.processNewRelatedEntities(dbRecords, relationConfig, relatedEntityConfig, processRecordData, currentDepth, ancestryPath, relatedRecords, processedIds, verbose);
97
+ return relatedRecords;
98
+ }
99
+ /**
100
+ * Builds a map of database records by primary key for efficient lookup
101
+ */
102
+ buildDatabaseRecordMap(dbRecords) {
103
+ const dbRecordMap = new Map();
104
+ for (const relatedRecord of dbRecords) {
105
+ const relatedPrimaryKey = this.getRecordPrimaryKey(relatedRecord);
106
+ if (relatedPrimaryKey) {
107
+ dbRecordMap.set(relatedPrimaryKey, relatedRecord);
108
+ }
109
+ }
110
+ return dbRecordMap;
111
+ }
112
+ /**
113
+ * Processes existing related entities
114
+ */
115
+ async processExistingRelatedEntities(existingRelatedEntities, dbRecordMap, relationConfig, relatedEntityConfig, processRecordData, currentDepth, ancestryPath, relatedRecords, processedIds, verbose) {
116
+ for (const existingRelatedEntity of existingRelatedEntities) {
117
+ const existingPrimaryKey = existingRelatedEntity.primaryKey?.ID;
118
+ if (!existingPrimaryKey) {
119
+ this.logWarning('Existing related entity missing primary key, skipping', verbose);
120
+ continue;
121
+ }
122
+ const dbRecord = dbRecordMap.get(existingPrimaryKey);
123
+ if (!dbRecord) {
124
+ if (verbose) {
125
+ console.log(`Related entity ${existingPrimaryKey} no longer exists in database, removing from results`);
126
+ }
127
+ continue; // Skip deleted records
128
+ }
129
+ const recordData = await this.processExistingRelatedEntity(dbRecord, existingPrimaryKey, relationConfig, relatedEntityConfig, processRecordData, existingRelatedEntity, currentDepth, ancestryPath, verbose);
130
+ if (recordData) {
131
+ relatedRecords.push(recordData);
132
+ processedIds.add(existingPrimaryKey);
133
+ }
134
+ }
135
+ }
136
+ /**
137
+ * Processes a single existing related entity
138
+ */
139
+ async processExistingRelatedEntity(dbRecord, existingPrimaryKey, relationConfig, relatedEntityConfig, processRecordData, existingRelatedEntity, currentDepth, ancestryPath, verbose) {
140
+ const relatedRecordPrimaryKey = this.buildPrimaryKeyForRecord(existingPrimaryKey, dbRecord, relationConfig.entity);
141
+ const fieldOverrides = this.createFieldOverrides(dbRecord, relationConfig);
142
+ if (!fieldOverrides) {
143
+ return null;
144
+ }
145
+ return await processRecordData(dbRecord, relatedRecordPrimaryKey, '', // targetDir not needed for related entities
146
+ relatedEntityConfig, verbose, false, // isNewRecord = false for existing records
147
+ existingRelatedEntity, // Pass existing data for change detection
148
+ currentDepth + 1, ancestryPath, fieldOverrides // Pass the field override for @parent:ID
149
+ );
150
+ }
151
+ /**
152
+ * Processes new related entities
153
+ */
154
+ async processNewRelatedEntities(dbRecords, relationConfig, relatedEntityConfig, processRecordData, currentDepth, ancestryPath, relatedRecords, processedIds, verbose) {
155
+ for (const relatedRecord of dbRecords) {
156
+ const relatedPrimaryKey = this.getRecordPrimaryKey(relatedRecord);
157
+ if (!relatedPrimaryKey || processedIds.has(relatedPrimaryKey)) {
158
+ continue; // Skip already processed records
159
+ }
160
+ const recordData = await this.processNewRelatedEntity(relatedRecord, relatedPrimaryKey, relationConfig, relatedEntityConfig, processRecordData, currentDepth, ancestryPath, verbose);
161
+ if (recordData) {
162
+ relatedRecords.push(recordData);
163
+ processedIds.add(relatedPrimaryKey);
164
+ }
165
+ }
166
+ }
167
+ /**
168
+ * Processes a single new related entity
169
+ */
170
+ async processNewRelatedEntity(relatedRecord, relatedPrimaryKey, relationConfig, relatedEntityConfig, processRecordData, currentDepth, ancestryPath, verbose) {
171
+ const relatedRecordPrimaryKey = this.buildPrimaryKeyForRecord(relatedPrimaryKey, relatedRecord, relationConfig.entity);
172
+ const fieldOverrides = this.createFieldOverrides(relatedRecord, relationConfig);
173
+ if (!fieldOverrides) {
174
+ return null;
175
+ }
176
+ return await processRecordData(relatedRecord, relatedRecordPrimaryKey, '', // targetDir not needed for related entities
177
+ relatedEntityConfig, verbose, true, // isNewRecord = true for new records
178
+ undefined, // No existing data for new records
179
+ currentDepth + 1, ancestryPath, fieldOverrides // Pass the field override for @parent:ID
180
+ );
181
+ }
182
+ /**
183
+ * Creates field overrides for @parent:ID replacement
184
+ */
185
+ createFieldOverrides(record, relationConfig) {
186
+ if (typeof record.GetAll === 'function') {
187
+ const data = record.GetAll();
188
+ if (data[relationConfig.foreignKey] !== undefined) {
189
+ return { [relationConfig.foreignKey]: '@parent:ID' };
190
+ }
191
+ }
192
+ else {
193
+ if (record[relationConfig.foreignKey] !== undefined) {
194
+ return { [relationConfig.foreignKey]: '@parent:ID' };
195
+ }
196
+ }
197
+ return null;
198
+ }
199
+ /**
200
+ * Builds primary key for a record
201
+ */
202
+ buildPrimaryKeyForRecord(primaryKeyValue, record, entityName) {
203
+ const relatedRecordPrimaryKey = {};
204
+ const entityInfo = this.syncEngine.getEntityInfo(entityName);
205
+ for (const pk of entityInfo?.PrimaryKeys || []) {
206
+ if (pk.Name === 'ID') {
207
+ relatedRecordPrimaryKey[pk.Name] = primaryKeyValue;
208
+ }
209
+ else {
210
+ // For compound keys, get the value from the related record
211
+ relatedRecordPrimaryKey[pk.Name] = this.getFieldValue(record, pk.Name);
212
+ }
213
+ }
214
+ return relatedRecordPrimaryKey;
215
+ }
216
+ /**
217
+ * Get the primary key value from a record
218
+ */
219
+ getRecordPrimaryKey(record) {
220
+ if (!record)
221
+ return null;
222
+ // Try to get ID directly
223
+ if (record.ID)
224
+ return record.ID;
225
+ // Try to get from GetAll() method if it's an entity object
226
+ if (typeof record.GetAll === 'function') {
227
+ const data = record.GetAll();
228
+ if (data.ID)
229
+ return data.ID;
230
+ }
231
+ // Try common variations
232
+ if (record.id)
233
+ return record.id;
234
+ if (record.Id)
235
+ return record.Id;
236
+ return null;
237
+ }
238
+ /**
239
+ * Get a field value from a record, handling both entity objects and plain objects
240
+ */
241
+ getFieldValue(record, fieldName) {
242
+ if (!record)
243
+ return null;
244
+ // Try to get field directly using bracket notation with type assertion
245
+ if (record[fieldName] !== undefined)
246
+ return record[fieldName];
247
+ // Try to get from GetAll() method if it's an entity object
248
+ if (typeof record.GetAll === 'function') {
249
+ const data = record.GetAll();
250
+ if (data[fieldName] !== undefined)
251
+ return data[fieldName];
252
+ }
253
+ return null;
254
+ }
255
+ /**
256
+ * Log warning message if verbose mode is enabled
257
+ */
258
+ logWarning(message, verbose) {
259
+ if (verbose) {
260
+ console.warn(message);
261
+ }
262
+ }
263
+ /**
264
+ * Log error message if verbose mode is enabled
265
+ */
266
+ logError(message, error, verbose) {
267
+ if (verbose) {
268
+ console.error(`${message}: ${error}`);
269
+ }
270
+ }
271
+ }
272
+ exports.RelatedEntityHandler = RelatedEntityHandler;
273
+ //# sourceMappingURL=RelatedEntityHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RelatedEntityHandler.js","sourceRoot":"","sources":["../../src/lib/RelatedEntityHandler.ts"],"names":[],"mappings":"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","sourcesContent":["import { BaseEntity, RunView, UserInfo } from '@memberjunction/core';\nimport { SyncEngine, RecordData } from '../lib/sync-engine';\nimport { RelatedEntityConfig, EntityConfig } from '../config';\n\n/**\n * Handles loading and processing of related entities for records\n */\nexport class RelatedEntityHandler {\n constructor(\n private syncEngine: SyncEngine,\n private contextUser: UserInfo\n ) {}\n\n /**\n * Load related entities for a record\n */\n async loadRelatedEntities(\n parentRecord: BaseEntity,\n relationConfig: RelatedEntityConfig,\n parentEntityConfig: EntityConfig,\n existingRelatedEntities: RecordData[],\n processRecordData: (\n record: BaseEntity,\n primaryKey: Record<string, any>,\n targetDir: string,\n entityConfig: EntityConfig,\n verbose?: boolean,\n isNewRecord?: boolean,\n existingRecordData?: RecordData,\n currentDepth?: number,\n ancestryPath?: Set<string>,\n fieldOverrides?: Record<string, any>\n ) => Promise<RecordData>,\n currentDepth: number,\n ancestryPath: Set<string>,\n verbose?: boolean\n ): Promise<RecordData[]> {\n try {\n const parentPrimaryKey = this.getRecordPrimaryKey(parentRecord);\n if (!parentPrimaryKey) {\n this.logWarning('Unable to determine primary key for parent record', verbose);\n return [];\n }\n\n const relatedRecords = await this.queryRelatedEntities(\n parentPrimaryKey, \n relationConfig, \n verbose\n );\n\n if (!relatedRecords) {\n return [];\n }\n\n const relatedEntityConfig = this.createRelatedEntityConfig(relationConfig, parentEntityConfig);\n \n return await this.processRelatedRecords(\n relatedRecords,\n relationConfig,\n relatedEntityConfig,\n existingRelatedEntities,\n processRecordData,\n currentDepth,\n ancestryPath,\n verbose\n );\n } catch (error) {\n this.logError(`Error loading related entities for ${relationConfig.entity}`, error, verbose);\n return [];\n }\n }\n\n /**\n * Queries the database for related entities\n */\n private async queryRelatedEntities(\n parentPrimaryKey: string,\n relationConfig: RelatedEntityConfig,\n verbose?: boolean\n ): Promise<BaseEntity[] | null> {\n const filter = this.buildRelatedEntityFilter(parentPrimaryKey, relationConfig);\n \n if (verbose) {\n console.log(`Loading related entities: ${relationConfig.entity} with filter: ${filter}`);\n }\n\n const rv = new RunView();\n const result = await rv.RunView({\n EntityName: relationConfig.entity,\n ExtraFilter: filter,\n ResultType: 'entity_object'\n }, this.contextUser);\n\n if (!result.Success) {\n this.logWarning(`Failed to load related entities ${relationConfig.entity}: ${result.ErrorMessage}`, verbose);\n return null;\n }\n\n if (verbose) {\n console.log(`Found ${result.Results.length} related records for ${relationConfig.entity}`);\n }\n\n return result.Results;\n }\n\n /**\n * Builds the filter for querying related entities\n */\n private buildRelatedEntityFilter(parentPrimaryKey: string, relationConfig: RelatedEntityConfig): string {\n let filter = `${relationConfig.foreignKey} = '${parentPrimaryKey}'`;\n if (relationConfig.filter) {\n filter += ` AND (${relationConfig.filter})`;\n }\n return filter;\n }\n\n /**\n * Creates entity config for related entity processing\n */\n private createRelatedEntityConfig(\n relationConfig: RelatedEntityConfig, \n parentEntityConfig: EntityConfig\n ): EntityConfig {\n return {\n entity: relationConfig.entity,\n pull: {\n excludeFields: relationConfig.excludeFields || [],\n lookupFields: relationConfig.lookupFields || {},\n externalizeFields: relationConfig.externalizeFields || [],\n relatedEntities: relationConfig.relatedEntities || {},\n ignoreVirtualFields: parentEntityConfig.pull?.ignoreVirtualFields || false,\n ignoreNullFields: parentEntityConfig.pull?.ignoreNullFields || false\n }\n };\n }\n\n /**\n * Processes all related records (both existing and new)\n */\n private async processRelatedRecords(\n dbRecords: BaseEntity[],\n relationConfig: RelatedEntityConfig,\n relatedEntityConfig: EntityConfig,\n existingRelatedEntities: RecordData[],\n processRecordData: Function,\n currentDepth: number,\n ancestryPath: Set<string>,\n verbose?: boolean\n ): Promise<RecordData[]> {\n const dbRecordMap = this.buildDatabaseRecordMap(dbRecords);\n const relatedRecords: RecordData[] = [];\n const processedIds = new Set<string>();\n\n // Process existing related entities first (preserving order)\n await this.processExistingRelatedEntities(\n existingRelatedEntities,\n dbRecordMap,\n relationConfig,\n relatedEntityConfig,\n processRecordData,\n currentDepth,\n ancestryPath,\n relatedRecords,\n processedIds,\n verbose\n );\n\n // Process new related entities (append to end)\n await this.processNewRelatedEntities(\n dbRecords,\n relationConfig,\n relatedEntityConfig,\n processRecordData,\n currentDepth,\n ancestryPath,\n relatedRecords,\n processedIds,\n verbose\n );\n\n return relatedRecords;\n }\n\n /**\n * Builds a map of database records by primary key for efficient lookup\n */\n private buildDatabaseRecordMap(dbRecords: BaseEntity[]): Map<string, BaseEntity> {\n const dbRecordMap = new Map<string, BaseEntity>();\n \n for (const relatedRecord of dbRecords) {\n const relatedPrimaryKey = this.getRecordPrimaryKey(relatedRecord);\n if (relatedPrimaryKey) {\n dbRecordMap.set(relatedPrimaryKey, relatedRecord);\n }\n }\n \n return dbRecordMap;\n }\n\n /**\n * Processes existing related entities\n */\n private async processExistingRelatedEntities(\n existingRelatedEntities: RecordData[],\n dbRecordMap: Map<string, BaseEntity>,\n relationConfig: RelatedEntityConfig,\n relatedEntityConfig: EntityConfig,\n processRecordData: Function,\n currentDepth: number,\n ancestryPath: Set<string>,\n relatedRecords: RecordData[],\n processedIds: Set<string>,\n verbose?: boolean\n ): Promise<void> {\n for (const existingRelatedEntity of existingRelatedEntities) {\n const existingPrimaryKey = existingRelatedEntity.primaryKey?.ID;\n if (!existingPrimaryKey) {\n this.logWarning('Existing related entity missing primary key, skipping', verbose);\n continue;\n }\n\n const dbRecord = dbRecordMap.get(existingPrimaryKey);\n if (!dbRecord) {\n if (verbose) {\n console.log(`Related entity ${existingPrimaryKey} no longer exists in database, removing from results`);\n }\n continue; // Skip deleted records\n }\n\n const recordData = await this.processExistingRelatedEntity(\n dbRecord,\n existingPrimaryKey,\n relationConfig,\n relatedEntityConfig,\n processRecordData,\n existingRelatedEntity,\n currentDepth,\n ancestryPath,\n verbose\n );\n\n if (recordData) {\n relatedRecords.push(recordData);\n processedIds.add(existingPrimaryKey);\n }\n }\n }\n\n /**\n * Processes a single existing related entity\n */\n private async processExistingRelatedEntity(\n dbRecord: BaseEntity,\n existingPrimaryKey: string,\n relationConfig: RelatedEntityConfig,\n relatedEntityConfig: EntityConfig,\n processRecordData: Function,\n existingRelatedEntity: RecordData,\n currentDepth: number,\n ancestryPath: Set<string>,\n verbose?: boolean\n ): Promise<RecordData | null> {\n const relatedRecordPrimaryKey = this.buildPrimaryKeyForRecord(\n existingPrimaryKey, \n dbRecord, \n relationConfig.entity\n );\n\n const fieldOverrides = this.createFieldOverrides(dbRecord, relationConfig);\n if (!fieldOverrides) {\n return null;\n }\n\n return await processRecordData(\n dbRecord,\n relatedRecordPrimaryKey,\n '', // targetDir not needed for related entities\n relatedEntityConfig,\n verbose,\n false, // isNewRecord = false for existing records\n existingRelatedEntity, // Pass existing data for change detection\n currentDepth + 1,\n ancestryPath,\n fieldOverrides // Pass the field override for @parent:ID\n );\n }\n\n /**\n * Processes new related entities\n */\n private async processNewRelatedEntities(\n dbRecords: BaseEntity[],\n relationConfig: RelatedEntityConfig,\n relatedEntityConfig: EntityConfig,\n processRecordData: Function,\n currentDepth: number,\n ancestryPath: Set<string>,\n relatedRecords: RecordData[],\n processedIds: Set<string>,\n verbose?: boolean\n ): Promise<void> {\n for (const relatedRecord of dbRecords) {\n const relatedPrimaryKey = this.getRecordPrimaryKey(relatedRecord);\n if (!relatedPrimaryKey || processedIds.has(relatedPrimaryKey)) {\n continue; // Skip already processed records\n }\n\n const recordData = await this.processNewRelatedEntity(\n relatedRecord,\n relatedPrimaryKey,\n relationConfig,\n relatedEntityConfig,\n processRecordData,\n currentDepth,\n ancestryPath,\n verbose\n );\n\n if (recordData) {\n relatedRecords.push(recordData);\n processedIds.add(relatedPrimaryKey);\n }\n }\n }\n\n /**\n * Processes a single new related entity\n */\n private async processNewRelatedEntity(\n relatedRecord: BaseEntity,\n relatedPrimaryKey: string,\n relationConfig: RelatedEntityConfig,\n relatedEntityConfig: EntityConfig,\n processRecordData: Function,\n currentDepth: number,\n ancestryPath: Set<string>,\n verbose?: boolean\n ): Promise<RecordData | null> {\n const relatedRecordPrimaryKey = this.buildPrimaryKeyForRecord(\n relatedPrimaryKey, \n relatedRecord, \n relationConfig.entity\n );\n\n const fieldOverrides = this.createFieldOverrides(relatedRecord, relationConfig);\n if (!fieldOverrides) {\n return null;\n }\n\n return await processRecordData(\n relatedRecord,\n relatedRecordPrimaryKey,\n '', // targetDir not needed for related entities\n relatedEntityConfig,\n verbose,\n true, // isNewRecord = true for new records\n undefined, // No existing data for new records\n currentDepth + 1,\n ancestryPath,\n fieldOverrides // Pass the field override for @parent:ID\n );\n }\n\n /**\n * Creates field overrides for @parent:ID replacement\n */\n private createFieldOverrides(record: BaseEntity, relationConfig: RelatedEntityConfig): Record<string, any> | null {\n if (typeof record.GetAll === 'function') {\n const data = record.GetAll();\n if (data[relationConfig.foreignKey] !== undefined) {\n return { [relationConfig.foreignKey]: '@parent:ID' };\n }\n } else {\n if ((record as any)[relationConfig.foreignKey] !== undefined) {\n return { [relationConfig.foreignKey]: '@parent:ID' };\n }\n }\n return null;\n }\n\n /**\n * Builds primary key for a record\n */\n private buildPrimaryKeyForRecord(\n primaryKeyValue: string, \n record: BaseEntity, \n entityName: string\n ): Record<string, any> {\n const relatedRecordPrimaryKey: Record<string, any> = {};\n const entityInfo = this.syncEngine.getEntityInfo(entityName);\n \n for (const pk of entityInfo?.PrimaryKeys || []) {\n if (pk.Name === 'ID') {\n relatedRecordPrimaryKey[pk.Name] = primaryKeyValue;\n } else {\n // For compound keys, get the value from the related record\n relatedRecordPrimaryKey[pk.Name] = this.getFieldValue(record, pk.Name);\n }\n }\n \n return relatedRecordPrimaryKey;\n }\n\n /**\n * Get the primary key value from a record\n */\n private getRecordPrimaryKey(record: BaseEntity): string | null {\n if (!record) return null;\n \n // Try to get ID directly\n if ((record as any).ID) return (record as any).ID;\n \n // Try to get from GetAll() method if it's an entity object\n if (typeof record.GetAll === 'function') {\n const data = record.GetAll();\n if (data.ID) return data.ID;\n }\n \n // Try common variations\n if ((record as any).id) return (record as any).id;\n if ((record as any).Id) return (record as any).Id;\n \n return null;\n }\n\n /**\n * Get a field value from a record, handling both entity objects and plain objects\n */\n private getFieldValue(record: BaseEntity, fieldName: string): any {\n if (!record) return null;\n \n // Try to get field directly using bracket notation with type assertion\n if ((record as any)[fieldName] !== undefined) return (record as any)[fieldName];\n \n // Try to get from GetAll() method if it's an entity object\n if (typeof record.GetAll === 'function') {\n const data = record.GetAll();\n if (data[fieldName] !== undefined) return data[fieldName];\n }\n \n return null;\n }\n\n /**\n * Log warning message if verbose mode is enabled\n */\n private logWarning(message: string, verbose?: boolean): void {\n if (verbose) {\n console.warn(message);\n }\n }\n\n /**\n * Log error message if verbose mode is enabled\n */\n private logError(message: string, error: any, verbose?: boolean): void {\n if (verbose) {\n console.error(`${message}: ${error}`);\n }\n }\n}"]}
@@ -0,0 +1,61 @@
1
+ import { RecordData } from './sync-engine';
2
+ /**
3
+ * Batches file write operations to improve performance and ensure consistent property ordering.
4
+ * Collects all changes during processing and writes each file only once at the end.
5
+ */
6
+ export declare class FileWriteBatch {
7
+ private changes;
8
+ private fileContents;
9
+ /**
10
+ * Queue a complete file write operation
11
+ * @param filePath - Path to the file
12
+ * @param data - RecordData or array of RecordData to write
13
+ */
14
+ queueWrite(filePath: string, data: RecordData | RecordData[]): void;
15
+ /**
16
+ * Queue an array record update operation
17
+ * @param filePath - Path to the file containing the array
18
+ * @param updatedRecord - The updated record data
19
+ * @param primaryKeyLookup - Primary key lookup string to identify the record
20
+ */
21
+ queueArrayUpdate(filePath: string, updatedRecord: RecordData, primaryKeyLookup: string): void;
22
+ /**
23
+ * Queue a single record update operation
24
+ * @param filePath - Path to the file
25
+ * @param updatedRecord - The updated record data
26
+ */
27
+ queueSingleUpdate(filePath: string, updatedRecord: RecordData): void;
28
+ /**
29
+ * Load and cache file contents if not already loaded
30
+ */
31
+ private ensureFileLoaded;
32
+ /**
33
+ * Apply all queued changes to in-memory file contents
34
+ */
35
+ private applyChanges;
36
+ /**
37
+ * Write all batched changes to files using JsonWriteHelper for consistent ordering
38
+ * @returns Number of files written
39
+ */
40
+ flush(): Promise<number>;
41
+ /**
42
+ * Clear all batched changes without writing
43
+ */
44
+ clear(): void;
45
+ /**
46
+ * Get the number of files that will be written
47
+ */
48
+ getPendingFileCount(): number;
49
+ /**
50
+ * Get all pending file paths
51
+ */
52
+ getPendingFiles(): string[];
53
+ /**
54
+ * Add a change to the batch
55
+ */
56
+ private addChange;
57
+ /**
58
+ * Create a primary key lookup string (same logic as PullService)
59
+ */
60
+ private createPrimaryKeyLookup;
61
+ }
@@ -0,0 +1,180 @@
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.FileWriteBatch = void 0;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const json_write_helper_1 = require("./json-write-helper");
10
+ /**
11
+ * Batches file write operations to improve performance and ensure consistent property ordering.
12
+ * Collects all changes during processing and writes each file only once at the end.
13
+ */
14
+ class FileWriteBatch {
15
+ changes = new Map();
16
+ fileContents = new Map();
17
+ /**
18
+ * Queue a complete file write operation
19
+ * @param filePath - Path to the file
20
+ * @param data - RecordData or array of RecordData to write
21
+ */
22
+ queueWrite(filePath, data) {
23
+ const absolutePath = path_1.default.resolve(filePath);
24
+ this.addChange(absolutePath, {
25
+ filePath: absolutePath,
26
+ operation: 'write',
27
+ data
28
+ });
29
+ }
30
+ /**
31
+ * Queue an array record update operation
32
+ * @param filePath - Path to the file containing the array
33
+ * @param updatedRecord - The updated record data
34
+ * @param primaryKeyLookup - Primary key lookup string to identify the record
35
+ */
36
+ queueArrayUpdate(filePath, updatedRecord, primaryKeyLookup) {
37
+ const absolutePath = path_1.default.resolve(filePath);
38
+ this.addChange(absolutePath, {
39
+ filePath: absolutePath,
40
+ operation: 'update-array',
41
+ data: updatedRecord,
42
+ primaryKeyLookup
43
+ });
44
+ }
45
+ /**
46
+ * Queue a single record update operation
47
+ * @param filePath - Path to the file
48
+ * @param updatedRecord - The updated record data
49
+ */
50
+ queueSingleUpdate(filePath, updatedRecord) {
51
+ const absolutePath = path_1.default.resolve(filePath);
52
+ this.addChange(absolutePath, {
53
+ filePath: absolutePath,
54
+ operation: 'update-single',
55
+ data: updatedRecord
56
+ });
57
+ }
58
+ /**
59
+ * Load and cache file contents if not already loaded
60
+ */
61
+ async ensureFileLoaded(filePath) {
62
+ if (this.fileContents.has(filePath)) {
63
+ return;
64
+ }
65
+ try {
66
+ if (await fs_extra_1.default.pathExists(filePath)) {
67
+ const content = await fs_extra_1.default.readJson(filePath);
68
+ this.fileContents.set(filePath, content);
69
+ }
70
+ }
71
+ catch (error) {
72
+ // If file doesn't exist or can't be read, we'll create it fresh
73
+ // Don't throw here - let the calling code handle it
74
+ }
75
+ }
76
+ /**
77
+ * Apply all queued changes to in-memory file contents
78
+ */
79
+ async applyChanges() {
80
+ for (const [filePath, changes] of this.changes) {
81
+ await this.ensureFileLoaded(filePath);
82
+ let currentContent = this.fileContents.get(filePath) || [];
83
+ for (const change of changes) {
84
+ switch (change.operation) {
85
+ case 'write':
86
+ // Complete overwrite
87
+ currentContent = change.data;
88
+ break;
89
+ case 'update-array':
90
+ // Update a specific record in an array
91
+ if (Array.isArray(currentContent) && change.primaryKeyLookup) {
92
+ const index = currentContent.findIndex(r => this.createPrimaryKeyLookup(r.primaryKey || {}) === change.primaryKeyLookup);
93
+ if (index >= 0) {
94
+ currentContent[index] = change.data;
95
+ }
96
+ else {
97
+ // Record not found, append it
98
+ currentContent.push(change.data);
99
+ }
100
+ }
101
+ else {
102
+ // File doesn't contain an array, make it one
103
+ currentContent = [change.data];
104
+ }
105
+ break;
106
+ case 'update-single':
107
+ // Replace the entire file content with a single record
108
+ currentContent = change.data;
109
+ break;
110
+ }
111
+ }
112
+ // Update the in-memory content
113
+ if (currentContent !== undefined) {
114
+ this.fileContents.set(filePath, currentContent);
115
+ }
116
+ }
117
+ }
118
+ /**
119
+ * Write all batched changes to files using JsonWriteHelper for consistent ordering
120
+ * @returns Number of files written
121
+ */
122
+ async flush() {
123
+ if (this.changes.size === 0) {
124
+ return 0;
125
+ }
126
+ // Apply all changes to in-memory content first
127
+ await this.applyChanges();
128
+ // Write all files using JsonWriteHelper
129
+ const writePromises = [];
130
+ for (const [filePath, content] of this.fileContents.entries()) {
131
+ // Ensure directory exists
132
+ const dir = path_1.default.dirname(filePath);
133
+ await fs_extra_1.default.ensureDir(dir);
134
+ // Write using JsonWriteHelper for consistent property ordering
135
+ writePromises.push(json_write_helper_1.JsonWriteHelper.writeOrderedRecordData(filePath, content));
136
+ }
137
+ await Promise.all(writePromises);
138
+ const filesWritten = this.fileContents.size;
139
+ // Clear all batched changes
140
+ this.clear();
141
+ return filesWritten;
142
+ }
143
+ /**
144
+ * Clear all batched changes without writing
145
+ */
146
+ clear() {
147
+ this.changes.clear();
148
+ this.fileContents.clear();
149
+ }
150
+ /**
151
+ * Get the number of files that will be written
152
+ */
153
+ getPendingFileCount() {
154
+ return this.changes.size;
155
+ }
156
+ /**
157
+ * Get all pending file paths
158
+ */
159
+ getPendingFiles() {
160
+ return Array.from(this.changes.keys());
161
+ }
162
+ /**
163
+ * Add a change to the batch
164
+ */
165
+ addChange(filePath, change) {
166
+ if (!this.changes.has(filePath)) {
167
+ this.changes.set(filePath, []);
168
+ }
169
+ this.changes.get(filePath).push(change);
170
+ }
171
+ /**
172
+ * Create a primary key lookup string (same logic as PullService)
173
+ */
174
+ createPrimaryKeyLookup(primaryKey) {
175
+ const keys = Object.keys(primaryKey).sort();
176
+ return keys.map(k => `${k}:${primaryKey[k]}`).join('|');
177
+ }
178
+ }
179
+ exports.FileWriteBatch = FileWriteBatch;
180
+ //# sourceMappingURL=file-write-batch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-write-batch.js","sourceRoot":"","sources":["../../src/lib/file-write-batch.ts"],"names":[],"mappings":"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","sourcesContent":["import fs from 'fs-extra';\nimport path from 'path';\nimport { RecordData } from './sync-engine';\nimport { JsonWriteHelper } from './json-write-helper';\n\n/**\n * Represents a pending change to a file\n */\ninterface FileChange {\n /** Path to the file */\n filePath: string;\n /** Type of change operation */\n operation: 'write' | 'update-array' | 'update-single';\n /** The data to write */\n data: RecordData | RecordData[];\n /** For array updates: the index of the record to update */\n arrayIndex?: number;\n /** For updates: the primary key lookup to identify the record */\n primaryKeyLookup?: string;\n}\n\n/**\n * Batches file write operations to improve performance and ensure consistent property ordering.\n * Collects all changes during processing and writes each file only once at the end.\n */\nexport class FileWriteBatch {\n private changes = new Map<string, FileChange[]>();\n private fileContents = new Map<string, RecordData | RecordData[]>();\n \n /**\n * Queue a complete file write operation\n * @param filePath - Path to the file\n * @param data - RecordData or array of RecordData to write\n */\n queueWrite(filePath: string, data: RecordData | RecordData[]): void {\n const absolutePath = path.resolve(filePath);\n this.addChange(absolutePath, {\n filePath: absolutePath,\n operation: 'write',\n data\n });\n }\n \n /**\n * Queue an array record update operation\n * @param filePath - Path to the file containing the array\n * @param updatedRecord - The updated record data\n * @param primaryKeyLookup - Primary key lookup string to identify the record\n */\n queueArrayUpdate(filePath: string, updatedRecord: RecordData, primaryKeyLookup: string): void {\n const absolutePath = path.resolve(filePath);\n this.addChange(absolutePath, {\n filePath: absolutePath,\n operation: 'update-array',\n data: updatedRecord,\n primaryKeyLookup\n });\n }\n \n /**\n * Queue a single record update operation\n * @param filePath - Path to the file\n * @param updatedRecord - The updated record data\n */\n queueSingleUpdate(filePath: string, updatedRecord: RecordData): void {\n const absolutePath = path.resolve(filePath);\n this.addChange(absolutePath, {\n filePath: absolutePath,\n operation: 'update-single',\n data: updatedRecord\n });\n }\n \n /**\n * Load and cache file contents if not already loaded\n */\n private async ensureFileLoaded(filePath: string): Promise<void> {\n if (this.fileContents.has(filePath)) {\n return;\n }\n \n try {\n if (await fs.pathExists(filePath)) {\n const content = await fs.readJson(filePath);\n this.fileContents.set(filePath, content);\n }\n } catch (error) {\n // If file doesn't exist or can't be read, we'll create it fresh\n // Don't throw here - let the calling code handle it\n }\n }\n \n /**\n * Apply all queued changes to in-memory file contents\n */\n private async applyChanges(): Promise<void> {\n for (const [filePath, changes] of this.changes) {\n await this.ensureFileLoaded(filePath);\n \n let currentContent: RecordData | RecordData[] = this.fileContents.get(filePath) || [];\n \n for (const change of changes) {\n switch (change.operation) {\n case 'write':\n // Complete overwrite\n currentContent = change.data;\n break;\n \n case 'update-array':\n // Update a specific record in an array\n if (Array.isArray(currentContent) && change.primaryKeyLookup) {\n const index = currentContent.findIndex(r => \n this.createPrimaryKeyLookup(r.primaryKey || {}) === change.primaryKeyLookup\n );\n \n if (index >= 0) {\n currentContent[index] = change.data as RecordData;\n } else {\n // Record not found, append it\n currentContent.push(change.data as RecordData);\n }\n } else {\n // File doesn't contain an array, make it one\n currentContent = [change.data as RecordData];\n }\n break;\n \n case 'update-single':\n // Replace the entire file content with a single record\n currentContent = change.data;\n break;\n }\n }\n \n // Update the in-memory content\n if (currentContent !== undefined) {\n this.fileContents.set(filePath, currentContent);\n }\n }\n }\n \n /**\n * Write all batched changes to files using JsonWriteHelper for consistent ordering\n * @returns Number of files written\n */\n async flush(): Promise<number> {\n if (this.changes.size === 0) {\n return 0;\n }\n \n // Apply all changes to in-memory content first\n await this.applyChanges();\n \n // Write all files using JsonWriteHelper\n const writePromises: Promise<void>[] = [];\n \n for (const [filePath, content] of this.fileContents.entries()) {\n // Ensure directory exists\n const dir = path.dirname(filePath);\n await fs.ensureDir(dir);\n \n // Write using JsonWriteHelper for consistent property ordering\n writePromises.push(\n JsonWriteHelper.writeOrderedRecordData(filePath, content)\n );\n }\n \n await Promise.all(writePromises);\n \n const filesWritten = this.fileContents.size;\n \n // Clear all batched changes\n this.clear();\n \n return filesWritten;\n }\n \n /**\n * Clear all batched changes without writing\n */\n clear(): void {\n this.changes.clear();\n this.fileContents.clear();\n }\n \n /**\n * Get the number of files that will be written\n */\n getPendingFileCount(): number {\n return this.changes.size;\n }\n \n /**\n * Get all pending file paths\n */\n getPendingFiles(): string[] {\n return Array.from(this.changes.keys());\n }\n \n /**\n * Add a change to the batch\n */\n private addChange(filePath: string, change: FileChange): void {\n if (!this.changes.has(filePath)) {\n this.changes.set(filePath, []);\n }\n this.changes.get(filePath)!.push(change);\n }\n \n /**\n * Create a primary key lookup string (same logic as PullService)\n */\n private createPrimaryKeyLookup(primaryKey: Record<string, any>): string {\n const keys = Object.keys(primaryKey).sort();\n return keys.map(k => `${k}:${primaryKey[k]}`).join('|');\n }\n}"]}