@memberjunction/metadata-sync 2.112.0 → 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.
Files changed (42) hide show
  1. package/README.md +136 -61
  2. package/dist/constants/metadata-keywords.d.ts +282 -0
  3. package/dist/constants/metadata-keywords.js +364 -0
  4. package/dist/constants/metadata-keywords.js.map +1 -0
  5. package/dist/lib/EntityPropertyExtractor.d.ts +1 -1
  6. package/dist/lib/EntityPropertyExtractor.js +4 -18
  7. package/dist/lib/EntityPropertyExtractor.js.map +1 -1
  8. package/dist/lib/FieldExternalizer.d.ts +1 -1
  9. package/dist/lib/FieldExternalizer.js +6 -5
  10. package/dist/lib/FieldExternalizer.js.map +1 -1
  11. package/dist/lib/RecordProcessor.d.ts +1 -1
  12. package/dist/lib/RecordProcessor.js +14 -12
  13. package/dist/lib/RecordProcessor.js.map +1 -1
  14. package/dist/lib/RelatedEntityHandler.d.ts +1 -1
  15. package/dist/lib/RelatedEntityHandler.js +5 -5
  16. package/dist/lib/RelatedEntityHandler.js.map +1 -1
  17. package/dist/lib/json-preprocessor.js +7 -6
  18. package/dist/lib/json-preprocessor.js.map +1 -1
  19. package/dist/lib/provider-utils.d.ts +1 -1
  20. package/dist/lib/provider-utils.js +19 -16
  21. package/dist/lib/provider-utils.js.map +1 -1
  22. package/dist/lib/record-dependency-analyzer.js +44 -37
  23. package/dist/lib/record-dependency-analyzer.js.map +1 -1
  24. package/dist/lib/singleton-manager.d.ts +1 -1
  25. package/dist/lib/singleton-manager.js.map +1 -1
  26. package/dist/lib/sync-engine.d.ts +1 -1
  27. package/dist/lib/sync-engine.js +36 -44
  28. package/dist/lib/sync-engine.js.map +1 -1
  29. package/dist/services/PullService.d.ts +1 -1
  30. package/dist/services/PullService.js +18 -22
  31. package/dist/services/PullService.js.map +1 -1
  32. package/dist/services/PushService.d.ts +1 -1
  33. package/dist/services/PushService.js +21 -15
  34. package/dist/services/PushService.js.map +1 -1
  35. package/dist/services/ValidationService.js +32 -44
  36. package/dist/services/ValidationService.js.map +1 -1
  37. package/dist/services/WatchService.d.ts +1 -1
  38. package/dist/services/WatchService.js +14 -13
  39. package/dist/services/WatchService.js.map +1 -1
  40. package/dist/types/validation.d.ts +6 -1
  41. package/dist/types/validation.js.map +1 -1
  42. package/package.json +7 -6
@@ -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"]}
@@ -1,4 +1,4 @@
1
- import { BaseEntity } from '@memberjunction/global';
1
+ import { BaseEntity } from '@memberjunction/core';
2
2
  /**
3
3
  * Handles discovery and extraction of all properties from BaseEntity objects,
4
4
  * including both database fields and virtual properties defined in subclasses.
@@ -86,7 +86,7 @@ class EntityPropertyExtractor {
86
86
  const dbFieldNames = new Set();
87
87
  if (typeof record.GetAll === 'function') {
88
88
  const dbFields = record.GetAll();
89
- Object.keys(dbFields).forEach((key) => dbFieldNames.add(key));
89
+ Object.keys(dbFields).forEach(key => dbFieldNames.add(key));
90
90
  }
91
91
  return dbFieldNames;
92
92
  }
@@ -155,23 +155,9 @@ class EntityPropertyExtractor {
155
155
  */
156
156
  isBaseEntityMethod(propertyName) {
157
157
  const baseEntityMethods = [
158
- 'Get',
159
- 'Set',
160
- 'GetAll',
161
- 'SetMany',
162
- 'LoadFromData',
163
- 'Save',
164
- 'Load',
165
- 'Delete',
166
- 'Fields',
167
- 'Dirty',
168
- 'IsSaved',
169
- 'PrimaryKeys',
170
- 'EntityInfo',
171
- 'ContextCurrentUser',
172
- 'ProviderToUse',
173
- 'RecordChanges',
174
- 'TransactionGroup',
158
+ 'Get', 'Set', 'GetAll', 'SetMany', 'LoadFromData', 'Save', 'Load', 'Delete',
159
+ 'Fields', 'Dirty', 'IsSaved', 'PrimaryKeys', 'EntityInfo', 'ContextCurrentUser',
160
+ 'ProviderToUse', 'RecordChanges', 'TransactionGroup'
175
161
  ];
176
162
  return baseEntityMethods.includes(propertyName);
177
163
  }
@@ -1 +1 @@
1
- {"version":3,"file":"EntityPropertyExtractor.js","sourceRoot":"","sources":["../../src/lib/EntityPropertyExtractor.ts"],"names":[],"mappings":"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","sourcesContent":["import { BaseEntity } from '@memberjunction/global';\n\n/**\n * Handles discovery and extraction of all properties from BaseEntity objects,\n * including both database fields and virtual properties defined in subclasses.\n */\nexport class EntityPropertyExtractor {\n /**\n * Gets ALL properties from a BaseEntity object, including both:\n * 1. Database fields (from record.GetAll())\n * 2. Virtual properties (getters defined in subclasses like TemplateText)\n * @param record The BaseEntity object to get properties from\n * @param fieldOverrides Optional field value overrides (e.g., for @parent:ID syntax)\n */\n extractAllProperties(record: BaseEntity, fieldOverrides?: Record<string, any>): Record<string, any> {\n const allProperties: Record<string, any> = {};\n\n // 1. Get database fields using GetAll()\n this.extractDatabaseFields(record, allProperties);\n\n // 2. Apply field overrides (e.g., for @parent:ID replacement in related entities)\n this.applyFieldOverrides(allProperties, fieldOverrides);\n\n // 3. Extract virtual properties by walking the prototype chain\n this.extractVirtualProperties(record, allProperties, fieldOverrides);\n\n return allProperties;\n }\n\n /**\n * Extracts database fields from the entity using GetAll()\n */\n private extractDatabaseFields(record: BaseEntity, allProperties: Record<string, any>): void {\n if (typeof record.GetAll === 'function') {\n const dbFields = record.GetAll();\n Object.assign(allProperties, dbFields);\n }\n }\n\n /**\n * Applies field overrides to the properties collection\n */\n private applyFieldOverrides(allProperties: Record<string, any>, fieldOverrides?: Record<string, any>): void {\n if (fieldOverrides) {\n Object.assign(allProperties, fieldOverrides);\n }\n }\n\n /**\n * Extracts virtual properties by walking the prototype chain\n */\n private extractVirtualProperties(record: BaseEntity, allProperties: Record<string, any>, fieldOverrides?: Record<string, any>): void {\n const virtualProperties = this.discoverVirtualProperties(record);\n\n for (const propertyName of virtualProperties) {\n try {\n // Skip if this property is overridden\n if (fieldOverrides && propertyName in fieldOverrides) {\n continue;\n }\n\n // Use bracket notation to access the getter\n const value = (record as any)[propertyName];\n\n // Only include if the value is not undefined and not a function\n if (value !== undefined && typeof value !== 'function') {\n allProperties[propertyName] = value;\n }\n } catch (error) {\n // Skip properties that throw errors when accessed\n continue;\n }\n }\n }\n\n /**\n * Discovers virtual properties (getters) defined in BaseEntity subclasses\n * Returns property names that are getters but not in the base database fields\n */\n private discoverVirtualProperties(record: BaseEntity): string[] {\n const virtualProperties: string[] = [];\n const dbFieldNames = this.getDatabaseFieldNames(record);\n\n // Walk the prototype chain to find getters\n let currentPrototype = Object.getPrototypeOf(record);\n\n while (currentPrototype && currentPrototype !== Object.prototype) {\n this.extractPropertiesFromPrototype(currentPrototype, virtualProperties, dbFieldNames);\n currentPrototype = Object.getPrototypeOf(currentPrototype);\n }\n\n return virtualProperties;\n }\n\n /**\n * Gets the set of database field names from the entity\n */\n private getDatabaseFieldNames(record: BaseEntity): Set<string> {\n const dbFieldNames = new Set<string>();\n\n if (typeof record.GetAll === 'function') {\n const dbFields = record.GetAll();\n Object.keys(dbFields).forEach((key) => dbFieldNames.add(key));\n }\n\n return dbFieldNames;\n }\n\n /**\n * Extracts properties from a single prototype level\n */\n private extractPropertiesFromPrototype(prototype: any, virtualProperties: string[], dbFieldNames: Set<string>): void {\n const propertyNames = Object.getOwnPropertyNames(prototype);\n\n for (const propertyName of propertyNames) {\n if (this.shouldIncludeProperty(propertyName, virtualProperties, dbFieldNames)) {\n const descriptor = Object.getOwnPropertyDescriptor(prototype, propertyName);\n if (this.isVirtualProperty(descriptor)) {\n virtualProperties.push(propertyName);\n }\n }\n }\n }\n\n /**\n * Determines if a property should be considered for inclusion\n */\n private shouldIncludeProperty(propertyName: string, virtualProperties: string[], dbFieldNames: Set<string>): boolean {\n // Skip if already found or is a database field\n if (virtualProperties.includes(propertyName) || dbFieldNames.has(propertyName)) {\n return false;\n }\n\n // Skip internal properties and methods\n return !this.shouldSkipProperty(propertyName);\n }\n\n /**\n * Determines if a property descriptor represents a virtual property\n */\n private isVirtualProperty(descriptor: PropertyDescriptor | undefined): boolean {\n if (!descriptor) return false;\n\n // Skip read-only getters (might be computed properties)\n if (typeof descriptor.get === 'function' && !descriptor.set) {\n return false;\n }\n\n // Include read-write getter/setter pairs (likely virtual properties)\n return typeof descriptor.get === 'function' && typeof descriptor.set === 'function';\n }\n\n /**\n * Determines if a property should be skipped during virtual property discovery\n */\n private shouldSkipProperty(propertyName: string): boolean {\n // Skip private properties (starting with _ or __)\n if (propertyName.startsWith('_') || propertyName.startsWith('__')) {\n return true;\n }\n\n // Skip constructor and common Object.prototype methods\n if (this.isCommonObjectMethod(propertyName)) {\n return true;\n }\n\n // Skip known BaseEntity methods and properties\n return this.isBaseEntityMethod(propertyName);\n }\n\n /**\n * Checks if property is a common Object.prototype method\n */\n private isCommonObjectMethod(propertyName: string): boolean {\n const commonMethods = ['constructor', 'toString', 'valueOf'];\n return commonMethods.includes(propertyName);\n }\n\n /**\n * Checks if property is a known BaseEntity method or property\n */\n private isBaseEntityMethod(propertyName: string): boolean {\n const baseEntityMethods = [\n 'Get',\n 'Set',\n 'GetAll',\n 'SetMany',\n 'LoadFromData',\n 'Save',\n 'Load',\n 'Delete',\n 'Fields',\n 'Dirty',\n 'IsSaved',\n 'PrimaryKeys',\n 'EntityInfo',\n 'ContextCurrentUser',\n 'ProviderToUse',\n 'RecordChanges',\n 'TransactionGroup',\n ];\n\n return baseEntityMethods.includes(propertyName);\n }\n}\n"]}
1
+ {"version":3,"file":"EntityPropertyExtractor.js","sourceRoot":"","sources":["../../src/lib/EntityPropertyExtractor.ts"],"names":[],"mappings":"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","sourcesContent":["import { BaseEntity } from '@memberjunction/core';\n\n/**\n * Handles discovery and extraction of all properties from BaseEntity objects,\n * including both database fields and virtual properties defined in subclasses.\n */\nexport class EntityPropertyExtractor {\n /**\n * Gets ALL properties from a BaseEntity object, including both:\n * 1. Database fields (from record.GetAll())\n * 2. Virtual properties (getters defined in subclasses like TemplateText)\n * @param record The BaseEntity object to get properties from\n * @param fieldOverrides Optional field value overrides (e.g., for @parent:ID syntax)\n */\n extractAllProperties(record: BaseEntity, fieldOverrides?: Record<string, any>): Record<string, any> {\n const allProperties: Record<string, any> = {};\n \n // 1. Get database fields using GetAll()\n this.extractDatabaseFields(record, allProperties);\n \n // 2. Apply field overrides (e.g., for @parent:ID replacement in related entities)\n this.applyFieldOverrides(allProperties, fieldOverrides);\n \n // 3. Extract virtual properties by walking the prototype chain\n this.extractVirtualProperties(record, allProperties, fieldOverrides);\n \n return allProperties;\n }\n\n /**\n * Extracts database fields from the entity using GetAll()\n */\n private extractDatabaseFields(record: BaseEntity, allProperties: Record<string, any>): void {\n if (typeof record.GetAll === 'function') {\n const dbFields = record.GetAll();\n Object.assign(allProperties, dbFields);\n }\n }\n\n /**\n * Applies field overrides to the properties collection\n */\n private applyFieldOverrides(allProperties: Record<string, any>, fieldOverrides?: Record<string, any>): void {\n if (fieldOverrides) {\n Object.assign(allProperties, fieldOverrides);\n }\n }\n\n /**\n * Extracts virtual properties by walking the prototype chain\n */\n private extractVirtualProperties(\n record: BaseEntity, \n allProperties: Record<string, any>, \n fieldOverrides?: Record<string, any>\n ): void {\n const virtualProperties = this.discoverVirtualProperties(record);\n \n for (const propertyName of virtualProperties) {\n try {\n // Skip if this property is overridden\n if (fieldOverrides && propertyName in fieldOverrides) {\n continue;\n }\n \n // Use bracket notation to access the getter\n const value = (record as any)[propertyName];\n \n // Only include if the value is not undefined and not a function\n if (value !== undefined && typeof value !== 'function') {\n allProperties[propertyName] = value;\n }\n } catch (error) {\n // Skip properties that throw errors when accessed\n continue;\n }\n }\n }\n\n /**\n * Discovers virtual properties (getters) defined in BaseEntity subclasses\n * Returns property names that are getters but not in the base database fields\n */\n private discoverVirtualProperties(record: BaseEntity): string[] {\n const virtualProperties: string[] = [];\n const dbFieldNames = this.getDatabaseFieldNames(record);\n \n // Walk the prototype chain to find getters\n let currentPrototype = Object.getPrototypeOf(record);\n \n while (currentPrototype && currentPrototype !== Object.prototype) {\n this.extractPropertiesFromPrototype(currentPrototype, virtualProperties, dbFieldNames);\n currentPrototype = Object.getPrototypeOf(currentPrototype);\n }\n \n return virtualProperties;\n }\n\n /**\n * Gets the set of database field names from the entity\n */\n private getDatabaseFieldNames(record: BaseEntity): Set<string> {\n const dbFieldNames = new Set<string>();\n \n if (typeof record.GetAll === 'function') {\n const dbFields = record.GetAll();\n Object.keys(dbFields).forEach(key => dbFieldNames.add(key));\n }\n \n return dbFieldNames;\n }\n\n /**\n * Extracts properties from a single prototype level\n */\n private extractPropertiesFromPrototype(\n prototype: any, \n virtualProperties: string[], \n dbFieldNames: Set<string>\n ): void {\n const propertyNames = Object.getOwnPropertyNames(prototype);\n \n for (const propertyName of propertyNames) {\n if (this.shouldIncludeProperty(propertyName, virtualProperties, dbFieldNames)) {\n const descriptor = Object.getOwnPropertyDescriptor(prototype, propertyName);\n if (this.isVirtualProperty(descriptor)) {\n virtualProperties.push(propertyName);\n }\n }\n }\n }\n\n /**\n * Determines if a property should be considered for inclusion\n */\n private shouldIncludeProperty(\n propertyName: string, \n virtualProperties: string[], \n dbFieldNames: Set<string>\n ): boolean {\n // Skip if already found or is a database field\n if (virtualProperties.includes(propertyName) || dbFieldNames.has(propertyName)) {\n return false;\n }\n \n // Skip internal properties and methods\n return !this.shouldSkipProperty(propertyName);\n }\n\n /**\n * Determines if a property descriptor represents a virtual property\n */\n private isVirtualProperty(descriptor: PropertyDescriptor | undefined): boolean {\n if (!descriptor) return false;\n \n // Skip read-only getters (might be computed properties)\n if (typeof descriptor.get === 'function' && !descriptor.set) {\n return false;\n }\n \n // Include read-write getter/setter pairs (likely virtual properties)\n return typeof descriptor.get === 'function' && typeof descriptor.set === 'function';\n }\n\n /**\n * Determines if a property should be skipped during virtual property discovery\n */\n private shouldSkipProperty(propertyName: string): boolean {\n // Skip private properties (starting with _ or __)\n if (propertyName.startsWith('_') || propertyName.startsWith('__')) {\n return true;\n }\n \n // Skip constructor and common Object.prototype methods\n if (this.isCommonObjectMethod(propertyName)) {\n return true;\n }\n \n // Skip known BaseEntity methods and properties\n return this.isBaseEntityMethod(propertyName);\n }\n\n /**\n * Checks if property is a common Object.prototype method\n */\n private isCommonObjectMethod(propertyName: string): boolean {\n const commonMethods = ['constructor', 'toString', 'valueOf'];\n return commonMethods.includes(propertyName);\n }\n\n /**\n * Checks if property is a known BaseEntity method or property\n */\n private isBaseEntityMethod(propertyName: string): boolean {\n const baseEntityMethods = [\n 'Get', 'Set', 'GetAll', 'SetMany', 'LoadFromData', 'Save', 'Load', 'Delete',\n 'Fields', 'Dirty', 'IsSaved', 'PrimaryKeys', 'EntityInfo', 'ContextCurrentUser',\n 'ProviderToUse', 'RecordChanges', 'TransactionGroup'\n ];\n \n return baseEntityMethods.includes(propertyName);\n }\n}"]}
@@ -1,4 +1,4 @@
1
- import { BaseEntity } from '@memberjunction/global';
1
+ import { BaseEntity } from '@memberjunction/core';
2
2
  /**
3
3
  * Handles externalization of field values to separate files with @file: references
4
4
  */
@@ -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
  */
@@ -37,16 +38,16 @@ class FieldExternalizer {
37
38
  * Checks if we should use an existing file reference
38
39
  */
39
40
  shouldUseExistingReference(existingFileReference, mergeStrategy = 'merge') {
40
- return (mergeStrategy === 'merge' &&
41
+ return mergeStrategy === 'merge' &&
41
42
  !!existingFileReference &&
42
43
  typeof existingFileReference === 'string' &&
43
- existingFileReference.startsWith('@file:'));
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 = existingFileReference.substring(6); // Remove @file: prefix
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 = `@file:${cleanPattern}`;
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('@file:') ? pattern.substring(6) : pattern;
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/global';\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 (\n mergeStrategy === 'merge' &&\n !!existingFileReference &&\n typeof existingFileReference === 'string' &&\n existingFileReference.startsWith('@file:')\n );\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(finalFilePath: string, fieldValue: any, fieldName: string, verbose?: boolean): 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}\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}"]}
@@ -1,4 +1,4 @@
1
- import { BaseEntity, UserInfo } from '@memberjunction/global';
1
+ import { BaseEntity, UserInfo } from '@memberjunction/core';
2
2
  import { SyncEngine, RecordData } from '../lib/sync-engine';
3
3
  import { EntityConfig } from '../config';
4
4
  /**