@memberjunction/metadata-sync 2.128.0 → 2.129.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -683,6 +683,203 @@ metadata/
683
683
  }
684
684
  ```
685
685
 
686
+ ## Adding Comments to JSON Metadata Files
687
+
688
+ Since JSON does not natively support comments, MetadataSync provides a convention for adding documentation to your metadata files using custom keys that are preserved but ignored during sync operations.
689
+
690
+ ### Comment Convention
691
+
692
+ Any key that is not one of the reserved MetadataSync keys (`fields`, `relatedEntities`, `primaryKey`, `sync`, `deleteRecord`) will be preserved in your JSON files but ignored during push/pull operations. By convention, use an underscore prefix (`_`) for comment keys to clearly distinguish them from future MetadataSync features.
693
+
694
+ **Reserved keys (processed by MetadataSync):**
695
+ - `fields` - Entity field values
696
+ - `relatedEntities` - Embedded related entity records
697
+ - `primaryKey` - Record identifier
698
+ - `sync` - Sync metadata (lastModified, checksum)
699
+ - `__mj_sync_notes` - System-managed resolution tracking (see [Resolution Tracking](#resolution-tracking-with-__mj_sync_notes))
700
+ - `deleteRecord` - Deletion directive
701
+
702
+ **Recommended comment pattern:**
703
+ - `_comments` - Array of comment strings
704
+ - `_note` - Single comment string
705
+ - `_description` - Descriptive text
706
+ - Any key starting with `_` - Reserved for user documentation
707
+
708
+ ### Example: Top-Level Comments
709
+ ```json
710
+ {
711
+ "_comments": [
712
+ "This file configures encryption settings for the Test Tables entity",
713
+ "The encryption key is defined in /metadata/encryption-keys/"
714
+ ],
715
+ "fields": {
716
+ "Name": "Test Tables",
717
+ "BaseView": "vwTestTables"
718
+ },
719
+ "primaryKey": {
720
+ "ID": "0fde4c2c-26b1-45e9-b504-5d4a6f4201cf"
721
+ }
722
+ }
723
+ ```
724
+
725
+ ### Example: Comments on Related Entities
726
+ ```json
727
+ {
728
+ "_comments": ["Parent entity configuration"],
729
+ "fields": {
730
+ "Name": "My Entity"
731
+ },
732
+ "relatedEntities": {
733
+ "Entity Fields": [
734
+ {
735
+ "fields": {
736
+ "Encrypt": true,
737
+ "AllowDecryptInAPI": false
738
+ },
739
+ "_comments": ["This field stores server-only encrypted data"],
740
+ "primaryKey": {
741
+ "ID": "F501E294-5F5F-44C6-AD06-5C9754A13D29"
742
+ }
743
+ },
744
+ {
745
+ "fields": {
746
+ "Encrypt": true,
747
+ "AllowDecryptInAPI": true
748
+ },
749
+ "_comments": ["This field is decrypted for API responses"],
750
+ "primaryKey": {
751
+ "ID": "CF4B94E4-8E68-4692-B13A-9A0D51D397B7"
752
+ }
753
+ }
754
+ ]
755
+ }
756
+ }
757
+ ```
758
+
759
+ ### Key Ordering Preservation
760
+
761
+ MetadataSync preserves the original order of keys in your JSON files. When you run `mj sync push` or `mj sync pull`, your comments will remain exactly where you placed them:
762
+
763
+ ```json
764
+ {
765
+ "_comments": ["This comment stays at the top"],
766
+ "fields": { ... },
767
+ "_note": "This note stays between fields and relatedEntities",
768
+ "relatedEntities": { ... }
769
+ }
770
+ ```
771
+
772
+ ### Best Practices
773
+
774
+ 1. **Use underscore prefix**: Start custom keys with `_` to reserve the alphabetic namespace for future MetadataSync features
775
+ 2. **Use arrays for multi-line comments**: `"_comments": ["Line 1", "Line 2"]` provides clean formatting
776
+ 3. **Place comments near relevant content**: Add `_comments` inside related entity objects to document specific records
777
+ 4. **Document complex configurations**: Use comments to explain lookup references, encryption settings, or business rules
778
+ 5. **Version control friendly**: Comments make metadata files more readable in code reviews and git diffs
779
+
780
+ ## Resolution Tracking with `__mj_sync_notes`
781
+
782
+ When you use `@lookup` or `@parent` references in your metadata files, MetadataSync automatically tracks how these references were resolved during push operations. This information is written to a `__mj_sync_notes` key in each record, providing transparency into the resolution process.
783
+
784
+ ### Purpose
785
+
786
+ The `__mj_sync_notes` feature helps you:
787
+ - **Debug lookup issues**: See exactly what value a `@lookup` resolved to
788
+ - **Understand parent references**: Track how `@parent:` references were resolved
789
+ - **Verify nested lookups**: View the resolution chain for nested `@lookup` expressions
790
+ - **Document resolved values**: Provides a reference for what GUIDs correspond to which lookup expressions
791
+
792
+ ### How It Works
793
+
794
+ After each `mj sync push` operation, records with `@lookup` or `@parent` references will have a `__mj_sync_notes` section added automatically:
795
+
796
+ ```json
797
+ {
798
+ "fields": {
799
+ "Name": "ServerOnlyEncrypted",
800
+ "Encrypt": true,
801
+ "EncryptionKeyID": "@lookup:MJ: Encryption Keys.Name=Test Encryption Key"
802
+ },
803
+ "primaryKey": {
804
+ "ID": "@lookup:Entity Fields.EntityID=@lookup:Entities.Name=Test Tables&Name=ServerOnlyEncrypted"
805
+ },
806
+ "sync": {
807
+ "lastModified": "2025-12-25T16:14:32.605Z",
808
+ "checksum": "7e989e08396f6cffb8b2d70958018b21..."
809
+ },
810
+ "__mj_sync_notes": [
811
+ {
812
+ "type": "lookup",
813
+ "field": "primaryKey.ID",
814
+ "expression": "@lookup:Entity Fields.EntityID=@lookup:Entities.Name=Test Tables&Name=ServerOnlyEncrypted",
815
+ "resolved": "F501E294-5F5F-44C6-AD06-5C9754A13D29",
816
+ "nested": [
817
+ {
818
+ "expression": "@lookup:Entities.Name=Test Tables",
819
+ "resolved": "0fde4c2c-26b1-45e9-b504-5d4a6f4201cf"
820
+ }
821
+ ]
822
+ },
823
+ {
824
+ "type": "lookup",
825
+ "field": "fields.EncryptionKeyID",
826
+ "expression": "@lookup:MJ: Encryption Keys.Name=Test Encryption Key",
827
+ "resolved": "85B814C8-A01B-4AE3-A252-DC9D54C914C7"
828
+ }
829
+ ]
830
+ }
831
+ ```
832
+
833
+ ### Note Structure
834
+
835
+ Each resolution note contains:
836
+
837
+ | Field | Description |
838
+ |-------|-------------|
839
+ | `type` | Resolution type: `"lookup"` for `@lookup` references, `"parent"` for `@parent` references |
840
+ | `field` | Field path where the resolution occurred (e.g., `"primaryKey.ID"`, `"fields.CategoryID"`) |
841
+ | `expression` | The original reference expression before resolution |
842
+ | `resolved` | The resolved value (typically a GUID) |
843
+ | `nested` | (Optional) Array of nested resolutions for expressions containing nested `@lookup` references |
844
+
845
+ ### System-Managed Key
846
+
847
+ The `__mj_sync_notes` key uses a double underscore prefix (`__`) to clearly indicate it is system-managed:
848
+ - **Do not manually edit** this section - it is regenerated on each push
849
+ - **Do not delete** it - it will be recreated automatically
850
+ - The key is automatically removed if a record has no `@lookup` or `@parent` references
851
+ - Key ordering is preserved - `__mj_sync_notes` appears after `sync` in the file
852
+
853
+ ### Example: Parent Reference Resolution
854
+
855
+ When using `@parent:` references in related entities:
856
+
857
+ ```json
858
+ {
859
+ "fields": {
860
+ "Name": "My Template"
861
+ },
862
+ "relatedEntities": {
863
+ "Template Contents": [
864
+ {
865
+ "fields": {
866
+ "TemplateID": "@parent:ID",
867
+ "Content": "Hello World"
868
+ },
869
+ "__mj_sync_notes": [
870
+ {
871
+ "type": "parent",
872
+ "field": "fields.TemplateID",
873
+ "expression": "@parent:ID",
874
+ "resolved": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890"
875
+ }
876
+ ]
877
+ }
878
+ ]
879
+ }
880
+ }
881
+ ```
882
+
686
883
  ## Default Value Inheritance
687
884
 
688
885
  The tool implements a cascading inheritance system for field defaults, similar to CSS or OOP inheritance:
@@ -716,6 +913,29 @@ The tool automatically detects primary key fields from entity metadata:
716
913
  - **Composite primary keys**: Multiple fields that together form the primary key
717
914
  - **Auto-detection**: Tool reads entity metadata to determine primary key structure
718
915
  - **No hardcoding**: Works with any primary key field name(s)
916
+ - **Reference support**: Primary key values can use `@lookup`, `@parent`, and other reference types
917
+
918
+ #### Using @lookup in Primary Keys
919
+ You can use `@lookup` references in primary key fields to avoid hardcoding GUIDs. This is especially useful when decorating existing records:
920
+
921
+ ```json
922
+ {
923
+ "fields": {
924
+ "Encrypt": true,
925
+ "AllowDecryptInAPI": false
926
+ },
927
+ "primaryKey": {
928
+ "ID": "@lookup:Entity Fields.EntityID=@lookup:Entities.Name=Test Tables&Name=ServerOnlyEncrypted"
929
+ }
930
+ }
931
+ ```
932
+
933
+ In this example:
934
+ 1. The inner `@lookup:Entities.Name=Test Tables` resolves to the Entity ID
935
+ 2. That ID is used to find the Entity Field with the matching `EntityID` and `Name`
936
+ 3. The resulting Entity Field ID becomes the primary key
937
+
938
+ **Note:** Primary key lookups must resolve immediately - the `?allowDefer` flag is not supported in primary key fields since the primary key is needed to determine if a record exists.
719
939
 
720
940
  ### deleteRecord Directive
721
941
  The tool now supports deleting records from the database using a special `deleteRecord` directive in JSON files. This allows you to remove obsolete records as part of your metadata sync workflow:
@@ -35,6 +35,10 @@ export declare class RecordProcessor {
35
35
  * Processes a single field value through various transformations
36
36
  */
37
37
  private processFieldValue;
38
+ /**
39
+ * Serializes Date objects to ISO strings for JSON storage
40
+ */
41
+ private serializeDateValue;
38
42
  /**
39
43
  * Applies lookup field conversion if configured
40
44
  */
@@ -102,6 +102,8 @@ class RecordProcessor {
102
102
  */
103
103
  async processFieldValue(fieldName, fieldValue, allProperties, targetDir, entityConfig, existingRecordData, verbose) {
104
104
  let processedValue = fieldValue;
105
+ // Convert Date objects to ISO strings
106
+ processedValue = this.serializeDateValue(processedValue);
105
107
  // Apply lookup field conversion if configured
106
108
  processedValue = await this.applyLookupFieldConversion(fieldName, processedValue, entityConfig, verbose);
107
109
  // Trim string values
@@ -110,6 +112,19 @@ class RecordProcessor {
110
112
  processedValue = await this.applyFieldExternalization(fieldName, processedValue, allProperties, targetDir, entityConfig, existingRecordData, verbose);
111
113
  return processedValue;
112
114
  }
115
+ /**
116
+ * Serializes Date objects to ISO strings for JSON storage
117
+ */
118
+ serializeDateValue(value) {
119
+ if (value instanceof Date) {
120
+ // Check if the date is valid
121
+ if (isNaN(value.getTime())) {
122
+ return null; // Invalid dates become null
123
+ }
124
+ return value.toISOString();
125
+ }
126
+ return value;
127
+ }
113
128
  /**
114
129
  * Applies lookup field conversion if configured
115
130
  */
@@ -244,23 +259,31 @@ class RecordProcessor {
244
259
  console.log(`Calculated checksum including external file content for record`);
245
260
  }
246
261
  // Compare with existing checksum to determine if data changed
247
- if (existingRecordData?.sync?.checksum === checksum) {
262
+ const existingChecksum = existingRecordData?.sync?.checksum;
263
+ const existingTimestamp = existingRecordData?.sync?.lastModified;
264
+ if (existingChecksum === checksum) {
248
265
  // No change detected - preserve existing sync metadata
249
266
  if (verbose) {
250
267
  console.log(`No changes detected for record, preserving existing timestamp`);
251
268
  }
252
269
  return {
253
- lastModified: existingRecordData.sync.lastModified,
270
+ lastModified: existingTimestamp,
254
271
  checksum: checksum
255
272
  };
256
273
  }
257
274
  else {
258
275
  // Change detected - update timestamp
259
- if (verbose && existingRecordData?.sync?.checksum) {
260
- console.log(`Changes detected for record, updating timestamp`);
276
+ const newTimestamp = new Date().toISOString();
277
+ if (verbose) {
278
+ if (existingChecksum) {
279
+ console.log(`Changes detected for record, updating timestamp`);
280
+ }
281
+ else {
282
+ console.log(`New record, generating initial timestamp`);
283
+ }
261
284
  }
262
285
  return {
263
- lastModified: new Date().toISOString(),
286
+ lastModified: newTimestamp,
264
287
  checksum: checksum
265
288
  };
266
289
  }
@@ -1 +1 @@
1
- {"version":3,"file":"RecordProcessor.js","sourceRoot":"","sources":["../../src/lib/RecordProcessor.ts"],"names":[],"mappings":"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","sourcesContent":["import { BaseEntity, RunView, UserInfo, EntityInfo } from '@memberjunction/core';\nimport { SyncEngine, RecordData } from '../lib/sync-engine';\nimport { EntityConfig } from '../config';\nimport { JsonWriteHelper } from './json-write-helper';\nimport { EntityPropertyExtractor } from './EntityPropertyExtractor';\nimport { FieldExternalizer } from './FieldExternalizer';\nimport { RelatedEntityHandler } from './RelatedEntityHandler';\nimport { METADATA_KEYWORDS, createKeywordReference } from '../constants/metadata-keywords';\n\n/**\n * Handles the core processing of individual record data into the sync format\n */\nexport class RecordProcessor {\n private propertyExtractor: EntityPropertyExtractor;\n private fieldExternalizer: FieldExternalizer;\n private relatedEntityHandler: RelatedEntityHandler;\n\n constructor(\n private syncEngine: SyncEngine,\n private contextUser: UserInfo\n ) {\n this.propertyExtractor = new EntityPropertyExtractor();\n this.fieldExternalizer = new FieldExternalizer();\n this.relatedEntityHandler = new RelatedEntityHandler(syncEngine, contextUser);\n }\n\n /**\n * Processes a record into the standardized RecordData format\n */\n async processRecord(\n record: BaseEntity, \n primaryKey: Record<string, any>,\n targetDir: string, \n entityConfig: EntityConfig,\n verbose?: boolean,\n isNewRecord: boolean = true,\n existingRecordData?: RecordData,\n currentDepth: number = 0,\n ancestryPath: Set<string> = new Set(),\n fieldOverrides?: Record<string, any>\n ): Promise<RecordData> {\n // Extract all properties from the entity\n const allProperties = this.propertyExtractor.extractAllProperties(record, fieldOverrides);\n \n // Process fields and related entities\n const { fields, relatedEntities } = await this.processEntityData(\n allProperties,\n record,\n primaryKey,\n targetDir,\n entityConfig,\n existingRecordData,\n currentDepth,\n ancestryPath,\n verbose\n );\n \n // Calculate checksum and sync metadata\n const syncData = await this.calculateSyncMetadata(\n fields, \n targetDir, \n entityConfig, \n existingRecordData, \n verbose\n );\n \n // Build the final record data with proper ordering\n return JsonWriteHelper.createOrderedRecordData(\n fields,\n relatedEntities,\n primaryKey,\n syncData\n );\n }\n\n /**\n * Processes entity data into fields and related entities\n */\n private async processEntityData(\n allProperties: Record<string, any>,\n record: BaseEntity,\n primaryKey: Record<string, any>,\n targetDir: string,\n entityConfig: EntityConfig,\n existingRecordData: RecordData | undefined,\n currentDepth: number,\n ancestryPath: Set<string>,\n verbose?: boolean\n ): Promise<{ fields: Record<string, any>; relatedEntities: Record<string, RecordData[]> }> {\n const fields: Record<string, any> = {};\n const relatedEntities: Record<string, RecordData[]> = {};\n \n // Process individual fields\n await this.processFields(\n allProperties, \n primaryKey, \n targetDir, \n entityConfig, \n existingRecordData, \n fields, \n verbose\n );\n \n // Process related entities if configured\n await this.processRelatedEntities(\n record, \n entityConfig, \n existingRecordData, \n currentDepth, \n ancestryPath, \n relatedEntities, \n verbose\n );\n \n return { fields, relatedEntities };\n }\n\n /**\n * Processes individual fields from the entity\n */\n private async processFields(\n allProperties: Record<string, any>,\n primaryKey: Record<string, any>,\n targetDir: string,\n entityConfig: EntityConfig,\n existingRecordData: RecordData | undefined,\n fields: Record<string, any>,\n verbose?: boolean\n ): Promise<void> {\n const entityInfo = this.syncEngine.getEntityInfo(entityConfig.entity);\n \n for (const [fieldName, fieldValue] of Object.entries(allProperties)) {\n if (this.shouldSkipField(fieldName, fieldValue, primaryKey, entityConfig, entityInfo)) {\n continue;\n }\n \n let processedValue = await this.processFieldValue(\n fieldName,\n fieldValue,\n allProperties,\n targetDir,\n entityConfig,\n existingRecordData,\n verbose\n );\n \n fields[fieldName] = processedValue;\n }\n }\n\n /**\n * Determines if a field should be skipped during processing\n */\n private shouldSkipField(\n fieldName: string,\n fieldValue: any,\n primaryKey: Record<string, any>,\n entityConfig: EntityConfig,\n entityInfo: EntityInfo | null\n ): boolean {\n // Skip primary key fields\n if (primaryKey[fieldName] !== undefined) {\n return true;\n }\n \n // Skip internal fields\n if (fieldName.startsWith('__mj_')) {\n return true;\n }\n \n // Skip excluded fields\n if (entityConfig.pull?.excludeFields?.includes(fieldName)) {\n return true;\n }\n \n // Skip virtual fields if configured\n if (this.shouldSkipVirtualField(fieldName, entityConfig, entityInfo)) {\n return true;\n }\n \n // Skip null fields if configured\n if (entityConfig.pull?.ignoreNullFields && fieldValue === null) {\n return true;\n }\n \n return false;\n }\n\n /**\n * Checks if a virtual field should be skipped\n */\n private shouldSkipVirtualField(\n fieldName: string,\n entityConfig: EntityConfig,\n entityInfo: EntityInfo | null\n ): boolean {\n if (!entityConfig.pull?.ignoreVirtualFields || !entityInfo) {\n return false;\n }\n \n const fieldInfo = entityInfo.Fields.find(f => f.Name === fieldName);\n return fieldInfo?.IsVirtual === true;\n }\n\n /**\n * Processes a single field value through various transformations\n */\n private async processFieldValue(\n fieldName: string,\n fieldValue: any,\n allProperties: Record<string, any>,\n targetDir: string,\n entityConfig: EntityConfig,\n existingRecordData: RecordData | undefined,\n verbose?: boolean\n ): Promise<any> {\n let processedValue = fieldValue;\n \n // Apply lookup field conversion if configured\n processedValue = await this.applyLookupFieldConversion(\n fieldName, \n processedValue, \n entityConfig, \n verbose\n );\n \n // Trim string values\n processedValue = this.trimStringValue(processedValue);\n \n // Apply field externalization if configured\n processedValue = await this.applyFieldExternalization(\n fieldName,\n processedValue,\n allProperties,\n targetDir,\n entityConfig,\n existingRecordData,\n verbose\n );\n \n return processedValue;\n }\n\n /**\n * Applies lookup field conversion if configured\n */\n private async applyLookupFieldConversion(\n fieldName: string,\n fieldValue: any,\n entityConfig: EntityConfig,\n verbose?: boolean\n ): Promise<any> {\n const lookupConfig = entityConfig.pull?.lookupFields?.[fieldName];\n if (!lookupConfig || fieldValue == null) {\n return fieldValue;\n }\n \n try {\n return await this.convertGuidToLookup(String(fieldValue), lookupConfig, verbose);\n } catch (error) {\n if (verbose) {\n console.warn(`Failed to convert ${fieldName} to lookup: ${error}`);\n }\n return fieldValue; // Keep original value if lookup fails\n }\n }\n\n /**\n * Trims string values to remove whitespace\n */\n private trimStringValue(value: any): any {\n return typeof value === 'string' ? value.trim() : value;\n }\n\n /**\n * Applies field externalization if configured\n */\n private async applyFieldExternalization(\n fieldName: string,\n fieldValue: any,\n allProperties: Record<string, any>,\n targetDir: string,\n entityConfig: EntityConfig,\n existingRecordData: RecordData | undefined,\n verbose?: boolean\n ): Promise<any> {\n if (!entityConfig.pull?.externalizeFields || fieldValue == null) {\n return fieldValue;\n }\n \n const externalizePattern = this.getExternalizationPattern(fieldName, entityConfig);\n if (!externalizePattern) {\n return fieldValue;\n }\n \n try {\n const existingFileReference = existingRecordData?.fields?.[fieldName];\n const recordData = this.createRecordDataForExternalization(allProperties);\n \n return await this.fieldExternalizer.externalizeField(\n fieldName,\n fieldValue,\n externalizePattern,\n recordData,\n targetDir,\n existingFileReference,\n entityConfig.pull?.mergeStrategy || 'merge',\n verbose\n );\n } catch (error) {\n if (verbose) {\n console.warn(`Failed to externalize field ${fieldName}: ${error}`);\n }\n return fieldValue; // Keep original value if externalization fails\n }\n }\n\n /**\n * Gets the externalization pattern for a field\n */\n private getExternalizationPattern(fieldName: string, entityConfig: EntityConfig): string | null {\n const externalizeConfig = entityConfig.pull?.externalizeFields;\n if (!externalizeConfig) return null;\n \n if (Array.isArray(externalizeConfig)) {\n return this.getArrayExternalizationPattern(fieldName, externalizeConfig);\n } else {\n return this.getObjectExternalizationPattern(fieldName, externalizeConfig);\n }\n }\n\n /**\n * Gets externalization pattern from array configuration\n */\n private getArrayExternalizationPattern(\n fieldName: string, \n externalizeConfig: any[]\n ): string | null {\n if (externalizeConfig.length > 0 && typeof externalizeConfig[0] === 'string') {\n // Simple string array format\n if ((externalizeConfig as string[]).includes(fieldName)) {\n return createKeywordReference('file', `{Name}.${fieldName.toLowerCase()}.md`);\n }\n } else {\n // Array of objects format\n const fieldConfig = (externalizeConfig as Array<{field: string; pattern: string}>)\n .find(config => config.field === fieldName);\n if (fieldConfig) {\n return fieldConfig.pattern;\n }\n }\n return null;\n }\n\n /**\n * Gets externalization pattern from object configuration\n */\n private getObjectExternalizationPattern(\n fieldName: string, \n externalizeConfig: Record<string, any>\n ): string | null {\n const fieldConfig = externalizeConfig[fieldName];\n if (fieldConfig) {\n const extension = fieldConfig.extension || '.md';\n return createKeywordReference('file', `{Name}.${fieldName.toLowerCase()}${extension}`);\n }\n return null;\n }\n\n /**\n * Creates a BaseEntity-like object for externalization processing\n */\n private createRecordDataForExternalization(allProperties: Record<string, any>): BaseEntity {\n return allProperties as any as BaseEntity;\n }\n\n /**\n * Processes related entities for the record\n */\n private async processRelatedEntities(\n record: BaseEntity,\n entityConfig: EntityConfig,\n existingRecordData: RecordData | undefined,\n currentDepth: number,\n ancestryPath: Set<string>,\n relatedEntities: Record<string, RecordData[]>,\n verbose?: boolean\n ): Promise<void> {\n if (!entityConfig.pull?.relatedEntities) {\n return;\n }\n \n for (const [relationKey, relationConfig] of Object.entries(entityConfig.pull.relatedEntities)) {\n try {\n const existingRelated = existingRecordData?.relatedEntities?.[relationKey] || [];\n \n const relatedRecords = await this.relatedEntityHandler.loadRelatedEntities(\n record,\n relationConfig,\n entityConfig,\n existingRelated,\n this.processRecord.bind(this), // Pass bound method reference\n currentDepth,\n ancestryPath,\n verbose\n );\n \n if (relatedRecords.length > 0) {\n relatedEntities[relationKey] = relatedRecords;\n }\n } catch (error) {\n if (verbose) {\n console.warn(`Failed to load related entities for ${relationKey}: ${error}`);\n }\n }\n }\n }\n\n /**\n * Calculates sync metadata including checksum and last modified timestamp\n */\n private async calculateSyncMetadata(\n fields: Record<string, any>,\n targetDir: string,\n entityConfig: EntityConfig,\n existingRecordData: RecordData | undefined,\n verbose?: boolean\n ): Promise<{ lastModified: string; checksum: string }> {\n // Determine if we should include external file content in checksum\n const hasExternalizedFields = this.hasExternalizedFields(fields, entityConfig);\n \n const checksum = hasExternalizedFields\n ? await this.syncEngine.calculateChecksumWithFileContent(fields, targetDir)\n : this.syncEngine.calculateChecksum(fields);\n \n if (verbose && hasExternalizedFields) {\n console.log(`Calculated checksum including external file content for record`);\n }\n \n // Compare with existing checksum to determine if data changed\n if (existingRecordData?.sync?.checksum === checksum) {\n // No change detected - preserve existing sync metadata\n if (verbose) {\n console.log(`No changes detected for record, preserving existing timestamp`);\n }\n return {\n lastModified: existingRecordData.sync.lastModified,\n checksum: checksum\n };\n } else {\n // Change detected - update timestamp\n if (verbose && existingRecordData?.sync?.checksum) {\n console.log(`Changes detected for record, updating timestamp`);\n }\n return {\n lastModified: new Date().toISOString(),\n checksum: checksum\n };\n }\n }\n\n /**\n * Checks if the record has externalized fields\n */\n private hasExternalizedFields(fields: Record<string, any>, entityConfig: EntityConfig): boolean {\n return !!entityConfig.pull?.externalizeFields &&\n Object.values(fields).some(value =>\n typeof value === 'string' && value.startsWith(METADATA_KEYWORDS.FILE)\n );\n }\n\n /**\n * Convert a GUID value to @lookup syntax by looking up the human-readable value\n */\n private async convertGuidToLookup(\n guidValue: string,\n lookupConfig: { entity: string; field: string },\n verbose?: boolean\n ): Promise<string> {\n if (!guidValue || typeof guidValue !== 'string') {\n return guidValue;\n }\n\n try {\n const rv = new RunView();\n const result = await rv.RunView({\n EntityName: lookupConfig.entity,\n ExtraFilter: `ID = '${guidValue}'`,\n ResultType: 'entity_object'\n }, this.contextUser);\n\n if (result.Success && result.Results && result.Results.length > 0) {\n const targetRecord = result.Results[0];\n const lookupValue = targetRecord[lookupConfig.field];\n \n if (lookupValue != null) {\n return createKeywordReference('lookup', `${lookupConfig.entity}.${lookupConfig.field}=${lookupValue}`);\n }\n }\n\n if (verbose) {\n console.warn(`Lookup failed for ${guidValue} in ${lookupConfig.entity}.${lookupConfig.field}`);\n }\n \n return guidValue; // Return original GUID if lookup fails\n } catch (error) {\n if (verbose) {\n console.warn(`Error during lookup conversion: ${error}`);\n }\n return guidValue;\n }\n }\n}"]}
1
+ {"version":3,"file":"RecordProcessor.js","sourceRoot":"","sources":["../../src/lib/RecordProcessor.ts"],"names":[],"mappings":"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","sourcesContent":["import { BaseEntity, RunView, UserInfo, EntityInfo } from '@memberjunction/core';\nimport { SyncEngine, RecordData } from '../lib/sync-engine';\nimport { EntityConfig } from '../config';\nimport { JsonWriteHelper } from './json-write-helper';\nimport { EntityPropertyExtractor } from './EntityPropertyExtractor';\nimport { FieldExternalizer } from './FieldExternalizer';\nimport { RelatedEntityHandler } from './RelatedEntityHandler';\nimport { METADATA_KEYWORDS, createKeywordReference } from '../constants/metadata-keywords';\n\n/**\n * Handles the core processing of individual record data into the sync format\n */\nexport class RecordProcessor {\n private propertyExtractor: EntityPropertyExtractor;\n private fieldExternalizer: FieldExternalizer;\n private relatedEntityHandler: RelatedEntityHandler;\n\n constructor(\n private syncEngine: SyncEngine,\n private contextUser: UserInfo\n ) {\n this.propertyExtractor = new EntityPropertyExtractor();\n this.fieldExternalizer = new FieldExternalizer();\n this.relatedEntityHandler = new RelatedEntityHandler(syncEngine, contextUser);\n }\n\n /**\n * Processes a record into the standardized RecordData format\n */\n async processRecord(\n record: BaseEntity, \n primaryKey: Record<string, any>,\n targetDir: string, \n entityConfig: EntityConfig,\n verbose?: boolean,\n isNewRecord: boolean = true,\n existingRecordData?: RecordData,\n currentDepth: number = 0,\n ancestryPath: Set<string> = new Set(),\n fieldOverrides?: Record<string, any>\n ): Promise<RecordData> {\n // Extract all properties from the entity\n const allProperties = this.propertyExtractor.extractAllProperties(record, fieldOverrides);\n \n // Process fields and related entities\n const { fields, relatedEntities } = await this.processEntityData(\n allProperties,\n record,\n primaryKey,\n targetDir,\n entityConfig,\n existingRecordData,\n currentDepth,\n ancestryPath,\n verbose\n );\n \n // Calculate checksum and sync metadata\n const syncData = await this.calculateSyncMetadata(\n fields, \n targetDir, \n entityConfig, \n existingRecordData, \n verbose\n );\n \n // Build the final record data with proper ordering\n return JsonWriteHelper.createOrderedRecordData(\n fields,\n relatedEntities,\n primaryKey,\n syncData\n );\n }\n\n /**\n * Processes entity data into fields and related entities\n */\n private async processEntityData(\n allProperties: Record<string, any>,\n record: BaseEntity,\n primaryKey: Record<string, any>,\n targetDir: string,\n entityConfig: EntityConfig,\n existingRecordData: RecordData | undefined,\n currentDepth: number,\n ancestryPath: Set<string>,\n verbose?: boolean\n ): Promise<{ fields: Record<string, any>; relatedEntities: Record<string, RecordData[]> }> {\n const fields: Record<string, any> = {};\n const relatedEntities: Record<string, RecordData[]> = {};\n \n // Process individual fields\n await this.processFields(\n allProperties, \n primaryKey, \n targetDir, \n entityConfig, \n existingRecordData, \n fields, \n verbose\n );\n \n // Process related entities if configured\n await this.processRelatedEntities(\n record, \n entityConfig, \n existingRecordData, \n currentDepth, \n ancestryPath, \n relatedEntities, \n verbose\n );\n \n return { fields, relatedEntities };\n }\n\n /**\n * Processes individual fields from the entity\n */\n private async processFields(\n allProperties: Record<string, any>,\n primaryKey: Record<string, any>,\n targetDir: string,\n entityConfig: EntityConfig,\n existingRecordData: RecordData | undefined,\n fields: Record<string, any>,\n verbose?: boolean\n ): Promise<void> {\n const entityInfo = this.syncEngine.getEntityInfo(entityConfig.entity);\n \n for (const [fieldName, fieldValue] of Object.entries(allProperties)) {\n if (this.shouldSkipField(fieldName, fieldValue, primaryKey, entityConfig, entityInfo)) {\n continue;\n }\n \n let processedValue = await this.processFieldValue(\n fieldName,\n fieldValue,\n allProperties,\n targetDir,\n entityConfig,\n existingRecordData,\n verbose\n );\n \n fields[fieldName] = processedValue;\n }\n }\n\n /**\n * Determines if a field should be skipped during processing\n */\n private shouldSkipField(\n fieldName: string,\n fieldValue: any,\n primaryKey: Record<string, any>,\n entityConfig: EntityConfig,\n entityInfo: EntityInfo | null\n ): boolean {\n // Skip primary key fields\n if (primaryKey[fieldName] !== undefined) {\n return true;\n }\n \n // Skip internal fields\n if (fieldName.startsWith('__mj_')) {\n return true;\n }\n \n // Skip excluded fields\n if (entityConfig.pull?.excludeFields?.includes(fieldName)) {\n return true;\n }\n \n // Skip virtual fields if configured\n if (this.shouldSkipVirtualField(fieldName, entityConfig, entityInfo)) {\n return true;\n }\n \n // Skip null fields if configured\n if (entityConfig.pull?.ignoreNullFields && fieldValue === null) {\n return true;\n }\n \n return false;\n }\n\n /**\n * Checks if a virtual field should be skipped\n */\n private shouldSkipVirtualField(\n fieldName: string,\n entityConfig: EntityConfig,\n entityInfo: EntityInfo | null\n ): boolean {\n if (!entityConfig.pull?.ignoreVirtualFields || !entityInfo) {\n return false;\n }\n \n const fieldInfo = entityInfo.Fields.find(f => f.Name === fieldName);\n return fieldInfo?.IsVirtual === true;\n }\n\n /**\n * Processes a single field value through various transformations\n */\n private async processFieldValue(\n fieldName: string,\n fieldValue: any,\n allProperties: Record<string, any>,\n targetDir: string,\n entityConfig: EntityConfig,\n existingRecordData: RecordData | undefined,\n verbose?: boolean\n ): Promise<any> {\n let processedValue = fieldValue;\n\n // Convert Date objects to ISO strings\n processedValue = this.serializeDateValue(processedValue);\n\n // Apply lookup field conversion if configured\n processedValue = await this.applyLookupFieldConversion(\n fieldName,\n processedValue,\n entityConfig,\n verbose\n );\n\n // Trim string values\n processedValue = this.trimStringValue(processedValue);\n\n // Apply field externalization if configured\n processedValue = await this.applyFieldExternalization(\n fieldName,\n processedValue,\n allProperties,\n targetDir,\n entityConfig,\n existingRecordData,\n verbose\n );\n\n return processedValue;\n }\n\n /**\n * Serializes Date objects to ISO strings for JSON storage\n */\n private serializeDateValue(value: any): any {\n if (value instanceof Date) {\n // Check if the date is valid\n if (isNaN(value.getTime())) {\n return null; // Invalid dates become null\n }\n return value.toISOString();\n }\n return value;\n }\n\n /**\n * Applies lookup field conversion if configured\n */\n private async applyLookupFieldConversion(\n fieldName: string,\n fieldValue: any,\n entityConfig: EntityConfig,\n verbose?: boolean\n ): Promise<any> {\n const lookupConfig = entityConfig.pull?.lookupFields?.[fieldName];\n if (!lookupConfig || fieldValue == null) {\n return fieldValue;\n }\n \n try {\n return await this.convertGuidToLookup(String(fieldValue), lookupConfig, verbose);\n } catch (error) {\n if (verbose) {\n console.warn(`Failed to convert ${fieldName} to lookup: ${error}`);\n }\n return fieldValue; // Keep original value if lookup fails\n }\n }\n\n /**\n * Trims string values to remove whitespace\n */\n private trimStringValue(value: any): any {\n return typeof value === 'string' ? value.trim() : value;\n }\n\n /**\n * Applies field externalization if configured\n */\n private async applyFieldExternalization(\n fieldName: string,\n fieldValue: any,\n allProperties: Record<string, any>,\n targetDir: string,\n entityConfig: EntityConfig,\n existingRecordData: RecordData | undefined,\n verbose?: boolean\n ): Promise<any> {\n if (!entityConfig.pull?.externalizeFields || fieldValue == null) {\n return fieldValue;\n }\n \n const externalizePattern = this.getExternalizationPattern(fieldName, entityConfig);\n if (!externalizePattern) {\n return fieldValue;\n }\n \n try {\n const existingFileReference = existingRecordData?.fields?.[fieldName];\n const recordData = this.createRecordDataForExternalization(allProperties);\n \n return await this.fieldExternalizer.externalizeField(\n fieldName,\n fieldValue,\n externalizePattern,\n recordData,\n targetDir,\n existingFileReference,\n entityConfig.pull?.mergeStrategy || 'merge',\n verbose\n );\n } catch (error) {\n if (verbose) {\n console.warn(`Failed to externalize field ${fieldName}: ${error}`);\n }\n return fieldValue; // Keep original value if externalization fails\n }\n }\n\n /**\n * Gets the externalization pattern for a field\n */\n private getExternalizationPattern(fieldName: string, entityConfig: EntityConfig): string | null {\n const externalizeConfig = entityConfig.pull?.externalizeFields;\n if (!externalizeConfig) return null;\n \n if (Array.isArray(externalizeConfig)) {\n return this.getArrayExternalizationPattern(fieldName, externalizeConfig);\n } else {\n return this.getObjectExternalizationPattern(fieldName, externalizeConfig);\n }\n }\n\n /**\n * Gets externalization pattern from array configuration\n */\n private getArrayExternalizationPattern(\n fieldName: string, \n externalizeConfig: any[]\n ): string | null {\n if (externalizeConfig.length > 0 && typeof externalizeConfig[0] === 'string') {\n // Simple string array format\n if ((externalizeConfig as string[]).includes(fieldName)) {\n return createKeywordReference('file', `{Name}.${fieldName.toLowerCase()}.md`);\n }\n } else {\n // Array of objects format\n const fieldConfig = (externalizeConfig as Array<{field: string; pattern: string}>)\n .find(config => config.field === fieldName);\n if (fieldConfig) {\n return fieldConfig.pattern;\n }\n }\n return null;\n }\n\n /**\n * Gets externalization pattern from object configuration\n */\n private getObjectExternalizationPattern(\n fieldName: string, \n externalizeConfig: Record<string, any>\n ): string | null {\n const fieldConfig = externalizeConfig[fieldName];\n if (fieldConfig) {\n const extension = fieldConfig.extension || '.md';\n return createKeywordReference('file', `{Name}.${fieldName.toLowerCase()}${extension}`);\n }\n return null;\n }\n\n /**\n * Creates a BaseEntity-like object for externalization processing\n */\n private createRecordDataForExternalization(allProperties: Record<string, any>): BaseEntity {\n return allProperties as any as BaseEntity;\n }\n\n /**\n * Processes related entities for the record\n */\n private async processRelatedEntities(\n record: BaseEntity,\n entityConfig: EntityConfig,\n existingRecordData: RecordData | undefined,\n currentDepth: number,\n ancestryPath: Set<string>,\n relatedEntities: Record<string, RecordData[]>,\n verbose?: boolean\n ): Promise<void> {\n if (!entityConfig.pull?.relatedEntities) {\n return;\n }\n \n for (const [relationKey, relationConfig] of Object.entries(entityConfig.pull.relatedEntities)) {\n try {\n const existingRelated = existingRecordData?.relatedEntities?.[relationKey] || [];\n \n const relatedRecords = await this.relatedEntityHandler.loadRelatedEntities(\n record,\n relationConfig,\n entityConfig,\n existingRelated,\n this.processRecord.bind(this), // Pass bound method reference\n currentDepth,\n ancestryPath,\n verbose\n );\n \n if (relatedRecords.length > 0) {\n relatedEntities[relationKey] = relatedRecords;\n }\n } catch (error) {\n if (verbose) {\n console.warn(`Failed to load related entities for ${relationKey}: ${error}`);\n }\n }\n }\n }\n\n /**\n * Calculates sync metadata including checksum and last modified timestamp\n */\n private async calculateSyncMetadata(\n fields: Record<string, any>,\n targetDir: string,\n entityConfig: EntityConfig,\n existingRecordData: RecordData | undefined,\n verbose?: boolean\n ): Promise<{ lastModified: string; checksum: string }> {\n // Determine if we should include external file content in checksum\n const hasExternalizedFields = this.hasExternalizedFields(fields, entityConfig);\n\n const checksum = hasExternalizedFields\n ? await this.syncEngine.calculateChecksumWithFileContent(fields, targetDir)\n : this.syncEngine.calculateChecksum(fields);\n\n if (verbose && hasExternalizedFields) {\n console.log(`Calculated checksum including external file content for record`);\n }\n\n // Compare with existing checksum to determine if data changed\n const existingChecksum = existingRecordData?.sync?.checksum;\n const existingTimestamp = existingRecordData?.sync?.lastModified;\n\n if (existingChecksum === checksum) {\n // No change detected - preserve existing sync metadata\n if (verbose) {\n console.log(`No changes detected for record, preserving existing timestamp`);\n }\n return {\n lastModified: existingTimestamp!,\n checksum: checksum\n };\n } else {\n // Change detected - update timestamp\n const newTimestamp = new Date().toISOString();\n if (verbose) {\n if (existingChecksum) {\n console.log(`Changes detected for record, updating timestamp`);\n } else {\n console.log(`New record, generating initial timestamp`);\n }\n }\n return {\n lastModified: newTimestamp,\n checksum: checksum\n };\n }\n }\n\n /**\n * Checks if the record has externalized fields\n */\n private hasExternalizedFields(fields: Record<string, any>, entityConfig: EntityConfig): boolean {\n return !!entityConfig.pull?.externalizeFields &&\n Object.values(fields).some(value =>\n typeof value === 'string' && value.startsWith(METADATA_KEYWORDS.FILE)\n );\n }\n\n /**\n * Convert a GUID value to @lookup syntax by looking up the human-readable value\n */\n private async convertGuidToLookup(\n guidValue: string,\n lookupConfig: { entity: string; field: string },\n verbose?: boolean\n ): Promise<string> {\n if (!guidValue || typeof guidValue !== 'string') {\n return guidValue;\n }\n\n try {\n const rv = new RunView();\n const result = await rv.RunView({\n EntityName: lookupConfig.entity,\n ExtraFilter: `ID = '${guidValue}'`,\n ResultType: 'entity_object'\n }, this.contextUser);\n\n if (result.Success && result.Results && result.Results.length > 0) {\n const targetRecord = result.Results[0];\n const lookupValue = targetRecord[lookupConfig.field];\n \n if (lookupValue != null) {\n return createKeywordReference('lookup', `${lookupConfig.entity}.${lookupConfig.field}=${lookupValue}`);\n }\n }\n\n if (verbose) {\n console.warn(`Lookup failed for ${guidValue} in ${lookupConfig.entity}.${lookupConfig.field}`);\n }\n \n return guidValue; // Return original GUID if lookup fails\n } catch (error) {\n if (verbose) {\n console.warn(`Error during lookup conversion: ${error}`);\n }\n return guidValue;\n }\n }\n}"]}
@@ -79,6 +79,7 @@ class FileWriteBatch {
79
79
  async applyChanges() {
80
80
  for (const [filePath, changes] of this.changes) {
81
81
  await this.ensureFileLoaded(filePath);
82
+ // Start with existing file content or empty array
82
83
  let currentContent = this.fileContents.get(filePath) || [];
83
84
  for (const change of changes) {
84
85
  switch (change.operation) {
@@ -86,23 +87,28 @@ class FileWriteBatch {
86
87
  // Complete overwrite
87
88
  currentContent = change.data;
88
89
  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);
90
+ case 'update-array': {
91
+ // Ensure content is an array
92
+ const contentArray = Array.isArray(currentContent) ? currentContent : [];
93
+ if (change.primaryKeyLookup) {
94
+ // Find existing record with matching primary key
95
+ const index = contentArray.findIndex(r => this.createPrimaryKeyLookup(r.primaryKey || {}) === change.primaryKeyLookup);
93
96
  if (index >= 0) {
94
- currentContent[index] = change.data;
97
+ // Update existing record
98
+ contentArray[index] = change.data;
95
99
  }
96
100
  else {
97
101
  // Record not found, append it
98
- currentContent.push(change.data);
102
+ contentArray.push(change.data);
99
103
  }
100
104
  }
101
105
  else {
102
- // File doesn't contain an array, make it one
103
- currentContent = [change.data];
106
+ // No lookup key, just append
107
+ contentArray.push(change.data);
104
108
  }
109
+ currentContent = contentArray;
105
110
  break;
111
+ }
106
112
  case 'update-single':
107
113
  // Replace the entire file content with a single record
108
114
  currentContent = change.data;
@@ -1 +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}"]}
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 // Start with existing file content or empty array\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 // Ensure content is an array\n const contentArray = Array.isArray(currentContent) ? currentContent : [];\n\n if (change.primaryKeyLookup) {\n // Find existing record with matching primary key\n const index = contentArray.findIndex(r =>\n this.createPrimaryKeyLookup(r.primaryKey || {}) === change.primaryKeyLookup\n );\n\n if (index >= 0) {\n // Update existing record\n contentArray[index] = change.data as RecordData;\n } else {\n // Record not found, append it\n contentArray.push(change.data as RecordData);\n }\n } else {\n // No lookup key, just append\n contentArray.push(change.data as RecordData);\n }\n\n currentContent = contentArray;\n break;\n }\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}"]}
@@ -34,23 +34,22 @@ class JsonWriteHelper {
34
34
  if (data && typeof data === 'object') {
35
35
  // Check if this looks like a RecordData object
36
36
  if (data.fields !== undefined) {
37
- // This is a RecordData object - rebuild with correct order
37
+ // This is a RecordData object - rebuild preserving original key order
38
+ // but ensuring known keys maintain their relative order when present
38
39
  const ordered = {};
39
- // Add properties in desired order: fields, relatedEntities, primaryKey, sync, deleteRecord
40
- if (data.fields !== undefined) {
41
- ordered.fields = this.normalizeRecordDataOrder(data.fields);
42
- }
43
- if (data.relatedEntities !== undefined) {
44
- ordered.relatedEntities = this.normalizeRecordDataOrder(data.relatedEntities);
45
- }
46
- if (data.primaryKey !== undefined) {
47
- ordered.primaryKey = this.normalizeRecordDataOrder(data.primaryKey);
48
- }
49
- if (data.sync !== undefined) {
50
- ordered.sync = this.normalizeRecordDataOrder(data.sync);
51
- }
52
- if (data.deleteRecord !== undefined) {
53
- ordered.deleteRecord = this.normalizeRecordDataOrder(data.deleteRecord);
40
+ // Known keys that are part of RecordData structure
41
+ // __mj_sync_notes is a system-managed key that should appear after sync
42
+ const knownKeys = ['fields', 'relatedEntities', 'primaryKey', 'sync', '__mj_sync_notes', 'deleteRecord'];
43
+ // Process keys in original order, preserving user's ordering
44
+ for (const key of Object.keys(data)) {
45
+ if (knownKeys.includes(key)) {
46
+ // Known key - process recursively
47
+ ordered[key] = this.normalizeRecordDataOrder(data[key]);
48
+ }
49
+ else {
50
+ // Unknown key (like _comments) - preserve exactly as-is
51
+ ordered[key] = data[key];
52
+ }
54
53
  }
55
54
  return ordered;
56
55
  }
@@ -1 +1 @@
1
- {"version":3,"file":"json-write-helper.js","sourceRoot":"","sources":["../../src/lib/json-write-helper.ts"],"names":[],"mappings":"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","sourcesContent":["import fs, { JsonWriteOptions } from 'fs-extra';\nimport { RecordData } from './sync-engine';\n\n/**\n * Helper class for writing JSON files with consistent property ordering for RecordData objects.\n * Ensures that all metadata files have the same property order: fields, relatedEntities, primaryKey, sync\n */\nexport class JsonWriteHelper {\n \n /**\n * Write RecordData or arrays of RecordData with consistent property ordering\n * @param filePath - Path to the JSON file to write\n * @param data - RecordData object or array of RecordData objects\n */\n static async writeOrderedRecordData(filePath: string, data: RecordData | RecordData[]): Promise<void> {\n // Pre-process the data to ensure correct ordering before JSON.stringify\n const normalizedData = this.normalizeRecordDataOrder(data);\n \n // Use JSON.stringify with proper spacing\n const jsonString = JSON.stringify(normalizedData, null, 2);\n await fs.writeFile(filePath, jsonString, 'utf8');\n }\n\n /**\n * Recursively normalize RecordData objects to ensure correct property ordering\n * @param data - RecordData object, array of RecordData objects, or any nested structure\n * @returns Normalized data with consistent property ordering\n */\n private static normalizeRecordDataOrder(data: any): any {\n if (Array.isArray(data)) {\n return data.map(item => this.normalizeRecordDataOrder(item));\n }\n \n if (data && typeof data === 'object') {\n // Check if this looks like a RecordData object\n if (data.fields !== undefined) {\n // This is a RecordData object - rebuild with correct order\n const ordered: any = {};\n \n // Add properties in desired order: fields, relatedEntities, primaryKey, sync, deleteRecord\n if (data.fields !== undefined) {\n ordered.fields = this.normalizeRecordDataOrder(data.fields);\n }\n if (data.relatedEntities !== undefined) {\n ordered.relatedEntities = this.normalizeRecordDataOrder(data.relatedEntities);\n }\n if (data.primaryKey !== undefined) {\n ordered.primaryKey = this.normalizeRecordDataOrder(data.primaryKey);\n }\n if (data.sync !== undefined) {\n ordered.sync = this.normalizeRecordDataOrder(data.sync);\n }\n if (data.deleteRecord !== undefined) {\n ordered.deleteRecord = this.normalizeRecordDataOrder(data.deleteRecord);\n }\n \n return ordered;\n } else {\n // Regular object - recursively process properties\n const processed: any = {};\n for (const [key, value] of Object.entries(data)) {\n processed[key] = this.normalizeRecordDataOrder(value);\n }\n return processed;\n }\n }\n \n return data;\n }\n\n /**\n * Create a RecordData object with explicit property ordering for consistent JSON output\n * @param fields - Entity field data\n * @param relatedEntities - Related entity data\n * @param primaryKey - Primary key data\n * @param sync - Sync metadata\n * @returns RecordData object with guaranteed property order\n */\n static createOrderedRecordData(\n fields: Record<string, any>,\n relatedEntities: Record<string, RecordData[]>,\n primaryKey: Record<string, any>,\n sync: { lastModified: string; checksum: string }\n ): RecordData {\n // Use a Map to preserve insertion order, then convert to object\n const orderedProps = new Map<string, any>();\n \n // Add properties in the desired order\n orderedProps.set('fields', fields);\n \n if (Object.keys(relatedEntities).length > 0) {\n orderedProps.set('relatedEntities', relatedEntities);\n }\n \n orderedProps.set('primaryKey', primaryKey);\n orderedProps.set('sync', sync);\n \n // Convert Map to object while preserving order\n const recordData = {} as RecordData;\n for (const [key, value] of orderedProps) {\n (recordData as any)[key] = value;\n }\n \n return recordData;\n }\n\n /**\n * Write regular JSON data (non-RecordData) with standard formatting\n * @param filePath - Path to the JSON file to write\n * @param data - Any JSON-serializable data\n * @param options - Optional JSON write options\n */\n static async writeJson(filePath: string, data: any, options?: JsonWriteOptions): Promise<void> {\n const defaultOptions = { spaces: 2 };\n const writeOptions = typeof options === 'object' && options !== null \n ? { ...defaultOptions, ...options }\n : defaultOptions;\n await fs.writeJson(filePath, data, writeOptions);\n }\n}"]}
1
+ {"version":3,"file":"json-write-helper.js","sourceRoot":"","sources":["../../src/lib/json-write-helper.ts"],"names":[],"mappings":"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","sourcesContent":["import fs, { JsonWriteOptions } from 'fs-extra';\nimport { RecordData } from './sync-engine';\n\n/**\n * Helper class for writing JSON files with consistent property ordering for RecordData objects.\n * Ensures that all metadata files have the same property order: fields, relatedEntities, primaryKey, sync\n */\nexport class JsonWriteHelper {\n \n /**\n * Write RecordData or arrays of RecordData with consistent property ordering\n * @param filePath - Path to the JSON file to write\n * @param data - RecordData object or array of RecordData objects\n */\n static async writeOrderedRecordData(filePath: string, data: RecordData | RecordData[]): Promise<void> {\n // Pre-process the data to ensure correct ordering before JSON.stringify\n const normalizedData = this.normalizeRecordDataOrder(data);\n \n // Use JSON.stringify with proper spacing\n const jsonString = JSON.stringify(normalizedData, null, 2);\n await fs.writeFile(filePath, jsonString, 'utf8');\n }\n\n /**\n * Recursively normalize RecordData objects to ensure correct property ordering\n * @param data - RecordData object, array of RecordData objects, or any nested structure\n * @returns Normalized data with consistent property ordering\n */\n private static normalizeRecordDataOrder(data: any): any {\n if (Array.isArray(data)) {\n return data.map(item => this.normalizeRecordDataOrder(item));\n }\n\n if (data && typeof data === 'object') {\n // Check if this looks like a RecordData object\n if (data.fields !== undefined) {\n // This is a RecordData object - rebuild preserving original key order\n // but ensuring known keys maintain their relative order when present\n const ordered: any = {};\n // Known keys that are part of RecordData structure\n // __mj_sync_notes is a system-managed key that should appear after sync\n const knownKeys = ['fields', 'relatedEntities', 'primaryKey', 'sync', '__mj_sync_notes', 'deleteRecord'];\n\n // Process keys in original order, preserving user's ordering\n for (const key of Object.keys(data)) {\n if (knownKeys.includes(key)) {\n // Known key - process recursively\n ordered[key] = this.normalizeRecordDataOrder(data[key]);\n } else {\n // Unknown key (like _comments) - preserve exactly as-is\n ordered[key] = data[key];\n }\n }\n\n return ordered;\n } else {\n // Regular object - recursively process properties\n const processed: any = {};\n for (const [key, value] of Object.entries(data)) {\n processed[key] = this.normalizeRecordDataOrder(value);\n }\n return processed;\n }\n }\n\n return data;\n }\n\n /**\n * Create a RecordData object with explicit property ordering for consistent JSON output\n * @param fields - Entity field data\n * @param relatedEntities - Related entity data\n * @param primaryKey - Primary key data\n * @param sync - Sync metadata\n * @returns RecordData object with guaranteed property order\n */\n static createOrderedRecordData(\n fields: Record<string, any>,\n relatedEntities: Record<string, RecordData[]>,\n primaryKey: Record<string, any>,\n sync: { lastModified: string; checksum: string }\n ): RecordData {\n // Use a Map to preserve insertion order, then convert to object\n const orderedProps = new Map<string, any>();\n \n // Add properties in the desired order\n orderedProps.set('fields', fields);\n \n if (Object.keys(relatedEntities).length > 0) {\n orderedProps.set('relatedEntities', relatedEntities);\n }\n \n orderedProps.set('primaryKey', primaryKey);\n orderedProps.set('sync', sync);\n \n // Convert Map to object while preserving order\n const recordData = {} as RecordData;\n for (const [key, value] of orderedProps) {\n (recordData as any)[key] = value;\n }\n \n return recordData;\n }\n\n /**\n * Write regular JSON data (non-RecordData) with standard formatting\n * @param filePath - Path to the JSON file to write\n * @param data - Any JSON-serializable data\n * @param options - Optional JSON write options\n */\n static async writeJson(filePath: string, data: any, options?: JsonWriteOptions): Promise<void> {\n const defaultOptions = { spaces: 2 };\n const writeOptions = typeof options === 'object' && options !== null \n ? { ...defaultOptions, ...options }\n : defaultOptions;\n await fs.writeJson(filePath, data, writeOptions);\n }\n}"]}