@memberjunction/metadata-sync 2.66.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.
- package/README.md +57 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/EntityPropertyExtractor.d.ts +60 -0
- package/dist/lib/EntityPropertyExtractor.js +166 -0
- package/dist/lib/EntityPropertyExtractor.js.map +1 -0
- package/dist/lib/FieldExternalizer.d.ts +62 -0
- package/dist/lib/FieldExternalizer.js +177 -0
- package/dist/lib/FieldExternalizer.js.map +1 -0
- package/dist/lib/RecordProcessor.d.ts +82 -0
- package/dist/lib/RecordProcessor.js +309 -0
- package/dist/lib/RecordProcessor.js.map +1 -0
- package/dist/lib/RelatedEntityHandler.d.ts +75 -0
- package/dist/lib/RelatedEntityHandler.js +273 -0
- package/dist/lib/RelatedEntityHandler.js.map +1 -0
- package/dist/lib/file-write-batch.d.ts +61 -0
- package/dist/lib/file-write-batch.js +180 -0
- package/dist/lib/file-write-batch.js.map +1 -0
- package/dist/lib/json-write-helper.d.ts +39 -0
- package/dist/lib/json-write-helper.js +105 -0
- package/dist/lib/json-write-helper.js.map +1 -0
- package/dist/services/FileResetService.js +2 -1
- package/dist/services/FileResetService.js.map +1 -1
- package/dist/services/PullService.d.ts +22 -2
- package/dist/services/PullService.js +268 -173
- package/dist/services/PullService.js.map +1 -1
- package/dist/services/PushService.js +3 -2
- package/dist/services/PushService.js.map +1 -1
- package/dist/services/WatchService.js +3 -2
- package/dist/services/WatchService.js.map +1 -1
- package/package.json +7 -7
|
@@ -0,0 +1,177 @@
|
|
|
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.FieldExternalizer = void 0;
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
/**
|
|
10
|
+
* Handles externalization of field values to separate files with @file: references
|
|
11
|
+
*/
|
|
12
|
+
class FieldExternalizer {
|
|
13
|
+
/**
|
|
14
|
+
* Externalize a field value to a separate file and return @file: reference
|
|
15
|
+
*/
|
|
16
|
+
async externalizeField(fieldName, fieldValue, pattern, recordData, targetDir, existingFileReference, mergeStrategy = 'merge', verbose) {
|
|
17
|
+
const { finalFilePath, fileReference } = this.determineFilePath(pattern, recordData, targetDir, existingFileReference, mergeStrategy, fieldName, verbose);
|
|
18
|
+
const shouldWrite = await this.shouldWriteFile(finalFilePath, fieldValue, fieldName);
|
|
19
|
+
if (shouldWrite) {
|
|
20
|
+
await this.writeExternalFile(finalFilePath, fieldValue, fieldName, verbose);
|
|
21
|
+
}
|
|
22
|
+
else if (verbose) {
|
|
23
|
+
console.log(`External file ${finalFilePath} unchanged, skipping write`);
|
|
24
|
+
}
|
|
25
|
+
return fileReference;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Determines the file path and reference for externalization
|
|
29
|
+
*/
|
|
30
|
+
determineFilePath(pattern, recordData, targetDir, existingFileReference, mergeStrategy = 'merge', fieldName = '', verbose) {
|
|
31
|
+
if (this.shouldUseExistingReference(existingFileReference, mergeStrategy)) {
|
|
32
|
+
return this.useExistingFileReference(existingFileReference, targetDir, verbose);
|
|
33
|
+
}
|
|
34
|
+
return this.createNewFileReference(pattern, recordData, targetDir, fieldName, verbose);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Checks if we should use an existing file reference
|
|
38
|
+
*/
|
|
39
|
+
shouldUseExistingReference(existingFileReference, mergeStrategy = 'merge') {
|
|
40
|
+
return mergeStrategy === 'merge' &&
|
|
41
|
+
!!existingFileReference &&
|
|
42
|
+
typeof existingFileReference === 'string' &&
|
|
43
|
+
existingFileReference.startsWith('@file:');
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Uses an existing file reference
|
|
47
|
+
*/
|
|
48
|
+
useExistingFileReference(existingFileReference, targetDir, verbose) {
|
|
49
|
+
const existingPath = existingFileReference.substring(6); // Remove @file: prefix
|
|
50
|
+
const finalFilePath = path_1.default.resolve(targetDir, existingPath);
|
|
51
|
+
if (verbose) {
|
|
52
|
+
console.log(`Using existing external file: ${finalFilePath}`);
|
|
53
|
+
}
|
|
54
|
+
return { finalFilePath, fileReference: existingFileReference };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Creates a new file reference using the pattern
|
|
58
|
+
*/
|
|
59
|
+
createNewFileReference(pattern, recordData, targetDir, fieldName, verbose) {
|
|
60
|
+
const processedPattern = this.processPattern(pattern, recordData, fieldName);
|
|
61
|
+
const cleanPattern = this.removeFilePrefix(processedPattern);
|
|
62
|
+
const finalFilePath = path_1.default.resolve(targetDir, cleanPattern);
|
|
63
|
+
const fileReference = `@file:${cleanPattern}`;
|
|
64
|
+
if (verbose) {
|
|
65
|
+
console.log(`Creating new external file: ${finalFilePath}`);
|
|
66
|
+
}
|
|
67
|
+
return { finalFilePath, fileReference };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Processes pattern placeholders with actual values
|
|
71
|
+
*/
|
|
72
|
+
processPattern(pattern, recordData, fieldName) {
|
|
73
|
+
let processedPattern = pattern;
|
|
74
|
+
// Replace common placeholders
|
|
75
|
+
processedPattern = this.replacePlaceholder(processedPattern, 'Name', recordData.Name);
|
|
76
|
+
processedPattern = this.replacePlaceholder(processedPattern, 'ID', recordData.ID);
|
|
77
|
+
processedPattern = this.replacePlaceholder(processedPattern, 'FieldName', fieldName);
|
|
78
|
+
// Replace any other field placeholders
|
|
79
|
+
processedPattern = this.replaceFieldPlaceholders(processedPattern, recordData);
|
|
80
|
+
return processedPattern;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Replaces a single placeholder in the pattern
|
|
84
|
+
*/
|
|
85
|
+
replacePlaceholder(pattern, placeholder, value) {
|
|
86
|
+
if (value != null) {
|
|
87
|
+
const sanitizedValue = this.sanitizeForFilename(String(value));
|
|
88
|
+
return pattern.replace(new RegExp(`\\{${placeholder}\\}`, 'g'), sanitizedValue);
|
|
89
|
+
}
|
|
90
|
+
return pattern;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Replaces field placeholders with values from the record
|
|
94
|
+
*/
|
|
95
|
+
replaceFieldPlaceholders(pattern, recordData) {
|
|
96
|
+
let processedPattern = pattern;
|
|
97
|
+
for (const [key, value] of Object.entries(recordData)) {
|
|
98
|
+
if (value != null) {
|
|
99
|
+
const sanitizedValue = this.sanitizeForFilename(String(value));
|
|
100
|
+
processedPattern = processedPattern.replace(new RegExp(`\\{${key}\\}`, 'g'), sanitizedValue);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return processedPattern;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Removes @file: prefix if present
|
|
107
|
+
*/
|
|
108
|
+
removeFilePrefix(pattern) {
|
|
109
|
+
return pattern.startsWith('@file:') ? pattern.substring(6) : pattern;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Determines if the file should be written based on content comparison
|
|
113
|
+
*/
|
|
114
|
+
async shouldWriteFile(finalFilePath, fieldValue, fieldName) {
|
|
115
|
+
if (!(await fs_extra_1.default.pathExists(finalFilePath))) {
|
|
116
|
+
return true; // File doesn't exist, should write
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const existingContent = await fs_extra_1.default.readFile(finalFilePath, 'utf8');
|
|
120
|
+
const contentToWrite = this.prepareContentForWriting(fieldValue, fieldName);
|
|
121
|
+
return existingContent !== contentToWrite;
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
return true; // Error reading existing file, should write
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Writes the external file with the field content
|
|
129
|
+
*/
|
|
130
|
+
async writeExternalFile(finalFilePath, fieldValue, fieldName, verbose) {
|
|
131
|
+
// Ensure the directory exists
|
|
132
|
+
await fs_extra_1.default.ensureDir(path_1.default.dirname(finalFilePath));
|
|
133
|
+
// Write the field value to the file
|
|
134
|
+
const contentToWrite = this.prepareContentForWriting(fieldValue, fieldName);
|
|
135
|
+
await fs_extra_1.default.writeFile(finalFilePath, contentToWrite, 'utf8');
|
|
136
|
+
if (verbose) {
|
|
137
|
+
console.log(`Wrote externalized field ${fieldName} to ${finalFilePath}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Prepares content for writing, with JSON pretty-printing if applicable
|
|
142
|
+
*/
|
|
143
|
+
prepareContentForWriting(fieldValue, fieldName) {
|
|
144
|
+
let contentToWrite = String(fieldValue);
|
|
145
|
+
// If the value looks like JSON, try to pretty-print it
|
|
146
|
+
if (this.shouldPrettyPrintAsJson(fieldName)) {
|
|
147
|
+
try {
|
|
148
|
+
const parsed = JSON.parse(contentToWrite);
|
|
149
|
+
contentToWrite = JSON.stringify(parsed, null, 2);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// Not valid JSON, use as-is
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return contentToWrite;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Determines if content should be pretty-printed as JSON
|
|
159
|
+
*/
|
|
160
|
+
shouldPrettyPrintAsJson(fieldName) {
|
|
161
|
+
const lowerFieldName = fieldName.toLowerCase();
|
|
162
|
+
return lowerFieldName.includes('json') || lowerFieldName.includes('example');
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Sanitize a string for use in filenames
|
|
166
|
+
*/
|
|
167
|
+
sanitizeForFilename(input) {
|
|
168
|
+
return input
|
|
169
|
+
.toLowerCase()
|
|
170
|
+
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
|
171
|
+
.replace(/[^a-z0-9.-]/g, '') // Remove special characters except dots and hyphens
|
|
172
|
+
.replace(/--+/g, '-') // Replace multiple hyphens with single hyphen
|
|
173
|
+
.replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
exports.FieldExternalizer = FieldExternalizer;
|
|
177
|
+
//# sourceMappingURL=FieldExternalizer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FieldExternalizer.js","sourceRoot":"","sources":["../../src/lib/FieldExternalizer.ts"],"names":[],"mappings":"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","sourcesContent":["import fs from 'fs-extra';\nimport path from 'path';\nimport { BaseEntity } from '@memberjunction/core';\n\n/**\n * Handles externalization of field values to separate files with @file: references\n */\nexport class FieldExternalizer {\n /**\n * Externalize a field value to a separate file and return @file: reference\n */\n async externalizeField(\n fieldName: string,\n fieldValue: any,\n pattern: string,\n recordData: BaseEntity,\n targetDir: string,\n existingFileReference?: string,\n mergeStrategy: string = 'merge',\n verbose?: boolean\n ): Promise<string> {\n const { finalFilePath, fileReference } = this.determineFilePath(\n pattern, \n recordData, \n targetDir, \n existingFileReference, \n mergeStrategy, \n fieldName, \n verbose\n );\n \n const shouldWrite = await this.shouldWriteFile(finalFilePath, fieldValue, fieldName);\n \n if (shouldWrite) {\n await this.writeExternalFile(finalFilePath, fieldValue, fieldName, verbose);\n } else if (verbose) {\n console.log(`External file ${finalFilePath} unchanged, skipping write`);\n }\n \n return fileReference;\n }\n\n /**\n * Determines the file path and reference for externalization\n */\n private determineFilePath(\n pattern: string,\n recordData: BaseEntity,\n targetDir: string,\n existingFileReference?: string,\n mergeStrategy: string = 'merge',\n fieldName: string = '',\n verbose?: boolean\n ): { finalFilePath: string; fileReference: string } {\n if (this.shouldUseExistingReference(existingFileReference, mergeStrategy)) {\n return this.useExistingFileReference(existingFileReference!, targetDir, verbose);\n }\n \n return this.createNewFileReference(pattern, recordData, targetDir, fieldName, verbose);\n }\n\n /**\n * Checks if we should use an existing file reference\n */\n private shouldUseExistingReference(existingFileReference?: string, mergeStrategy: string = 'merge'): boolean {\n return mergeStrategy === 'merge' && \n !!existingFileReference && \n typeof existingFileReference === 'string' && \n existingFileReference.startsWith('@file:');\n }\n\n /**\n * Uses an existing file reference\n */\n private useExistingFileReference(\n existingFileReference: string, \n targetDir: string, \n verbose?: boolean\n ): { finalFilePath: string; fileReference: string } {\n const existingPath = existingFileReference.substring(6); // Remove @file: prefix\n const finalFilePath = path.resolve(targetDir, existingPath);\n \n if (verbose) {\n console.log(`Using existing external file: ${finalFilePath}`);\n }\n \n return { finalFilePath, fileReference: existingFileReference };\n }\n\n /**\n * Creates a new file reference using the pattern\n */\n private createNewFileReference(\n pattern: string,\n recordData: BaseEntity,\n targetDir: string,\n fieldName: string,\n verbose?: boolean\n ): { finalFilePath: string; fileReference: string } {\n const processedPattern = this.processPattern(pattern, recordData, fieldName);\n const cleanPattern = this.removeFilePrefix(processedPattern);\n const finalFilePath = path.resolve(targetDir, cleanPattern);\n const fileReference = `@file:${cleanPattern}`;\n \n if (verbose) {\n console.log(`Creating new external file: ${finalFilePath}`);\n }\n \n return { finalFilePath, fileReference };\n }\n\n /**\n * Processes pattern placeholders with actual values\n */\n private processPattern(pattern: string, recordData: BaseEntity, fieldName: string): string {\n let processedPattern = pattern;\n \n // Replace common placeholders\n processedPattern = this.replacePlaceholder(processedPattern, 'Name', (recordData as any).Name);\n processedPattern = this.replacePlaceholder(processedPattern, 'ID', (recordData as any).ID);\n processedPattern = this.replacePlaceholder(processedPattern, 'FieldName', fieldName);\n \n // Replace any other field placeholders\n processedPattern = this.replaceFieldPlaceholders(processedPattern, recordData);\n \n return processedPattern;\n }\n\n /**\n * Replaces a single placeholder in the pattern\n */\n private replacePlaceholder(pattern: string, placeholder: string, value: any): string {\n if (value != null) {\n const sanitizedValue = this.sanitizeForFilename(String(value));\n return pattern.replace(new RegExp(`\\\\{${placeholder}\\\\}`, 'g'), sanitizedValue);\n }\n return pattern;\n }\n\n /**\n * Replaces field placeholders with values from the record\n */\n private replaceFieldPlaceholders(pattern: string, recordData: BaseEntity): string {\n let processedPattern = pattern;\n \n for (const [key, value] of Object.entries(recordData as any)) {\n if (value != null) {\n const sanitizedValue = this.sanitizeForFilename(String(value));\n processedPattern = processedPattern.replace(new RegExp(`\\\\{${key}\\\\}`, 'g'), sanitizedValue);\n }\n }\n \n return processedPattern;\n }\n\n /**\n * Removes @file: prefix if present\n */\n private removeFilePrefix(pattern: string): string {\n return pattern.startsWith('@file:') ? pattern.substring(6) : pattern;\n }\n\n /**\n * Determines if the file should be written based on content comparison\n */\n private async shouldWriteFile(finalFilePath: string, fieldValue: any, fieldName: string): Promise<boolean> {\n if (!(await fs.pathExists(finalFilePath))) {\n return true; // File doesn't exist, should write\n }\n \n try {\n const existingContent = await fs.readFile(finalFilePath, 'utf8');\n const contentToWrite = this.prepareContentForWriting(fieldValue, fieldName);\n \n return existingContent !== contentToWrite;\n } catch (error) {\n return true; // Error reading existing file, should write\n }\n }\n\n /**\n * Writes the external file with the field content\n */\n private async writeExternalFile(\n finalFilePath: string, \n fieldValue: any, \n fieldName: string, \n verbose?: boolean\n ): Promise<void> {\n // Ensure the directory exists\n await fs.ensureDir(path.dirname(finalFilePath));\n \n // Write the field value to the file\n const contentToWrite = this.prepareContentForWriting(fieldValue, fieldName);\n await fs.writeFile(finalFilePath, contentToWrite, 'utf8');\n \n if (verbose) {\n console.log(`Wrote externalized field ${fieldName} to ${finalFilePath}`);\n }\n }\n\n /**\n * Prepares content for writing, with JSON pretty-printing if applicable\n */\n private prepareContentForWriting(fieldValue: any, fieldName: string): string {\n let contentToWrite = String(fieldValue);\n \n // If the value looks like JSON, try to pretty-print it\n if (this.shouldPrettyPrintAsJson(fieldName)) {\n try {\n const parsed = JSON.parse(contentToWrite);\n contentToWrite = JSON.stringify(parsed, null, 2);\n } catch {\n // Not valid JSON, use as-is\n }\n }\n \n return contentToWrite;\n }\n\n /**\n * Determines if content should be pretty-printed as JSON\n */\n private shouldPrettyPrintAsJson(fieldName: string): boolean {\n const lowerFieldName = fieldName.toLowerCase();\n return lowerFieldName.includes('json') || lowerFieldName.includes('example');\n }\n\n /**\n * Sanitize a string for use in filenames\n */\n private sanitizeForFilename(input: string): string {\n return input\n .toLowerCase()\n .replace(/\\s+/g, '-') // Replace spaces with hyphens\n .replace(/[^a-z0-9.-]/g, '') // Remove special characters except dots and hyphens\n .replace(/--+/g, '-') // Replace multiple hyphens with single hyphen\n .replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens\n }\n}"]}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { BaseEntity, UserInfo } from '@memberjunction/core';
|
|
2
|
+
import { SyncEngine, RecordData } from '../lib/sync-engine';
|
|
3
|
+
import { EntityConfig } from '../config';
|
|
4
|
+
/**
|
|
5
|
+
* Handles the core processing of individual record data into the sync format
|
|
6
|
+
*/
|
|
7
|
+
export declare class RecordProcessor {
|
|
8
|
+
private syncEngine;
|
|
9
|
+
private contextUser;
|
|
10
|
+
private propertyExtractor;
|
|
11
|
+
private fieldExternalizer;
|
|
12
|
+
private relatedEntityHandler;
|
|
13
|
+
constructor(syncEngine: SyncEngine, contextUser: UserInfo);
|
|
14
|
+
/**
|
|
15
|
+
* Processes a record into the standardized RecordData format
|
|
16
|
+
*/
|
|
17
|
+
processRecord(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>;
|
|
18
|
+
/**
|
|
19
|
+
* Processes entity data into fields and related entities
|
|
20
|
+
*/
|
|
21
|
+
private processEntityData;
|
|
22
|
+
/**
|
|
23
|
+
* Processes individual fields from the entity
|
|
24
|
+
*/
|
|
25
|
+
private processFields;
|
|
26
|
+
/**
|
|
27
|
+
* Determines if a field should be skipped during processing
|
|
28
|
+
*/
|
|
29
|
+
private shouldSkipField;
|
|
30
|
+
/**
|
|
31
|
+
* Checks if a virtual field should be skipped
|
|
32
|
+
*/
|
|
33
|
+
private shouldSkipVirtualField;
|
|
34
|
+
/**
|
|
35
|
+
* Processes a single field value through various transformations
|
|
36
|
+
*/
|
|
37
|
+
private processFieldValue;
|
|
38
|
+
/**
|
|
39
|
+
* Applies lookup field conversion if configured
|
|
40
|
+
*/
|
|
41
|
+
private applyLookupFieldConversion;
|
|
42
|
+
/**
|
|
43
|
+
* Trims string values to remove whitespace
|
|
44
|
+
*/
|
|
45
|
+
private trimStringValue;
|
|
46
|
+
/**
|
|
47
|
+
* Applies field externalization if configured
|
|
48
|
+
*/
|
|
49
|
+
private applyFieldExternalization;
|
|
50
|
+
/**
|
|
51
|
+
* Gets the externalization pattern for a field
|
|
52
|
+
*/
|
|
53
|
+
private getExternalizationPattern;
|
|
54
|
+
/**
|
|
55
|
+
* Gets externalization pattern from array configuration
|
|
56
|
+
*/
|
|
57
|
+
private getArrayExternalizationPattern;
|
|
58
|
+
/**
|
|
59
|
+
* Gets externalization pattern from object configuration
|
|
60
|
+
*/
|
|
61
|
+
private getObjectExternalizationPattern;
|
|
62
|
+
/**
|
|
63
|
+
* Creates a BaseEntity-like object for externalization processing
|
|
64
|
+
*/
|
|
65
|
+
private createRecordDataForExternalization;
|
|
66
|
+
/**
|
|
67
|
+
* Processes related entities for the record
|
|
68
|
+
*/
|
|
69
|
+
private processRelatedEntities;
|
|
70
|
+
/**
|
|
71
|
+
* Calculates sync metadata including checksum and last modified timestamp
|
|
72
|
+
*/
|
|
73
|
+
private calculateSyncMetadata;
|
|
74
|
+
/**
|
|
75
|
+
* Checks if the record has externalized fields
|
|
76
|
+
*/
|
|
77
|
+
private hasExternalizedFields;
|
|
78
|
+
/**
|
|
79
|
+
* Convert a GUID value to @lookup syntax by looking up the human-readable value
|
|
80
|
+
*/
|
|
81
|
+
private convertGuidToLookup;
|
|
82
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RecordProcessor = void 0;
|
|
4
|
+
const core_1 = require("@memberjunction/core");
|
|
5
|
+
const json_write_helper_1 = require("./json-write-helper");
|
|
6
|
+
const EntityPropertyExtractor_1 = require("./EntityPropertyExtractor");
|
|
7
|
+
const FieldExternalizer_1 = require("./FieldExternalizer");
|
|
8
|
+
const RelatedEntityHandler_1 = require("./RelatedEntityHandler");
|
|
9
|
+
/**
|
|
10
|
+
* Handles the core processing of individual record data into the sync format
|
|
11
|
+
*/
|
|
12
|
+
class RecordProcessor {
|
|
13
|
+
syncEngine;
|
|
14
|
+
contextUser;
|
|
15
|
+
propertyExtractor;
|
|
16
|
+
fieldExternalizer;
|
|
17
|
+
relatedEntityHandler;
|
|
18
|
+
constructor(syncEngine, contextUser) {
|
|
19
|
+
this.syncEngine = syncEngine;
|
|
20
|
+
this.contextUser = contextUser;
|
|
21
|
+
this.propertyExtractor = new EntityPropertyExtractor_1.EntityPropertyExtractor();
|
|
22
|
+
this.fieldExternalizer = new FieldExternalizer_1.FieldExternalizer();
|
|
23
|
+
this.relatedEntityHandler = new RelatedEntityHandler_1.RelatedEntityHandler(syncEngine, contextUser);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Processes a record into the standardized RecordData format
|
|
27
|
+
*/
|
|
28
|
+
async processRecord(record, primaryKey, targetDir, entityConfig, verbose, isNewRecord = true, existingRecordData, currentDepth = 0, ancestryPath = new Set(), fieldOverrides) {
|
|
29
|
+
// Extract all properties from the entity
|
|
30
|
+
const allProperties = this.propertyExtractor.extractAllProperties(record, fieldOverrides);
|
|
31
|
+
// Process fields and related entities
|
|
32
|
+
const { fields, relatedEntities } = await this.processEntityData(allProperties, record, primaryKey, targetDir, entityConfig, existingRecordData, currentDepth, ancestryPath, verbose);
|
|
33
|
+
// Calculate checksum and sync metadata
|
|
34
|
+
const syncData = await this.calculateSyncMetadata(fields, targetDir, entityConfig, existingRecordData, verbose);
|
|
35
|
+
// Build the final record data with proper ordering
|
|
36
|
+
return json_write_helper_1.JsonWriteHelper.createOrderedRecordData(fields, relatedEntities, primaryKey, syncData);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Processes entity data into fields and related entities
|
|
40
|
+
*/
|
|
41
|
+
async processEntityData(allProperties, record, primaryKey, targetDir, entityConfig, existingRecordData, currentDepth, ancestryPath, verbose) {
|
|
42
|
+
const fields = {};
|
|
43
|
+
const relatedEntities = {};
|
|
44
|
+
// Process individual fields
|
|
45
|
+
await this.processFields(allProperties, primaryKey, targetDir, entityConfig, existingRecordData, fields, verbose);
|
|
46
|
+
// Process related entities if configured
|
|
47
|
+
await this.processRelatedEntities(record, entityConfig, existingRecordData, currentDepth, ancestryPath, relatedEntities, verbose);
|
|
48
|
+
return { fields, relatedEntities };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Processes individual fields from the entity
|
|
52
|
+
*/
|
|
53
|
+
async processFields(allProperties, primaryKey, targetDir, entityConfig, existingRecordData, fields, verbose) {
|
|
54
|
+
const entityInfo = this.syncEngine.getEntityInfo(entityConfig.entity);
|
|
55
|
+
for (const [fieldName, fieldValue] of Object.entries(allProperties)) {
|
|
56
|
+
if (this.shouldSkipField(fieldName, fieldValue, primaryKey, entityConfig, entityInfo)) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
let processedValue = await this.processFieldValue(fieldName, fieldValue, allProperties, targetDir, entityConfig, existingRecordData, verbose);
|
|
60
|
+
fields[fieldName] = processedValue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Determines if a field should be skipped during processing
|
|
65
|
+
*/
|
|
66
|
+
shouldSkipField(fieldName, fieldValue, primaryKey, entityConfig, entityInfo) {
|
|
67
|
+
// Skip primary key fields
|
|
68
|
+
if (primaryKey[fieldName] !== undefined) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
// Skip internal fields
|
|
72
|
+
if (fieldName.startsWith('__mj_')) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
// Skip excluded fields
|
|
76
|
+
if (entityConfig.pull?.excludeFields?.includes(fieldName)) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
// Skip virtual fields if configured
|
|
80
|
+
if (this.shouldSkipVirtualField(fieldName, entityConfig, entityInfo)) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
// Skip null fields if configured
|
|
84
|
+
if (entityConfig.pull?.ignoreNullFields && fieldValue === null) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Checks if a virtual field should be skipped
|
|
91
|
+
*/
|
|
92
|
+
shouldSkipVirtualField(fieldName, entityConfig, entityInfo) {
|
|
93
|
+
if (!entityConfig.pull?.ignoreVirtualFields || !entityInfo) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
const fieldInfo = entityInfo.Fields.find(f => f.Name === fieldName);
|
|
97
|
+
return fieldInfo?.IsVirtual === true;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Processes a single field value through various transformations
|
|
101
|
+
*/
|
|
102
|
+
async processFieldValue(fieldName, fieldValue, allProperties, targetDir, entityConfig, existingRecordData, verbose) {
|
|
103
|
+
let processedValue = fieldValue;
|
|
104
|
+
// Apply lookup field conversion if configured
|
|
105
|
+
processedValue = await this.applyLookupFieldConversion(fieldName, processedValue, entityConfig, verbose);
|
|
106
|
+
// Trim string values
|
|
107
|
+
processedValue = this.trimStringValue(processedValue);
|
|
108
|
+
// Apply field externalization if configured
|
|
109
|
+
processedValue = await this.applyFieldExternalization(fieldName, processedValue, allProperties, targetDir, entityConfig, existingRecordData, verbose);
|
|
110
|
+
return processedValue;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Applies lookup field conversion if configured
|
|
114
|
+
*/
|
|
115
|
+
async applyLookupFieldConversion(fieldName, fieldValue, entityConfig, verbose) {
|
|
116
|
+
const lookupConfig = entityConfig.pull?.lookupFields?.[fieldName];
|
|
117
|
+
if (!lookupConfig || fieldValue == null) {
|
|
118
|
+
return fieldValue;
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
return await this.convertGuidToLookup(String(fieldValue), lookupConfig, verbose);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (verbose) {
|
|
125
|
+
console.warn(`Failed to convert ${fieldName} to lookup: ${error}`);
|
|
126
|
+
}
|
|
127
|
+
return fieldValue; // Keep original value if lookup fails
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Trims string values to remove whitespace
|
|
132
|
+
*/
|
|
133
|
+
trimStringValue(value) {
|
|
134
|
+
return typeof value === 'string' ? value.trim() : value;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Applies field externalization if configured
|
|
138
|
+
*/
|
|
139
|
+
async applyFieldExternalization(fieldName, fieldValue, allProperties, targetDir, entityConfig, existingRecordData, verbose) {
|
|
140
|
+
if (!entityConfig.pull?.externalizeFields || fieldValue == null) {
|
|
141
|
+
return fieldValue;
|
|
142
|
+
}
|
|
143
|
+
const externalizePattern = this.getExternalizationPattern(fieldName, entityConfig);
|
|
144
|
+
if (!externalizePattern) {
|
|
145
|
+
return fieldValue;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const existingFileReference = existingRecordData?.fields?.[fieldName];
|
|
149
|
+
const recordData = this.createRecordDataForExternalization(allProperties);
|
|
150
|
+
return await this.fieldExternalizer.externalizeField(fieldName, fieldValue, externalizePattern, recordData, targetDir, existingFileReference, entityConfig.pull?.mergeStrategy || 'merge', verbose);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (verbose) {
|
|
154
|
+
console.warn(`Failed to externalize field ${fieldName}: ${error}`);
|
|
155
|
+
}
|
|
156
|
+
return fieldValue; // Keep original value if externalization fails
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Gets the externalization pattern for a field
|
|
161
|
+
*/
|
|
162
|
+
getExternalizationPattern(fieldName, entityConfig) {
|
|
163
|
+
const externalizeConfig = entityConfig.pull?.externalizeFields;
|
|
164
|
+
if (!externalizeConfig)
|
|
165
|
+
return null;
|
|
166
|
+
if (Array.isArray(externalizeConfig)) {
|
|
167
|
+
return this.getArrayExternalizationPattern(fieldName, externalizeConfig);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
return this.getObjectExternalizationPattern(fieldName, externalizeConfig);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Gets externalization pattern from array configuration
|
|
175
|
+
*/
|
|
176
|
+
getArrayExternalizationPattern(fieldName, externalizeConfig) {
|
|
177
|
+
if (externalizeConfig.length > 0 && typeof externalizeConfig[0] === 'string') {
|
|
178
|
+
// Simple string array format
|
|
179
|
+
if (externalizeConfig.includes(fieldName)) {
|
|
180
|
+
return `@file:{Name}.${fieldName.toLowerCase()}.md`;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
// Array of objects format
|
|
185
|
+
const fieldConfig = externalizeConfig
|
|
186
|
+
.find(config => config.field === fieldName);
|
|
187
|
+
if (fieldConfig) {
|
|
188
|
+
return fieldConfig.pattern;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Gets externalization pattern from object configuration
|
|
195
|
+
*/
|
|
196
|
+
getObjectExternalizationPattern(fieldName, externalizeConfig) {
|
|
197
|
+
const fieldConfig = externalizeConfig[fieldName];
|
|
198
|
+
if (fieldConfig) {
|
|
199
|
+
const extension = fieldConfig.extension || '.md';
|
|
200
|
+
return `@file:{Name}.${fieldName.toLowerCase()}${extension}`;
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Creates a BaseEntity-like object for externalization processing
|
|
206
|
+
*/
|
|
207
|
+
createRecordDataForExternalization(allProperties) {
|
|
208
|
+
return allProperties;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Processes related entities for the record
|
|
212
|
+
*/
|
|
213
|
+
async processRelatedEntities(record, entityConfig, existingRecordData, currentDepth, ancestryPath, relatedEntities, verbose) {
|
|
214
|
+
if (!entityConfig.pull?.relatedEntities) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
for (const [relationKey, relationConfig] of Object.entries(entityConfig.pull.relatedEntities)) {
|
|
218
|
+
try {
|
|
219
|
+
const existingRelated = existingRecordData?.relatedEntities?.[relationKey] || [];
|
|
220
|
+
const relatedRecords = await this.relatedEntityHandler.loadRelatedEntities(record, relationConfig, entityConfig, existingRelated, this.processRecord.bind(this), // Pass bound method reference
|
|
221
|
+
currentDepth, ancestryPath, verbose);
|
|
222
|
+
if (relatedRecords.length > 0) {
|
|
223
|
+
relatedEntities[relationKey] = relatedRecords;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
if (verbose) {
|
|
228
|
+
console.warn(`Failed to load related entities for ${relationKey}: ${error}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Calculates sync metadata including checksum and last modified timestamp
|
|
235
|
+
*/
|
|
236
|
+
async calculateSyncMetadata(fields, targetDir, entityConfig, existingRecordData, verbose) {
|
|
237
|
+
// Determine if we should include external file content in checksum
|
|
238
|
+
const hasExternalizedFields = this.hasExternalizedFields(fields, entityConfig);
|
|
239
|
+
const checksum = hasExternalizedFields
|
|
240
|
+
? await this.syncEngine.calculateChecksumWithFileContent(fields, targetDir)
|
|
241
|
+
: this.syncEngine.calculateChecksum(fields);
|
|
242
|
+
if (verbose && hasExternalizedFields) {
|
|
243
|
+
console.log(`Calculated checksum including external file content for record`);
|
|
244
|
+
}
|
|
245
|
+
// Compare with existing checksum to determine if data changed
|
|
246
|
+
if (existingRecordData?.sync?.checksum === checksum) {
|
|
247
|
+
// No change detected - preserve existing sync metadata
|
|
248
|
+
if (verbose) {
|
|
249
|
+
console.log(`No changes detected for record, preserving existing timestamp`);
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
lastModified: existingRecordData.sync.lastModified,
|
|
253
|
+
checksum: checksum
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
// Change detected - update timestamp
|
|
258
|
+
if (verbose && existingRecordData?.sync?.checksum) {
|
|
259
|
+
console.log(`Changes detected for record, updating timestamp`);
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
lastModified: new Date().toISOString(),
|
|
263
|
+
checksum: checksum
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Checks if the record has externalized fields
|
|
269
|
+
*/
|
|
270
|
+
hasExternalizedFields(fields, entityConfig) {
|
|
271
|
+
return !!entityConfig.pull?.externalizeFields &&
|
|
272
|
+
Object.values(fields).some(value => typeof value === 'string' && value.startsWith('@file:'));
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Convert a GUID value to @lookup syntax by looking up the human-readable value
|
|
276
|
+
*/
|
|
277
|
+
async convertGuidToLookup(guidValue, lookupConfig, verbose) {
|
|
278
|
+
if (!guidValue || typeof guidValue !== 'string') {
|
|
279
|
+
return guidValue;
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
const rv = new core_1.RunView();
|
|
283
|
+
const result = await rv.RunView({
|
|
284
|
+
EntityName: lookupConfig.entity,
|
|
285
|
+
ExtraFilter: `ID = '${guidValue}'`,
|
|
286
|
+
ResultType: 'entity_object'
|
|
287
|
+
}, this.contextUser);
|
|
288
|
+
if (result.Success && result.Results && result.Results.length > 0) {
|
|
289
|
+
const targetRecord = result.Results[0];
|
|
290
|
+
const lookupValue = targetRecord[lookupConfig.field];
|
|
291
|
+
if (lookupValue != null) {
|
|
292
|
+
return `@lookup:${lookupConfig.entity}.${lookupConfig.field}=${lookupValue}`;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (verbose) {
|
|
296
|
+
console.warn(`Lookup failed for ${guidValue} in ${lookupConfig.entity}.${lookupConfig.field}`);
|
|
297
|
+
}
|
|
298
|
+
return guidValue; // Return original GUID if lookup fails
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
if (verbose) {
|
|
302
|
+
console.warn(`Error during lookup conversion: ${error}`);
|
|
303
|
+
}
|
|
304
|
+
return guidValue;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
exports.RecordProcessor = RecordProcessor;
|
|
309
|
+
//# sourceMappingURL=RecordProcessor.js.map
|
|
@@ -0,0 +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';\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 `@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 `@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('@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 `@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}"]}
|