@memberjunction/metadata-sync 2.47.0 → 2.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +814 -15
- package/dist/commands/init/index.js +25 -0
- package/dist/commands/init/index.js.map +1 -1
- package/dist/commands/pull/index.d.ts +97 -5
- package/dist/commands/pull/index.js +1009 -115
- package/dist/commands/pull/index.js.map +1 -1
- package/dist/commands/push/index.js +181 -12
- package/dist/commands/push/index.js.map +1 -1
- package/dist/commands/status/index.js +49 -7
- package/dist/commands/status/index.js.map +1 -1
- package/dist/commands/watch/index.js +59 -7
- package/dist/commands/watch/index.js.map +1 -1
- package/dist/config.d.ts +96 -0
- package/dist/config.js +2 -13
- package/dist/config.js.map +1 -1
- package/dist/hooks/init.js +3 -0
- package/dist/hooks/init.js.map +1 -1
- package/dist/lib/config-manager.d.ts +56 -0
- package/dist/lib/config-manager.js +104 -0
- package/dist/lib/config-manager.js.map +1 -0
- package/dist/lib/provider-utils.d.ts +26 -5
- package/dist/lib/provider-utils.js +127 -43
- package/dist/lib/provider-utils.js.map +1 -1
- package/dist/lib/singleton-manager.d.ts +34 -0
- package/dist/lib/singleton-manager.js +62 -0
- package/dist/lib/singleton-manager.js.map +1 -0
- package/dist/lib/sync-engine.js +16 -1
- package/dist/lib/sync-engine.js.map +1 -1
- package/oclif.manifest.json +50 -43
- package/package.json +8 -7
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* This module provides utilities for initializing and managing the database
|
|
7
7
|
* connection, accessing system users, and finding entity directories. It handles
|
|
8
|
-
* the
|
|
8
|
+
* the mssql ConnectionPool lifecycle and MemberJunction provider initialization.
|
|
9
9
|
*/
|
|
10
10
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
11
|
if (k2 === undefined) k2 = k;
|
|
@@ -31,17 +31,21 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
31
31
|
return result;
|
|
32
32
|
};
|
|
33
33
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
-
exports.findEntityDirectories = exports.getSystemUser = exports.cleanupProvider = exports.initializeProvider = void 0;
|
|
35
|
-
const
|
|
34
|
+
exports.findEntityDirectories = exports.getDataProvider = exports.getSystemUser = exports.cleanupProvider = exports.initializeProvider = void 0;
|
|
35
|
+
const sql = __importStar(require("mssql"));
|
|
36
36
|
const sqlserver_dataprovider_1 = require("@memberjunction/sqlserver-dataprovider");
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
|
-
/** Global
|
|
40
|
-
let
|
|
39
|
+
/** Global ConnectionPool instance for connection lifecycle management */
|
|
40
|
+
let globalPool = null;
|
|
41
|
+
/** Global provider instance to ensure single initialization */
|
|
42
|
+
let globalProvider = null;
|
|
43
|
+
/** Promise to track ongoing initialization */
|
|
44
|
+
let initializationPromise = null;
|
|
41
45
|
/**
|
|
42
46
|
* Initialize a SQLServerDataProvider with the given configuration
|
|
43
47
|
*
|
|
44
|
-
* Creates and initializes a
|
|
48
|
+
* Creates and initializes a mssql ConnectionPool for SQL Server, then sets up
|
|
45
49
|
* the MemberJunction SQLServerDataProvider. The connection is stored globally
|
|
46
50
|
* for proper cleanup. Auto-detects Azure SQL databases for encryption settings.
|
|
47
51
|
*
|
|
@@ -57,38 +61,50 @@ let globalDataSource = null;
|
|
|
57
61
|
* ```
|
|
58
62
|
*/
|
|
59
63
|
async function initializeProvider(config) {
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
64
|
+
// Return existing provider if already initialized
|
|
65
|
+
if (globalProvider) {
|
|
66
|
+
return globalProvider;
|
|
67
|
+
}
|
|
68
|
+
// Return ongoing initialization if in progress
|
|
69
|
+
if (initializationPromise) {
|
|
70
|
+
return initializationPromise;
|
|
71
|
+
}
|
|
72
|
+
// Start new initialization
|
|
73
|
+
initializationPromise = (async () => {
|
|
74
|
+
// Create mssql config
|
|
75
|
+
const poolConfig = {
|
|
76
|
+
server: config.dbHost,
|
|
77
|
+
port: config.dbPort ? Number(config.dbPort) : 1433,
|
|
78
|
+
database: config.dbDatabase,
|
|
79
|
+
user: config.dbUsername,
|
|
80
|
+
password: config.dbPassword,
|
|
81
|
+
options: {
|
|
82
|
+
encrypt: config.dbEncrypt === 'Y' || config.dbEncrypt === 'true' ||
|
|
83
|
+
config.dbHost.includes('.database.windows.net'), // Auto-detect Azure SQL
|
|
84
|
+
trustServerCertificate: config.dbTrustServerCertificate === 'Y',
|
|
85
|
+
instanceName: config.dbInstanceName,
|
|
86
|
+
enableArithAbort: true
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
// Create and connect pool
|
|
90
|
+
const pool = new sql.ConnectionPool(poolConfig);
|
|
91
|
+
await pool.connect();
|
|
92
|
+
// Store for cleanup
|
|
93
|
+
globalPool = pool;
|
|
94
|
+
// Create provider config
|
|
95
|
+
const providerConfig = new sqlserver_dataprovider_1.SQLServerProviderConfigData(pool, 'system@sync.cli', // Default user for CLI
|
|
96
|
+
config.mjCoreSchema || '__mj', 0);
|
|
97
|
+
// Use setupSQLServerClient to properly initialize
|
|
98
|
+
globalProvider = await (0, sqlserver_dataprovider_1.setupSQLServerClient)(providerConfig);
|
|
99
|
+
return globalProvider;
|
|
100
|
+
})();
|
|
101
|
+
return initializationPromise;
|
|
86
102
|
}
|
|
87
103
|
exports.initializeProvider = initializeProvider;
|
|
88
104
|
/**
|
|
89
105
|
* Clean up the global database connection
|
|
90
106
|
*
|
|
91
|
-
*
|
|
107
|
+
* Closes the mssql ConnectionPool if it exists and is connected.
|
|
92
108
|
* Should be called when the CLI command completes to ensure proper cleanup.
|
|
93
109
|
*
|
|
94
110
|
* @returns Promise that resolves when cleanup is complete
|
|
@@ -103,10 +119,12 @@ exports.initializeProvider = initializeProvider;
|
|
|
103
119
|
* ```
|
|
104
120
|
*/
|
|
105
121
|
async function cleanupProvider() {
|
|
106
|
-
if (
|
|
107
|
-
await
|
|
108
|
-
|
|
122
|
+
if (globalPool && globalPool.connected) {
|
|
123
|
+
await globalPool.close();
|
|
124
|
+
globalPool = null;
|
|
109
125
|
}
|
|
126
|
+
globalProvider = null;
|
|
127
|
+
initializationPromise = null;
|
|
110
128
|
}
|
|
111
129
|
exports.cleanupProvider = cleanupProvider;
|
|
112
130
|
/**
|
|
@@ -132,6 +150,26 @@ function getSystemUser() {
|
|
|
132
150
|
return sysUser;
|
|
133
151
|
}
|
|
134
152
|
exports.getSystemUser = getSystemUser;
|
|
153
|
+
/**
|
|
154
|
+
* Get the current data provider instance
|
|
155
|
+
*
|
|
156
|
+
* Returns the global SQLServerDataProvider instance that was initialized by
|
|
157
|
+
* initializeProvider. This allows access to data provider features like SQL logging.
|
|
158
|
+
*
|
|
159
|
+
* @returns The global SQLServerDataProvider instance or null if not initialized
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* const provider = getDataProvider();
|
|
164
|
+
* if (provider?.createSqlLogger) {
|
|
165
|
+
* const logger = await provider.createSqlLogger('/path/to/log.sql');
|
|
166
|
+
* }
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
function getDataProvider() {
|
|
170
|
+
return globalProvider;
|
|
171
|
+
}
|
|
172
|
+
exports.getDataProvider = getDataProvider;
|
|
135
173
|
/**
|
|
136
174
|
* Find entity directories at the immediate level only
|
|
137
175
|
*
|
|
@@ -141,7 +179,8 @@ exports.getSystemUser = getSystemUser;
|
|
|
141
179
|
*
|
|
142
180
|
* @param dir - Base directory to search from
|
|
143
181
|
* @param specificDir - Optional specific subdirectory name to check
|
|
144
|
-
* @
|
|
182
|
+
* @param directoryOrder - Optional array specifying the order directories should be processed
|
|
183
|
+
* @returns Array of absolute directory paths containing .mj-sync.json files, ordered according to directoryOrder
|
|
145
184
|
*
|
|
146
185
|
* @example
|
|
147
186
|
* ```typescript
|
|
@@ -150,33 +189,78 @@ exports.getSystemUser = getSystemUser;
|
|
|
150
189
|
*
|
|
151
190
|
* // Check specific directory
|
|
152
191
|
* const dirs = findEntityDirectories(process.cwd(), 'ai-prompts');
|
|
192
|
+
*
|
|
193
|
+
* // Find directories with custom ordering
|
|
194
|
+
* const dirs = findEntityDirectories(process.cwd(), undefined, ['prompts', 'agent-types']);
|
|
153
195
|
* ```
|
|
154
196
|
*/
|
|
155
|
-
function findEntityDirectories(dir, specificDir) {
|
|
197
|
+
function findEntityDirectories(dir, specificDir, directoryOrder) {
|
|
156
198
|
const results = [];
|
|
157
|
-
// If specific directory is provided, check if it's an entity directory
|
|
199
|
+
// If specific directory is provided, check if it's an entity directory or root config directory
|
|
158
200
|
if (specificDir) {
|
|
159
201
|
const targetDir = path.isAbsolute(specificDir) ? specificDir : path.join(dir, specificDir);
|
|
160
202
|
if (fs.existsSync(targetDir)) {
|
|
161
|
-
const
|
|
203
|
+
const syncConfigPath = path.join(targetDir, '.mj-sync.json');
|
|
204
|
+
const hasSyncConfig = fs.existsSync(syncConfigPath);
|
|
162
205
|
if (hasSyncConfig) {
|
|
163
|
-
|
|
206
|
+
try {
|
|
207
|
+
const config = JSON.parse(fs.readFileSync(syncConfigPath, 'utf8'));
|
|
208
|
+
// If this config has an entity field, it's an entity directory
|
|
209
|
+
if (config.entity) {
|
|
210
|
+
results.push(targetDir);
|
|
211
|
+
return results;
|
|
212
|
+
}
|
|
213
|
+
// If this config has directoryOrder but no entity, treat it as a root config
|
|
214
|
+
// and look for entity directories in its subdirectories
|
|
215
|
+
if (config.directoryOrder) {
|
|
216
|
+
return findEntityDirectories(targetDir, undefined, config.directoryOrder);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
// If we can't parse the config, treat it as a regular directory
|
|
221
|
+
}
|
|
164
222
|
}
|
|
223
|
+
// Fallback: look for entity subdirectories in the target directory
|
|
224
|
+
return findEntityDirectories(targetDir, undefined, directoryOrder);
|
|
165
225
|
}
|
|
166
226
|
return results;
|
|
167
227
|
}
|
|
168
228
|
// Otherwise, find all immediate subdirectories with .mj-sync.json
|
|
169
229
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
230
|
+
const foundDirectories = [];
|
|
170
231
|
for (const entry of entries) {
|
|
171
232
|
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
172
233
|
const subDir = path.join(dir, entry.name);
|
|
173
234
|
const hasSyncConfig = fs.existsSync(path.join(subDir, '.mj-sync.json'));
|
|
174
235
|
if (hasSyncConfig) {
|
|
175
|
-
|
|
236
|
+
foundDirectories.push(subDir);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// If directoryOrder is specified, sort directories according to it
|
|
241
|
+
if (directoryOrder && directoryOrder.length > 0) {
|
|
242
|
+
const orderedDirs = [];
|
|
243
|
+
const unorderedDirs = [];
|
|
244
|
+
// First, add directories in the specified order
|
|
245
|
+
for (const dirName of directoryOrder) {
|
|
246
|
+
const matchingDir = foundDirectories.find(fullPath => path.basename(fullPath) === dirName);
|
|
247
|
+
if (matchingDir) {
|
|
248
|
+
orderedDirs.push(matchingDir);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Then, add any remaining directories in alphabetical order
|
|
252
|
+
for (const foundDir of foundDirectories) {
|
|
253
|
+
const dirName = path.basename(foundDir);
|
|
254
|
+
if (!directoryOrder.includes(dirName)) {
|
|
255
|
+
unorderedDirs.push(foundDir);
|
|
176
256
|
}
|
|
177
257
|
}
|
|
258
|
+
// Sort unordered directories alphabetically
|
|
259
|
+
unorderedDirs.sort((a, b) => path.basename(a).localeCompare(path.basename(b)));
|
|
260
|
+
return [...orderedDirs, ...unorderedDirs];
|
|
178
261
|
}
|
|
179
|
-
return
|
|
262
|
+
// No ordering specified, return in alphabetical order (existing behavior)
|
|
263
|
+
return foundDirectories.sort((a, b) => path.basename(a).localeCompare(path.basename(b)));
|
|
180
264
|
}
|
|
181
265
|
exports.findEntityDirectories = findEntityDirectories;
|
|
182
266
|
//# sourceMappingURL=provider-utils.js.map
|
|
@@ -1 +1 @@
|
|
|
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
|
+
{"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 mssql ConnectionPool lifecycle and MemberJunction provider initialization.\n */\n\nimport * as sql from 'mssql';\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';\nimport { configManager } from './config-manager';\n\n/** Global ConnectionPool instance for connection lifecycle management */\nlet globalPool: sql.ConnectionPool | null = null;\n\n/** Global provider instance to ensure single initialization */\nlet globalProvider: SQLServerDataProvider | null = null;\n\n/** Promise to track ongoing initialization */\nlet initializationPromise: Promise<SQLServerDataProvider> | null = null;\n\n/**\n * Initialize a SQLServerDataProvider with the given configuration\n * \n * Creates and initializes a mssql ConnectionPool 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 // Return existing provider if already initialized\n if (globalProvider) {\n return globalProvider;\n }\n \n // Return ongoing initialization if in progress\n if (initializationPromise) {\n return initializationPromise;\n }\n \n // Start new initialization\n initializationPromise = (async () => {\n // Create mssql config\n const poolConfig: sql.config = {\n server: config.dbHost,\n port: config.dbPort ? Number(config.dbPort) : 1433,\n database: config.dbDatabase,\n user: config.dbUsername,\n password: config.dbPassword,\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 enableArithAbort: true\n }\n };\n \n // Create and connect pool\n const pool = new sql.ConnectionPool(poolConfig);\n await pool.connect();\n \n // Store for cleanup\n globalPool = pool;\n \n // Create provider config\n const providerConfig = new SQLServerProviderConfigData(\n pool,\n 'system@sync.cli', // Default user for CLI\n config.mjCoreSchema || '__mj',\n 0\n );\n \n // Use setupSQLServerClient to properly initialize\n globalProvider = await setupSQLServerClient(providerConfig);\n return globalProvider;\n })();\n \n return initializationPromise;\n}\n\n/**\n * Clean up the global database connection\n * \n * Closes the mssql ConnectionPool if it exists and is connected.\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 (globalPool && globalPool.connected) {\n await globalPool.close();\n globalPool = null;\n }\n globalProvider = null;\n initializationPromise = null;\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 * Get the current data provider instance\n * \n * Returns the global SQLServerDataProvider instance that was initialized by\n * initializeProvider. This allows access to data provider features like SQL logging.\n * \n * @returns The global SQLServerDataProvider instance or null if not initialized\n * \n * @example\n * ```typescript\n * const provider = getDataProvider();\n * if (provider?.createSqlLogger) {\n * const logger = await provider.createSqlLogger('/path/to/log.sql');\n * }\n * ```\n */\nexport function getDataProvider(): SQLServerDataProvider | null {\n return globalProvider;\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 * @param directoryOrder - Optional array specifying the order directories should be processed\n * @returns Array of absolute directory paths containing .mj-sync.json files, ordered according to directoryOrder\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 * // Find directories with custom ordering\n * const dirs = findEntityDirectories(process.cwd(), undefined, ['prompts', 'agent-types']);\n * ```\n */\nexport function findEntityDirectories(dir: string, specificDir?: string, directoryOrder?: string[]): string[] {\n const results: string[] = [];\n \n // If specific directory is provided, check if it's an entity directory or root config directory\n if (specificDir) {\n const targetDir = path.isAbsolute(specificDir) ? specificDir : path.join(dir, specificDir);\n if (fs.existsSync(targetDir)) {\n const syncConfigPath = path.join(targetDir, '.mj-sync.json');\n const hasSyncConfig = fs.existsSync(syncConfigPath);\n \n if (hasSyncConfig) {\n try {\n const config = JSON.parse(fs.readFileSync(syncConfigPath, 'utf8'));\n \n // If this config has an entity field, it's an entity directory\n if (config.entity) {\n results.push(targetDir);\n return results;\n }\n \n // If this config has directoryOrder but no entity, treat it as a root config\n // and look for entity directories in its subdirectories\n if (config.directoryOrder) {\n return findEntityDirectories(targetDir, undefined, config.directoryOrder);\n }\n } catch (error) {\n // If we can't parse the config, treat it as a regular directory\n }\n }\n \n // Fallback: look for entity subdirectories in the target directory\n return findEntityDirectories(targetDir, undefined, directoryOrder);\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 const foundDirectories: string[] = [];\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 foundDirectories.push(subDir);\n }\n }\n }\n \n // If directoryOrder is specified, sort directories according to it\n if (directoryOrder && directoryOrder.length > 0) {\n const orderedDirs: string[] = [];\n const unorderedDirs: string[] = [];\n \n // First, add directories in the specified order\n for (const dirName of directoryOrder) {\n const matchingDir = foundDirectories.find(fullPath => \n path.basename(fullPath) === dirName\n );\n if (matchingDir) {\n orderedDirs.push(matchingDir);\n }\n }\n \n // Then, add any remaining directories in alphabetical order\n for (const foundDir of foundDirectories) {\n const dirName = path.basename(foundDir);\n if (!directoryOrder.includes(dirName)) {\n unorderedDirs.push(foundDir);\n }\n }\n \n // Sort unordered directories alphabetically\n unorderedDirs.sort((a, b) => path.basename(a).localeCompare(path.basename(b)));\n \n return [...orderedDirs, ...unorderedDirs];\n }\n \n // No ordering specified, return in alphabetical order (existing behavior)\n return foundDirectories.sort((a, b) => path.basename(a).localeCompare(path.basename(b)));\n}"]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Singleton manager for shared instances across MetadataSync
|
|
3
|
+
* @module singleton-manager
|
|
4
|
+
*
|
|
5
|
+
* This module ensures that expensive resources like SyncEngine are only
|
|
6
|
+
* initialized once per process, preventing duplicate metadata refreshes
|
|
7
|
+
* and improving performance.
|
|
8
|
+
*/
|
|
9
|
+
import { SyncEngine } from './sync-engine';
|
|
10
|
+
import { UserInfo } from '@memberjunction/core';
|
|
11
|
+
/**
|
|
12
|
+
* Get or create a singleton SyncEngine instance
|
|
13
|
+
*
|
|
14
|
+
* Ensures that only one SyncEngine is created and initialized per process,
|
|
15
|
+
* preventing duplicate metadata refreshes that cause the double GetAllMetadata()
|
|
16
|
+
* console output.
|
|
17
|
+
*
|
|
18
|
+
* @param contextUser - The user context for database operations
|
|
19
|
+
* @returns Promise resolving to initialized SyncEngine instance
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* const syncEngine = await getSyncEngine(getSystemUser());
|
|
24
|
+
* // Use syncEngine for operations
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare function getSyncEngine(contextUser: UserInfo): Promise<SyncEngine>;
|
|
28
|
+
/**
|
|
29
|
+
* Reset the singleton SyncEngine instance
|
|
30
|
+
*
|
|
31
|
+
* Should be called when cleaning up resources to ensure a fresh
|
|
32
|
+
* instance is created on the next request.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resetSyncEngine(): void;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Singleton manager for shared instances across MetadataSync
|
|
4
|
+
* @module singleton-manager
|
|
5
|
+
*
|
|
6
|
+
* This module ensures that expensive resources like SyncEngine are only
|
|
7
|
+
* initialized once per process, preventing duplicate metadata refreshes
|
|
8
|
+
* and improving performance.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.resetSyncEngine = exports.getSyncEngine = void 0;
|
|
12
|
+
const sync_engine_1 = require("./sync-engine");
|
|
13
|
+
/** Global SyncEngine instance */
|
|
14
|
+
let globalSyncEngine = null;
|
|
15
|
+
/** Promise to track ongoing SyncEngine initialization */
|
|
16
|
+
let syncEngineInitPromise = null;
|
|
17
|
+
/**
|
|
18
|
+
* Get or create a singleton SyncEngine instance
|
|
19
|
+
*
|
|
20
|
+
* Ensures that only one SyncEngine is created and initialized per process,
|
|
21
|
+
* preventing duplicate metadata refreshes that cause the double GetAllMetadata()
|
|
22
|
+
* console output.
|
|
23
|
+
*
|
|
24
|
+
* @param contextUser - The user context for database operations
|
|
25
|
+
* @returns Promise resolving to initialized SyncEngine instance
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* const syncEngine = await getSyncEngine(getSystemUser());
|
|
30
|
+
* // Use syncEngine for operations
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
async function getSyncEngine(contextUser) {
|
|
34
|
+
// Return existing engine if already initialized
|
|
35
|
+
if (globalSyncEngine) {
|
|
36
|
+
return globalSyncEngine;
|
|
37
|
+
}
|
|
38
|
+
// Return ongoing initialization if in progress
|
|
39
|
+
if (syncEngineInitPromise) {
|
|
40
|
+
return syncEngineInitPromise;
|
|
41
|
+
}
|
|
42
|
+
// Start new initialization
|
|
43
|
+
syncEngineInitPromise = (async () => {
|
|
44
|
+
globalSyncEngine = new sync_engine_1.SyncEngine(contextUser);
|
|
45
|
+
await globalSyncEngine.initialize();
|
|
46
|
+
return globalSyncEngine;
|
|
47
|
+
})();
|
|
48
|
+
return syncEngineInitPromise;
|
|
49
|
+
}
|
|
50
|
+
exports.getSyncEngine = getSyncEngine;
|
|
51
|
+
/**
|
|
52
|
+
* Reset the singleton SyncEngine instance
|
|
53
|
+
*
|
|
54
|
+
* Should be called when cleaning up resources to ensure a fresh
|
|
55
|
+
* instance is created on the next request.
|
|
56
|
+
*/
|
|
57
|
+
function resetSyncEngine() {
|
|
58
|
+
globalSyncEngine = null;
|
|
59
|
+
syncEngineInitPromise = null;
|
|
60
|
+
}
|
|
61
|
+
exports.resetSyncEngine = resetSyncEngine;
|
|
62
|
+
//# sourceMappingURL=singleton-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"singleton-manager.js","sourceRoot":"","sources":["../../src/lib/singleton-manager.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;;AAEH,+CAA2C;AAG3C,iCAAiC;AACjC,IAAI,gBAAgB,GAAsB,IAAI,CAAC;AAE/C,yDAAyD;AACzD,IAAI,qBAAqB,GAA+B,IAAI,CAAC;AAE7D;;;;;;;;;;;;;;;GAeG;AACI,KAAK,UAAU,aAAa,CAAC,WAAqB;IACvD,gDAAgD;IAChD,IAAI,gBAAgB,EAAE,CAAC;QACrB,OAAO,gBAAgB,CAAC;IAC1B,CAAC;IAED,+CAA+C;IAC/C,IAAI,qBAAqB,EAAE,CAAC;QAC1B,OAAO,qBAAqB,CAAC;IAC/B,CAAC;IAED,2BAA2B;IAC3B,qBAAqB,GAAG,CAAC,KAAK,IAAI,EAAE;QAClC,gBAAgB,GAAG,IAAI,wBAAU,CAAC,WAAW,CAAC,CAAC;QAC/C,MAAM,gBAAgB,CAAC,UAAU,EAAE,CAAC;QACpC,OAAO,gBAAgB,CAAC;IAC1B,CAAC,CAAC,EAAE,CAAC;IAEL,OAAO,qBAAqB,CAAC;AAC/B,CAAC;AAnBD,sCAmBC;AAED;;;;;GAKG;AACH,SAAgB,eAAe;IAC7B,gBAAgB,GAAG,IAAI,CAAC;IACxB,qBAAqB,GAAG,IAAI,CAAC;AAC/B,CAAC;AAHD,0CAGC","sourcesContent":["/**\n * @fileoverview Singleton manager for shared instances across MetadataSync\n * @module singleton-manager\n * \n * This module ensures that expensive resources like SyncEngine are only\n * initialized once per process, preventing duplicate metadata refreshes\n * and improving performance.\n */\n\nimport { SyncEngine } from './sync-engine';\nimport { UserInfo } from '@memberjunction/core';\n\n/** Global SyncEngine instance */\nlet globalSyncEngine: SyncEngine | null = null;\n\n/** Promise to track ongoing SyncEngine initialization */\nlet syncEngineInitPromise: Promise<SyncEngine> | null = null;\n\n/**\n * Get or create a singleton SyncEngine instance\n * \n * Ensures that only one SyncEngine is created and initialized per process,\n * preventing duplicate metadata refreshes that cause the double GetAllMetadata()\n * console output.\n * \n * @param contextUser - The user context for database operations\n * @returns Promise resolving to initialized SyncEngine instance\n * \n * @example\n * ```typescript\n * const syncEngine = await getSyncEngine(getSystemUser());\n * // Use syncEngine for operations\n * ```\n */\nexport async function getSyncEngine(contextUser: UserInfo): Promise<SyncEngine> {\n // Return existing engine if already initialized\n if (globalSyncEngine) {\n return globalSyncEngine;\n }\n \n // Return ongoing initialization if in progress\n if (syncEngineInitPromise) {\n return syncEngineInitPromise;\n }\n \n // Start new initialization\n syncEngineInitPromise = (async () => {\n globalSyncEngine = new SyncEngine(contextUser);\n await globalSyncEngine.initialize();\n return globalSyncEngine;\n })();\n \n return syncEngineInitPromise;\n}\n\n/**\n * Reset the singleton SyncEngine instance\n * \n * Should be called when cleaning up resources to ensure a fresh\n * instance is created on the next request.\n */\nexport function resetSyncEngine(): void {\n globalSyncEngine = null;\n syncEngineInitPromise = null;\n}"]}
|
package/dist/lib/sync-engine.js
CHANGED
|
@@ -18,6 +18,7 @@ const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
|
18
18
|
const crypto_1 = __importDefault(require("crypto"));
|
|
19
19
|
const axios_1 = __importDefault(require("axios"));
|
|
20
20
|
const core_1 = require("@memberjunction/core");
|
|
21
|
+
const global_1 = require("@memberjunction/global");
|
|
21
22
|
/**
|
|
22
23
|
* Core engine for synchronizing MemberJunction metadata between database and files
|
|
23
24
|
*
|
|
@@ -215,6 +216,16 @@ class SyncEngine {
|
|
|
215
216
|
throw new Error(`Failed to create entity object for: ${entityName}`);
|
|
216
217
|
}
|
|
217
218
|
newEntity.NewRecord();
|
|
219
|
+
// Handle explicit ID setting for new records
|
|
220
|
+
if (entityInfo.PrimaryKeys.length > 0) {
|
|
221
|
+
for (const pk of entityInfo.PrimaryKeys) {
|
|
222
|
+
if (!pk.AutoIncrement && pk.Type.toLowerCase() === 'uniqueidentifier') {
|
|
223
|
+
// Generate UUID for this primary key and set it explicitly
|
|
224
|
+
const uuid = (0, global_1.uuidv4)();
|
|
225
|
+
newEntity[pk.Name] = uuid;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
218
229
|
// Set the lookup field
|
|
219
230
|
if (fieldName in newEntity) {
|
|
220
231
|
newEntity[fieldName] = fieldValue;
|
|
@@ -228,7 +239,11 @@ class SyncEngine {
|
|
|
228
239
|
// Save the new record
|
|
229
240
|
const saved = await newEntity.Save();
|
|
230
241
|
if (!saved) {
|
|
231
|
-
const
|
|
242
|
+
const message = newEntity.LatestResult?.Message;
|
|
243
|
+
if (message) {
|
|
244
|
+
throw new Error(`Failed to auto-create ${entityName}: ${message}`);
|
|
245
|
+
}
|
|
246
|
+
const errors = newEntity.LatestResult?.Errors?.map(err => typeof err === 'string' ? err : (err?.message || JSON.stringify(err)))?.join(', ') || 'Unknown error';
|
|
232
247
|
throw new Error(`Failed to auto-create ${entityName}: ${errors}`);
|
|
233
248
|
}
|
|
234
249
|
// Return the new ID
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-engine.js","sourceRoot":"","sources":["../../src/lib/sync-engine.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;;;;AAEH,gDAAwB;AACxB,wDAA0B;AAC1B,oDAA4B;AAC5B,kDAA0B;AAC1B,+CAAyG;AAsBzG;;;;;;;;;;;;GAYG;AACH,MAAa,UAAU;IACb,QAAQ,CAAW;IACnB,WAAW,CAAW;IAE9B;;;OAGG;IACH,YAAY,WAAqB;QAC/B,IAAI,CAAC,QAAQ,GAAG,IAAI,eAAQ,EAAE,CAAC;QAC/B,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;IACjC,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,UAAU;QACd,sBAAsB;QACtB,MAAM,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;IAChC,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,KAAK,CAAC,iBAAiB,CAAC,KAAU,EAAE,OAAe,EAAE,YAAgC,EAAE,UAA8B;QACnH,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC9B,OAAO,KAAK,CAAC;QACf,CAAC;QAED,+BAA+B;QAC/B,IAAI,KAAK,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;YACjC,IAAI,CAAC,YAAY,EAAE,CAAC;gBAClB,MAAM,IAAI,KAAK,CAAC,0DAA0D,KAAK,EAAE,CAAC,CAAC;YACrF,CAAC;YACD,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;YACrC,OAAO,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACrC,CAAC;QAED,6BAA6B;QAC7B,IAAI,KAAK,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC/B,IAAI,CAAC,UAAU,EAAE,CAAC;gBAChB,MAAM,IAAI,KAAK,CAAC,sDAAsD,KAAK,EAAE,CAAC,CAAC;YACjF,CAAC;YACD,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;YACrC,OAAO,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACnC,CAAC;QAED,6BAA6B;QAC7B,IAAI,KAAK,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC/B,MAAM,QAAQ,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;YACpC,MAAM,QAAQ,GAAG,cAAI,CAAC,OAAO,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;YAEjD,IAAI,MAAM,kBAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAClC,OAAO,MAAM,kBAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;YAC9C,CAAC;iBAAM,CAAC;gBACN,MAAM,IAAI,KAAK,CAAC,mBAAmB,QAAQ,EAAE,CAAC,CAAC;YACjD,CAAC;QACH,CAAC;QAED,4BAA4B;QAC5B,IAAI,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YAC9B,MAAM,GAAG,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;YAE/B,IAAI,CAAC;gBACH,MAAM,QAAQ,GAAG,MAAM,eAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;gBACtC,OAAO,QAAQ,CAAC,IAAI,CAAC;YACvB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,IAAI,KAAK,CAAC,wBAAwB,GAAG,MAAM,KAAK,EAAE,CAAC,CAAC;YAC5D,CAAC;QACH,CAAC;QAED,+BAA+B;QAC/B,IAAI,KAAK,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;YACjC,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;YAErC,2CAA2C;YAC3C,6DAA6D;YAC7D,MAAM,WAAW,GAAG,SAAS,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;YAClD,IAAI,CAAC,WAAW,EAAE,CAAC;gBACjB,MAAM,IAAI,KAAK,CAAC,0BAA0B,KAAK,EAAE,CAAC,CAAC;YACrD,CAAC;YAED,MAAM,UAAU,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;YAClC,MAAM,SAAS,GAAG,SAAS,CAAC,SAAS,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YAE7D,mCAAmC;YACnC,MAAM,SAAS,GAAG,SAAS,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;YAChD,MAAM,UAAU,GAAG,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YAEnE,8BAA8B;YAC9B,MAAM,UAAU,GAAG,UAAU,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;YACpD,IAAI,CAAC,UAAU,EAAE,CAAC;gBAChB,MAAM,IAAI,KAAK,CAAC,0BAA0B,KAAK,EAAE,CAAC,CAAC;YACrD,CAAC;YAED,MAAM,CAAC,EAAE,SAAS,EAAE,UAAU,CAAC,GAAG,UAAU,CAAC;YAE7C,6DAA6D;YAC7D,IAAI,YAAY,GAAwB,EAAE,CAAC;YAC3C,IAAI,SAAS,IAAI,SAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;gBAChD,MAAM,UAAU,GAAG,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;gBAClD,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;gBACpC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;oBACzB,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;oBACnC,IAAI,GAAG,IAAI,GAAG,EAAE,CAAC;wBACf,YAAY,CAAC,GAAG,CAAC,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC;oBAC9C,CAAC;gBACH,CAAC;YACH,CAAC;YAED,OAAO,MAAM,IAAI,CAAC,aAAa,CAAC,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC;QAC9F,CAAC;QAED,4BAA4B;QAC5B,IAAI,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YAC9B,MAAM,MAAM,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;YAClC,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YAErC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;gBAC3B,MAAM,IAAI,KAAK,CAAC,mCAAmC,MAAM,EAAE,CAAC,CAAC;YAC/D,CAAC;YAED,OAAO,QAAQ,CAAC;QAClB,CAAC;QAED,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,KAAK,CAAC,aAAa,CACjB,UAAkB,EAClB,SAAiB,EACjB,UAAkB,EAClB,aAAsB,KAAK,EAC3B,eAAoC,EAAE;QAEtC,4CAA4C;QAE5C,MAAM,EAAE,GAAG,IAAI,cAAO,EAAE,CAAC;QACzB,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;QAC1D,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CAAC,qBAAqB,UAAU,EAAE,CAAC,CAAC;QACrD,CAAC;QAED,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,KAAK,SAAS,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;QAC1G,MAAM,MAAM,GAAG,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7C,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC;YAC9B,UAAU,EAAE,UAAU;YACtB,WAAW,EAAE,GAAG,SAAS,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,MAAM,EAAE;YACjF,OAAO,EAAE,CAAC;SACX,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;QAErB,IAAI,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAChD,IAAI,UAAU,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACtC,MAAM,SAAS,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;gBACjD,MAAM,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;gBACxC,OAAO,EAAE,CAAC;YACZ,CAAC;QACH,CAAC;QAED,6DAA6D;QAC7D,IAAI,UAAU,EAAE,CAAC;YAEf,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,UAAU,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;YACpF,IAAI,CAAC,SAAS,EAAE,CAAC;gBACf,MAAM,IAAI,KAAK,CAAC,uCAAuC,UAAU,EAAE,CAAC,CAAC;YACvE,CAAC;YAED,SAAS,CAAC,SAAS,EAAE,CAAC;YAEtB,uBAAuB;YACvB,IAAI,SAAS,IAAI,SAAS,EAAE,CAAC;gBAC1B,SAAiB,CAAC,SAAS,CAAC,GAAG,UAAU,CAAC;YAC7C,CAAC;YAED,qCAAqC;YACrC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,CAAC;gBACxD,IAAI,GAAG,IAAI,SAAS,EAAE,CAAC;oBACpB,SAAiB,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;gBAClC,CAAC;YACH,CAAC;YAED,sBAAsB;YACtB,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,IAAI,EAAE,CAAC;YACrC,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,MAAM,MAAM,GAAG,SAAS,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,eAAe,CAAC;gBAC7E,MAAM,IAAI,KAAK,CAAC,yBAAyB,UAAU,KAAK,MAAM,EAAE,CAAC,CAAC;YACpE,CAAC;YAED,oBAAoB;YACpB,IAAI,UAAU,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACtC,MAAM,SAAS,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;gBACjD,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;gBACvC,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC;QAED,MAAM,IAAI,KAAK,CAAC,sCAAsC,UAAU,WAAW,SAAS,KAAK,UAAU,GAAG,CAAC,CAAC;IAC1G,CAAC;IAED;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,aAAa,CAAC,QAAgB,EAAE,YAA0B;QAC9D,MAAM,KAAK,GAAG,cAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,cAAI,CAAC,GAAG,CAAC,CAAC;QACrD,IAAI,QAAQ,GAAwB,EAAE,GAAG,YAAY,CAAC,QAAQ,EAAE,CAAC;QAEjE,+CAA+C;QAC/C,IAAI,WAAW,GAAG,EAAE,CAAC;QACrB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,WAAW,GAAG,cAAI,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;YAC3C,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,gBAAgB,CAAC,WAAW,CAAC,CAAC;YAE9D,IAAI,YAAY,EAAE,QAAQ,EAAE,CAAC;gBAC3B,QAAQ,GAAG,EAAE,GAAG,QAAQ,EAAE,GAAG,YAAY,CAAC,QAAQ,EAAE,CAAC;YACvD,CAAC;QACH,CAAC;QAED,8DAA8D;QAC9D,MAAM,iBAAiB,GAAwB,EAAE,CAAC;QAClD,MAAM,OAAO,GAAG,cAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAEvC,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;YACtD,IAAI,CAAC;gBACH,iBAAiB,CAAC,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;YACtF,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,IAAI,KAAK,CAAC,wCAAwC,KAAK,MAAM,KAAK,EAAE,CAAC,CAAC;YAC9E,CAAC;QACH,CAAC;QAED,OAAO,iBAAiB,CAAC;IAC3B,CAAC;IAED;;;;;;OAMG;IACK,KAAK,CAAC,gBAAgB,CAAC,GAAW;QACxC,MAAM,UAAU,GAAG,cAAI,CAAC,IAAI,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC;QAErD,IAAI,MAAM,kBAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;YACpC,IAAI,CAAC;gBACH,OAAO,MAAM,kBAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;YACvC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,CAAC,KAAK,CAAC,kCAAkC,UAAU,GAAG,EAAE,KAAK,CAAC,CAAC;gBACtE,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;;;;;;;;;;;;;;;;;OAmBG;IACH,iBAAiB,CAAC,IAAS;QACzB,MAAM,IAAI,GAAG,gBAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;QACzC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;QAC3C,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC5B,CAAC;IAED;;;;;;;;;;;;;;;;OAgBG;IACH,aAAa,CAAC,UAAkB;QAC9B,OAAO,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;IAChD,CAAC;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,KAAK,CAAC,kBAAkB,CAAC,UAAkB;QACzC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,UAAU,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;QACjF,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CAAC,uCAAuC,UAAU,EAAE,CAAC,CAAC;QACvE,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,KAAK,CAAC,UAAU,CAAC,UAAkB,EAAE,UAA+B;QAClE,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC;QACzD,MAAM,UAAU,GAAG,IAAI,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;QAElD,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CAAC,qBAAqB,UAAU,EAAE,CAAC,CAAC;QACrD,CAAC;QAED,wBAAwB;QACxB,MAAM,YAAY,GAAG,IAAI,mBAAY,EAAE,CAAC;QACxC,YAAY,CAAC,oBAAoB,CAAC,UAAU,CAAC,CAAC;QAC9C,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;QAEpD,OAAO,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;IAChC,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH,KAAK,CAAC,gBAAgB,CAAC,IAAS,EAAE,OAAe;QAC/C,gBAAgB;QAChB,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YACxB,MAAM,cAAc,GAAG,EAAE,CAAC;YAC1B,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;gBACxB,cAAc,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,gBAAgB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;YAClE,CAAC;YACD,OAAO,cAAc,CAAC;QACxB,CAAC;QAED,iBAAiB;QACjB,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;YACrC,gCAAgC;YAChC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;gBAC9D,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;gBACxC,OAAO,MAAM,IAAI,CAAC,sBAAsB,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;YAClE,CAAC;YAED,+CAA+C;YAC/C,MAAM,SAAS,GAAQ,EAAE,CAAC;YAC1B,IAAI,YAAY,GAAQ,EAAE,CAAC;YAE3B,uDAAuD;YACvD,IAAI,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;gBACtB,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;gBAE7F,2CAA2C;gBAC3C,KAAK,MAAM,WAAW,IAAI,SAAS,EAAE,CAAC;oBACpC,MAAM,eAAe,GAAG,MAAM,IAAI,CAAC,sBAAsB,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;oBAChF,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC,YAAY,EAAE,eAAe,CAAC,CAAC;gBAC/D,CAAC;YACH,CAAC;YAED,2BAA2B;YAC3B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;gBAChD,IAAI,GAAG,KAAK,WAAW;oBAAE,SAAS,CAAC,iCAAiC;gBAEpE,gCAAgC;gBAChC,SAAS,CAAC,GAAG,CAAC,GAAG,MAAM,IAAI,CAAC,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;YAC/D,CAAC;YAED,4EAA4E;YAC5E,OAAO,IAAI,CAAC,SAAS,CAAC,YAAY,EAAE,SAAS,CAAC,CAAC;QACjD,CAAC;QAED,gCAAgC;QAChC,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;;;;;;;;;;OAYG;IACK,KAAK,CAAC,sBAAsB,CAAC,YAAoB,EAAE,OAAe;QACxE,MAAM,QAAQ,GAAG,cAAI,CAAC,OAAO,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;QAErD,IAAI,CAAC,MAAM,kBAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YACnC,MAAM,IAAI,KAAK,CAAC,4BAA4B,QAAQ,EAAE,CAAC,CAAC;QAC1D,CAAC;QAED,IAAI,CAAC;YACH,MAAM,eAAe,GAAG,MAAM,kBAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;YAEpD,2CAA2C;YAC3C,MAAM,WAAW,GAAG,cAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;YAC3C,OAAO,MAAM,IAAI,CAAC,gBAAgB,CAAC,eAAe,EAAE,WAAW,CAAC,CAAC;QACnE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,2BAA2B,QAAQ,KAAK,KAAK,EAAE,CAAC,CAAC;QACnE,CAAC;IACH,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACK,SAAS,CAAC,MAAW,EAAE,MAAW;QACxC,IAAI,CAAC,MAAM;YAAE,OAAO,MAAM,CAAC;QAC3B,IAAI,CAAC,MAAM;YAAE,OAAO,MAAM,CAAC;QAE3B,6DAA6D;QAC7D,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3E,OAAO,MAAM,CAAC;QAChB,CAAC;QAED,0CAA0C;QAC1C,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3E,OAAO,MAAM,CAAC;QAChB,CAAC;QAED,+BAA+B;QAC/B,MAAM,MAAM,GAAQ,EAAE,GAAG,MAAM,EAAE,CAAC;QAElC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAClD,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACxB,SAAS,CAAC,wBAAwB;YACpC,CAAC;YAED,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;gBACpE,OAAO,MAAM,CAAC,GAAG,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,GAAG,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;gBAC3F,sCAAsC;gBACtC,MAAM,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC;YACnD,CAAC;iBAAM,CAAC;gBACN,+BAA+B;gBAC/B,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;YACtB,CAAC;QACH,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;CACF;AAjlBD,gCAilBC","sourcesContent":["/**\n * @fileoverview Core synchronization engine for MemberJunction metadata\n * @module sync-engine\n * \n * This module provides the core functionality for synchronizing metadata between\n * the MemberJunction database and local file system representations. It handles\n * special reference types (@file, @url, @lookup, @env, @parent, @root, @template),\n * manages entity operations, and provides utilities for data transformation.\n */\n\nimport path from 'path';\nimport fs from 'fs-extra';\nimport crypto from 'crypto';\nimport axios from 'axios';\nimport { EntityInfo, Metadata, RunView, BaseEntity, CompositeKey, UserInfo } from '@memberjunction/core';\nimport { EntityConfig, FolderConfig } from '../config';\n\n/**\n * Represents the structure of a metadata record with optional sync tracking\n */\nexport interface RecordData {\n /** Primary key field(s) and their values */\n primaryKey?: Record<string, any>;\n /** Entity field names and their values */\n fields: Record<string, any>;\n /** Related entities organized by entity name */\n relatedEntities?: Record<string, RecordData[]>;\n /** Synchronization metadata for change tracking */\n sync?: {\n /** ISO timestamp of last modification */\n lastModified: string;\n /** SHA256 checksum of the fields object */\n checksum: string;\n };\n}\n\n/**\n * Core engine for synchronizing MemberJunction metadata between database and files\n * \n * @class SyncEngine\n * @example\n * ```typescript\n * const syncEngine = new SyncEngine(systemUser);\n * await syncEngine.initialize();\n * \n * // Process a field value with special references\n * const value = await syncEngine.processFieldValue('@lookup:Users.Email=admin@example.com', '/path/to/base');\n * ```\n */\nexport class SyncEngine {\n private metadata: Metadata;\n private contextUser: UserInfo;\n\n /**\n * Creates a new SyncEngine instance\n * @param contextUser - The user context for database operations\n */\n constructor(contextUser: UserInfo) {\n this.metadata = new Metadata();\n this.contextUser = contextUser;\n }\n \n /**\n * Initializes the sync engine by refreshing metadata cache\n * @returns Promise that resolves when initialization is complete\n */\n async initialize(): Promise<void> {\n // Initialize metadata\n await this.metadata.Refresh();\n }\n \n /**\n * Process special references in field values\n * \n * Handles the following reference types:\n * - `@parent:fieldName` - References a field from the parent record\n * - `@root:fieldName` - References a field from the root record\n * - `@file:path` - Reads content from an external file\n * - `@url:address` - Fetches content from a URL\n * - `@lookup:Entity.Field=Value` - Looks up an entity ID by field value\n * - `@env:VARIABLE` - Reads an environment variable\n * \n * @param value - The field value to process\n * @param baseDir - Base directory for resolving relative file paths\n * @param parentRecord - Optional parent entity for @parent references\n * @param rootRecord - Optional root entity for @root references\n * @returns The processed value with all references resolved\n * @throws Error if a reference cannot be resolved\n * \n * @example\n * ```typescript\n * // File reference\n * const content = await processFieldValue('@file:template.md', '/path/to/dir');\n * \n * // Lookup with auto-create\n * const userId = await processFieldValue('@lookup:Users.Email=john@example.com?create', '/path');\n * ```\n */\n async processFieldValue(value: any, baseDir: string, parentRecord?: BaseEntity | null, rootRecord?: BaseEntity | null): Promise<any> {\n if (typeof value !== 'string') {\n return value;\n }\n \n // Check for @parent: reference\n if (value.startsWith('@parent:')) {\n if (!parentRecord) {\n throw new Error(`@parent reference used but no parent record available: ${value}`);\n }\n const fieldName = value.substring(8);\n return parentRecord.Get(fieldName);\n }\n \n // Check for @root: reference\n if (value.startsWith('@root:')) {\n if (!rootRecord) {\n throw new Error(`@root reference used but no root record available: ${value}`);\n }\n const fieldName = value.substring(6);\n return rootRecord.Get(fieldName);\n }\n \n // Check for @file: reference\n if (value.startsWith('@file:')) {\n const filePath = value.substring(6);\n const fullPath = path.resolve(baseDir, filePath);\n \n if (await fs.pathExists(fullPath)) {\n return await fs.readFile(fullPath, 'utf-8');\n } else {\n throw new Error(`File not found: ${fullPath}`);\n }\n }\n \n // Check for @url: reference\n if (value.startsWith('@url:')) {\n const url = value.substring(5);\n \n try {\n const response = await axios.get(url);\n return response.data;\n } catch (error) {\n throw new Error(`Failed to fetch URL: ${url} - ${error}`);\n }\n }\n \n // Check for @lookup: reference\n if (value.startsWith('@lookup:')) {\n const lookupStr = value.substring(8);\n \n // Parse lookup with optional create syntax\n // Format: EntityName.FieldName=Value?create&OtherField=Value\n const entityMatch = lookupStr.match(/^([^.]+)\\./);\n if (!entityMatch) {\n throw new Error(`Invalid lookup format: ${value}`);\n }\n \n const entityName = entityMatch[1];\n const remaining = lookupStr.substring(entityName.length + 1);\n \n // Check if this has ?create syntax\n const hasCreate = remaining.includes('?create');\n const lookupPart = hasCreate ? remaining.split('?')[0] : remaining;\n \n // Parse the main lookup field\n const fieldMatch = lookupPart.match(/^(.+?)=(.+)$/);\n if (!fieldMatch) {\n throw new Error(`Invalid lookup format: ${value}`);\n }\n \n const [, fieldName, fieldValue] = fieldMatch;\n \n // Parse additional fields for creation if ?create is present\n let createFields: Record<string, any> = {};\n if (hasCreate && remaining.includes('?create&')) {\n const createPart = remaining.split('?create&')[1];\n const pairs = createPart.split('&');\n for (const pair of pairs) {\n const [key, val] = pair.split('=');\n if (key && val) {\n createFields[key] = decodeURIComponent(val);\n }\n }\n }\n \n return await this.resolveLookup(entityName, fieldName, fieldValue, hasCreate, createFields);\n }\n \n // Check for @env: reference\n if (value.startsWith('@env:')) {\n const envVar = value.substring(5);\n const envValue = process.env[envVar];\n \n if (envValue === undefined) {\n throw new Error(`Environment variable not found: ${envVar}`);\n }\n \n return envValue;\n }\n \n return value;\n }\n \n /**\n * Resolve a lookup reference to an ID, optionally creating the record if it doesn't exist\n * \n * @param entityName - Name of the entity to search in\n * @param fieldName - Field to match against\n * @param fieldValue - Value to search for\n * @param autoCreate - Whether to create the record if not found\n * @param createFields - Additional fields to set when creating\n * @returns The ID of the found or created record\n * @throws Error if lookup fails and autoCreate is false\n * \n * @example\n * ```typescript\n * // Simple lookup\n * const categoryId = await resolveLookup('Categories', 'Name', 'Technology');\n * \n * // Lookup with auto-create\n * const tagId = await resolveLookup('Tags', 'Name', 'New Tag', true, {\n * Description: 'Auto-created tag',\n * Status: 'Active'\n * });\n * ```\n */\n async resolveLookup(\n entityName: string, \n fieldName: string, \n fieldValue: string,\n autoCreate: boolean = false,\n createFields: Record<string, any> = {}\n ): Promise<string> {\n // Debug logging handled by caller if needed\n \n const rv = new RunView();\n const entityInfo = this.metadata.EntityByName(entityName);\n if (!entityInfo) {\n throw new Error(`Entity not found: ${entityName}`);\n }\n \n const field = entityInfo.Fields.find(f => f.Name.trim().toLowerCase() === fieldName.trim().toLowerCase());\n const quotes = field?.NeedsQuotes ? \"'\" : '';\n const result = await rv.RunView({\n EntityName: entityName,\n ExtraFilter: `${fieldName} = ${quotes}${fieldValue.replace(/'/g, \"''\")}${quotes}`,\n MaxRows: 1\n }, this.contextUser);\n \n if (result.Success && result.Results.length > 0) {\n if (entityInfo.PrimaryKeys.length > 0) {\n const pkeyField = entityInfo.PrimaryKeys[0].Name;\n const id = result.Results[0][pkeyField];\n return id;\n }\n }\n \n // If not found and auto-create is enabled, create the record\n if (autoCreate) {\n \n const newEntity = await this.metadata.GetEntityObject(entityName, this.contextUser);\n if (!newEntity) {\n throw new Error(`Failed to create entity object for: ${entityName}`);\n }\n \n newEntity.NewRecord();\n \n // Set the lookup field\n if (fieldName in newEntity) {\n (newEntity as any)[fieldName] = fieldValue;\n }\n \n // Set any additional fields provided\n for (const [key, value] of Object.entries(createFields)) {\n if (key in newEntity) {\n (newEntity as any)[key] = value;\n }\n }\n \n // Save the new record\n const saved = await newEntity.Save();\n if (!saved) {\n const errors = newEntity.LatestResult?.Errors?.join(', ') || 'Unknown error';\n throw new Error(`Failed to auto-create ${entityName}: ${errors}`);\n }\n \n // Return the new ID\n if (entityInfo.PrimaryKeys.length > 0) {\n const pkeyField = entityInfo.PrimaryKeys[0].Name;\n const newId = newEntity.Get(pkeyField);\n return newId;\n }\n }\n \n throw new Error(`Lookup failed: No record found in '${entityName}' where ${fieldName}='${fieldValue}'`);\n }\n \n /**\n * Build cascading defaults for a file path and process field values\n * \n * Walks up the directory tree from the file location, collecting defaults from\n * entity config and folder configs, with deeper folders overriding parent values.\n * All default values are processed for special references.\n * \n * @param filePath - Path to the file being processed\n * @param entityConfig - Entity configuration containing base defaults\n * @returns Processed defaults with all references resolved\n * @throws Error if any default value processing fails\n */\n async buildDefaults(filePath: string, entityConfig: EntityConfig): Promise<Record<string, any>> {\n const parts = path.dirname(filePath).split(path.sep);\n let defaults: Record<string, any> = { ...entityConfig.defaults };\n \n // Walk up the directory tree building defaults\n let currentPath = '';\n for (const part of parts) {\n currentPath = path.join(currentPath, part);\n const folderConfig = await this.loadFolderConfig(currentPath);\n \n if (folderConfig?.defaults) {\n defaults = { ...defaults, ...folderConfig.defaults };\n }\n }\n \n // Process all default values (lookups, file references, etc.)\n const processedDefaults: Record<string, any> = {};\n const baseDir = path.dirname(filePath);\n \n for (const [field, value] of Object.entries(defaults)) {\n try {\n processedDefaults[field] = await this.processFieldValue(value, baseDir, null, null);\n } catch (error) {\n throw new Error(`Failed to process default for field '${field}': ${error}`);\n }\n }\n \n return processedDefaults;\n }\n \n /**\n * Load folder configuration from .mj-folder.json file\n * \n * @param dir - Directory to check for configuration\n * @returns Folder configuration or null if not found/invalid\n * @private\n */\n private async loadFolderConfig(dir: string): Promise<FolderConfig | null> {\n const configPath = path.join(dir, '.mj-folder.json');\n \n if (await fs.pathExists(configPath)) {\n try {\n return await fs.readJson(configPath);\n } catch (error) {\n console.error(`Error loading folder config at ${configPath}:`, error);\n return null;\n }\n }\n \n return null;\n }\n \n /**\n * Calculate SHA256 checksum for data\n * \n * Generates a deterministic hash of the provided data by converting it to\n * formatted JSON and calculating a SHA256 digest. Used for change detection\n * in sync operations.\n * \n * @param data - Any data structure to calculate checksum for\n * @returns Hexadecimal string representation of the SHA256 hash\n * \n * @example\n * ```typescript\n * const checksum = syncEngine.calculateChecksum({\n * name: 'Test Record',\n * value: 42,\n * tags: ['a', 'b']\n * });\n * // Returns consistent hash for same data structure\n * ```\n */\n calculateChecksum(data: any): string {\n const hash = crypto.createHash('sha256');\n hash.update(JSON.stringify(data, null, 2));\n return hash.digest('hex');\n }\n \n /**\n * Get entity metadata information by name\n * \n * Retrieves the EntityInfo object containing schema metadata for the specified entity.\n * Returns null if the entity is not found in the metadata cache.\n * \n * @param entityName - Name of the entity to look up\n * @returns EntityInfo object with schema details or null if not found\n * \n * @example\n * ```typescript\n * const entityInfo = syncEngine.getEntityInfo('AI Prompts');\n * if (entityInfo) {\n * console.log(`Primary keys: ${entityInfo.PrimaryKeys.map(pk => pk.Name).join(', ')}`);\n * }\n * ```\n */\n getEntityInfo(entityName: string): EntityInfo | null {\n return this.metadata.EntityByName(entityName);\n }\n \n /**\n * Create a new entity object instance\n * \n * Uses the MemberJunction metadata system to properly instantiate an entity object.\n * This ensures correct class registration and respects any custom entity subclasses.\n * \n * @param entityName - Name of the entity to create\n * @returns Promise resolving to the new BaseEntity instance\n * @throws Error if entity creation fails\n * \n * @example\n * ```typescript\n * const entity = await syncEngine.createEntityObject('AI Prompts');\n * entity.NewRecord();\n * entity.Set('Name', 'My Prompt');\n * await entity.Save();\n * ```\n */\n async createEntityObject(entityName: string): Promise<BaseEntity> {\n const entity = await this.metadata.GetEntityObject(entityName, this.contextUser);\n if (!entity) {\n throw new Error(`Failed to create entity object for: ${entityName}`);\n }\n return entity;\n }\n \n /**\n * Load an entity record by primary key\n * \n * Retrieves an existing entity record from the database using its primary key values.\n * Supports both single and composite primary keys. Returns null if the record is not found.\n * \n * @param entityName - Name of the entity to load\n * @param primaryKey - Object containing primary key field names and values\n * @returns Promise resolving to the loaded entity or null if not found\n * @throws Error if entity metadata is not found\n * \n * @example\n * ```typescript\n * // Single primary key\n * const entity = await syncEngine.loadEntity('Users', { ID: '123-456' });\n * \n * // Composite primary key\n * const entity = await syncEngine.loadEntity('UserRoles', { \n * UserID: '123-456',\n * RoleID: '789-012'\n * });\n * ```\n */\n async loadEntity(entityName: string, primaryKey: Record<string, any>): Promise<BaseEntity | null> {\n const entity = await this.createEntityObject(entityName);\n const entityInfo = this.getEntityInfo(entityName);\n \n if (!entityInfo) {\n throw new Error(`Entity not found: ${entityName}`);\n }\n \n // Handle composite keys\n const compositeKey = new CompositeKey();\n compositeKey.LoadFromSimpleObject(primaryKey);\n const loaded = await entity.InnerLoad(compositeKey);\n \n return loaded ? entity : null;\n }\n \n /**\n * Process JSON object with template references\n * \n * Recursively processes JSON data structures to resolve `@template` references.\n * Templates can be defined at any level and support:\n * - Single template references: `\"@template:path/to/template.json\"`\n * - Object with @template field: `{ \"@template\": \"file.json\", \"override\": \"value\" }`\n * - Array of templates for merging: `{ \"@template\": [\"base.json\", \"overrides.json\"] }`\n * - Nested template references within templates\n * \n * @param data - JSON data structure to process\n * @param baseDir - Base directory for resolving relative template paths\n * @returns Promise resolving to the processed data with all templates resolved\n * @throws Error if template file is not found or contains invalid JSON\n * \n * @example\n * ```typescript\n * // Input data with template reference\n * const data = {\n * \"@template\": \"defaults/ai-prompt.json\",\n * \"Name\": \"Custom Prompt\",\n * \"Prompt\": \"Override the template prompt\"\n * };\n * \n * // Resolves template and merges with overrides\n * const result = await syncEngine.processTemplates(data, '/path/to/dir');\n * ```\n */\n async processTemplates(data: any, baseDir: string): Promise<any> {\n // Handle arrays\n if (Array.isArray(data)) {\n const processedArray = [];\n for (const item of data) {\n processedArray.push(await this.processTemplates(item, baseDir));\n }\n return processedArray;\n }\n \n // Handle objects\n if (data && typeof data === 'object') {\n // Check for @template reference\n if (typeof data === 'string' && data.startsWith('@template:')) {\n const templatePath = data.substring(10);\n return await this.loadAndProcessTemplate(templatePath, baseDir);\n }\n \n // Process object with possible @template field\n const processed: any = {};\n let templateData: any = {};\n \n // First, check if there's a @template field to process\n if (data['@template']) {\n const templates = Array.isArray(data['@template']) ? data['@template'] : [data['@template']];\n \n // Process templates in order, merging them\n for (const templateRef of templates) {\n const templateContent = await this.loadAndProcessTemplate(templateRef, baseDir);\n templateData = this.deepMerge(templateData, templateContent);\n }\n }\n \n // Process all other fields\n for (const [key, value] of Object.entries(data)) {\n if (key === '@template') continue; // Skip the template field itself\n \n // Process the value recursively\n processed[key] = await this.processTemplates(value, baseDir);\n }\n \n // Merge template data with processed data (processed data takes precedence)\n return this.deepMerge(templateData, processed);\n }\n \n // Return primitive values as-is\n return data;\n }\n \n /**\n * Load and process a template file\n * \n * Loads a JSON template file from the filesystem and recursively processes any\n * nested template references within it. Template paths are resolved relative to\n * the template file's directory, enabling template composition.\n * \n * @param templatePath - Path to the template file (relative or absolute)\n * @param baseDir - Base directory for resolving relative paths\n * @returns Promise resolving to the processed template content\n * @throws Error if template file not found or contains invalid JSON\n * @private\n */\n private async loadAndProcessTemplate(templatePath: string, baseDir: string): Promise<any> {\n const fullPath = path.resolve(baseDir, templatePath);\n \n if (!await fs.pathExists(fullPath)) {\n throw new Error(`Template file not found: ${fullPath}`);\n }\n \n try {\n const templateContent = await fs.readJson(fullPath);\n \n // Recursively process any nested templates\n const templateDir = path.dirname(fullPath);\n return await this.processTemplates(templateContent, templateDir);\n } catch (error) {\n throw new Error(`Failed to load template ${fullPath}: ${error}`);\n }\n }\n \n /**\n * Deep merge two objects with target taking precedence\n * \n * Recursively merges two objects, with values from the target object overriding\n * values from the source object. Arrays and primitive values are not merged but\n * replaced entirely by the target value. Undefined values in target are skipped.\n * \n * @param source - Base object to merge from\n * @param target - Object with values that override source\n * @returns New object with merged values\n * @private\n * \n * @example\n * ```typescript\n * const source = {\n * a: 1,\n * b: { x: 10, y: 20 },\n * c: [1, 2, 3]\n * };\n * const target = {\n * a: 2,\n * b: { y: 30, z: 40 },\n * d: 'new'\n * };\n * const result = deepMerge(source, target);\n * // Result: { a: 2, b: { x: 10, y: 30, z: 40 }, c: [1, 2, 3], d: 'new' }\n * ```\n */\n private deepMerge(source: any, target: any): any {\n if (!source) return target;\n if (!target) return source;\n \n // If target is not an object, it completely overrides source\n if (typeof target !== 'object' || target === null || Array.isArray(target)) {\n return target;\n }\n \n // If source is not an object, target wins\n if (typeof source !== 'object' || source === null || Array.isArray(source)) {\n return target;\n }\n \n // Both are objects, merge them\n const result: any = { ...source };\n \n for (const [key, value] of Object.entries(target)) {\n if (value === undefined) {\n continue; // Skip undefined values\n }\n \n if (typeof value === 'object' && value !== null && !Array.isArray(value) &&\n typeof result[key] === 'object' && result[key] !== null && !Array.isArray(result[key])) {\n // Both are objects, merge recursively\n result[key] = this.deepMerge(result[key], value);\n } else {\n // Otherwise, target value wins\n result[key] = value;\n }\n }\n \n return result;\n }\n}"]}
|
|
1
|
+
{"version":3,"file":"sync-engine.js","sourceRoot":"","sources":["../../src/lib/sync-engine.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview Core synchronization engine for MemberJunction metadata\n * @module sync-engine\n * \n * This module provides the core functionality for synchronizing metadata between\n * the MemberJunction database and local file system representations. It handles\n * special reference types (@file, @url, @lookup, @env, @parent, @root, @template),\n * manages entity operations, and provides utilities for data transformation.\n */\n\nimport path from 'path';\nimport fs from 'fs-extra';\nimport crypto from 'crypto';\nimport axios from 'axios';\nimport { EntityInfo, Metadata, RunView, BaseEntity, CompositeKey, UserInfo } from '@memberjunction/core';\nimport { uuidv4 } from '@memberjunction/global';\nimport { EntityConfig, FolderConfig } from '../config';\n\n/**\n * Represents the structure of a metadata record with optional sync tracking\n */\nexport interface RecordData {\n /** Primary key field(s) and their values */\n primaryKey?: Record<string, any>;\n /** Entity field names and their values */\n fields: Record<string, any>;\n /** Related entities organized by entity name */\n relatedEntities?: Record<string, RecordData[]>;\n /** Synchronization metadata for change tracking */\n sync?: {\n /** ISO timestamp of last modification */\n lastModified: string;\n /** SHA256 checksum of the fields object */\n checksum: string;\n };\n}\n\n/**\n * Core engine for synchronizing MemberJunction metadata between database and files\n * \n * @class SyncEngine\n * @example\n * ```typescript\n * const syncEngine = new SyncEngine(systemUser);\n * await syncEngine.initialize();\n * \n * // Process a field value with special references\n * const value = await syncEngine.processFieldValue('@lookup:Users.Email=admin@example.com', '/path/to/base');\n * ```\n */\nexport class SyncEngine {\n private metadata: Metadata;\n private contextUser: UserInfo;\n\n /**\n * Creates a new SyncEngine instance\n * @param contextUser - The user context for database operations\n */\n constructor(contextUser: UserInfo) {\n this.metadata = new Metadata();\n this.contextUser = contextUser;\n }\n \n /**\n * Initializes the sync engine by refreshing metadata cache\n * @returns Promise that resolves when initialization is complete\n */\n async initialize(): Promise<void> {\n // Initialize metadata\n await this.metadata.Refresh();\n }\n \n /**\n * Process special references in field values\n * \n * Handles the following reference types:\n * - `@parent:fieldName` - References a field from the parent record\n * - `@root:fieldName` - References a field from the root record\n * - `@file:path` - Reads content from an external file\n * - `@url:address` - Fetches content from a URL\n * - `@lookup:Entity.Field=Value` - Looks up an entity ID by field value\n * - `@env:VARIABLE` - Reads an environment variable\n * \n * @param value - The field value to process\n * @param baseDir - Base directory for resolving relative file paths\n * @param parentRecord - Optional parent entity for @parent references\n * @param rootRecord - Optional root entity for @root references\n * @returns The processed value with all references resolved\n * @throws Error if a reference cannot be resolved\n * \n * @example\n * ```typescript\n * // File reference\n * const content = await processFieldValue('@file:template.md', '/path/to/dir');\n * \n * // Lookup with auto-create\n * const userId = await processFieldValue('@lookup:Users.Email=john@example.com?create', '/path');\n * ```\n */\n async processFieldValue(value: any, baseDir: string, parentRecord?: BaseEntity | null, rootRecord?: BaseEntity | null): Promise<any> {\n if (typeof value !== 'string') {\n return value;\n }\n \n // Check for @parent: reference\n if (value.startsWith('@parent:')) {\n if (!parentRecord) {\n throw new Error(`@parent reference used but no parent record available: ${value}`);\n }\n const fieldName = value.substring(8);\n return parentRecord.Get(fieldName);\n }\n \n // Check for @root: reference\n if (value.startsWith('@root:')) {\n if (!rootRecord) {\n throw new Error(`@root reference used but no root record available: ${value}`);\n }\n const fieldName = value.substring(6);\n return rootRecord.Get(fieldName);\n }\n \n // Check for @file: reference\n if (value.startsWith('@file:')) {\n const filePath = value.substring(6);\n const fullPath = path.resolve(baseDir, filePath);\n \n if (await fs.pathExists(fullPath)) {\n return await fs.readFile(fullPath, 'utf-8');\n } else {\n throw new Error(`File not found: ${fullPath}`);\n }\n }\n \n // Check for @url: reference\n if (value.startsWith('@url:')) {\n const url = value.substring(5);\n \n try {\n const response = await axios.get(url);\n return response.data;\n } catch (error) {\n throw new Error(`Failed to fetch URL: ${url} - ${error}`);\n }\n }\n \n // Check for @lookup: reference\n if (value.startsWith('@lookup:')) {\n const lookupStr = value.substring(8);\n \n // Parse lookup with optional create syntax\n // Format: EntityName.FieldName=Value?create&OtherField=Value\n const entityMatch = lookupStr.match(/^([^.]+)\\./);\n if (!entityMatch) {\n throw new Error(`Invalid lookup format: ${value}`);\n }\n \n const entityName = entityMatch[1];\n const remaining = lookupStr.substring(entityName.length + 1);\n \n // Check if this has ?create syntax\n const hasCreate = remaining.includes('?create');\n const lookupPart = hasCreate ? remaining.split('?')[0] : remaining;\n \n // Parse the main lookup field\n const fieldMatch = lookupPart.match(/^(.+?)=(.+)$/);\n if (!fieldMatch) {\n throw new Error(`Invalid lookup format: ${value}`);\n }\n \n const [, fieldName, fieldValue] = fieldMatch;\n \n // Parse additional fields for creation if ?create is present\n let createFields: Record<string, any> = {};\n if (hasCreate && remaining.includes('?create&')) {\n const createPart = remaining.split('?create&')[1];\n const pairs = createPart.split('&');\n for (const pair of pairs) {\n const [key, val] = pair.split('=');\n if (key && val) {\n createFields[key] = decodeURIComponent(val);\n }\n }\n }\n \n return await this.resolveLookup(entityName, fieldName, fieldValue, hasCreate, createFields);\n }\n \n // Check for @env: reference\n if (value.startsWith('@env:')) {\n const envVar = value.substring(5);\n const envValue = process.env[envVar];\n \n if (envValue === undefined) {\n throw new Error(`Environment variable not found: ${envVar}`);\n }\n \n return envValue;\n }\n \n return value;\n }\n \n /**\n * Resolve a lookup reference to an ID, optionally creating the record if it doesn't exist\n * \n * @param entityName - Name of the entity to search in\n * @param fieldName - Field to match against\n * @param fieldValue - Value to search for\n * @param autoCreate - Whether to create the record if not found\n * @param createFields - Additional fields to set when creating\n * @returns The ID of the found or created record\n * @throws Error if lookup fails and autoCreate is false\n * \n * @example\n * ```typescript\n * // Simple lookup\n * const categoryId = await resolveLookup('Categories', 'Name', 'Technology');\n * \n * // Lookup with auto-create\n * const tagId = await resolveLookup('Tags', 'Name', 'New Tag', true, {\n * Description: 'Auto-created tag',\n * Status: 'Active'\n * });\n * ```\n */\n async resolveLookup(\n entityName: string, \n fieldName: string, \n fieldValue: string,\n autoCreate: boolean = false,\n createFields: Record<string, any> = {}\n ): Promise<string> {\n // Debug logging handled by caller if needed\n \n const rv = new RunView();\n const entityInfo = this.metadata.EntityByName(entityName);\n if (!entityInfo) {\n throw new Error(`Entity not found: ${entityName}`);\n }\n \n const field = entityInfo.Fields.find(f => f.Name.trim().toLowerCase() === fieldName.trim().toLowerCase());\n const quotes = field?.NeedsQuotes ? \"'\" : '';\n const result = await rv.RunView({\n EntityName: entityName,\n ExtraFilter: `${fieldName} = ${quotes}${fieldValue.replace(/'/g, \"''\")}${quotes}`,\n MaxRows: 1\n }, this.contextUser);\n \n if (result.Success && result.Results.length > 0) {\n if (entityInfo.PrimaryKeys.length > 0) {\n const pkeyField = entityInfo.PrimaryKeys[0].Name;\n const id = result.Results[0][pkeyField];\n return id;\n }\n }\n \n // If not found and auto-create is enabled, create the record\n if (autoCreate) {\n \n const newEntity = await this.metadata.GetEntityObject(entityName, this.contextUser);\n if (!newEntity) {\n throw new Error(`Failed to create entity object for: ${entityName}`);\n }\n \n newEntity.NewRecord();\n \n // Handle explicit ID setting for new records\n if (entityInfo.PrimaryKeys.length > 0) {\n for (const pk of entityInfo.PrimaryKeys) {\n if (!pk.AutoIncrement && pk.Type.toLowerCase() === 'uniqueidentifier') {\n // Generate UUID for this primary key and set it explicitly\n const uuid = uuidv4();\n (newEntity as any)[pk.Name] = uuid;\n }\n }\n }\n \n // Set the lookup field\n if (fieldName in newEntity) {\n (newEntity as any)[fieldName] = fieldValue;\n }\n \n // Set any additional fields provided\n for (const [key, value] of Object.entries(createFields)) {\n if (key in newEntity) {\n (newEntity as any)[key] = value;\n }\n }\n \n // Save the new record\n const saved = await newEntity.Save();\n if (!saved) {\n const message = newEntity.LatestResult?.Message;\n if (message) {\n throw new Error(`Failed to auto-create ${entityName}: ${message}`);\n }\n \n const errors = newEntity.LatestResult?.Errors?.map(err => \n typeof err === 'string' ? err : (err?.message || JSON.stringify(err))\n )?.join(', ') || 'Unknown error';\n throw new Error(`Failed to auto-create ${entityName}: ${errors}`);\n }\n \n // Return the new ID\n if (entityInfo.PrimaryKeys.length > 0) {\n const pkeyField = entityInfo.PrimaryKeys[0].Name;\n const newId = newEntity.Get(pkeyField);\n return newId;\n }\n }\n \n throw new Error(`Lookup failed: No record found in '${entityName}' where ${fieldName}='${fieldValue}'`);\n }\n \n /**\n * Build cascading defaults for a file path and process field values\n * \n * Walks up the directory tree from the file location, collecting defaults from\n * entity config and folder configs, with deeper folders overriding parent values.\n * All default values are processed for special references.\n * \n * @param filePath - Path to the file being processed\n * @param entityConfig - Entity configuration containing base defaults\n * @returns Processed defaults with all references resolved\n * @throws Error if any default value processing fails\n */\n async buildDefaults(filePath: string, entityConfig: EntityConfig): Promise<Record<string, any>> {\n const parts = path.dirname(filePath).split(path.sep);\n let defaults: Record<string, any> = { ...entityConfig.defaults };\n \n // Walk up the directory tree building defaults\n let currentPath = '';\n for (const part of parts) {\n currentPath = path.join(currentPath, part);\n const folderConfig = await this.loadFolderConfig(currentPath);\n \n if (folderConfig?.defaults) {\n defaults = { ...defaults, ...folderConfig.defaults };\n }\n }\n \n // Process all default values (lookups, file references, etc.)\n const processedDefaults: Record<string, any> = {};\n const baseDir = path.dirname(filePath);\n \n for (const [field, value] of Object.entries(defaults)) {\n try {\n processedDefaults[field] = await this.processFieldValue(value, baseDir, null, null);\n } catch (error) {\n throw new Error(`Failed to process default for field '${field}': ${error}`);\n }\n }\n \n return processedDefaults;\n }\n \n /**\n * Load folder configuration from .mj-folder.json file\n * \n * @param dir - Directory to check for configuration\n * @returns Folder configuration or null if not found/invalid\n * @private\n */\n private async loadFolderConfig(dir: string): Promise<FolderConfig | null> {\n const configPath = path.join(dir, '.mj-folder.json');\n \n if (await fs.pathExists(configPath)) {\n try {\n return await fs.readJson(configPath);\n } catch (error) {\n console.error(`Error loading folder config at ${configPath}:`, error);\n return null;\n }\n }\n \n return null;\n }\n \n /**\n * Calculate SHA256 checksum for data\n * \n * Generates a deterministic hash of the provided data by converting it to\n * formatted JSON and calculating a SHA256 digest. Used for change detection\n * in sync operations.\n * \n * @param data - Any data structure to calculate checksum for\n * @returns Hexadecimal string representation of the SHA256 hash\n * \n * @example\n * ```typescript\n * const checksum = syncEngine.calculateChecksum({\n * name: 'Test Record',\n * value: 42,\n * tags: ['a', 'b']\n * });\n * // Returns consistent hash for same data structure\n * ```\n */\n calculateChecksum(data: any): string {\n const hash = crypto.createHash('sha256');\n hash.update(JSON.stringify(data, null, 2));\n return hash.digest('hex');\n }\n \n /**\n * Get entity metadata information by name\n * \n * Retrieves the EntityInfo object containing schema metadata for the specified entity.\n * Returns null if the entity is not found in the metadata cache.\n * \n * @param entityName - Name of the entity to look up\n * @returns EntityInfo object with schema details or null if not found\n * \n * @example\n * ```typescript\n * const entityInfo = syncEngine.getEntityInfo('AI Prompts');\n * if (entityInfo) {\n * console.log(`Primary keys: ${entityInfo.PrimaryKeys.map(pk => pk.Name).join(', ')}`);\n * }\n * ```\n */\n getEntityInfo(entityName: string): EntityInfo | null {\n return this.metadata.EntityByName(entityName);\n }\n \n /**\n * Create a new entity object instance\n * \n * Uses the MemberJunction metadata system to properly instantiate an entity object.\n * This ensures correct class registration and respects any custom entity subclasses.\n * \n * @param entityName - Name of the entity to create\n * @returns Promise resolving to the new BaseEntity instance\n * @throws Error if entity creation fails\n * \n * @example\n * ```typescript\n * const entity = await syncEngine.createEntityObject('AI Prompts');\n * entity.NewRecord();\n * entity.Set('Name', 'My Prompt');\n * await entity.Save();\n * ```\n */\n async createEntityObject(entityName: string): Promise<BaseEntity> {\n const entity = await this.metadata.GetEntityObject(entityName, this.contextUser);\n if (!entity) {\n throw new Error(`Failed to create entity object for: ${entityName}`);\n }\n return entity;\n }\n \n /**\n * Load an entity record by primary key\n * \n * Retrieves an existing entity record from the database using its primary key values.\n * Supports both single and composite primary keys. Returns null if the record is not found.\n * \n * @param entityName - Name of the entity to load\n * @param primaryKey - Object containing primary key field names and values\n * @returns Promise resolving to the loaded entity or null if not found\n * @throws Error if entity metadata is not found\n * \n * @example\n * ```typescript\n * // Single primary key\n * const entity = await syncEngine.loadEntity('Users', { ID: '123-456' });\n * \n * // Composite primary key\n * const entity = await syncEngine.loadEntity('UserRoles', { \n * UserID: '123-456',\n * RoleID: '789-012'\n * });\n * ```\n */\n async loadEntity(entityName: string, primaryKey: Record<string, any>): Promise<BaseEntity | null> {\n const entity = await this.createEntityObject(entityName);\n const entityInfo = this.getEntityInfo(entityName);\n \n if (!entityInfo) {\n throw new Error(`Entity not found: ${entityName}`);\n }\n \n // Handle composite keys\n const compositeKey = new CompositeKey();\n compositeKey.LoadFromSimpleObject(primaryKey);\n const loaded = await entity.InnerLoad(compositeKey);\n \n return loaded ? entity : null;\n }\n \n /**\n * Process JSON object with template references\n * \n * Recursively processes JSON data structures to resolve `@template` references.\n * Templates can be defined at any level and support:\n * - Single template references: `\"@template:path/to/template.json\"`\n * - Object with @template field: `{ \"@template\": \"file.json\", \"override\": \"value\" }`\n * - Array of templates for merging: `{ \"@template\": [\"base.json\", \"overrides.json\"] }`\n * - Nested template references within templates\n * \n * @param data - JSON data structure to process\n * @param baseDir - Base directory for resolving relative template paths\n * @returns Promise resolving to the processed data with all templates resolved\n * @throws Error if template file is not found or contains invalid JSON\n * \n * @example\n * ```typescript\n * // Input data with template reference\n * const data = {\n * \"@template\": \"defaults/ai-prompt.json\",\n * \"Name\": \"Custom Prompt\",\n * \"Prompt\": \"Override the template prompt\"\n * };\n * \n * // Resolves template and merges with overrides\n * const result = await syncEngine.processTemplates(data, '/path/to/dir');\n * ```\n */\n async processTemplates(data: any, baseDir: string): Promise<any> {\n // Handle arrays\n if (Array.isArray(data)) {\n const processedArray = [];\n for (const item of data) {\n processedArray.push(await this.processTemplates(item, baseDir));\n }\n return processedArray;\n }\n \n // Handle objects\n if (data && typeof data === 'object') {\n // Check for @template reference\n if (typeof data === 'string' && data.startsWith('@template:')) {\n const templatePath = data.substring(10);\n return await this.loadAndProcessTemplate(templatePath, baseDir);\n }\n \n // Process object with possible @template field\n const processed: any = {};\n let templateData: any = {};\n \n // First, check if there's a @template field to process\n if (data['@template']) {\n const templates = Array.isArray(data['@template']) ? data['@template'] : [data['@template']];\n \n // Process templates in order, merging them\n for (const templateRef of templates) {\n const templateContent = await this.loadAndProcessTemplate(templateRef, baseDir);\n templateData = this.deepMerge(templateData, templateContent);\n }\n }\n \n // Process all other fields\n for (const [key, value] of Object.entries(data)) {\n if (key === '@template') continue; // Skip the template field itself\n \n // Process the value recursively\n processed[key] = await this.processTemplates(value, baseDir);\n }\n \n // Merge template data with processed data (processed data takes precedence)\n return this.deepMerge(templateData, processed);\n }\n \n // Return primitive values as-is\n return data;\n }\n \n /**\n * Load and process a template file\n * \n * Loads a JSON template file from the filesystem and recursively processes any\n * nested template references within it. Template paths are resolved relative to\n * the template file's directory, enabling template composition.\n * \n * @param templatePath - Path to the template file (relative or absolute)\n * @param baseDir - Base directory for resolving relative paths\n * @returns Promise resolving to the processed template content\n * @throws Error if template file not found or contains invalid JSON\n * @private\n */\n private async loadAndProcessTemplate(templatePath: string, baseDir: string): Promise<any> {\n const fullPath = path.resolve(baseDir, templatePath);\n \n if (!await fs.pathExists(fullPath)) {\n throw new Error(`Template file not found: ${fullPath}`);\n }\n \n try {\n const templateContent = await fs.readJson(fullPath);\n \n // Recursively process any nested templates\n const templateDir = path.dirname(fullPath);\n return await this.processTemplates(templateContent, templateDir);\n } catch (error) {\n throw new Error(`Failed to load template ${fullPath}: ${error}`);\n }\n }\n \n /**\n * Deep merge two objects with target taking precedence\n * \n * Recursively merges two objects, with values from the target object overriding\n * values from the source object. Arrays and primitive values are not merged but\n * replaced entirely by the target value. Undefined values in target are skipped.\n * \n * @param source - Base object to merge from\n * @param target - Object with values that override source\n * @returns New object with merged values\n * @private\n * \n * @example\n * ```typescript\n * const source = {\n * a: 1,\n * b: { x: 10, y: 20 },\n * c: [1, 2, 3]\n * };\n * const target = {\n * a: 2,\n * b: { y: 30, z: 40 },\n * d: 'new'\n * };\n * const result = deepMerge(source, target);\n * // Result: { a: 2, b: { x: 10, y: 30, z: 40 }, c: [1, 2, 3], d: 'new' }\n * ```\n */\n private deepMerge(source: any, target: any): any {\n if (!source) return target;\n if (!target) return source;\n \n // If target is not an object, it completely overrides source\n if (typeof target !== 'object' || target === null || Array.isArray(target)) {\n return target;\n }\n \n // If source is not an object, target wins\n if (typeof source !== 'object' || source === null || Array.isArray(source)) {\n return target;\n }\n \n // Both are objects, merge them\n const result: any = { ...source };\n \n for (const [key, value] of Object.entries(target)) {\n if (value === undefined) {\n continue; // Skip undefined values\n }\n \n if (typeof value === 'object' && value !== null && !Array.isArray(value) &&\n typeof result[key] === 'object' && result[key] !== null && !Array.isArray(result[key])) {\n // Both are objects, merge recursively\n result[key] = this.deepMerge(result[key], value);\n } else {\n // Otherwise, target value wins\n result[key] = value;\n }\n }\n \n return result;\n }\n \n}"]}
|