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