@memberjunction/metadata-sync 2.111.1 → 2.113.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 +86 -11
- package/dist/constants/metadata-keywords.d.ts +282 -0
- package/dist/constants/metadata-keywords.js +364 -0
- package/dist/constants/metadata-keywords.js.map +1 -0
- package/dist/lib/FieldExternalizer.js +5 -4
- package/dist/lib/FieldExternalizer.js.map +1 -1
- package/dist/lib/RecordProcessor.js +5 -4
- package/dist/lib/RecordProcessor.js.map +1 -1
- package/dist/lib/json-preprocessor.js +7 -6
- package/dist/lib/json-preprocessor.js.map +1 -1
- package/dist/lib/record-dependency-analyzer.js +18 -17
- package/dist/lib/record-dependency-analyzer.js.map +1 -1
- package/dist/lib/sync-engine.js +22 -26
- package/dist/lib/sync-engine.js.map +1 -1
- package/dist/services/ValidationService.js +28 -40
- package/dist/services/ValidationService.js.map +1 -1
- package/dist/types/validation.d.ts +6 -1
- package/dist/types/validation.js.map +1 -1
- package/package.json +7 -7
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Centralized metadata keyword constants for MemberJunction MetadataSync package.
|
|
4
|
+
*
|
|
5
|
+
* These keywords are special prefixes used in metadata JSON files to reference external
|
|
6
|
+
* content, perform lookups, access environment variables, and establish hierarchical relationships.
|
|
7
|
+
*
|
|
8
|
+
* @module metadata-keywords
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* // Using @file: to reference external content
|
|
12
|
+
* {
|
|
13
|
+
* "Prompt": "@file:greeting.prompt.md"
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // Using @lookup: to find an entity by field value
|
|
18
|
+
* {
|
|
19
|
+
* "CategoryID": "@lookup:AI Prompt Categories.Name=Examples"
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* // Using @parent: to reference parent entity fields
|
|
24
|
+
* {
|
|
25
|
+
* "PromptID": "@parent:ID"
|
|
26
|
+
* }
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.createKeywordReference = exports.isExternalReferenceKeyword = exports.isContextDependentKeyword = exports.RUNTIME_KEYWORDS = exports.LOOKUP_KEYWORDS = exports.EXTERNAL_REFERENCE_KEYWORDS = exports.CONTEXT_DEPENDENT_KEYWORDS = exports.extractKeywordValue = exports.isNonKeywordAtSymbol = exports.hasMetadataKeyword = exports.getMetadataKeywordType = exports.isMetadataKeyword = exports.METADATA_KEYWORD_PREFIXES = exports.METADATA_KEYWORDS = void 0;
|
|
30
|
+
/**
|
|
31
|
+
* Metadata keyword constants.
|
|
32
|
+
* These are the special @ prefixes recognized by MetadataSync for field value processing.
|
|
33
|
+
*/
|
|
34
|
+
exports.METADATA_KEYWORDS = {
|
|
35
|
+
/**
|
|
36
|
+
* @file: - Loads content from an external file
|
|
37
|
+
*
|
|
38
|
+
* Reads the contents of a file (relative to the JSON metadata file) and uses it as the field value.
|
|
39
|
+
* Supports text files, markdown, JSON, and other formats.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* "@file:greeting.prompt.md"
|
|
43
|
+
* "@file:./shared/common-prompt.md"
|
|
44
|
+
* "@file:../templates/standard-header.md"
|
|
45
|
+
*/
|
|
46
|
+
FILE: '@file:',
|
|
47
|
+
/**
|
|
48
|
+
* @lookup: - Looks up an entity record by field value(s)
|
|
49
|
+
*
|
|
50
|
+
* Finds an entity record matching the specified criteria and uses its ID.
|
|
51
|
+
* Supports single-field and multi-field lookups, with optional auto-creation.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* "@lookup:AI Prompt Types.Name=Chat"
|
|
55
|
+
* "@lookup:Users.Email=john@example.com&Department=Sales"
|
|
56
|
+
* "@lookup:Categories.Name=Examples?create"
|
|
57
|
+
* "@lookup:Categories.Name=Examples?create&Description=Example prompts"
|
|
58
|
+
*/
|
|
59
|
+
LOOKUP: '@lookup:',
|
|
60
|
+
/**
|
|
61
|
+
* @parent: - References a field from the parent entity
|
|
62
|
+
*
|
|
63
|
+
* In nested/related entity structures, accesses a field value from the immediate parent record.
|
|
64
|
+
* Only valid when processing nested entities that have a parent context.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* "@parent:ID"
|
|
68
|
+
* "@parent:Name"
|
|
69
|
+
* "@parent:CategoryID"
|
|
70
|
+
*/
|
|
71
|
+
PARENT: '@parent:',
|
|
72
|
+
/**
|
|
73
|
+
* @root: - References a field from the root entity
|
|
74
|
+
*
|
|
75
|
+
* In nested/related entity structures, accesses a field value from the top-level root record.
|
|
76
|
+
* Only valid when processing nested entities that have a root context.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* "@root:ID"
|
|
80
|
+
* "@root:Name"
|
|
81
|
+
*/
|
|
82
|
+
ROOT: '@root:',
|
|
83
|
+
/**
|
|
84
|
+
* @env: - Reads an environment variable
|
|
85
|
+
*
|
|
86
|
+
* Gets the value of an environment variable at runtime.
|
|
87
|
+
* Useful for configuration values that differ between environments.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* "@env:VARIABLE_NAME"
|
|
91
|
+
* "@env:NODE_ENV"
|
|
92
|
+
*/
|
|
93
|
+
ENV: '@env:',
|
|
94
|
+
/**
|
|
95
|
+
* @url: - Fetches content from a URL
|
|
96
|
+
*
|
|
97
|
+
* Downloads content from a remote URL and uses it as the field value.
|
|
98
|
+
* Supports HTTP/HTTPS URLs and file:// URLs.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* "@url:https://example.com/prompts/greeting.md"
|
|
102
|
+
* "@url:https://raw.githubusercontent.com/company/prompts/main/customer.md"
|
|
103
|
+
*/
|
|
104
|
+
URL: '@url:',
|
|
105
|
+
/**
|
|
106
|
+
* @template: - Loads a template JSON file
|
|
107
|
+
*
|
|
108
|
+
* Loads a JSON template file and uses its contents, replacing any template variables.
|
|
109
|
+
* Useful for standardizing common configurations across multiple records.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* "@template:templates/standard-ai-models.json"
|
|
113
|
+
*/
|
|
114
|
+
TEMPLATE: '@template:',
|
|
115
|
+
/**
|
|
116
|
+
* @include or @include.* - Includes content from another file
|
|
117
|
+
*
|
|
118
|
+
* Special directive (not a field value) that merges content from external files.
|
|
119
|
+
* Can be used in both objects and arrays. Supports spread and property modes.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* { "@include": "common-fields.json" }
|
|
123
|
+
* [ "@include:items.json" ]
|
|
124
|
+
* { "@include.models": { "file": "models.json", "mode": "property" } }
|
|
125
|
+
*/
|
|
126
|
+
INCLUDE: '@include',
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Array of all metadata keyword prefixes for iteration.
|
|
130
|
+
* This array maintains the order of keywords for consistent processing.
|
|
131
|
+
*/
|
|
132
|
+
exports.METADATA_KEYWORD_PREFIXES = Object.values(exports.METADATA_KEYWORDS);
|
|
133
|
+
/**
|
|
134
|
+
* Checks if a value is a string that starts with any recognized metadata keyword.
|
|
135
|
+
*
|
|
136
|
+
* This is a type-safe check that handles non-string values gracefully.
|
|
137
|
+
* Returns false for null, undefined, objects, numbers, etc.
|
|
138
|
+
*
|
|
139
|
+
* @param value - The value to check (can be any type)
|
|
140
|
+
* @returns true if value is a string starting with a metadata keyword, false otherwise
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* isMetadataKeyword('@file:template.md') // true
|
|
144
|
+
* isMetadataKeyword('@lookup:Users.Email=test@example.com') // true
|
|
145
|
+
* isMetadataKeyword('@parent:ID') // true
|
|
146
|
+
* isMetadataKeyword('regular string') // false
|
|
147
|
+
* isMetadataKeyword(123) // false
|
|
148
|
+
* isMetadataKeyword(null) // false
|
|
149
|
+
* isMetadataKeyword('@unknown:value') // false
|
|
150
|
+
*/
|
|
151
|
+
function isMetadataKeyword(value) {
|
|
152
|
+
if (typeof value !== 'string') {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
return exports.METADATA_KEYWORD_PREFIXES.some(prefix => value.startsWith(prefix));
|
|
156
|
+
}
|
|
157
|
+
exports.isMetadataKeyword = isMetadataKeyword;
|
|
158
|
+
/**
|
|
159
|
+
* Determines which metadata keyword type a string value uses.
|
|
160
|
+
*
|
|
161
|
+
* Examines the prefix of a string and returns the corresponding keyword type.
|
|
162
|
+
* Returns null if the value doesn't use any recognized metadata keyword.
|
|
163
|
+
*
|
|
164
|
+
* @param value - The string value to analyze
|
|
165
|
+
* @returns The keyword type ('file', 'lookup', etc.) or null if no keyword is found
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* getMetadataKeywordType('@file:template.md') // 'file'
|
|
169
|
+
* getMetadataKeywordType('@lookup:Users.Name=John') // 'lookup'
|
|
170
|
+
* getMetadataKeywordType('@parent:ID') // 'parent'
|
|
171
|
+
* getMetadataKeywordType('@include') // 'include'
|
|
172
|
+
* getMetadataKeywordType('regular string') // null
|
|
173
|
+
* getMetadataKeywordType('@unknown:value') // null
|
|
174
|
+
*/
|
|
175
|
+
function getMetadataKeywordType(value) {
|
|
176
|
+
if (typeof value !== 'string') {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
// Special handling for @include which doesn't require a colon
|
|
180
|
+
if (value === exports.METADATA_KEYWORDS.INCLUDE || value.startsWith(`${exports.METADATA_KEYWORDS.INCLUDE}.`)) {
|
|
181
|
+
return 'include';
|
|
182
|
+
}
|
|
183
|
+
// Check all other keywords
|
|
184
|
+
for (const [key, prefix] of Object.entries(exports.METADATA_KEYWORDS)) {
|
|
185
|
+
if (key === 'INCLUDE')
|
|
186
|
+
continue; // Already handled above
|
|
187
|
+
if (value.startsWith(prefix)) {
|
|
188
|
+
return key.toLowerCase();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
exports.getMetadataKeywordType = getMetadataKeywordType;
|
|
194
|
+
/**
|
|
195
|
+
* Type-safe check for metadata keywords that handles any value type.
|
|
196
|
+
*
|
|
197
|
+
* This is an alias for isMetadataKeyword() provided for semantic clarity
|
|
198
|
+
* when you want to explicitly check if a value has a metadata keyword.
|
|
199
|
+
*
|
|
200
|
+
* @param value - Any value to check
|
|
201
|
+
* @returns true if value is a string with a metadata keyword, false otherwise
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* const fieldValue = record.Get('SomeField');
|
|
205
|
+
* if (hasMetadataKeyword(fieldValue)) {
|
|
206
|
+
* // fieldValue is guaranteed to be a string with a metadata keyword
|
|
207
|
+
* const type = getMetadataKeywordType(fieldValue);
|
|
208
|
+
* }
|
|
209
|
+
*/
|
|
210
|
+
function hasMetadataKeyword(value) {
|
|
211
|
+
return isMetadataKeyword(value);
|
|
212
|
+
}
|
|
213
|
+
exports.hasMetadataKeyword = hasMetadataKeyword;
|
|
214
|
+
/**
|
|
215
|
+
* Checks if a string starts with @ but is NOT a metadata keyword.
|
|
216
|
+
*
|
|
217
|
+
* This is useful for filtering out @ strings that are not metadata keywords,
|
|
218
|
+
* such as npm package names (@mui/material, @angular/core) or email addresses.
|
|
219
|
+
*
|
|
220
|
+
* @param value - The value to check
|
|
221
|
+
* @returns true if value starts with @ but is not a metadata keyword
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* isNonKeywordAtSymbol('@mui/material') // true
|
|
225
|
+
* isNonKeywordAtSymbol('@angular/core') // true
|
|
226
|
+
* isNonKeywordAtSymbol('@file:template.md') // false
|
|
227
|
+
* isNonKeywordAtSymbol('regular string') // false
|
|
228
|
+
*/
|
|
229
|
+
function isNonKeywordAtSymbol(value) {
|
|
230
|
+
return typeof value === 'string' &&
|
|
231
|
+
value.startsWith('@') &&
|
|
232
|
+
!isMetadataKeyword(value);
|
|
233
|
+
}
|
|
234
|
+
exports.isNonKeywordAtSymbol = isNonKeywordAtSymbol;
|
|
235
|
+
/**
|
|
236
|
+
* Extracts the value portion after a metadata keyword prefix.
|
|
237
|
+
*
|
|
238
|
+
* Removes the keyword prefix from a string and returns the remaining value.
|
|
239
|
+
* Returns null if the string doesn't use a metadata keyword.
|
|
240
|
+
*
|
|
241
|
+
* @param value - The string containing a metadata keyword
|
|
242
|
+
* @returns The value after the keyword prefix, or null if no keyword found
|
|
243
|
+
*
|
|
244
|
+
* @example
|
|
245
|
+
* extractKeywordValue('@file:template.md') // 'template.md'
|
|
246
|
+
* extractKeywordValue('@lookup:Users.Email=test@example.com') // 'Users.Email=test@example.com'
|
|
247
|
+
* extractKeywordValue('@parent:ID') // 'ID'
|
|
248
|
+
* extractKeywordValue('regular string') // null
|
|
249
|
+
*/
|
|
250
|
+
function extractKeywordValue(value) {
|
|
251
|
+
if (typeof value !== 'string') {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
const keywordType = getMetadataKeywordType(value);
|
|
255
|
+
if (!keywordType) {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
// Special handling for @include
|
|
259
|
+
if (keywordType === 'include') {
|
|
260
|
+
if (value === exports.METADATA_KEYWORDS.INCLUDE) {
|
|
261
|
+
return ''; // Just '@include' with no suffix
|
|
262
|
+
}
|
|
263
|
+
if (value.startsWith(`${exports.METADATA_KEYWORDS.INCLUDE}.`)) {
|
|
264
|
+
return value.substring(exports.METADATA_KEYWORDS.INCLUDE.length + 1); // Remove '@include.'
|
|
265
|
+
}
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
// For all other keywords, find the matching prefix and remove it
|
|
269
|
+
for (const prefix of exports.METADATA_KEYWORD_PREFIXES) {
|
|
270
|
+
if (value.startsWith(prefix)) {
|
|
271
|
+
return value.substring(prefix.length);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
exports.extractKeywordValue = extractKeywordValue;
|
|
277
|
+
/**
|
|
278
|
+
* List of metadata keywords that require a parent context to function.
|
|
279
|
+
* These keywords cannot be used at the top level of metadata - they only work
|
|
280
|
+
* in nested/related entities where a parent record exists.
|
|
281
|
+
*/
|
|
282
|
+
exports.CONTEXT_DEPENDENT_KEYWORDS = [
|
|
283
|
+
exports.METADATA_KEYWORDS.PARENT,
|
|
284
|
+
exports.METADATA_KEYWORDS.ROOT,
|
|
285
|
+
];
|
|
286
|
+
/**
|
|
287
|
+
* List of metadata keywords that reference external resources.
|
|
288
|
+
* These keywords load content from files, URLs, or other external sources.
|
|
289
|
+
*/
|
|
290
|
+
exports.EXTERNAL_REFERENCE_KEYWORDS = [
|
|
291
|
+
exports.METADATA_KEYWORDS.FILE,
|
|
292
|
+
exports.METADATA_KEYWORDS.URL,
|
|
293
|
+
exports.METADATA_KEYWORDS.TEMPLATE,
|
|
294
|
+
];
|
|
295
|
+
/**
|
|
296
|
+
* List of metadata keywords that perform database lookups.
|
|
297
|
+
*/
|
|
298
|
+
exports.LOOKUP_KEYWORDS = [
|
|
299
|
+
exports.METADATA_KEYWORDS.LOOKUP,
|
|
300
|
+
];
|
|
301
|
+
/**
|
|
302
|
+
* List of metadata keywords that access runtime configuration.
|
|
303
|
+
*/
|
|
304
|
+
exports.RUNTIME_KEYWORDS = [
|
|
305
|
+
exports.METADATA_KEYWORDS.ENV,
|
|
306
|
+
];
|
|
307
|
+
/**
|
|
308
|
+
* Checks if a keyword requires a parent/root context.
|
|
309
|
+
*
|
|
310
|
+
* @param keyword - The keyword to check
|
|
311
|
+
* @returns true if the keyword requires context (parent or root)
|
|
312
|
+
*
|
|
313
|
+
* @example
|
|
314
|
+
* isContextDependentKeyword('@parent:') // true
|
|
315
|
+
* isContextDependentKeyword('@root:') // true
|
|
316
|
+
* isContextDependentKeyword('@file:') // false
|
|
317
|
+
*/
|
|
318
|
+
function isContextDependentKeyword(keyword) {
|
|
319
|
+
return exports.CONTEXT_DEPENDENT_KEYWORDS.some(k => keyword.startsWith(k));
|
|
320
|
+
}
|
|
321
|
+
exports.isContextDependentKeyword = isContextDependentKeyword;
|
|
322
|
+
/**
|
|
323
|
+
* Checks if a keyword references an external resource.
|
|
324
|
+
*
|
|
325
|
+
* @param keyword - The keyword to check
|
|
326
|
+
* @returns true if the keyword loads external content
|
|
327
|
+
*
|
|
328
|
+
* @example
|
|
329
|
+
* isExternalReferenceKeyword('@file:') // true
|
|
330
|
+
* isExternalReferenceKeyword('@url:') // true
|
|
331
|
+
* isExternalReferenceKeyword('@lookup:') // false
|
|
332
|
+
*/
|
|
333
|
+
function isExternalReferenceKeyword(keyword) {
|
|
334
|
+
return exports.EXTERNAL_REFERENCE_KEYWORDS.some(k => keyword.startsWith(k));
|
|
335
|
+
}
|
|
336
|
+
exports.isExternalReferenceKeyword = isExternalReferenceKeyword;
|
|
337
|
+
/**
|
|
338
|
+
* Creates a metadata keyword reference string.
|
|
339
|
+
*
|
|
340
|
+
* Helper function to construct properly formatted keyword references.
|
|
341
|
+
* This ensures consistent formatting across the codebase.
|
|
342
|
+
*
|
|
343
|
+
* @param type - The keyword type
|
|
344
|
+
* @param value - The value after the keyword
|
|
345
|
+
* @returns Formatted keyword reference string
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* createKeywordReference('file', 'template.md') // '@file:template.md'
|
|
349
|
+
* createKeywordReference('lookup', 'Users.Email=test@example.com') // '@lookup:Users.Email=test@example.com'
|
|
350
|
+
* createKeywordReference('parent', 'ID') // '@parent:ID'
|
|
351
|
+
*/
|
|
352
|
+
function createKeywordReference(type, value) {
|
|
353
|
+
const keyword = exports.METADATA_KEYWORDS[type.toUpperCase()];
|
|
354
|
+
if (!keyword) {
|
|
355
|
+
throw new Error(`Unknown metadata keyword type: ${type}`);
|
|
356
|
+
}
|
|
357
|
+
// Special handling for @include
|
|
358
|
+
if (type === 'include') {
|
|
359
|
+
return value ? `${keyword}.${value}` : keyword;
|
|
360
|
+
}
|
|
361
|
+
return `${keyword}${value}`;
|
|
362
|
+
}
|
|
363
|
+
exports.createKeywordReference = createKeywordReference;
|
|
364
|
+
//# sourceMappingURL=metadata-keywords.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata-keywords.js","sourceRoot":"","sources":["../../src/constants/metadata-keywords.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Centralized metadata keyword constants for MemberJunction MetadataSync package.\n *\n * These keywords are special prefixes used in metadata JSON files to reference external\n * content, perform lookups, access environment variables, and establish hierarchical relationships.\n *\n * @module metadata-keywords\n *\n * @example\n * // Using @file: to reference external content\n * {\n * \"Prompt\": \"@file:greeting.prompt.md\"\n * }\n *\n * @example\n * // Using @lookup: to find an entity by field value\n * {\n * \"CategoryID\": \"@lookup:AI Prompt Categories.Name=Examples\"\n * }\n *\n * @example\n * // Using @parent: to reference parent entity fields\n * {\n * \"PromptID\": \"@parent:ID\"\n * }\n */\n\n/**\n * Metadata keyword constants.\n * These are the special @ prefixes recognized by MetadataSync for field value processing.\n */\nexport const METADATA_KEYWORDS = {\n /**\n * @file: - Loads content from an external file\n *\n * Reads the contents of a file (relative to the JSON metadata file) and uses it as the field value.\n * Supports text files, markdown, JSON, and other formats.\n *\n * @example\n * \"@file:greeting.prompt.md\"\n * \"@file:./shared/common-prompt.md\"\n * \"@file:../templates/standard-header.md\"\n */\n FILE: '@file:',\n\n /**\n * @lookup: - Looks up an entity record by field value(s)\n *\n * Finds an entity record matching the specified criteria and uses its ID.\n * Supports single-field and multi-field lookups, with optional auto-creation.\n *\n * @example\n * \"@lookup:AI Prompt Types.Name=Chat\"\n * \"@lookup:Users.Email=john@example.com&Department=Sales\"\n * \"@lookup:Categories.Name=Examples?create\"\n * \"@lookup:Categories.Name=Examples?create&Description=Example prompts\"\n */\n LOOKUP: '@lookup:',\n\n /**\n * @parent: - References a field from the parent entity\n *\n * In nested/related entity structures, accesses a field value from the immediate parent record.\n * Only valid when processing nested entities that have a parent context.\n *\n * @example\n * \"@parent:ID\"\n * \"@parent:Name\"\n * \"@parent:CategoryID\"\n */\n PARENT: '@parent:',\n\n /**\n * @root: - References a field from the root entity\n *\n * In nested/related entity structures, accesses a field value from the top-level root record.\n * Only valid when processing nested entities that have a root context.\n *\n * @example\n * \"@root:ID\"\n * \"@root:Name\"\n */\n ROOT: '@root:',\n\n /**\n * @env: - Reads an environment variable\n *\n * Gets the value of an environment variable at runtime.\n * Useful for configuration values that differ between environments.\n *\n * @example\n * \"@env:VARIABLE_NAME\"\n * \"@env:NODE_ENV\"\n */\n ENV: '@env:',\n\n /**\n * @url: - Fetches content from a URL\n *\n * Downloads content from a remote URL and uses it as the field value.\n * Supports HTTP/HTTPS URLs and file:// URLs.\n *\n * @example\n * \"@url:https://example.com/prompts/greeting.md\"\n * \"@url:https://raw.githubusercontent.com/company/prompts/main/customer.md\"\n */\n URL: '@url:',\n\n /**\n * @template: - Loads a template JSON file\n *\n * Loads a JSON template file and uses its contents, replacing any template variables.\n * Useful for standardizing common configurations across multiple records.\n *\n * @example\n * \"@template:templates/standard-ai-models.json\"\n */\n TEMPLATE: '@template:',\n\n /**\n * @include or @include.* - Includes content from another file\n *\n * Special directive (not a field value) that merges content from external files.\n * Can be used in both objects and arrays. Supports spread and property modes.\n *\n * @example\n * { \"@include\": \"common-fields.json\" }\n * [ \"@include:items.json\" ]\n * { \"@include.models\": { \"file\": \"models.json\", \"mode\": \"property\" } }\n */\n INCLUDE: '@include',\n} as const;\n\n/**\n * Type representing all metadata keyword values.\n */\nexport type MetadataKeyword = typeof METADATA_KEYWORDS[keyof typeof METADATA_KEYWORDS];\n\n/**\n * Type representing metadata keyword types (without the colon for keywords that have it).\n */\nexport type MetadataKeywordType = 'file' | 'lookup' | 'parent' | 'root' | 'env' | 'url' | 'template' | 'include';\n\n/**\n * Array of all metadata keyword prefixes for iteration.\n * This array maintains the order of keywords for consistent processing.\n */\nexport const METADATA_KEYWORD_PREFIXES: ReadonlyArray<string> = Object.values(METADATA_KEYWORDS);\n\n/**\n * Checks if a value is a string that starts with any recognized metadata keyword.\n *\n * This is a type-safe check that handles non-string values gracefully.\n * Returns false for null, undefined, objects, numbers, etc.\n *\n * @param value - The value to check (can be any type)\n * @returns true if value is a string starting with a metadata keyword, false otherwise\n *\n * @example\n * isMetadataKeyword('@file:template.md') // true\n * isMetadataKeyword('@lookup:Users.Email=test@example.com') // true\n * isMetadataKeyword('@parent:ID') // true\n * isMetadataKeyword('regular string') // false\n * isMetadataKeyword(123) // false\n * isMetadataKeyword(null) // false\n * isMetadataKeyword('@unknown:value') // false\n */\nexport function isMetadataKeyword(value: unknown): value is string {\n if (typeof value !== 'string') {\n return false;\n }\n\n return METADATA_KEYWORD_PREFIXES.some(prefix => value.startsWith(prefix));\n}\n\n/**\n * Determines which metadata keyword type a string value uses.\n *\n * Examines the prefix of a string and returns the corresponding keyword type.\n * Returns null if the value doesn't use any recognized metadata keyword.\n *\n * @param value - The string value to analyze\n * @returns The keyword type ('file', 'lookup', etc.) or null if no keyword is found\n *\n * @example\n * getMetadataKeywordType('@file:template.md') // 'file'\n * getMetadataKeywordType('@lookup:Users.Name=John') // 'lookup'\n * getMetadataKeywordType('@parent:ID') // 'parent'\n * getMetadataKeywordType('@include') // 'include'\n * getMetadataKeywordType('regular string') // null\n * getMetadataKeywordType('@unknown:value') // null\n */\nexport function getMetadataKeywordType(value: string): MetadataKeywordType | null {\n if (typeof value !== 'string') {\n return null;\n }\n\n // Special handling for @include which doesn't require a colon\n if (value === METADATA_KEYWORDS.INCLUDE || value.startsWith(`${METADATA_KEYWORDS.INCLUDE}.`)) {\n return 'include';\n }\n\n // Check all other keywords\n for (const [key, prefix] of Object.entries(METADATA_KEYWORDS)) {\n if (key === 'INCLUDE') continue; // Already handled above\n\n if (value.startsWith(prefix)) {\n return key.toLowerCase() as MetadataKeywordType;\n }\n }\n\n return null;\n}\n\n/**\n * Type-safe check for metadata keywords that handles any value type.\n *\n * This is an alias for isMetadataKeyword() provided for semantic clarity\n * when you want to explicitly check if a value has a metadata keyword.\n *\n * @param value - Any value to check\n * @returns true if value is a string with a metadata keyword, false otherwise\n *\n * @example\n * const fieldValue = record.Get('SomeField');\n * if (hasMetadataKeyword(fieldValue)) {\n * // fieldValue is guaranteed to be a string with a metadata keyword\n * const type = getMetadataKeywordType(fieldValue);\n * }\n */\nexport function hasMetadataKeyword(value: unknown): value is string {\n return isMetadataKeyword(value);\n}\n\n/**\n * Checks if a string starts with @ but is NOT a metadata keyword.\n *\n * This is useful for filtering out @ strings that are not metadata keywords,\n * such as npm package names (@mui/material, @angular/core) or email addresses.\n *\n * @param value - The value to check\n * @returns true if value starts with @ but is not a metadata keyword\n *\n * @example\n * isNonKeywordAtSymbol('@mui/material') // true\n * isNonKeywordAtSymbol('@angular/core') // true\n * isNonKeywordAtSymbol('@file:template.md') // false\n * isNonKeywordAtSymbol('regular string') // false\n */\nexport function isNonKeywordAtSymbol(value: unknown): boolean {\n return typeof value === 'string' &&\n value.startsWith('@') &&\n !isMetadataKeyword(value);\n}\n\n/**\n * Extracts the value portion after a metadata keyword prefix.\n *\n * Removes the keyword prefix from a string and returns the remaining value.\n * Returns null if the string doesn't use a metadata keyword.\n *\n * @param value - The string containing a metadata keyword\n * @returns The value after the keyword prefix, or null if no keyword found\n *\n * @example\n * extractKeywordValue('@file:template.md') // 'template.md'\n * extractKeywordValue('@lookup:Users.Email=test@example.com') // 'Users.Email=test@example.com'\n * extractKeywordValue('@parent:ID') // 'ID'\n * extractKeywordValue('regular string') // null\n */\nexport function extractKeywordValue(value: string): string | null {\n if (typeof value !== 'string') {\n return null;\n }\n\n const keywordType = getMetadataKeywordType(value);\n if (!keywordType) {\n return null;\n }\n\n // Special handling for @include\n if (keywordType === 'include') {\n if (value === METADATA_KEYWORDS.INCLUDE) {\n return ''; // Just '@include' with no suffix\n }\n if (value.startsWith(`${METADATA_KEYWORDS.INCLUDE}.`)) {\n return value.substring(METADATA_KEYWORDS.INCLUDE.length + 1); // Remove '@include.'\n }\n return null;\n }\n\n // For all other keywords, find the matching prefix and remove it\n for (const prefix of METADATA_KEYWORD_PREFIXES) {\n if (value.startsWith(prefix)) {\n return value.substring(prefix.length);\n }\n }\n\n return null;\n}\n\n/**\n * List of metadata keywords that require a parent context to function.\n * These keywords cannot be used at the top level of metadata - they only work\n * in nested/related entities where a parent record exists.\n */\nexport const CONTEXT_DEPENDENT_KEYWORDS = [\n METADATA_KEYWORDS.PARENT,\n METADATA_KEYWORDS.ROOT,\n] as const;\n\n/**\n * List of metadata keywords that reference external resources.\n * These keywords load content from files, URLs, or other external sources.\n */\nexport const EXTERNAL_REFERENCE_KEYWORDS = [\n METADATA_KEYWORDS.FILE,\n METADATA_KEYWORDS.URL,\n METADATA_KEYWORDS.TEMPLATE,\n] as const;\n\n/**\n * List of metadata keywords that perform database lookups.\n */\nexport const LOOKUP_KEYWORDS = [\n METADATA_KEYWORDS.LOOKUP,\n] as const;\n\n/**\n * List of metadata keywords that access runtime configuration.\n */\nexport const RUNTIME_KEYWORDS = [\n METADATA_KEYWORDS.ENV,\n] as const;\n\n/**\n * Checks if a keyword requires a parent/root context.\n *\n * @param keyword - The keyword to check\n * @returns true if the keyword requires context (parent or root)\n *\n * @example\n * isContextDependentKeyword('@parent:') // true\n * isContextDependentKeyword('@root:') // true\n * isContextDependentKeyword('@file:') // false\n */\nexport function isContextDependentKeyword(keyword: string): boolean {\n return CONTEXT_DEPENDENT_KEYWORDS.some(k => keyword.startsWith(k));\n}\n\n/**\n * Checks if a keyword references an external resource.\n *\n * @param keyword - The keyword to check\n * @returns true if the keyword loads external content\n *\n * @example\n * isExternalReferenceKeyword('@file:') // true\n * isExternalReferenceKeyword('@url:') // true\n * isExternalReferenceKeyword('@lookup:') // false\n */\nexport function isExternalReferenceKeyword(keyword: string): boolean {\n return EXTERNAL_REFERENCE_KEYWORDS.some(k => keyword.startsWith(k));\n}\n\n/**\n * Creates a metadata keyword reference string.\n *\n * Helper function to construct properly formatted keyword references.\n * This ensures consistent formatting across the codebase.\n *\n * @param type - The keyword type\n * @param value - The value after the keyword\n * @returns Formatted keyword reference string\n *\n * @example\n * createKeywordReference('file', 'template.md') // '@file:template.md'\n * createKeywordReference('lookup', 'Users.Email=test@example.com') // '@lookup:Users.Email=test@example.com'\n * createKeywordReference('parent', 'ID') // '@parent:ID'\n */\nexport function createKeywordReference(type: MetadataKeywordType, value: string): string {\n const keyword = METADATA_KEYWORDS[type.toUpperCase() as keyof typeof METADATA_KEYWORDS];\n\n if (!keyword) {\n throw new Error(`Unknown metadata keyword type: ${type}`);\n }\n\n // Special handling for @include\n if (type === 'include') {\n return value ? `${keyword}.${value}` : keyword;\n }\n\n return `${keyword}${value}`;\n}\n"]}
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.FieldExternalizer = void 0;
|
|
7
7
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const metadata_keywords_1 = require("../constants/metadata-keywords");
|
|
9
10
|
/**
|
|
10
11
|
* Handles externalization of field values to separate files with @file: references
|
|
11
12
|
*/
|
|
@@ -40,13 +41,13 @@ class FieldExternalizer {
|
|
|
40
41
|
return mergeStrategy === 'merge' &&
|
|
41
42
|
!!existingFileReference &&
|
|
42
43
|
typeof existingFileReference === 'string' &&
|
|
43
|
-
existingFileReference.startsWith(
|
|
44
|
+
existingFileReference.startsWith(metadata_keywords_1.METADATA_KEYWORDS.FILE);
|
|
44
45
|
}
|
|
45
46
|
/**
|
|
46
47
|
* Uses an existing file reference
|
|
47
48
|
*/
|
|
48
49
|
useExistingFileReference(existingFileReference, targetDir, verbose) {
|
|
49
|
-
const existingPath =
|
|
50
|
+
const existingPath = (0, metadata_keywords_1.extractKeywordValue)(existingFileReference);
|
|
50
51
|
const finalFilePath = path_1.default.resolve(targetDir, existingPath);
|
|
51
52
|
if (verbose) {
|
|
52
53
|
console.log(`Using existing external file: ${finalFilePath}`);
|
|
@@ -60,7 +61,7 @@ class FieldExternalizer {
|
|
|
60
61
|
const processedPattern = this.processPattern(pattern, recordData, fieldName);
|
|
61
62
|
const cleanPattern = this.removeFilePrefix(processedPattern);
|
|
62
63
|
const finalFilePath = path_1.default.resolve(targetDir, cleanPattern);
|
|
63
|
-
const fileReference =
|
|
64
|
+
const fileReference = (0, metadata_keywords_1.createKeywordReference)('file', cleanPattern);
|
|
64
65
|
if (verbose) {
|
|
65
66
|
console.log(`Creating new external file: ${finalFilePath}`);
|
|
66
67
|
}
|
|
@@ -106,7 +107,7 @@ class FieldExternalizer {
|
|
|
106
107
|
* Removes @file: prefix if present
|
|
107
108
|
*/
|
|
108
109
|
removeFilePrefix(pattern) {
|
|
109
|
-
return pattern.startsWith(
|
|
110
|
+
return pattern.startsWith(metadata_keywords_1.METADATA_KEYWORDS.FILE) ? (0, metadata_keywords_1.extractKeywordValue)(pattern) : pattern;
|
|
110
111
|
}
|
|
111
112
|
/**
|
|
112
113
|
* Determines if the file should be written based on content comparison
|
|
@@ -1 +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}"]}
|
|
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';\nimport { METADATA_KEYWORDS, extractKeywordValue, createKeywordReference } from '../constants/metadata-keywords';\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(METADATA_KEYWORDS.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 = extractKeywordValue(existingFileReference) as string;\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 = createKeywordReference('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(METADATA_KEYWORDS.FILE) ? (extractKeywordValue(pattern) as string) : 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}"]}
|
|
@@ -6,6 +6,7 @@ const json_write_helper_1 = require("./json-write-helper");
|
|
|
6
6
|
const EntityPropertyExtractor_1 = require("./EntityPropertyExtractor");
|
|
7
7
|
const FieldExternalizer_1 = require("./FieldExternalizer");
|
|
8
8
|
const RelatedEntityHandler_1 = require("./RelatedEntityHandler");
|
|
9
|
+
const metadata_keywords_1 = require("../constants/metadata-keywords");
|
|
9
10
|
/**
|
|
10
11
|
* Handles the core processing of individual record data into the sync format
|
|
11
12
|
*/
|
|
@@ -177,7 +178,7 @@ class RecordProcessor {
|
|
|
177
178
|
if (externalizeConfig.length > 0 && typeof externalizeConfig[0] === 'string') {
|
|
178
179
|
// Simple string array format
|
|
179
180
|
if (externalizeConfig.includes(fieldName)) {
|
|
180
|
-
return
|
|
181
|
+
return (0, metadata_keywords_1.createKeywordReference)('file', `{Name}.${fieldName.toLowerCase()}.md`);
|
|
181
182
|
}
|
|
182
183
|
}
|
|
183
184
|
else {
|
|
@@ -197,7 +198,7 @@ class RecordProcessor {
|
|
|
197
198
|
const fieldConfig = externalizeConfig[fieldName];
|
|
198
199
|
if (fieldConfig) {
|
|
199
200
|
const extension = fieldConfig.extension || '.md';
|
|
200
|
-
return
|
|
201
|
+
return (0, metadata_keywords_1.createKeywordReference)('file', `{Name}.${fieldName.toLowerCase()}${extension}`);
|
|
201
202
|
}
|
|
202
203
|
return null;
|
|
203
204
|
}
|
|
@@ -269,7 +270,7 @@ class RecordProcessor {
|
|
|
269
270
|
*/
|
|
270
271
|
hasExternalizedFields(fields, entityConfig) {
|
|
271
272
|
return !!entityConfig.pull?.externalizeFields &&
|
|
272
|
-
Object.values(fields).some(value => typeof value === 'string' && value.startsWith(
|
|
273
|
+
Object.values(fields).some(value => typeof value === 'string' && value.startsWith(metadata_keywords_1.METADATA_KEYWORDS.FILE));
|
|
273
274
|
}
|
|
274
275
|
/**
|
|
275
276
|
* Convert a GUID value to @lookup syntax by looking up the human-readable value
|
|
@@ -289,7 +290,7 @@ class RecordProcessor {
|
|
|
289
290
|
const targetRecord = result.Results[0];
|
|
290
291
|
const lookupValue = targetRecord[lookupConfig.field];
|
|
291
292
|
if (lookupValue != null) {
|
|
292
|
-
return
|
|
293
|
+
return (0, metadata_keywords_1.createKeywordReference)('lookup', `${lookupConfig.entity}.${lookupConfig.field}=${lookupValue}`);
|
|
293
294
|
}
|
|
294
295
|
}
|
|
295
296
|
if (verbose) {
|