@memberjunction/metadata-sync 2.46.0 → 2.47.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,4 +1,12 @@
1
1
  "use strict";
2
+ /**
3
+ * @fileoverview Database provider utilities for MetadataSync
4
+ * @module provider-utils
5
+ *
6
+ * This module provides utilities for initializing and managing the database
7
+ * connection, accessing system users, and finding entity directories. It handles
8
+ * the TypeORM DataSource lifecycle and MemberJunction provider initialization.
9
+ */
2
10
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
11
  if (k2 === undefined) k2 = k;
4
12
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -28,12 +36,26 @@ const typeorm_1 = require("typeorm");
28
36
  const sqlserver_dataprovider_1 = require("@memberjunction/sqlserver-dataprovider");
29
37
  const fs = __importStar(require("fs"));
30
38
  const path = __importStar(require("path"));
39
+ /** Global DataSource instance for connection lifecycle management */
40
+ let globalDataSource = null;
31
41
  /**
32
42
  * Initialize a SQLServerDataProvider with the given configuration
33
- * @param config MemberJunction configuration with database connection details
34
- * @returns Initialized SQLServerDataProvider instance
43
+ *
44
+ * Creates and initializes a TypeORM DataSource for SQL Server, then sets up
45
+ * the MemberJunction SQLServerDataProvider. The connection is stored globally
46
+ * for proper cleanup. Auto-detects Azure SQL databases for encryption settings.
47
+ *
48
+ * @param config - MemberJunction configuration with database connection details
49
+ * @returns Promise resolving to initialized SQLServerDataProvider instance
50
+ * @throws Error if database connection fails
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * const config = loadMJConfig();
55
+ * const provider = await initializeProvider(config);
56
+ * // Provider is ready for use
57
+ * ```
35
58
  */
36
- let globalDataSource = null;
37
59
  async function initializeProvider(config) {
38
60
  // Create TypeORM DataSource
39
61
  const dataSource = new typeorm_1.DataSource({
@@ -46,7 +68,8 @@ async function initializeProvider(config) {
46
68
  synchronize: false,
47
69
  logging: false,
48
70
  options: {
49
- encrypt: config.dbTrustServerCertificate !== 'Y' ? false : true,
71
+ encrypt: config.dbEncrypt === 'Y' || config.dbEncrypt === 'true' ||
72
+ config.dbHost.includes('.database.windows.net'), // Auto-detect Azure SQL
50
73
  trustServerCertificate: config.dbTrustServerCertificate === 'Y',
51
74
  instanceName: config.dbInstanceName
52
75
  }
@@ -62,6 +85,23 @@ async function initializeProvider(config) {
62
85
  return await (0, sqlserver_dataprovider_1.setupSQLServerClient)(providerConfig);
63
86
  }
64
87
  exports.initializeProvider = initializeProvider;
88
+ /**
89
+ * Clean up the global database connection
90
+ *
91
+ * Destroys the TypeORM DataSource if it exists and is initialized.
92
+ * Should be called when the CLI command completes to ensure proper cleanup.
93
+ *
94
+ * @returns Promise that resolves when cleanup is complete
95
+ *
96
+ * @example
97
+ * ```typescript
98
+ * try {
99
+ * // Do work with database
100
+ * } finally {
101
+ * await cleanupProvider();
102
+ * }
103
+ * ```
104
+ */
65
105
  async function cleanupProvider() {
66
106
  if (globalDataSource && globalDataSource.isInitialized) {
67
107
  await globalDataSource.destroy();
@@ -69,6 +109,21 @@ async function cleanupProvider() {
69
109
  }
70
110
  }
71
111
  exports.cleanupProvider = cleanupProvider;
112
+ /**
113
+ * Get the system user from the UserCache
114
+ *
115
+ * Retrieves the "System" user from MemberJunction's UserCache. This user is
116
+ * typically used for CLI operations where no specific user context exists.
117
+ *
118
+ * @returns The System UserInfo object
119
+ * @throws Error if System user is not found in the cache
120
+ *
121
+ * @example
122
+ * ```typescript
123
+ * const systemUser = getSystemUser();
124
+ * const syncEngine = new SyncEngine(systemUser);
125
+ * ```
126
+ */
72
127
  function getSystemUser() {
73
128
  const sysUser = sqlserver_dataprovider_1.UserCache.Instance.UserByName("System", false);
74
129
  if (!sysUser) {
@@ -78,38 +133,48 @@ function getSystemUser() {
78
133
  }
79
134
  exports.getSystemUser = getSystemUser;
80
135
  /**
81
- * Recursively find all entity directories with .mj-sync.json files
82
- * @param dir Directory to search
83
- * @param specificDir Optional specific directory to limit search to
84
- * @returns Array of directory paths containing .mj-sync.json files
136
+ * Find entity directories at the immediate level only
137
+ *
138
+ * Searches for directories containing .mj-sync.json files, which indicate
139
+ * entity data directories. Only searches immediate subdirectories, not recursive.
140
+ * If a specific directory is provided, only checks that directory.
141
+ *
142
+ * @param dir - Base directory to search from
143
+ * @param specificDir - Optional specific subdirectory name to check
144
+ * @returns Array of absolute directory paths containing .mj-sync.json files
145
+ *
146
+ * @example
147
+ * ```typescript
148
+ * // Find all entity directories
149
+ * const dirs = findEntityDirectories(process.cwd());
150
+ *
151
+ * // Check specific directory
152
+ * const dirs = findEntityDirectories(process.cwd(), 'ai-prompts');
153
+ * ```
85
154
  */
86
155
  function findEntityDirectories(dir, specificDir) {
87
156
  const results = [];
88
- function search(currentDir) {
89
- const entries = fs.readdirSync(currentDir, { withFileTypes: true });
90
- // Check if this directory has .mj-sync.json (and it's not the root)
91
- const hasSyncConfig = entries.some(e => e.name === '.mj-sync.json');
92
- const isRoot = currentDir === dir;
93
- if (hasSyncConfig && !isRoot) {
94
- results.push(currentDir);
95
- }
96
- // Recursively search subdirectories
97
- for (const entry of entries) {
98
- if (entry.isDirectory() && !entry.name.startsWith('.')) {
99
- search(path.join(currentDir, entry.name));
100
- }
101
- }
102
- }
103
- // If specific directory is provided, only search within it
157
+ // If specific directory is provided, check if it's an entity directory
104
158
  if (specificDir) {
105
- // Handle both absolute and relative paths
106
159
  const targetDir = path.isAbsolute(specificDir) ? specificDir : path.join(dir, specificDir);
107
160
  if (fs.existsSync(targetDir)) {
108
- search(targetDir);
161
+ const hasSyncConfig = fs.existsSync(path.join(targetDir, '.mj-sync.json'));
162
+ if (hasSyncConfig) {
163
+ results.push(targetDir);
164
+ }
109
165
  }
166
+ return results;
110
167
  }
111
- else {
112
- search(dir);
168
+ // Otherwise, find all immediate subdirectories with .mj-sync.json
169
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
170
+ for (const entry of entries) {
171
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
172
+ const subDir = path.join(dir, entry.name);
173
+ const hasSyncConfig = fs.existsSync(path.join(subDir, '.mj-sync.json'));
174
+ if (hasSyncConfig) {
175
+ results.push(subDir);
176
+ }
177
+ }
113
178
  }
114
179
  return results;
115
180
  }
@@ -1 +1 @@
1
- {"version":3,"file":"provider-utils.js","sourceRoot":"","sources":["../../src/lib/provider-utils.ts"],"names":[],"mappings":"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","sourcesContent":["import { DataSource } from 'typeorm';\nimport { SQLServerDataProvider, SQLServerProviderConfigData, UserCache, setupSQLServerClient } from '@memberjunction/sqlserver-dataprovider';\nimport type { MJConfig } from '../config';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { UserInfo } from '@memberjunction/core';\n\n/**\n * Initialize a SQLServerDataProvider with the given configuration\n * @param config MemberJunction configuration with database connection details\n * @returns Initialized SQLServerDataProvider instance\n */\nlet globalDataSource: DataSource | null = null;\n\nexport async function initializeProvider(config: MJConfig): Promise<SQLServerDataProvider> {\n // Create TypeORM DataSource\n const dataSource = new DataSource({\n type: 'mssql',\n host: config.dbHost,\n port: config.dbPort ? Number(config.dbPort) : 1433,\n database: config.dbDatabase,\n username: config.dbUsername,\n password: config.dbPassword,\n synchronize: false,\n logging: false,\n options: {\n encrypt: config.dbTrustServerCertificate !== 'Y' ? false : true,\n trustServerCertificate: config.dbTrustServerCertificate === 'Y',\n instanceName: config.dbInstanceName\n }\n });\n \n // Initialize the data source\n await dataSource.initialize();\n \n // Store for cleanup\n globalDataSource = dataSource;\n \n // Create provider config\n const providerConfig = new SQLServerProviderConfigData(\n dataSource,\n 'system@sync.cli', // Default user for CLI\n config.mjCoreSchema || '__mj',\n 0\n );\n \n // Use setupSQLServerClient to properly initialize\n return await setupSQLServerClient(providerConfig);\n}\n\nexport async function cleanupProvider(): Promise<void> {\n if (globalDataSource && globalDataSource.isInitialized) {\n await globalDataSource.destroy();\n globalDataSource = null;\n }\n}\n\nexport function getSystemUser(): UserInfo {\n const sysUser = UserCache.Instance.UserByName(\"System\", false);\n if (!sysUser) {\n throw new Error(\"System user not found in cache. Ensure the system user exists in the database.\"); \n }\n return sysUser;\n}\n\n/**\n * Recursively find all entity directories with .mj-sync.json files\n * @param dir Directory to search\n * @param specificDir Optional specific directory to limit search to\n * @returns Array of directory paths containing .mj-sync.json files\n */\nexport function findEntityDirectories(dir: string, specificDir?: string): string[] {\n const results: string[] = [];\n \n function search(currentDir: string) {\n const entries = fs.readdirSync(currentDir, { withFileTypes: true });\n \n // Check if this directory has .mj-sync.json (and it's not the root)\n const hasSyncConfig = entries.some(e => e.name === '.mj-sync.json');\n const isRoot = currentDir === dir;\n \n if (hasSyncConfig && !isRoot) {\n results.push(currentDir);\n }\n \n // Recursively search subdirectories\n for (const entry of entries) {\n if (entry.isDirectory() && !entry.name.startsWith('.')) {\n search(path.join(currentDir, entry.name));\n }\n }\n }\n \n // If specific directory is provided, only search within it\n if (specificDir) {\n // Handle both absolute and relative paths\n const targetDir = path.isAbsolute(specificDir) ? specificDir : path.join(dir, specificDir);\n if (fs.existsSync(targetDir)) {\n search(targetDir);\n }\n } else {\n search(dir);\n }\n \n return results;\n}"]}
1
+ {"version":3,"file":"provider-utils.js","sourceRoot":"","sources":["../../src/lib/provider-utils.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview Database provider utilities for MetadataSync\n * @module provider-utils\n * \n * This module provides utilities for initializing and managing the database\n * connection, accessing system users, and finding entity directories. It handles\n * the TypeORM DataSource lifecycle and MemberJunction provider initialization.\n */\n\nimport { DataSource } from 'typeorm';\nimport { SQLServerDataProvider, SQLServerProviderConfigData, UserCache, setupSQLServerClient } from '@memberjunction/sqlserver-dataprovider';\nimport type { MJConfig } from '../config';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { UserInfo } from '@memberjunction/core';\n\n/** Global DataSource instance for connection lifecycle management */\nlet globalDataSource: DataSource | null = null;\n\n/**\n * Initialize a SQLServerDataProvider with the given configuration\n * \n * Creates and initializes a TypeORM DataSource for SQL Server, then sets up\n * the MemberJunction SQLServerDataProvider. The connection is stored globally\n * for proper cleanup. Auto-detects Azure SQL databases for encryption settings.\n * \n * @param config - MemberJunction configuration with database connection details\n * @returns Promise resolving to initialized SQLServerDataProvider instance\n * @throws Error if database connection fails\n * \n * @example\n * ```typescript\n * const config = loadMJConfig();\n * const provider = await initializeProvider(config);\n * // Provider is ready for use\n * ```\n */\nexport async function initializeProvider(config: MJConfig): Promise<SQLServerDataProvider> {\n // Create TypeORM DataSource\n const dataSource = new DataSource({\n type: 'mssql',\n host: config.dbHost,\n port: config.dbPort ? Number(config.dbPort) : 1433,\n database: config.dbDatabase,\n username: config.dbUsername,\n password: config.dbPassword,\n synchronize: false,\n logging: false,\n options: {\n encrypt: config.dbEncrypt === 'Y' || config.dbEncrypt === 'true' || \n config.dbHost.includes('.database.windows.net'), // Auto-detect Azure SQL\n trustServerCertificate: config.dbTrustServerCertificate === 'Y',\n instanceName: config.dbInstanceName\n }\n });\n \n // Initialize the data source\n await dataSource.initialize();\n \n // Store for cleanup\n globalDataSource = dataSource;\n \n // Create provider config\n const providerConfig = new SQLServerProviderConfigData(\n dataSource,\n 'system@sync.cli', // Default user for CLI\n config.mjCoreSchema || '__mj',\n 0\n );\n \n // Use setupSQLServerClient to properly initialize\n return await setupSQLServerClient(providerConfig);\n}\n\n/**\n * Clean up the global database connection\n * \n * Destroys the TypeORM DataSource if it exists and is initialized.\n * Should be called when the CLI command completes to ensure proper cleanup.\n * \n * @returns Promise that resolves when cleanup is complete\n * \n * @example\n * ```typescript\n * try {\n * // Do work with database\n * } finally {\n * await cleanupProvider();\n * }\n * ```\n */\nexport async function cleanupProvider(): Promise<void> {\n if (globalDataSource && globalDataSource.isInitialized) {\n await globalDataSource.destroy();\n globalDataSource = null;\n }\n}\n\n/**\n * Get the system user from the UserCache\n * \n * Retrieves the \"System\" user from MemberJunction's UserCache. This user is\n * typically used for CLI operations where no specific user context exists.\n * \n * @returns The System UserInfo object\n * @throws Error if System user is not found in the cache\n * \n * @example\n * ```typescript\n * const systemUser = getSystemUser();\n * const syncEngine = new SyncEngine(systemUser);\n * ```\n */\nexport function getSystemUser(): UserInfo {\n const sysUser = UserCache.Instance.UserByName(\"System\", false);\n if (!sysUser) {\n throw new Error(\"System user not found in cache. Ensure the system user exists in the database.\"); \n }\n return sysUser;\n}\n\n/**\n * Find entity directories at the immediate level only\n * \n * Searches for directories containing .mj-sync.json files, which indicate\n * entity data directories. Only searches immediate subdirectories, not recursive.\n * If a specific directory is provided, only checks that directory.\n * \n * @param dir - Base directory to search from\n * @param specificDir - Optional specific subdirectory name to check\n * @returns Array of absolute directory paths containing .mj-sync.json files\n * \n * @example\n * ```typescript\n * // Find all entity directories\n * const dirs = findEntityDirectories(process.cwd());\n * \n * // Check specific directory\n * const dirs = findEntityDirectories(process.cwd(), 'ai-prompts');\n * ```\n */\nexport function findEntityDirectories(dir: string, specificDir?: string): string[] {\n const results: string[] = [];\n \n // If specific directory is provided, check if it's an entity directory\n if (specificDir) {\n const targetDir = path.isAbsolute(specificDir) ? specificDir : path.join(dir, specificDir);\n if (fs.existsSync(targetDir)) {\n const hasSyncConfig = fs.existsSync(path.join(targetDir, '.mj-sync.json'));\n if (hasSyncConfig) {\n results.push(targetDir);\n }\n }\n return results;\n }\n \n // Otherwise, find all immediate subdirectories with .mj-sync.json\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n \n for (const entry of entries) {\n if (entry.isDirectory() && !entry.name.startsWith('.')) {\n const subDir = path.join(dir, entry.name);\n const hasSyncConfig = fs.existsSync(path.join(subDir, '.mj-sync.json'));\n \n if (hasSyncConfig) {\n results.push(subDir);\n }\n }\n }\n \n return results;\n}"]}
@@ -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
  }