@memberjunction/metadata-sync 2.67.0 → 2.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +57 -0
  2. package/dist/config.d.ts +4 -0
  3. package/dist/config.js.map +1 -1
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.js +5 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/lib/EntityPropertyExtractor.d.ts +60 -0
  8. package/dist/lib/EntityPropertyExtractor.js +166 -0
  9. package/dist/lib/EntityPropertyExtractor.js.map +1 -0
  10. package/dist/lib/FieldExternalizer.d.ts +62 -0
  11. package/dist/lib/FieldExternalizer.js +177 -0
  12. package/dist/lib/FieldExternalizer.js.map +1 -0
  13. package/dist/lib/RecordProcessor.d.ts +82 -0
  14. package/dist/lib/RecordProcessor.js +309 -0
  15. package/dist/lib/RecordProcessor.js.map +1 -0
  16. package/dist/lib/RelatedEntityHandler.d.ts +75 -0
  17. package/dist/lib/RelatedEntityHandler.js +273 -0
  18. package/dist/lib/RelatedEntityHandler.js.map +1 -0
  19. package/dist/lib/file-write-batch.d.ts +61 -0
  20. package/dist/lib/file-write-batch.js +180 -0
  21. package/dist/lib/file-write-batch.js.map +1 -0
  22. package/dist/lib/json-write-helper.d.ts +39 -0
  23. package/dist/lib/json-write-helper.js +105 -0
  24. package/dist/lib/json-write-helper.js.map +1 -0
  25. package/dist/services/FileResetService.js +2 -1
  26. package/dist/services/FileResetService.js.map +1 -1
  27. package/dist/services/PullService.d.ts +22 -2
  28. package/dist/services/PullService.js +268 -173
  29. package/dist/services/PullService.js.map +1 -1
  30. package/dist/services/PushService.js +3 -2
  31. package/dist/services/PushService.js.map +1 -1
  32. package/dist/services/WatchService.js +3 -2
  33. package/dist/services/WatchService.js.map +1 -1
  34. package/package.json +7 -7
@@ -0,0 +1,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}"]}