@memberjunction/metadata-sync 2.46.0 → 2.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +124 -13
- package/dist/commands/pull/index.d.ts +135 -0
- package/dist/commands/pull/index.js +187 -18
- package/dist/commands/pull/index.js.map +1 -1
- package/dist/commands/push/index.d.ts +1 -0
- package/dist/commands/push/index.js +75 -33
- package/dist/commands/push/index.js.map +1 -1
- package/dist/commands/status/index.js +35 -2
- package/dist/commands/status/index.js.map +1 -1
- package/dist/commands/watch/index.js +1 -1
- package/dist/commands/watch/index.js.map +1 -1
- package/dist/config.d.ts +141 -0
- package/dist/config.js +81 -0
- package/dist/config.js.map +1 -1
- package/dist/hooks/init.js +6 -1
- package/dist/hooks/init.js.map +1 -1
- package/dist/lib/provider-utils.d.ts +76 -4
- package/dist/lib/provider-utils.js +93 -28
- package/dist/lib/provider-utils.js.map +1 -1
- 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 +8 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -74,18 +74,29 @@ The Metadata Sync tool bridges the gap between database-stored metadata and file
|
|
|
74
74
|
|
|
75
75
|
## Supported Entities
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
-
|
|
85
|
-
-
|
|
86
|
-
-
|
|
87
|
-
|
|
88
|
-
|
|
77
|
+
The tool works with any MemberJunction entity - both core system entities and user-created entities. Each entity type can have its own directory structure, file naming conventions, and related entity configurations.
|
|
78
|
+
|
|
79
|
+
### Important Limitation: Database-Reflected Metadata
|
|
80
|
+
|
|
81
|
+
**This tool should NOT be used to modify metadata that is reflected from the underlying database catalog.** Examples include:
|
|
82
|
+
- Entity field data types
|
|
83
|
+
- Column lengths/precision
|
|
84
|
+
- Primary key definitions
|
|
85
|
+
- Foreign key relationships
|
|
86
|
+
- Table/column existence
|
|
87
|
+
|
|
88
|
+
These properties are designed to flow **from** the database catalog **up** into MJ metadata, not the other way around. Attempting to modify these via file sync could create inconsistencies between the metadata and actual database schema.
|
|
89
|
+
|
|
90
|
+
The tool is intended for managing business-level metadata such as:
|
|
91
|
+
- Descriptions and documentation
|
|
92
|
+
- Display names and user-facing text
|
|
93
|
+
- Categories and groupings
|
|
94
|
+
- Custom properties and settings
|
|
95
|
+
- AI prompts, templates, and other content
|
|
96
|
+
- Permissions and security settings
|
|
97
|
+
- Any other data that is not reflected **up** from the underlying system database catalogs
|
|
98
|
+
|
|
99
|
+
For more information about how CodeGen reflects system-level data from the database into the MJ metadata layer, see the [CodeGen documentation](../CodeGen/README.md).
|
|
89
100
|
|
|
90
101
|
## File Structure
|
|
91
102
|
|
|
@@ -97,6 +108,37 @@ The tool uses a hierarchical directory structure with cascading defaults:
|
|
|
97
108
|
- External files (`.md`, `.html`, etc.) are referenced from the JSON files
|
|
98
109
|
- Defaults cascade down through the folder hierarchy
|
|
99
110
|
|
|
111
|
+
### File Format Options
|
|
112
|
+
|
|
113
|
+
#### Single Record per File (Default)
|
|
114
|
+
Each JSON file contains one record:
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"fields": { ... },
|
|
118
|
+
"relatedEntities": { ... }
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
#### Multiple Records per File (NEW)
|
|
123
|
+
JSON files can contain arrays of records:
|
|
124
|
+
```json
|
|
125
|
+
[
|
|
126
|
+
{
|
|
127
|
+
"fields": { ... },
|
|
128
|
+
"relatedEntities": { ... }
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"fields": { ... },
|
|
132
|
+
"relatedEntities": { ... }
|
|
133
|
+
}
|
|
134
|
+
]
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
This is useful for:
|
|
138
|
+
- Grouping related records in a single file
|
|
139
|
+
- Reducing file clutter for entities with many small records
|
|
140
|
+
- Maintaining logical groupings while using `@file:` references for large content
|
|
141
|
+
|
|
100
142
|
### Example Structure
|
|
101
143
|
```
|
|
102
144
|
metadata/
|
|
@@ -112,7 +154,12 @@ metadata/
|
|
|
112
154
|
│ ├── .mj-folder.json # Folder metadata (CategoryID, etc.)
|
|
113
155
|
│ ├── daily-report.json # AI Prompt record
|
|
114
156
|
│ └── daily-report.prompt.md # Prompt content (referenced)
|
|
115
|
-
|
|
157
|
+
├── templates/ # Reusable JSON templates
|
|
158
|
+
│ ├── standard-prompt-settings.json # Common prompt configurations
|
|
159
|
+
│ ├── standard-ai-models.json # Standard model configurations
|
|
160
|
+
│ ├── high-performance-models.json # High-power model configurations
|
|
161
|
+
│ └── customer-service-defaults.json # CS-specific defaults
|
|
162
|
+
└── template-entities/
|
|
116
163
|
├── .mj-sync.json # Defines entity: "Templates"
|
|
117
164
|
├── email/
|
|
118
165
|
│ ├── .mj-folder.json # Folder metadata
|
|
@@ -302,6 +349,66 @@ Support environment-specific values:
|
|
|
302
349
|
- `@env:VARIABLE_NAME`
|
|
303
350
|
- Useful for different environments (dev/staging/prod)
|
|
304
351
|
|
|
352
|
+
### @template: References (NEW)
|
|
353
|
+
Enable JSON template composition for reusable configurations:
|
|
354
|
+
|
|
355
|
+
#### String Template Reference
|
|
356
|
+
Use `@template:` to replace any value with template content:
|
|
357
|
+
```json
|
|
358
|
+
{
|
|
359
|
+
"relatedEntities": {
|
|
360
|
+
"MJ: AI Prompt Models": "@template:templates/standard-ai-models.json"
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
#### Object Template Merging
|
|
366
|
+
Use `@template` field within objects to merge template content:
|
|
367
|
+
```json
|
|
368
|
+
{
|
|
369
|
+
"fields": {
|
|
370
|
+
"Name": "My Prompt",
|
|
371
|
+
"@template": "templates/standard-prompt-settings.json",
|
|
372
|
+
"Temperature": 0.9 // Overrides template value
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
#### Multiple Template Merging
|
|
378
|
+
Merge multiple templates in order (later templates override earlier ones):
|
|
379
|
+
```json
|
|
380
|
+
{
|
|
381
|
+
"fields": {
|
|
382
|
+
"@template": [
|
|
383
|
+
"templates/base-settings.json",
|
|
384
|
+
"templates/customer-service-defaults.json"
|
|
385
|
+
],
|
|
386
|
+
"Name": "Customer Bot" // Local fields override all templates
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
#### Nested Templates
|
|
392
|
+
Templates can reference other templates:
|
|
393
|
+
```json
|
|
394
|
+
// templates/high-performance-models.json
|
|
395
|
+
[
|
|
396
|
+
{
|
|
397
|
+
"fields": {
|
|
398
|
+
"@template": "../templates/model-defaults.json",
|
|
399
|
+
"ModelID": "@lookup:AI Models.Name=GPT 4o"
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
]
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
#### Template Benefits
|
|
406
|
+
- **DRY Principle**: Define configurations once, use everywhere
|
|
407
|
+
- **Maintainability**: Update template to affect all uses
|
|
408
|
+
- **Flexibility**: Use at any JSON level
|
|
409
|
+
- **Composability**: Build complex configurations from simple parts
|
|
410
|
+
- **Override Support**: Local values always override template values
|
|
411
|
+
|
|
305
412
|
## CLI Commands
|
|
306
413
|
|
|
307
414
|
```bash
|
|
@@ -314,6 +421,10 @@ mj-sync pull --entity="AI Prompts"
|
|
|
314
421
|
# Pull specific records by filter
|
|
315
422
|
mj-sync pull --entity="AI Prompts" --filter="CategoryID='customer-service-id'"
|
|
316
423
|
|
|
424
|
+
# Pull multiple records into a single file (NEW)
|
|
425
|
+
mj-sync pull --entity="AI Prompts" --multi-file="all-prompts"
|
|
426
|
+
mj-sync pull --entity="AI Prompts" --filter="Status='Active'" --multi-file="active-prompts.json"
|
|
427
|
+
|
|
317
428
|
# Push all changes from current directory and subdirectories
|
|
318
429
|
mj-sync push
|
|
319
430
|
|
|
@@ -1,4 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Pull command implementation for MetadataSync
|
|
3
|
+
* @module commands/pull
|
|
4
|
+
*
|
|
5
|
+
* This module implements the pull command which retrieves metadata records from
|
|
6
|
+
* the MemberJunction database and saves them as local JSON files. It supports:
|
|
7
|
+
* - Filtering records with SQL expressions
|
|
8
|
+
* - Pulling related entities with foreign key relationships
|
|
9
|
+
* - Externalizing large text fields to separate files
|
|
10
|
+
* - Creating multi-record JSON files
|
|
11
|
+
* - Recursive directory search for entity configurations
|
|
12
|
+
*/
|
|
1
13
|
import { Command } from '@oclif/core';
|
|
14
|
+
/**
|
|
15
|
+
* Pull metadata records from database to local files
|
|
16
|
+
*
|
|
17
|
+
* @class Pull
|
|
18
|
+
* @extends Command
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```bash
|
|
22
|
+
* # Pull all records for an entity
|
|
23
|
+
* mj-sync pull --entity="AI Prompts"
|
|
24
|
+
*
|
|
25
|
+
* # Pull with filter
|
|
26
|
+
* mj-sync pull --entity="AI Prompts" --filter="CategoryID='123'"
|
|
27
|
+
*
|
|
28
|
+
* # Pull to multi-record file
|
|
29
|
+
* mj-sync pull --entity="AI Prompts" --multi-file="all-prompts.json"
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
2
32
|
export default class Pull extends Command {
|
|
3
33
|
static description: string;
|
|
4
34
|
static examples: string[];
|
|
@@ -6,13 +36,118 @@ export default class Pull extends Command {
|
|
|
6
36
|
entity: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
7
37
|
filter: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
8
38
|
'dry-run': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
39
|
+
'multi-file': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
9
40
|
};
|
|
10
41
|
run(): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Find directories containing configuration for the specified entity
|
|
44
|
+
*
|
|
45
|
+
* Recursively searches the current working directory for .mj-sync.json files
|
|
46
|
+
* that specify the given entity name. Returns all matching directories to
|
|
47
|
+
* allow user selection when multiple locations exist.
|
|
48
|
+
*
|
|
49
|
+
* @param entityName - Name of the entity to search for
|
|
50
|
+
* @returns Promise resolving to array of directory paths
|
|
51
|
+
* @private
|
|
52
|
+
*/
|
|
11
53
|
private findEntityDirectories;
|
|
54
|
+
/**
|
|
55
|
+
* Process a single record and save to file
|
|
56
|
+
*
|
|
57
|
+
* Converts a database record into the file format and writes it to disk.
|
|
58
|
+
* This is a wrapper around processRecordData that handles file writing.
|
|
59
|
+
*
|
|
60
|
+
* @param record - Raw database record
|
|
61
|
+
* @param primaryKey - Primary key fields and values
|
|
62
|
+
* @param targetDir - Directory to save the file
|
|
63
|
+
* @param entityConfig - Entity configuration with pull settings
|
|
64
|
+
* @param syncEngine - Sync engine instance
|
|
65
|
+
* @returns Promise that resolves when file is written
|
|
66
|
+
* @private
|
|
67
|
+
*/
|
|
12
68
|
private processRecord;
|
|
69
|
+
/**
|
|
70
|
+
* Process record data for storage
|
|
71
|
+
*
|
|
72
|
+
* Transforms a raw database record into the RecordData format used for file storage.
|
|
73
|
+
* Handles field externalization, related entity pulling, and checksum calculation.
|
|
74
|
+
*
|
|
75
|
+
* @param record - Raw database record
|
|
76
|
+
* @param primaryKey - Primary key fields and values
|
|
77
|
+
* @param targetDir - Directory where files will be saved
|
|
78
|
+
* @param entityConfig - Entity configuration with defaults and settings
|
|
79
|
+
* @param syncEngine - Sync engine for checksum calculation
|
|
80
|
+
* @returns Promise resolving to formatted RecordData
|
|
81
|
+
* @private
|
|
82
|
+
*/
|
|
83
|
+
private processRecordData;
|
|
84
|
+
/**
|
|
85
|
+
* Determine if a field should be saved to an external file
|
|
86
|
+
*
|
|
87
|
+
* Checks if a field contains substantial text content that would be better
|
|
88
|
+
* stored in a separate file rather than inline in the JSON. Uses heuristics
|
|
89
|
+
* based on field name and content length.
|
|
90
|
+
*
|
|
91
|
+
* @param fieldName - Name of the field to check
|
|
92
|
+
* @param fieldValue - Value of the field
|
|
93
|
+
* @param entityConfig - Entity configuration (for future extension)
|
|
94
|
+
* @returns Promise resolving to true if field should be externalized
|
|
95
|
+
* @private
|
|
96
|
+
*/
|
|
13
97
|
private shouldExternalizeField;
|
|
98
|
+
/**
|
|
99
|
+
* Create an external file for a field value
|
|
100
|
+
*
|
|
101
|
+
* Saves large text content to a separate file and returns the filename.
|
|
102
|
+
* Automatically determines appropriate file extension based on field name
|
|
103
|
+
* and content type (e.g., .md for prompts, .html for templates).
|
|
104
|
+
*
|
|
105
|
+
* @param targetDir - Directory to save the file
|
|
106
|
+
* @param primaryKey - Primary key for filename generation
|
|
107
|
+
* @param fieldName - Name of the field being externalized
|
|
108
|
+
* @param content - Content to write to the file
|
|
109
|
+
* @returns Promise resolving to the created filename
|
|
110
|
+
* @private
|
|
111
|
+
*/
|
|
14
112
|
private createExternalFile;
|
|
113
|
+
/**
|
|
114
|
+
* Build a filename from primary key values
|
|
115
|
+
*
|
|
116
|
+
* Creates a safe filename based on the entity's primary key values.
|
|
117
|
+
* Handles GUIDs by using first 8 characters, sanitizes special characters,
|
|
118
|
+
* and creates composite names for multi-field keys.
|
|
119
|
+
*
|
|
120
|
+
* @param primaryKey - Primary key fields and values
|
|
121
|
+
* @param entityConfig - Entity configuration (for future extension)
|
|
122
|
+
* @returns Filename with .json extension
|
|
123
|
+
* @private
|
|
124
|
+
*/
|
|
15
125
|
private buildFileName;
|
|
126
|
+
/**
|
|
127
|
+
* Pull related entities for a parent record
|
|
128
|
+
*
|
|
129
|
+
* Retrieves child records that have foreign key relationships to the parent.
|
|
130
|
+
* Converts foreign key values to @parent references and supports nested
|
|
131
|
+
* related entities for deep object graphs.
|
|
132
|
+
*
|
|
133
|
+
* @param parentRecord - Parent entity record
|
|
134
|
+
* @param relatedConfig - Configuration for related entities to pull
|
|
135
|
+
* @param syncEngine - Sync engine instance
|
|
136
|
+
* @returns Promise resolving to map of entity names to related records
|
|
137
|
+
* @private
|
|
138
|
+
*/
|
|
16
139
|
private pullRelatedEntities;
|
|
140
|
+
/**
|
|
141
|
+
* Find which field in the parent record contains a specific value
|
|
142
|
+
*
|
|
143
|
+
* Used to convert foreign key references to @parent references by finding
|
|
144
|
+
* the parent field that contains the foreign key value. Typically finds
|
|
145
|
+
* the primary key field but can match any field.
|
|
146
|
+
*
|
|
147
|
+
* @param parentRecord - Parent record to search
|
|
148
|
+
* @param value - Value to search for
|
|
149
|
+
* @returns Field name containing the value, or null if not found
|
|
150
|
+
* @private
|
|
151
|
+
*/
|
|
17
152
|
private findParentField;
|
|
18
153
|
}
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Pull command implementation for MetadataSync
|
|
4
|
+
* @module commands/pull
|
|
5
|
+
*
|
|
6
|
+
* This module implements the pull command which retrieves metadata records from
|
|
7
|
+
* the MemberJunction database and saves them as local JSON files. It supports:
|
|
8
|
+
* - Filtering records with SQL expressions
|
|
9
|
+
* - Pulling related entities with foreign key relationships
|
|
10
|
+
* - Externalizing large text fields to separate files
|
|
11
|
+
* - Creating multi-record JSON files
|
|
12
|
+
* - Recursive directory search for entity configurations
|
|
13
|
+
*/
|
|
2
14
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
15
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
16
|
};
|
|
@@ -12,6 +24,24 @@ const config_1 = require("../../config");
|
|
|
12
24
|
const sync_engine_1 = require("../../lib/sync-engine");
|
|
13
25
|
const core_2 = require("@memberjunction/core");
|
|
14
26
|
const provider_utils_1 = require("../../lib/provider-utils");
|
|
27
|
+
/**
|
|
28
|
+
* Pull metadata records from database to local files
|
|
29
|
+
*
|
|
30
|
+
* @class Pull
|
|
31
|
+
* @extends Command
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```bash
|
|
35
|
+
* # Pull all records for an entity
|
|
36
|
+
* mj-sync pull --entity="AI Prompts"
|
|
37
|
+
*
|
|
38
|
+
* # Pull with filter
|
|
39
|
+
* mj-sync pull --entity="AI Prompts" --filter="CategoryID='123'"
|
|
40
|
+
*
|
|
41
|
+
* # Pull to multi-record file
|
|
42
|
+
* mj-sync pull --entity="AI Prompts" --multi-file="all-prompts.json"
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
15
45
|
class Pull extends core_1.Command {
|
|
16
46
|
static description = 'Pull metadata from database to local files';
|
|
17
47
|
static examples = [
|
|
@@ -22,6 +52,7 @@ class Pull extends core_1.Command {
|
|
|
22
52
|
entity: core_1.Flags.string({ description: 'Entity name to pull', required: true }),
|
|
23
53
|
filter: core_1.Flags.string({ description: 'Additional filter for pulling specific records' }),
|
|
24
54
|
'dry-run': core_1.Flags.boolean({ description: 'Show what would be pulled without actually pulling' }),
|
|
55
|
+
'multi-file': core_1.Flags.string({ description: 'Create a single file with multiple records (provide filename)' }),
|
|
25
56
|
};
|
|
26
57
|
async run() {
|
|
27
58
|
const { flags } = await this.parse(Pull);
|
|
@@ -88,23 +119,54 @@ class Pull extends core_1.Command {
|
|
|
88
119
|
}
|
|
89
120
|
spinner.start('Processing records');
|
|
90
121
|
let processed = 0;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
122
|
+
// If multi-file flag is set, collect all records
|
|
123
|
+
if (flags['multi-file']) {
|
|
124
|
+
const allRecords = [];
|
|
125
|
+
for (const record of result.Results) {
|
|
126
|
+
try {
|
|
127
|
+
// Build primary key
|
|
128
|
+
const primaryKey = {};
|
|
129
|
+
for (const pk of entityInfo.PrimaryKeys) {
|
|
130
|
+
primaryKey[pk.Name] = record[pk.Name];
|
|
131
|
+
}
|
|
132
|
+
// Process record for multi-file
|
|
133
|
+
const recordData = await this.processRecordData(record, primaryKey, targetDir, entityConfig, syncEngine);
|
|
134
|
+
allRecords.push(recordData);
|
|
135
|
+
processed++;
|
|
136
|
+
spinner.text = `Processing records (${processed}/${result.Results.length})`;
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
this.warn(`Failed to process record: ${error.message || error}`);
|
|
97
140
|
}
|
|
98
|
-
// Process record
|
|
99
|
-
await this.processRecord(record, primaryKey, targetDir, entityConfig, syncEngine);
|
|
100
|
-
processed++;
|
|
101
|
-
spinner.text = `Processing records (${processed}/${result.Results.length})`;
|
|
102
141
|
}
|
|
103
|
-
|
|
104
|
-
|
|
142
|
+
// Write all records to single file
|
|
143
|
+
if (allRecords.length > 0) {
|
|
144
|
+
const fileName = flags['multi-file'].endsWith('.json') ? flags['multi-file'] : `${flags['multi-file']}.json`;
|
|
145
|
+
const filePath = path_1.default.join(targetDir, fileName);
|
|
146
|
+
await fs_extra_1.default.writeJson(filePath, allRecords, { spaces: 2 });
|
|
147
|
+
spinner.succeed(`Pulled ${processed} records to ${filePath}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
// Original single-file-per-record logic
|
|
152
|
+
for (const record of result.Results) {
|
|
153
|
+
try {
|
|
154
|
+
// Build primary key
|
|
155
|
+
const primaryKey = {};
|
|
156
|
+
for (const pk of entityInfo.PrimaryKeys) {
|
|
157
|
+
primaryKey[pk.Name] = record[pk.Name];
|
|
158
|
+
}
|
|
159
|
+
// Process record
|
|
160
|
+
await this.processRecord(record, primaryKey, targetDir, entityConfig, syncEngine);
|
|
161
|
+
processed++;
|
|
162
|
+
spinner.text = `Processing records (${processed}/${result.Results.length})`;
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
this.warn(`Failed to process record: ${error.message || error}`);
|
|
166
|
+
}
|
|
105
167
|
}
|
|
168
|
+
spinner.succeed(`Pulled ${processed} records to ${targetDir}`);
|
|
106
169
|
}
|
|
107
|
-
spinner.succeed(`Pulled ${processed} records to ${targetDir}`);
|
|
108
170
|
}
|
|
109
171
|
catch (error) {
|
|
110
172
|
spinner.fail('Pull failed');
|
|
@@ -115,6 +177,17 @@ class Pull extends core_1.Command {
|
|
|
115
177
|
await (0, provider_utils_1.cleanupProvider)();
|
|
116
178
|
}
|
|
117
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Find directories containing configuration for the specified entity
|
|
182
|
+
*
|
|
183
|
+
* Recursively searches the current working directory for .mj-sync.json files
|
|
184
|
+
* that specify the given entity name. Returns all matching directories to
|
|
185
|
+
* allow user selection when multiple locations exist.
|
|
186
|
+
*
|
|
187
|
+
* @param entityName - Name of the entity to search for
|
|
188
|
+
* @returns Promise resolving to array of directory paths
|
|
189
|
+
* @private
|
|
190
|
+
*/
|
|
118
191
|
async findEntityDirectories(entityName) {
|
|
119
192
|
const dirs = [];
|
|
120
193
|
// Search for directories with matching entity config
|
|
@@ -137,7 +210,43 @@ class Pull extends core_1.Command {
|
|
|
137
210
|
await searchDirs(process.cwd());
|
|
138
211
|
return dirs;
|
|
139
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* Process a single record and save to file
|
|
215
|
+
*
|
|
216
|
+
* Converts a database record into the file format and writes it to disk.
|
|
217
|
+
* This is a wrapper around processRecordData that handles file writing.
|
|
218
|
+
*
|
|
219
|
+
* @param record - Raw database record
|
|
220
|
+
* @param primaryKey - Primary key fields and values
|
|
221
|
+
* @param targetDir - Directory to save the file
|
|
222
|
+
* @param entityConfig - Entity configuration with pull settings
|
|
223
|
+
* @param syncEngine - Sync engine instance
|
|
224
|
+
* @returns Promise that resolves when file is written
|
|
225
|
+
* @private
|
|
226
|
+
*/
|
|
140
227
|
async processRecord(record, primaryKey, targetDir, entityConfig, syncEngine) {
|
|
228
|
+
const recordData = await this.processRecordData(record, primaryKey, targetDir, entityConfig, syncEngine);
|
|
229
|
+
// Determine file path
|
|
230
|
+
const fileName = this.buildFileName(primaryKey, entityConfig);
|
|
231
|
+
const filePath = path_1.default.join(targetDir, fileName);
|
|
232
|
+
// Write JSON file
|
|
233
|
+
await fs_extra_1.default.writeJson(filePath, recordData, { spaces: 2 });
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Process record data for storage
|
|
237
|
+
*
|
|
238
|
+
* Transforms a raw database record into the RecordData format used for file storage.
|
|
239
|
+
* Handles field externalization, related entity pulling, and checksum calculation.
|
|
240
|
+
*
|
|
241
|
+
* @param record - Raw database record
|
|
242
|
+
* @param primaryKey - Primary key fields and values
|
|
243
|
+
* @param targetDir - Directory where files will be saved
|
|
244
|
+
* @param entityConfig - Entity configuration with defaults and settings
|
|
245
|
+
* @param syncEngine - Sync engine for checksum calculation
|
|
246
|
+
* @returns Promise resolving to formatted RecordData
|
|
247
|
+
* @private
|
|
248
|
+
*/
|
|
249
|
+
async processRecordData(record, primaryKey, targetDir, entityConfig, syncEngine) {
|
|
141
250
|
// Build record data
|
|
142
251
|
const recordData = {
|
|
143
252
|
primaryKey: primaryKey,
|
|
@@ -172,12 +281,21 @@ class Pull extends core_1.Command {
|
|
|
172
281
|
}
|
|
173
282
|
// Calculate checksum
|
|
174
283
|
recordData.sync.checksum = syncEngine.calculateChecksum(recordData.fields);
|
|
175
|
-
|
|
176
|
-
const fileName = this.buildFileName(primaryKey, entityConfig);
|
|
177
|
-
const filePath = path_1.default.join(targetDir, fileName);
|
|
178
|
-
// Write JSON file
|
|
179
|
-
await fs_extra_1.default.writeJson(filePath, recordData, { spaces: 2 });
|
|
284
|
+
return recordData;
|
|
180
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* Determine if a field should be saved to an external file
|
|
288
|
+
*
|
|
289
|
+
* Checks if a field contains substantial text content that would be better
|
|
290
|
+
* stored in a separate file rather than inline in the JSON. Uses heuristics
|
|
291
|
+
* based on field name and content length.
|
|
292
|
+
*
|
|
293
|
+
* @param fieldName - Name of the field to check
|
|
294
|
+
* @param fieldValue - Value of the field
|
|
295
|
+
* @param entityConfig - Entity configuration (for future extension)
|
|
296
|
+
* @returns Promise resolving to true if field should be externalized
|
|
297
|
+
* @private
|
|
298
|
+
*/
|
|
181
299
|
async shouldExternalizeField(fieldName, fieldValue, entityConfig) {
|
|
182
300
|
// Only externalize string fields with significant content
|
|
183
301
|
if (typeof fieldValue !== 'string') {
|
|
@@ -192,6 +310,20 @@ class Pull extends core_1.Command {
|
|
|
192
310
|
}
|
|
193
311
|
return false;
|
|
194
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* Create an external file for a field value
|
|
315
|
+
*
|
|
316
|
+
* Saves large text content to a separate file and returns the filename.
|
|
317
|
+
* Automatically determines appropriate file extension based on field name
|
|
318
|
+
* and content type (e.g., .md for prompts, .html for templates).
|
|
319
|
+
*
|
|
320
|
+
* @param targetDir - Directory to save the file
|
|
321
|
+
* @param primaryKey - Primary key for filename generation
|
|
322
|
+
* @param fieldName - Name of the field being externalized
|
|
323
|
+
* @param content - Content to write to the file
|
|
324
|
+
* @returns Promise resolving to the created filename
|
|
325
|
+
* @private
|
|
326
|
+
*/
|
|
195
327
|
async createExternalFile(targetDir, primaryKey, fieldName, content) {
|
|
196
328
|
// Determine file extension based on field name and content
|
|
197
329
|
let extension = '.txt';
|
|
@@ -218,6 +350,18 @@ class Pull extends core_1.Command {
|
|
|
218
350
|
await fs_extra_1.default.writeFile(filePath, content, 'utf-8');
|
|
219
351
|
return fileName;
|
|
220
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* Build a filename from primary key values
|
|
355
|
+
*
|
|
356
|
+
* Creates a safe filename based on the entity's primary key values.
|
|
357
|
+
* Handles GUIDs by using first 8 characters, sanitizes special characters,
|
|
358
|
+
* and creates composite names for multi-field keys.
|
|
359
|
+
*
|
|
360
|
+
* @param primaryKey - Primary key fields and values
|
|
361
|
+
* @param entityConfig - Entity configuration (for future extension)
|
|
362
|
+
* @returns Filename with .json extension
|
|
363
|
+
* @private
|
|
364
|
+
*/
|
|
221
365
|
buildFileName(primaryKey, entityConfig) {
|
|
222
366
|
// Use primary key values to build filename
|
|
223
367
|
const keys = Object.values(primaryKey);
|
|
@@ -236,6 +380,19 @@ class Pull extends core_1.Command {
|
|
|
236
380
|
// Multiple keys or numeric - create composite name
|
|
237
381
|
return keys.map(k => String(k).replace(/[^a-zA-Z0-9-_]/g, '_')).join('-') + '.json';
|
|
238
382
|
}
|
|
383
|
+
/**
|
|
384
|
+
* Pull related entities for a parent record
|
|
385
|
+
*
|
|
386
|
+
* Retrieves child records that have foreign key relationships to the parent.
|
|
387
|
+
* Converts foreign key values to @parent references and supports nested
|
|
388
|
+
* related entities for deep object graphs.
|
|
389
|
+
*
|
|
390
|
+
* @param parentRecord - Parent entity record
|
|
391
|
+
* @param relatedConfig - Configuration for related entities to pull
|
|
392
|
+
* @param syncEngine - Sync engine instance
|
|
393
|
+
* @returns Promise resolving to map of entity names to related records
|
|
394
|
+
* @private
|
|
395
|
+
*/
|
|
239
396
|
async pullRelatedEntities(parentRecord, relatedConfig, syncEngine) {
|
|
240
397
|
const relatedEntities = {};
|
|
241
398
|
for (const [key, config] of Object.entries(relatedConfig)) {
|
|
@@ -298,6 +455,18 @@ class Pull extends core_1.Command {
|
|
|
298
455
|
}
|
|
299
456
|
return relatedEntities;
|
|
300
457
|
}
|
|
458
|
+
/**
|
|
459
|
+
* Find which field in the parent record contains a specific value
|
|
460
|
+
*
|
|
461
|
+
* Used to convert foreign key references to @parent references by finding
|
|
462
|
+
* the parent field that contains the foreign key value. Typically finds
|
|
463
|
+
* the primary key field but can match any field.
|
|
464
|
+
*
|
|
465
|
+
* @param parentRecord - Parent record to search
|
|
466
|
+
* @param value - Value to search for
|
|
467
|
+
* @returns Field name containing the value, or null if not found
|
|
468
|
+
* @private
|
|
469
|
+
*/
|
|
301
470
|
findParentField(parentRecord, value) {
|
|
302
471
|
// Find which field in the parent contains this value
|
|
303
472
|
// Typically this will be the primary key field
|