@memberjunction/metadata-sync 2.46.0 → 2.48.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.
@@ -1,49 +1,283 @@
1
+ /**
2
+ * @fileoverview Core synchronization engine for MemberJunction metadata
3
+ * @module sync-engine
4
+ *
5
+ * This module provides the core functionality for synchronizing metadata between
6
+ * the MemberJunction database and local file system representations. It handles
7
+ * special reference types (@file, @url, @lookup, @env, @parent, @root, @template),
8
+ * manages entity operations, and provides utilities for data transformation.
9
+ */
1
10
  import { EntityInfo, BaseEntity, UserInfo } from '@memberjunction/core';
2
11
  import { EntityConfig } from '../config';
12
+ /**
13
+ * Represents the structure of a metadata record with optional sync tracking
14
+ */
3
15
  export interface RecordData {
16
+ /** Primary key field(s) and their values */
4
17
  primaryKey?: Record<string, any>;
18
+ /** Entity field names and their values */
5
19
  fields: Record<string, any>;
20
+ /** Related entities organized by entity name */
6
21
  relatedEntities?: Record<string, RecordData[]>;
22
+ /** Synchronization metadata for change tracking */
7
23
  sync?: {
24
+ /** ISO timestamp of last modification */
8
25
  lastModified: string;
26
+ /** SHA256 checksum of the fields object */
9
27
  checksum: string;
10
28
  };
11
29
  }
30
+ /**
31
+ * Core engine for synchronizing MemberJunction metadata between database and files
32
+ *
33
+ * @class SyncEngine
34
+ * @example
35
+ * ```typescript
36
+ * const syncEngine = new SyncEngine(systemUser);
37
+ * await syncEngine.initialize();
38
+ *
39
+ * // Process a field value with special references
40
+ * const value = await syncEngine.processFieldValue('@lookup:Users.Email=admin@example.com', '/path/to/base');
41
+ * ```
42
+ */
12
43
  export declare class SyncEngine {
13
44
  private metadata;
14
45
  private contextUser;
46
+ /**
47
+ * Creates a new SyncEngine instance
48
+ * @param contextUser - The user context for database operations
49
+ */
15
50
  constructor(contextUser: UserInfo);
51
+ /**
52
+ * Initializes the sync engine by refreshing metadata cache
53
+ * @returns Promise that resolves when initialization is complete
54
+ */
16
55
  initialize(): Promise<void>;
17
56
  /**
18
57
  * Process special references in field values
58
+ *
59
+ * Handles the following reference types:
60
+ * - `@parent:fieldName` - References a field from the parent record
61
+ * - `@root:fieldName` - References a field from the root record
62
+ * - `@file:path` - Reads content from an external file
63
+ * - `@url:address` - Fetches content from a URL
64
+ * - `@lookup:Entity.Field=Value` - Looks up an entity ID by field value
65
+ * - `@env:VARIABLE` - Reads an environment variable
66
+ *
67
+ * @param value - The field value to process
68
+ * @param baseDir - Base directory for resolving relative file paths
69
+ * @param parentRecord - Optional parent entity for @parent references
70
+ * @param rootRecord - Optional root entity for @root references
71
+ * @returns The processed value with all references resolved
72
+ * @throws Error if a reference cannot be resolved
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * // File reference
77
+ * const content = await processFieldValue('@file:template.md', '/path/to/dir');
78
+ *
79
+ * // Lookup with auto-create
80
+ * const userId = await processFieldValue('@lookup:Users.Email=john@example.com?create', '/path');
81
+ * ```
19
82
  */
20
83
  processFieldValue(value: any, baseDir: string, parentRecord?: BaseEntity | null, rootRecord?: BaseEntity | null): Promise<any>;
21
84
  /**
22
85
  * Resolve a lookup reference to an ID, optionally creating the record if it doesn't exist
86
+ *
87
+ * @param entityName - Name of the entity to search in
88
+ * @param fieldName - Field to match against
89
+ * @param fieldValue - Value to search for
90
+ * @param autoCreate - Whether to create the record if not found
91
+ * @param createFields - Additional fields to set when creating
92
+ * @returns The ID of the found or created record
93
+ * @throws Error if lookup fails and autoCreate is false
94
+ *
95
+ * @example
96
+ * ```typescript
97
+ * // Simple lookup
98
+ * const categoryId = await resolveLookup('Categories', 'Name', 'Technology');
99
+ *
100
+ * // Lookup with auto-create
101
+ * const tagId = await resolveLookup('Tags', 'Name', 'New Tag', true, {
102
+ * Description: 'Auto-created tag',
103
+ * Status: 'Active'
104
+ * });
105
+ * ```
23
106
  */
24
107
  resolveLookup(entityName: string, fieldName: string, fieldValue: string, autoCreate?: boolean, createFields?: Record<string, any>): Promise<string>;
25
108
  /**
26
109
  * Build cascading defaults for a file path and process field values
110
+ *
111
+ * Walks up the directory tree from the file location, collecting defaults from
112
+ * entity config and folder configs, with deeper folders overriding parent values.
113
+ * All default values are processed for special references.
114
+ *
115
+ * @param filePath - Path to the file being processed
116
+ * @param entityConfig - Entity configuration containing base defaults
117
+ * @returns Processed defaults with all references resolved
118
+ * @throws Error if any default value processing fails
27
119
  */
28
120
  buildDefaults(filePath: string, entityConfig: EntityConfig): Promise<Record<string, any>>;
29
121
  /**
30
- * Load folder configuration
122
+ * Load folder configuration from .mj-folder.json file
123
+ *
124
+ * @param dir - Directory to check for configuration
125
+ * @returns Folder configuration or null if not found/invalid
126
+ * @private
31
127
  */
32
128
  private loadFolderConfig;
33
129
  /**
34
- * Calculate checksum for data
130
+ * Calculate SHA256 checksum for data
131
+ *
132
+ * Generates a deterministic hash of the provided data by converting it to
133
+ * formatted JSON and calculating a SHA256 digest. Used for change detection
134
+ * in sync operations.
135
+ *
136
+ * @param data - Any data structure to calculate checksum for
137
+ * @returns Hexadecimal string representation of the SHA256 hash
138
+ *
139
+ * @example
140
+ * ```typescript
141
+ * const checksum = syncEngine.calculateChecksum({
142
+ * name: 'Test Record',
143
+ * value: 42,
144
+ * tags: ['a', 'b']
145
+ * });
146
+ * // Returns consistent hash for same data structure
147
+ * ```
35
148
  */
36
149
  calculateChecksum(data: any): string;
37
150
  /**
38
- * Get entity info by name
151
+ * Get entity metadata information by name
152
+ *
153
+ * Retrieves the EntityInfo object containing schema metadata for the specified entity.
154
+ * Returns null if the entity is not found in the metadata cache.
155
+ *
156
+ * @param entityName - Name of the entity to look up
157
+ * @returns EntityInfo object with schema details or null if not found
158
+ *
159
+ * @example
160
+ * ```typescript
161
+ * const entityInfo = syncEngine.getEntityInfo('AI Prompts');
162
+ * if (entityInfo) {
163
+ * console.log(`Primary keys: ${entityInfo.PrimaryKeys.map(pk => pk.Name).join(', ')}`);
164
+ * }
165
+ * ```
39
166
  */
40
167
  getEntityInfo(entityName: string): EntityInfo | null;
41
168
  /**
42
- * Create a new entity object
169
+ * Create a new entity object instance
170
+ *
171
+ * Uses the MemberJunction metadata system to properly instantiate an entity object.
172
+ * This ensures correct class registration and respects any custom entity subclasses.
173
+ *
174
+ * @param entityName - Name of the entity to create
175
+ * @returns Promise resolving to the new BaseEntity instance
176
+ * @throws Error if entity creation fails
177
+ *
178
+ * @example
179
+ * ```typescript
180
+ * const entity = await syncEngine.createEntityObject('AI Prompts');
181
+ * entity.NewRecord();
182
+ * entity.Set('Name', 'My Prompt');
183
+ * await entity.Save();
184
+ * ```
43
185
  */
44
186
  createEntityObject(entityName: string): Promise<BaseEntity>;
45
187
  /**
46
- * Load an entity by primary key
188
+ * Load an entity record by primary key
189
+ *
190
+ * Retrieves an existing entity record from the database using its primary key values.
191
+ * Supports both single and composite primary keys. Returns null if the record is not found.
192
+ *
193
+ * @param entityName - Name of the entity to load
194
+ * @param primaryKey - Object containing primary key field names and values
195
+ * @returns Promise resolving to the loaded entity or null if not found
196
+ * @throws Error if entity metadata is not found
197
+ *
198
+ * @example
199
+ * ```typescript
200
+ * // Single primary key
201
+ * const entity = await syncEngine.loadEntity('Users', { ID: '123-456' });
202
+ *
203
+ * // Composite primary key
204
+ * const entity = await syncEngine.loadEntity('UserRoles', {
205
+ * UserID: '123-456',
206
+ * RoleID: '789-012'
207
+ * });
208
+ * ```
47
209
  */
48
210
  loadEntity(entityName: string, primaryKey: Record<string, any>): Promise<BaseEntity | null>;
211
+ /**
212
+ * Process JSON object with template references
213
+ *
214
+ * Recursively processes JSON data structures to resolve `@template` references.
215
+ * Templates can be defined at any level and support:
216
+ * - Single template references: `"@template:path/to/template.json"`
217
+ * - Object with @template field: `{ "@template": "file.json", "override": "value" }`
218
+ * - Array of templates for merging: `{ "@template": ["base.json", "overrides.json"] }`
219
+ * - Nested template references within templates
220
+ *
221
+ * @param data - JSON data structure to process
222
+ * @param baseDir - Base directory for resolving relative template paths
223
+ * @returns Promise resolving to the processed data with all templates resolved
224
+ * @throws Error if template file is not found or contains invalid JSON
225
+ *
226
+ * @example
227
+ * ```typescript
228
+ * // Input data with template reference
229
+ * const data = {
230
+ * "@template": "defaults/ai-prompt.json",
231
+ * "Name": "Custom Prompt",
232
+ * "Prompt": "Override the template prompt"
233
+ * };
234
+ *
235
+ * // Resolves template and merges with overrides
236
+ * const result = await syncEngine.processTemplates(data, '/path/to/dir');
237
+ * ```
238
+ */
239
+ processTemplates(data: any, baseDir: string): Promise<any>;
240
+ /**
241
+ * Load and process a template file
242
+ *
243
+ * Loads a JSON template file from the filesystem and recursively processes any
244
+ * nested template references within it. Template paths are resolved relative to
245
+ * the template file's directory, enabling template composition.
246
+ *
247
+ * @param templatePath - Path to the template file (relative or absolute)
248
+ * @param baseDir - Base directory for resolving relative paths
249
+ * @returns Promise resolving to the processed template content
250
+ * @throws Error if template file not found or contains invalid JSON
251
+ * @private
252
+ */
253
+ private loadAndProcessTemplate;
254
+ /**
255
+ * Deep merge two objects with target taking precedence
256
+ *
257
+ * Recursively merges two objects, with values from the target object overriding
258
+ * values from the source object. Arrays and primitive values are not merged but
259
+ * replaced entirely by the target value. Undefined values in target are skipped.
260
+ *
261
+ * @param source - Base object to merge from
262
+ * @param target - Object with values that override source
263
+ * @returns New object with merged values
264
+ * @private
265
+ *
266
+ * @example
267
+ * ```typescript
268
+ * const source = {
269
+ * a: 1,
270
+ * b: { x: 10, y: 20 },
271
+ * c: [1, 2, 3]
272
+ * };
273
+ * const target = {
274
+ * a: 2,
275
+ * b: { y: 30, z: 40 },
276
+ * d: 'new'
277
+ * };
278
+ * const result = deepMerge(source, target);
279
+ * // Result: { a: 2, b: { x: 10, y: 30, z: 40 }, c: [1, 2, 3], d: 'new' }
280
+ * ```
281
+ */
282
+ private deepMerge;
49
283
  }
@@ -1,4 +1,13 @@
1
1
  "use strict";
2
+ /**
3
+ * @fileoverview Core synchronization engine for MemberJunction metadata
4
+ * @module sync-engine
5
+ *
6
+ * This module provides the core functionality for synchronizing metadata between
7
+ * the MemberJunction database and local file system representations. It handles
8
+ * special reference types (@file, @url, @lookup, @env, @parent, @root, @template),
9
+ * manages entity operations, and provides utilities for data transformation.
10
+ */
2
11
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
13
  };
@@ -9,19 +18,64 @@ const fs_extra_1 = __importDefault(require("fs-extra"));
9
18
  const crypto_1 = __importDefault(require("crypto"));
10
19
  const axios_1 = __importDefault(require("axios"));
11
20
  const core_1 = require("@memberjunction/core");
21
+ /**
22
+ * Core engine for synchronizing MemberJunction metadata between database and files
23
+ *
24
+ * @class SyncEngine
25
+ * @example
26
+ * ```typescript
27
+ * const syncEngine = new SyncEngine(systemUser);
28
+ * await syncEngine.initialize();
29
+ *
30
+ * // Process a field value with special references
31
+ * const value = await syncEngine.processFieldValue('@lookup:Users.Email=admin@example.com', '/path/to/base');
32
+ * ```
33
+ */
12
34
  class SyncEngine {
13
35
  metadata;
14
36
  contextUser;
37
+ /**
38
+ * Creates a new SyncEngine instance
39
+ * @param contextUser - The user context for database operations
40
+ */
15
41
  constructor(contextUser) {
16
42
  this.metadata = new core_1.Metadata();
17
43
  this.contextUser = contextUser;
18
44
  }
45
+ /**
46
+ * Initializes the sync engine by refreshing metadata cache
47
+ * @returns Promise that resolves when initialization is complete
48
+ */
19
49
  async initialize() {
20
50
  // Initialize metadata
21
51
  await this.metadata.Refresh();
22
52
  }
23
53
  /**
24
54
  * Process special references in field values
55
+ *
56
+ * Handles the following reference types:
57
+ * - `@parent:fieldName` - References a field from the parent record
58
+ * - `@root:fieldName` - References a field from the root record
59
+ * - `@file:path` - Reads content from an external file
60
+ * - `@url:address` - Fetches content from a URL
61
+ * - `@lookup:Entity.Field=Value` - Looks up an entity ID by field value
62
+ * - `@env:VARIABLE` - Reads an environment variable
63
+ *
64
+ * @param value - The field value to process
65
+ * @param baseDir - Base directory for resolving relative file paths
66
+ * @param parentRecord - Optional parent entity for @parent references
67
+ * @param rootRecord - Optional root entity for @root references
68
+ * @returns The processed value with all references resolved
69
+ * @throws Error if a reference cannot be resolved
70
+ *
71
+ * @example
72
+ * ```typescript
73
+ * // File reference
74
+ * const content = await processFieldValue('@file:template.md', '/path/to/dir');
75
+ *
76
+ * // Lookup with auto-create
77
+ * const userId = await processFieldValue('@lookup:Users.Email=john@example.com?create', '/path');
78
+ * ```
25
79
  */
26
80
  async processFieldValue(value, baseDir, parentRecord, rootRecord) {
27
81
  if (typeof value !== 'string') {
@@ -112,6 +166,26 @@ class SyncEngine {
112
166
  }
113
167
  /**
114
168
  * Resolve a lookup reference to an ID, optionally creating the record if it doesn't exist
169
+ *
170
+ * @param entityName - Name of the entity to search in
171
+ * @param fieldName - Field to match against
172
+ * @param fieldValue - Value to search for
173
+ * @param autoCreate - Whether to create the record if not found
174
+ * @param createFields - Additional fields to set when creating
175
+ * @returns The ID of the found or created record
176
+ * @throws Error if lookup fails and autoCreate is false
177
+ *
178
+ * @example
179
+ * ```typescript
180
+ * // Simple lookup
181
+ * const categoryId = await resolveLookup('Categories', 'Name', 'Technology');
182
+ *
183
+ * // Lookup with auto-create
184
+ * const tagId = await resolveLookup('Tags', 'Name', 'New Tag', true, {
185
+ * Description: 'Auto-created tag',
186
+ * Status: 'Active'
187
+ * });
188
+ * ```
115
189
  */
116
190
  async resolveLookup(entityName, fieldName, fieldValue, autoCreate = false, createFields = {}) {
117
191
  // Debug logging handled by caller if needed
@@ -168,6 +242,15 @@ class SyncEngine {
168
242
  }
169
243
  /**
170
244
  * Build cascading defaults for a file path and process field values
245
+ *
246
+ * Walks up the directory tree from the file location, collecting defaults from
247
+ * entity config and folder configs, with deeper folders overriding parent values.
248
+ * All default values are processed for special references.
249
+ *
250
+ * @param filePath - Path to the file being processed
251
+ * @param entityConfig - Entity configuration containing base defaults
252
+ * @returns Processed defaults with all references resolved
253
+ * @throws Error if any default value processing fails
171
254
  */
172
255
  async buildDefaults(filePath, entityConfig) {
173
256
  const parts = path_1.default.dirname(filePath).split(path_1.default.sep);
@@ -195,7 +278,11 @@ class SyncEngine {
195
278
  return processedDefaults;
196
279
  }
197
280
  /**
198
- * Load folder configuration
281
+ * Load folder configuration from .mj-folder.json file
282
+ *
283
+ * @param dir - Directory to check for configuration
284
+ * @returns Folder configuration or null if not found/invalid
285
+ * @private
199
286
  */
200
287
  async loadFolderConfig(dir) {
201
288
  const configPath = path_1.default.join(dir, '.mj-folder.json');
@@ -211,7 +298,24 @@ class SyncEngine {
211
298
  return null;
212
299
  }
213
300
  /**
214
- * Calculate checksum for data
301
+ * Calculate SHA256 checksum for data
302
+ *
303
+ * Generates a deterministic hash of the provided data by converting it to
304
+ * formatted JSON and calculating a SHA256 digest. Used for change detection
305
+ * in sync operations.
306
+ *
307
+ * @param data - Any data structure to calculate checksum for
308
+ * @returns Hexadecimal string representation of the SHA256 hash
309
+ *
310
+ * @example
311
+ * ```typescript
312
+ * const checksum = syncEngine.calculateChecksum({
313
+ * name: 'Test Record',
314
+ * value: 42,
315
+ * tags: ['a', 'b']
316
+ * });
317
+ * // Returns consistent hash for same data structure
318
+ * ```
215
319
  */
216
320
  calculateChecksum(data) {
217
321
  const hash = crypto_1.default.createHash('sha256');
@@ -219,13 +323,42 @@ class SyncEngine {
219
323
  return hash.digest('hex');
220
324
  }
221
325
  /**
222
- * Get entity info by name
326
+ * Get entity metadata information by name
327
+ *
328
+ * Retrieves the EntityInfo object containing schema metadata for the specified entity.
329
+ * Returns null if the entity is not found in the metadata cache.
330
+ *
331
+ * @param entityName - Name of the entity to look up
332
+ * @returns EntityInfo object with schema details or null if not found
333
+ *
334
+ * @example
335
+ * ```typescript
336
+ * const entityInfo = syncEngine.getEntityInfo('AI Prompts');
337
+ * if (entityInfo) {
338
+ * console.log(`Primary keys: ${entityInfo.PrimaryKeys.map(pk => pk.Name).join(', ')}`);
339
+ * }
340
+ * ```
223
341
  */
224
342
  getEntityInfo(entityName) {
225
343
  return this.metadata.EntityByName(entityName);
226
344
  }
227
345
  /**
228
- * Create a new entity object
346
+ * Create a new entity object instance
347
+ *
348
+ * Uses the MemberJunction metadata system to properly instantiate an entity object.
349
+ * This ensures correct class registration and respects any custom entity subclasses.
350
+ *
351
+ * @param entityName - Name of the entity to create
352
+ * @returns Promise resolving to the new BaseEntity instance
353
+ * @throws Error if entity creation fails
354
+ *
355
+ * @example
356
+ * ```typescript
357
+ * const entity = await syncEngine.createEntityObject('AI Prompts');
358
+ * entity.NewRecord();
359
+ * entity.Set('Name', 'My Prompt');
360
+ * await entity.Save();
361
+ * ```
229
362
  */
230
363
  async createEntityObject(entityName) {
231
364
  const entity = await this.metadata.GetEntityObject(entityName, this.contextUser);
@@ -235,7 +368,27 @@ class SyncEngine {
235
368
  return entity;
236
369
  }
237
370
  /**
238
- * Load an entity by primary key
371
+ * Load an entity record by primary key
372
+ *
373
+ * Retrieves an existing entity record from the database using its primary key values.
374
+ * Supports both single and composite primary keys. Returns null if the record is not found.
375
+ *
376
+ * @param entityName - Name of the entity to load
377
+ * @param primaryKey - Object containing primary key field names and values
378
+ * @returns Promise resolving to the loaded entity or null if not found
379
+ * @throws Error if entity metadata is not found
380
+ *
381
+ * @example
382
+ * ```typescript
383
+ * // Single primary key
384
+ * const entity = await syncEngine.loadEntity('Users', { ID: '123-456' });
385
+ *
386
+ * // Composite primary key
387
+ * const entity = await syncEngine.loadEntity('UserRoles', {
388
+ * UserID: '123-456',
389
+ * RoleID: '789-012'
390
+ * });
391
+ * ```
239
392
  */
240
393
  async loadEntity(entityName, primaryKey) {
241
394
  const entity = await this.createEntityObject(entityName);
@@ -249,6 +402,162 @@ class SyncEngine {
249
402
  const loaded = await entity.InnerLoad(compositeKey);
250
403
  return loaded ? entity : null;
251
404
  }
405
+ /**
406
+ * Process JSON object with template references
407
+ *
408
+ * Recursively processes JSON data structures to resolve `@template` references.
409
+ * Templates can be defined at any level and support:
410
+ * - Single template references: `"@template:path/to/template.json"`
411
+ * - Object with @template field: `{ "@template": "file.json", "override": "value" }`
412
+ * - Array of templates for merging: `{ "@template": ["base.json", "overrides.json"] }`
413
+ * - Nested template references within templates
414
+ *
415
+ * @param data - JSON data structure to process
416
+ * @param baseDir - Base directory for resolving relative template paths
417
+ * @returns Promise resolving to the processed data with all templates resolved
418
+ * @throws Error if template file is not found or contains invalid JSON
419
+ *
420
+ * @example
421
+ * ```typescript
422
+ * // Input data with template reference
423
+ * const data = {
424
+ * "@template": "defaults/ai-prompt.json",
425
+ * "Name": "Custom Prompt",
426
+ * "Prompt": "Override the template prompt"
427
+ * };
428
+ *
429
+ * // Resolves template and merges with overrides
430
+ * const result = await syncEngine.processTemplates(data, '/path/to/dir');
431
+ * ```
432
+ */
433
+ async processTemplates(data, baseDir) {
434
+ // Handle arrays
435
+ if (Array.isArray(data)) {
436
+ const processedArray = [];
437
+ for (const item of data) {
438
+ processedArray.push(await this.processTemplates(item, baseDir));
439
+ }
440
+ return processedArray;
441
+ }
442
+ // Handle objects
443
+ if (data && typeof data === 'object') {
444
+ // Check for @template reference
445
+ if (typeof data === 'string' && data.startsWith('@template:')) {
446
+ const templatePath = data.substring(10);
447
+ return await this.loadAndProcessTemplate(templatePath, baseDir);
448
+ }
449
+ // Process object with possible @template field
450
+ const processed = {};
451
+ let templateData = {};
452
+ // First, check if there's a @template field to process
453
+ if (data['@template']) {
454
+ const templates = Array.isArray(data['@template']) ? data['@template'] : [data['@template']];
455
+ // Process templates in order, merging them
456
+ for (const templateRef of templates) {
457
+ const templateContent = await this.loadAndProcessTemplate(templateRef, baseDir);
458
+ templateData = this.deepMerge(templateData, templateContent);
459
+ }
460
+ }
461
+ // Process all other fields
462
+ for (const [key, value] of Object.entries(data)) {
463
+ if (key === '@template')
464
+ continue; // Skip the template field itself
465
+ // Process the value recursively
466
+ processed[key] = await this.processTemplates(value, baseDir);
467
+ }
468
+ // Merge template data with processed data (processed data takes precedence)
469
+ return this.deepMerge(templateData, processed);
470
+ }
471
+ // Return primitive values as-is
472
+ return data;
473
+ }
474
+ /**
475
+ * Load and process a template file
476
+ *
477
+ * Loads a JSON template file from the filesystem and recursively processes any
478
+ * nested template references within it. Template paths are resolved relative to
479
+ * the template file's directory, enabling template composition.
480
+ *
481
+ * @param templatePath - Path to the template file (relative or absolute)
482
+ * @param baseDir - Base directory for resolving relative paths
483
+ * @returns Promise resolving to the processed template content
484
+ * @throws Error if template file not found or contains invalid JSON
485
+ * @private
486
+ */
487
+ async loadAndProcessTemplate(templatePath, baseDir) {
488
+ const fullPath = path_1.default.resolve(baseDir, templatePath);
489
+ if (!await fs_extra_1.default.pathExists(fullPath)) {
490
+ throw new Error(`Template file not found: ${fullPath}`);
491
+ }
492
+ try {
493
+ const templateContent = await fs_extra_1.default.readJson(fullPath);
494
+ // Recursively process any nested templates
495
+ const templateDir = path_1.default.dirname(fullPath);
496
+ return await this.processTemplates(templateContent, templateDir);
497
+ }
498
+ catch (error) {
499
+ throw new Error(`Failed to load template ${fullPath}: ${error}`);
500
+ }
501
+ }
502
+ /**
503
+ * Deep merge two objects with target taking precedence
504
+ *
505
+ * Recursively merges two objects, with values from the target object overriding
506
+ * values from the source object. Arrays and primitive values are not merged but
507
+ * replaced entirely by the target value. Undefined values in target are skipped.
508
+ *
509
+ * @param source - Base object to merge from
510
+ * @param target - Object with values that override source
511
+ * @returns New object with merged values
512
+ * @private
513
+ *
514
+ * @example
515
+ * ```typescript
516
+ * const source = {
517
+ * a: 1,
518
+ * b: { x: 10, y: 20 },
519
+ * c: [1, 2, 3]
520
+ * };
521
+ * const target = {
522
+ * a: 2,
523
+ * b: { y: 30, z: 40 },
524
+ * d: 'new'
525
+ * };
526
+ * const result = deepMerge(source, target);
527
+ * // Result: { a: 2, b: { x: 10, y: 30, z: 40 }, c: [1, 2, 3], d: 'new' }
528
+ * ```
529
+ */
530
+ deepMerge(source, target) {
531
+ if (!source)
532
+ return target;
533
+ if (!target)
534
+ return source;
535
+ // If target is not an object, it completely overrides source
536
+ if (typeof target !== 'object' || target === null || Array.isArray(target)) {
537
+ return target;
538
+ }
539
+ // If source is not an object, target wins
540
+ if (typeof source !== 'object' || source === null || Array.isArray(source)) {
541
+ return target;
542
+ }
543
+ // Both are objects, merge them
544
+ const result = { ...source };
545
+ for (const [key, value] of Object.entries(target)) {
546
+ if (value === undefined) {
547
+ continue; // Skip undefined values
548
+ }
549
+ if (typeof value === 'object' && value !== null && !Array.isArray(value) &&
550
+ typeof result[key] === 'object' && result[key] !== null && !Array.isArray(result[key])) {
551
+ // Both are objects, merge recursively
552
+ result[key] = this.deepMerge(result[key], value);
553
+ }
554
+ else {
555
+ // Otherwise, target value wins
556
+ result[key] = value;
557
+ }
558
+ }
559
+ return result;
560
+ }
252
561
  }
253
562
  exports.SyncEngine = SyncEngine;
254
563
  //# sourceMappingURL=sync-engine.js.map