@memberjunction/metadata-sync 2.46.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.
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const core_1 = require("@oclif/core");
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const prompts_1 = require("@inquirer/prompts");
10
+ const ora_classic_1 = __importDefault(require("ora-classic"));
11
+ class Init extends core_1.Command {
12
+ static description = 'Initialize a directory for metadata synchronization';
13
+ static examples = [
14
+ `<%= config.bin %> <%= command.id %>`,
15
+ ];
16
+ async run() {
17
+ const spinner = (0, ora_classic_1.default)();
18
+ try {
19
+ // Check if already initialized
20
+ if (await fs_extra_1.default.pathExists('.mj-sync.json')) {
21
+ const overwrite = await (0, prompts_1.select)({
22
+ message: 'Directory already initialized. Overwrite configuration?',
23
+ choices: [
24
+ { name: 'Yes', value: true },
25
+ { name: 'No', value: false }
26
+ ]
27
+ });
28
+ if (!overwrite) {
29
+ this.log('Initialization cancelled');
30
+ return;
31
+ }
32
+ }
33
+ // Create root configuration
34
+ const rootConfig = {
35
+ version: '1.0.0',
36
+ push: {
37
+ validateBeforePush: true,
38
+ requireConfirmation: true
39
+ },
40
+ watch: {
41
+ debounceMs: 1000,
42
+ ignorePatterns: ['*.tmp', '*.bak', '.DS_Store']
43
+ }
44
+ };
45
+ spinner.start('Creating root configuration');
46
+ await fs_extra_1.default.writeJson('.mj-sync.json', rootConfig, { spaces: 2 });
47
+ spinner.succeed('Created .mj-sync.json');
48
+ // Ask if they want to set up an entity directory
49
+ const setupEntity = await (0, prompts_1.select)({
50
+ message: 'Would you like to set up an entity directory now?',
51
+ choices: [
52
+ { name: 'Yes - AI Prompts', value: 'ai-prompts' },
53
+ { name: 'Yes - Other entity', value: 'other' },
54
+ { name: 'No - I\'ll set up later', value: 'no' }
55
+ ]
56
+ });
57
+ if (setupEntity !== 'no') {
58
+ const entityName = setupEntity === 'ai-prompts'
59
+ ? 'AI Prompts'
60
+ : await (0, prompts_1.input)({
61
+ message: 'Enter the entity name (e.g., "Templates", "AI Models"):',
62
+ });
63
+ const dirName = setupEntity === 'ai-prompts'
64
+ ? 'ai-prompts'
65
+ : await (0, prompts_1.input)({
66
+ message: 'Enter the directory name:',
67
+ default: entityName.toLowerCase().replace(/\s+/g, '-')
68
+ });
69
+ // Create entity directory
70
+ spinner.start(`Creating ${dirName} directory`);
71
+ await fs_extra_1.default.ensureDir(dirName);
72
+ // Create entity configuration
73
+ const entityConfig = {
74
+ entity: entityName,
75
+ filePattern: '*.json',
76
+ defaults: {}
77
+ };
78
+ await fs_extra_1.default.writeJson(path_1.default.join(dirName, '.mj-sync.json'), entityConfig, { spaces: 2 });
79
+ spinner.succeed(`Created ${dirName} directory with entity configuration`);
80
+ // Create example structure
81
+ if (setupEntity === 'ai-prompts') {
82
+ await this.createAIPromptsExample(dirName);
83
+ }
84
+ }
85
+ this.log('\n✅ Initialization complete!');
86
+ this.log('\nNext steps:');
87
+ this.log('1. Run "mj-sync pull --entity=\'AI Prompts\'" to pull existing data');
88
+ this.log('2. Edit files locally');
89
+ this.log('3. Run "mj-sync push" to sync changes back to the database');
90
+ }
91
+ catch (error) {
92
+ spinner.fail('Initialization failed');
93
+ this.error(error);
94
+ }
95
+ }
96
+ async createAIPromptsExample(dirName) {
97
+ const exampleDir = path_1.default.join(dirName, 'examples');
98
+ await fs_extra_1.default.ensureDir(exampleDir);
99
+ // Create folder config
100
+ const folderConfig = {
101
+ defaults: {
102
+ CategoryID: '@lookup:AI Prompt Categories.Name=Examples',
103
+ Temperature: 0.7
104
+ }
105
+ };
106
+ await fs_extra_1.default.writeJson(path_1.default.join(exampleDir, '.mj-folder.json'), folderConfig, { spaces: 2 });
107
+ // Create example prompt
108
+ const examplePrompt = {
109
+ _primaryKey: {
110
+ ID: 'example-001'
111
+ },
112
+ _fields: {
113
+ Name: 'Example Greeting Prompt',
114
+ Description: 'A simple example prompt to demonstrate the sync tool',
115
+ PromptTypeID: '@lookup:AI Prompt Types.Name=Chat',
116
+ Temperature: 0.8,
117
+ MaxTokens: 150,
118
+ Prompt: '@file:greeting.prompt.md'
119
+ }
120
+ };
121
+ await fs_extra_1.default.writeJson(path_1.default.join(exampleDir, 'greeting.json'), examplePrompt, { spaces: 2 });
122
+ // Create the markdown file
123
+ const promptContent = `You are a friendly assistant. Please greet the user warmly and ask how you can help them today.
124
+
125
+ Be conversational and welcoming in your tone.`;
126
+ await fs_extra_1.default.writeFile(path_1.default.join(exampleDir, 'greeting.prompt.md'), promptContent);
127
+ }
128
+ }
129
+ exports.default = Init;
130
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/commands/init/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { Command } from '@oclif/core';\nimport fs from 'fs-extra';\nimport path from 'path';\nimport { input, select } from '@inquirer/prompts';\nimport ora from 'ora-classic';\n\nexport default class Init extends Command {\n static description = 'Initialize a directory for metadata synchronization';\n \n static examples = [\n `<%= config.bin %> <%= command.id %>`,\n ];\n \n async run(): Promise<void> {\n const spinner = ora();\n \n try {\n // Check if already initialized\n if (await fs.pathExists('.mj-sync.json')) {\n const overwrite = await select({\n message: 'Directory already initialized. Overwrite configuration?',\n choices: [\n { name: 'Yes', value: true },\n { name: 'No', value: false }\n ]\n });\n \n if (!overwrite) {\n this.log('Initialization cancelled');\n return;\n }\n }\n \n // Create root configuration\n const rootConfig = {\n version: '1.0.0',\n push: {\n validateBeforePush: true,\n requireConfirmation: true\n },\n watch: {\n debounceMs: 1000,\n ignorePatterns: ['*.tmp', '*.bak', '.DS_Store']\n }\n };\n \n spinner.start('Creating root configuration');\n await fs.writeJson('.mj-sync.json', rootConfig, { spaces: 2 });\n spinner.succeed('Created .mj-sync.json');\n \n // Ask if they want to set up an entity directory\n const setupEntity = await select({\n message: 'Would you like to set up an entity directory now?',\n choices: [\n { name: 'Yes - AI Prompts', value: 'ai-prompts' },\n { name: 'Yes - Other entity', value: 'other' },\n { name: 'No - I\\'ll set up later', value: 'no' }\n ]\n });\n \n if (setupEntity !== 'no') {\n const entityName = setupEntity === 'ai-prompts' \n ? 'AI Prompts'\n : await input({\n message: 'Enter the entity name (e.g., \"Templates\", \"AI Models\"):',\n });\n \n const dirName = setupEntity === 'ai-prompts'\n ? 'ai-prompts'\n : await input({\n message: 'Enter the directory name:',\n default: entityName.toLowerCase().replace(/\\s+/g, '-')\n });\n \n // Create entity directory\n spinner.start(`Creating ${dirName} directory`);\n await fs.ensureDir(dirName);\n \n // Create entity configuration\n const entityConfig = {\n entity: entityName,\n filePattern: '*.json',\n defaults: {}\n };\n \n await fs.writeJson(path.join(dirName, '.mj-sync.json'), entityConfig, { spaces: 2 });\n spinner.succeed(`Created ${dirName} directory with entity configuration`);\n \n // Create example structure\n if (setupEntity === 'ai-prompts') {\n await this.createAIPromptsExample(dirName);\n }\n }\n \n this.log('\\n✅ Initialization complete!');\n this.log('\\nNext steps:');\n this.log('1. Run \"mj-sync pull --entity=\\'AI Prompts\\'\" to pull existing data');\n this.log('2. Edit files locally');\n this.log('3. Run \"mj-sync push\" to sync changes back to the database');\n \n } catch (error) {\n spinner.fail('Initialization failed');\n this.error(error as Error);\n }\n }\n \n private async createAIPromptsExample(dirName: string): Promise<void> {\n const exampleDir = path.join(dirName, 'examples');\n await fs.ensureDir(exampleDir);\n \n // Create folder config\n const folderConfig = {\n defaults: {\n CategoryID: '@lookup:AI Prompt Categories.Name=Examples',\n Temperature: 0.7\n }\n };\n \n await fs.writeJson(path.join(exampleDir, '.mj-folder.json'), folderConfig, { spaces: 2 });\n \n // Create example prompt\n const examplePrompt = {\n _primaryKey: {\n ID: 'example-001'\n },\n _fields: {\n Name: 'Example Greeting Prompt',\n Description: 'A simple example prompt to demonstrate the sync tool',\n PromptTypeID: '@lookup:AI Prompt Types.Name=Chat',\n Temperature: 0.8,\n MaxTokens: 150,\n Prompt: '@file:greeting.prompt.md'\n }\n };\n \n await fs.writeJson(\n path.join(exampleDir, 'greeting.json'), \n examplePrompt, \n { spaces: 2 }\n );\n \n // Create the markdown file\n const promptContent = `You are a friendly assistant. Please greet the user warmly and ask how you can help them today.\n\nBe conversational and welcoming in your tone.`;\n \n await fs.writeFile(\n path.join(exampleDir, 'greeting.prompt.md'),\n promptContent\n );\n }\n}"]}
@@ -0,0 +1,18 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Pull extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ entity: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
7
+ filter: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
8
+ 'dry-run': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
9
+ };
10
+ run(): Promise<void>;
11
+ private findEntityDirectories;
12
+ private processRecord;
13
+ private shouldExternalizeField;
14
+ private createExternalFile;
15
+ private buildFileName;
16
+ private pullRelatedEntities;
17
+ private findParentField;
18
+ }
@@ -0,0 +1,313 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const core_1 = require("@oclif/core");
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const prompts_1 = require("@inquirer/prompts");
10
+ const ora_classic_1 = __importDefault(require("ora-classic"));
11
+ const config_1 = require("../../config");
12
+ const sync_engine_1 = require("../../lib/sync-engine");
13
+ const core_2 = require("@memberjunction/core");
14
+ const provider_utils_1 = require("../../lib/provider-utils");
15
+ class Pull extends core_1.Command {
16
+ static description = 'Pull metadata from database to local files';
17
+ static examples = [
18
+ `<%= config.bin %> <%= command.id %> --entity="AI Prompts"`,
19
+ `<%= config.bin %> <%= command.id %> --entity="AI Prompts" --filter="CategoryID='customer-service-id'"`,
20
+ ];
21
+ static flags = {
22
+ entity: core_1.Flags.string({ description: 'Entity name to pull', required: true }),
23
+ filter: core_1.Flags.string({ description: 'Additional filter for pulling specific records' }),
24
+ 'dry-run': core_1.Flags.boolean({ description: 'Show what would be pulled without actually pulling' }),
25
+ };
26
+ async run() {
27
+ const { flags } = await this.parse(Pull);
28
+ const spinner = (0, ora_classic_1.default)();
29
+ try {
30
+ // Load MJ config
31
+ spinner.start('Loading configuration');
32
+ const mjConfig = (0, config_1.loadMJConfig)();
33
+ if (!mjConfig) {
34
+ this.error('No mj.config.cjs found in current directory or parent directories');
35
+ }
36
+ // Initialize data provider
37
+ const provider = await (0, provider_utils_1.initializeProvider)(mjConfig);
38
+ // Initialize sync engine
39
+ const syncEngine = new sync_engine_1.SyncEngine((0, provider_utils_1.getSystemUser)());
40
+ await syncEngine.initialize();
41
+ spinner.succeed('Configuration loaded');
42
+ // Find entity directory
43
+ const entityDirs = await this.findEntityDirectories(flags.entity);
44
+ if (entityDirs.length === 0) {
45
+ this.error(`No directory found for entity "${flags.entity}". Run "mj-sync init" first.`);
46
+ }
47
+ let targetDir;
48
+ if (entityDirs.length === 1) {
49
+ targetDir = entityDirs[0];
50
+ }
51
+ else {
52
+ // Multiple directories found, ask user
53
+ targetDir = await (0, prompts_1.select)({
54
+ message: `Multiple directories found for entity "${flags.entity}". Which one to use?`,
55
+ choices: entityDirs.map(dir => ({ name: dir, value: dir }))
56
+ });
57
+ }
58
+ const entityConfig = await (0, config_1.loadEntityConfig)(targetDir);
59
+ if (!entityConfig) {
60
+ this.error(`Invalid entity configuration in ${targetDir}`);
61
+ }
62
+ // Pull records
63
+ spinner.start(`Pulling ${flags.entity} records`);
64
+ const rv = new core_2.RunView();
65
+ let filter = '';
66
+ if (flags.filter) {
67
+ filter = flags.filter;
68
+ }
69
+ else if (entityConfig.pull?.filter) {
70
+ filter = entityConfig.pull.filter;
71
+ }
72
+ const result = await rv.RunView({
73
+ EntityName: flags.entity,
74
+ ExtraFilter: filter
75
+ }, (0, provider_utils_1.getSystemUser)());
76
+ if (!result.Success) {
77
+ this.error(`Failed to pull records: ${result.ErrorMessage}`);
78
+ }
79
+ spinner.succeed(`Found ${result.Results.length} records`);
80
+ if (flags['dry-run']) {
81
+ this.log(`\nDry run mode - would pull ${result.Results.length} records to ${targetDir}`);
82
+ return;
83
+ }
84
+ // Process each record
85
+ const entityInfo = syncEngine.getEntityInfo(flags.entity);
86
+ if (!entityInfo) {
87
+ this.error(`Entity information not found for: ${flags.entity}`);
88
+ }
89
+ spinner.start('Processing records');
90
+ let processed = 0;
91
+ for (const record of result.Results) {
92
+ try {
93
+ // Build primary key
94
+ const primaryKey = {};
95
+ for (const pk of entityInfo.PrimaryKeys) {
96
+ primaryKey[pk.Name] = record[pk.Name];
97
+ }
98
+ // Process record
99
+ await this.processRecord(record, primaryKey, targetDir, entityConfig, syncEngine);
100
+ processed++;
101
+ spinner.text = `Processing records (${processed}/${result.Results.length})`;
102
+ }
103
+ catch (error) {
104
+ this.warn(`Failed to process record: ${error.message || error}`);
105
+ }
106
+ }
107
+ spinner.succeed(`Pulled ${processed} records to ${targetDir}`);
108
+ }
109
+ catch (error) {
110
+ spinner.fail('Pull failed');
111
+ this.error(error);
112
+ }
113
+ finally {
114
+ // Clean up database connection
115
+ await (0, provider_utils_1.cleanupProvider)();
116
+ }
117
+ }
118
+ async findEntityDirectories(entityName) {
119
+ const dirs = [];
120
+ // Search for directories with matching entity config
121
+ const searchDirs = async (dir) => {
122
+ const entries = await fs_extra_1.default.readdir(dir, { withFileTypes: true });
123
+ for (const entry of entries) {
124
+ if (entry.isDirectory()) {
125
+ const fullPath = path_1.default.join(dir, entry.name);
126
+ const config = await (0, config_1.loadEntityConfig)(fullPath);
127
+ if (config && config.entity === entityName) {
128
+ dirs.push(fullPath);
129
+ }
130
+ else {
131
+ // Recurse
132
+ await searchDirs(fullPath);
133
+ }
134
+ }
135
+ }
136
+ };
137
+ await searchDirs(process.cwd());
138
+ return dirs;
139
+ }
140
+ async processRecord(record, primaryKey, targetDir, entityConfig, syncEngine) {
141
+ // Build record data
142
+ const recordData = {
143
+ primaryKey: primaryKey,
144
+ fields: {},
145
+ sync: {
146
+ lastModified: new Date().toISOString(),
147
+ checksum: ''
148
+ }
149
+ };
150
+ // Process fields
151
+ for (const [fieldName, fieldValue] of Object.entries(record)) {
152
+ // Skip primary key fields
153
+ if (primaryKey[fieldName] !== undefined) {
154
+ continue;
155
+ }
156
+ // Skip internal fields
157
+ if (fieldName.startsWith('__mj_')) {
158
+ continue;
159
+ }
160
+ // Check if this is an external file field
161
+ if (await this.shouldExternalizeField(fieldName, fieldValue, entityConfig)) {
162
+ const fileName = await this.createExternalFile(targetDir, primaryKey, fieldName, String(fieldValue));
163
+ recordData.fields[fieldName] = `@file:${fileName}`;
164
+ }
165
+ else {
166
+ recordData.fields[fieldName] = fieldValue;
167
+ }
168
+ }
169
+ // Pull related entities if configured
170
+ if (entityConfig.pull?.relatedEntities) {
171
+ recordData.relatedEntities = await this.pullRelatedEntities(record, entityConfig.pull.relatedEntities, syncEngine);
172
+ }
173
+ // Calculate checksum
174
+ recordData.sync.checksum = syncEngine.calculateChecksum(recordData.fields);
175
+ // Determine file path
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 });
180
+ }
181
+ async shouldExternalizeField(fieldName, fieldValue, entityConfig) {
182
+ // Only externalize string fields with significant content
183
+ if (typeof fieldValue !== 'string') {
184
+ return false;
185
+ }
186
+ // Check if it's a known large text field
187
+ const largeTextFields = ['Prompt', 'Template', 'Notes', 'Description',
188
+ 'Content', 'Body', 'Text', 'HTML', 'SQL'];
189
+ if (largeTextFields.some(f => fieldName.toLowerCase().includes(f.toLowerCase()))) {
190
+ // Only externalize if content is substantial (more than 100 chars or has newlines)
191
+ return fieldValue.length > 100 || fieldValue.includes('\n');
192
+ }
193
+ return false;
194
+ }
195
+ async createExternalFile(targetDir, primaryKey, fieldName, content) {
196
+ // Determine file extension based on field name and content
197
+ let extension = '.txt';
198
+ if (fieldName.toLowerCase().includes('prompt')) {
199
+ extension = '.md';
200
+ }
201
+ else if (fieldName.toLowerCase().includes('template')) {
202
+ if (content.includes('<html') || content.includes('<!DOCTYPE')) {
203
+ extension = '.html';
204
+ }
205
+ else if (content.includes('{{') || content.includes('{%')) {
206
+ extension = '.liquid';
207
+ }
208
+ }
209
+ else if (fieldName.toLowerCase().includes('sql')) {
210
+ extension = '.sql';
211
+ }
212
+ else if (fieldName.toLowerCase().includes('notes') || fieldName.toLowerCase().includes('description')) {
213
+ extension = '.md';
214
+ }
215
+ const baseFileName = this.buildFileName(primaryKey, null).replace('.json', '');
216
+ const fileName = `${baseFileName}.${fieldName.toLowerCase()}${extension}`;
217
+ const filePath = path_1.default.join(targetDir, fileName);
218
+ await fs_extra_1.default.writeFile(filePath, content, 'utf-8');
219
+ return fileName;
220
+ }
221
+ buildFileName(primaryKey, entityConfig) {
222
+ // Use primary key values to build filename
223
+ const keys = Object.values(primaryKey);
224
+ if (keys.length === 1 && typeof keys[0] === 'string') {
225
+ // Single string key - use as base if it's a guid
226
+ const key = keys[0];
227
+ if (key.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)) {
228
+ // It's a GUID, use first 8 chars
229
+ return `${key.substring(0, 8)}.json`;
230
+ }
231
+ // Use the whole key if not too long
232
+ if (key.length <= 50) {
233
+ return `${key.replace(/[^a-zA-Z0-9-_]/g, '_')}.json`;
234
+ }
235
+ }
236
+ // Multiple keys or numeric - create composite name
237
+ return keys.map(k => String(k).replace(/[^a-zA-Z0-9-_]/g, '_')).join('-') + '.json';
238
+ }
239
+ async pullRelatedEntities(parentRecord, relatedConfig, syncEngine) {
240
+ const relatedEntities = {};
241
+ for (const [key, config] of Object.entries(relatedConfig)) {
242
+ try {
243
+ // Get the parent's primary key value
244
+ const parentKeyValue = parentRecord[config.foreignKey];
245
+ if (!parentKeyValue) {
246
+ continue; // Skip if parent doesn't have the foreign key field
247
+ }
248
+ // Build filter for related records
249
+ let filter = `${config.foreignKey} = '${String(parentKeyValue).replace(/'/g, "''")}'`;
250
+ if (config.filter) {
251
+ filter += ` AND (${config.filter})`;
252
+ }
253
+ // Pull related records
254
+ const rv = new core_2.RunView();
255
+ const result = await rv.RunView({
256
+ EntityName: config.entity,
257
+ ExtraFilter: filter
258
+ }, (0, provider_utils_1.getSystemUser)());
259
+ if (!result.Success) {
260
+ this.warn(`Failed to pull related ${config.entity}: ${result.ErrorMessage}`);
261
+ continue;
262
+ }
263
+ // Process each related record
264
+ const relatedRecords = [];
265
+ for (const relatedRecord of result.Results) {
266
+ const recordData = {
267
+ fields: {}
268
+ };
269
+ // Process fields, omitting the foreign key since it will be set via @parent
270
+ for (const [fieldName, fieldValue] of Object.entries(relatedRecord)) {
271
+ // Skip internal fields
272
+ if (fieldName.startsWith('__mj_')) {
273
+ continue;
274
+ }
275
+ // Convert foreign key reference to @parent
276
+ if (fieldName === config.foreignKey) {
277
+ const parentFieldName = this.findParentField(parentRecord, parentKeyValue);
278
+ if (parentFieldName) {
279
+ recordData.fields[fieldName] = `@parent:${parentFieldName}`;
280
+ }
281
+ continue;
282
+ }
283
+ recordData.fields[fieldName] = fieldValue;
284
+ }
285
+ // Pull nested related entities if configured
286
+ if (config.relatedEntities) {
287
+ recordData.relatedEntities = await this.pullRelatedEntities(relatedRecord, config.relatedEntities, syncEngine);
288
+ }
289
+ relatedRecords.push(recordData);
290
+ }
291
+ if (relatedRecords.length > 0) {
292
+ relatedEntities[key] = relatedRecords;
293
+ }
294
+ }
295
+ catch (error) {
296
+ this.warn(`Error pulling related ${key}: ${error}`);
297
+ }
298
+ }
299
+ return relatedEntities;
300
+ }
301
+ findParentField(parentRecord, value) {
302
+ // Find which field in the parent contains this value
303
+ // Typically this will be the primary key field
304
+ for (const [fieldName, fieldValue] of Object.entries(parentRecord)) {
305
+ if (fieldValue === value && !fieldName.startsWith('__mj_')) {
306
+ return fieldName;
307
+ }
308
+ }
309
+ return null;
310
+ }
311
+ }
312
+ exports.default = Pull;
313
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/commands/pull/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { Command, Flags } from '@oclif/core';\nimport fs from 'fs-extra';\nimport path from 'path';\nimport { select } from '@inquirer/prompts';\nimport ora from 'ora-classic';\nimport { loadMJConfig, loadEntityConfig, RelatedEntityConfig } from '../../config';\nimport { SyncEngine, RecordData } from '../../lib/sync-engine';\nimport { RunView } from '@memberjunction/core';\nimport { getSystemUser, initializeProvider, cleanupProvider } from '../../lib/provider-utils';\n\nexport default class Pull extends Command {\n static description = 'Pull metadata from database to local files';\n \n static examples = [\n `<%= config.bin %> <%= command.id %> --entity=\"AI Prompts\"`,\n `<%= config.bin %> <%= command.id %> --entity=\"AI Prompts\" --filter=\"CategoryID='customer-service-id'\"`,\n ];\n \n static flags = {\n entity: Flags.string({ description: 'Entity name to pull', required: true }),\n filter: Flags.string({ description: 'Additional filter for pulling specific records' }),\n 'dry-run': Flags.boolean({ description: 'Show what would be pulled without actually pulling' }),\n };\n \n async run(): Promise<void> {\n const { flags } = await this.parse(Pull);\n const spinner = ora();\n \n try {\n // Load MJ config\n spinner.start('Loading configuration');\n const mjConfig = loadMJConfig();\n if (!mjConfig) {\n this.error('No mj.config.cjs found in current directory or parent directories');\n }\n \n // Initialize data provider\n const provider = await initializeProvider(mjConfig);\n \n // Initialize sync engine\n const syncEngine = new SyncEngine(getSystemUser());\n await syncEngine.initialize();\n spinner.succeed('Configuration loaded');\n \n // Find entity directory\n const entityDirs = await this.findEntityDirectories(flags.entity);\n \n if (entityDirs.length === 0) {\n this.error(`No directory found for entity \"${flags.entity}\". Run \"mj-sync init\" first.`);\n }\n \n let targetDir: string;\n if (entityDirs.length === 1) {\n targetDir = entityDirs[0];\n } else {\n // Multiple directories found, ask user\n targetDir = await select({\n message: `Multiple directories found for entity \"${flags.entity}\". Which one to use?`,\n choices: entityDirs.map(dir => ({ name: dir, value: dir }))\n });\n }\n \n const entityConfig = await loadEntityConfig(targetDir);\n if (!entityConfig) {\n this.error(`Invalid entity configuration in ${targetDir}`);\n }\n \n // Pull records\n spinner.start(`Pulling ${flags.entity} records`);\n const rv = new RunView();\n \n let filter = '';\n if (flags.filter) {\n filter = flags.filter;\n } else if (entityConfig.pull?.filter) {\n filter = entityConfig.pull.filter;\n }\n \n const result = await rv.RunView({\n EntityName: flags.entity,\n ExtraFilter: filter\n }, getSystemUser());\n \n if (!result.Success) {\n this.error(`Failed to pull records: ${result.ErrorMessage}`);\n }\n \n spinner.succeed(`Found ${result.Results.length} records`);\n \n if (flags['dry-run']) {\n this.log(`\\nDry run mode - would pull ${result.Results.length} records to ${targetDir}`);\n return;\n }\n \n // Process each record\n const entityInfo = syncEngine.getEntityInfo(flags.entity);\n if (!entityInfo) {\n this.error(`Entity information not found for: ${flags.entity}`);\n }\n \n spinner.start('Processing records');\n let processed = 0;\n \n for (const record of result.Results) {\n try {\n // Build primary key\n const primaryKey: Record<string, any> = {};\n for (const pk of entityInfo.PrimaryKeys) {\n primaryKey[pk.Name] = record[pk.Name];\n }\n \n // Process record\n await this.processRecord(record, primaryKey, targetDir, entityConfig, syncEngine);\n processed++;\n \n spinner.text = `Processing records (${processed}/${result.Results.length})`;\n } catch (error) {\n this.warn(`Failed to process record: ${(error as any).message || error}`);\n }\n }\n \n spinner.succeed(`Pulled ${processed} records to ${targetDir}`);\n \n } catch (error) {\n spinner.fail('Pull failed');\n this.error(error as Error);\n } finally {\n // Clean up database connection\n await cleanupProvider();\n }\n }\n \n private async findEntityDirectories(entityName: string): Promise<string[]> {\n const dirs: string[] = [];\n \n // Search for directories with matching entity config\n const searchDirs = async (dir: string) => {\n const entries = await fs.readdir(dir, { withFileTypes: true });\n \n for (const entry of entries) {\n if (entry.isDirectory()) {\n const fullPath = path.join(dir, entry.name);\n const config = await loadEntityConfig(fullPath);\n \n if (config && config.entity === entityName) {\n dirs.push(fullPath);\n } else {\n // Recurse\n await searchDirs(fullPath);\n }\n }\n }\n };\n \n await searchDirs(process.cwd());\n return dirs;\n }\n \n private async processRecord(\n record: any, \n primaryKey: Record<string, any>,\n targetDir: string, \n entityConfig: any,\n syncEngine: SyncEngine\n ): Promise<void> {\n // Build record data\n const recordData: RecordData = {\n primaryKey: primaryKey,\n fields: {},\n sync: {\n lastModified: new Date().toISOString(),\n checksum: ''\n }\n };\n \n // Process fields\n for (const [fieldName, fieldValue] of Object.entries(record)) {\n // Skip primary key fields\n if (primaryKey[fieldName] !== undefined) {\n continue;\n }\n \n // Skip internal fields\n if (fieldName.startsWith('__mj_')) {\n continue;\n }\n \n // Check if this is an external file field\n if (await this.shouldExternalizeField(fieldName, fieldValue, entityConfig)) {\n const fileName = await this.createExternalFile(\n targetDir,\n primaryKey,\n fieldName,\n String(fieldValue)\n );\n recordData.fields[fieldName] = `@file:${fileName}`;\n } else {\n recordData.fields[fieldName] = fieldValue;\n }\n }\n \n // Pull related entities if configured\n if (entityConfig.pull?.relatedEntities) {\n recordData.relatedEntities = await this.pullRelatedEntities(\n record,\n entityConfig.pull.relatedEntities,\n syncEngine\n );\n }\n \n // Calculate checksum\n recordData.sync!.checksum = syncEngine.calculateChecksum(recordData.fields);\n \n // Determine file path\n const fileName = this.buildFileName(primaryKey, entityConfig);\n const filePath = path.join(targetDir, fileName);\n \n // Write JSON file\n await fs.writeJson(filePath, recordData, { spaces: 2 });\n }\n \n private async shouldExternalizeField(\n fieldName: string, \n fieldValue: any,\n entityConfig: any\n ): Promise<boolean> {\n // Only externalize string fields with significant content\n if (typeof fieldValue !== 'string') {\n return false;\n }\n \n // Check if it's a known large text field\n const largeTextFields = ['Prompt', 'Template', 'Notes', 'Description', \n 'Content', 'Body', 'Text', 'HTML', 'SQL'];\n \n if (largeTextFields.some(f => fieldName.toLowerCase().includes(f.toLowerCase()))) {\n // Only externalize if content is substantial (more than 100 chars or has newlines)\n return fieldValue.length > 100 || fieldValue.includes('\\n');\n }\n \n return false;\n }\n \n private async createExternalFile(\n targetDir: string,\n primaryKey: Record<string, any>,\n fieldName: string,\n content: string\n ): Promise<string> {\n // Determine file extension based on field name and content\n let extension = '.txt';\n \n if (fieldName.toLowerCase().includes('prompt')) {\n extension = '.md';\n } else if (fieldName.toLowerCase().includes('template')) {\n if (content.includes('<html') || content.includes('<!DOCTYPE')) {\n extension = '.html';\n } else if (content.includes('{{') || content.includes('{%')) {\n extension = '.liquid';\n }\n } else if (fieldName.toLowerCase().includes('sql')) {\n extension = '.sql';\n } else if (fieldName.toLowerCase().includes('notes') || fieldName.toLowerCase().includes('description')) {\n extension = '.md';\n }\n \n const baseFileName = this.buildFileName(primaryKey, null).replace('.json', '');\n const fileName = `${baseFileName}.${fieldName.toLowerCase()}${extension}`;\n const filePath = path.join(targetDir, fileName);\n \n await fs.writeFile(filePath, content, 'utf-8');\n \n return fileName;\n }\n \n private buildFileName(primaryKey: Record<string, any>, entityConfig: any): string {\n // Use primary key values to build filename\n const keys = Object.values(primaryKey);\n \n if (keys.length === 1 && typeof keys[0] === 'string') {\n // Single string key - use as base if it's a guid\n const key = keys[0];\n if (key.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)) {\n // It's a GUID, use first 8 chars\n return `${key.substring(0, 8)}.json`;\n }\n // Use the whole key if not too long\n if (key.length <= 50) {\n return `${key.replace(/[^a-zA-Z0-9-_]/g, '_')}.json`;\n }\n }\n \n // Multiple keys or numeric - create composite name\n return keys.map(k => String(k).replace(/[^a-zA-Z0-9-_]/g, '_')).join('-') + '.json';\n }\n \n private async pullRelatedEntities(\n parentRecord: any,\n relatedConfig: Record<string, RelatedEntityConfig>,\n syncEngine: SyncEngine\n ): Promise<Record<string, RecordData[]>> {\n const relatedEntities: Record<string, RecordData[]> = {};\n \n for (const [key, config] of Object.entries(relatedConfig)) {\n try {\n // Get the parent's primary key value\n const parentKeyValue = parentRecord[config.foreignKey];\n if (!parentKeyValue) {\n continue; // Skip if parent doesn't have the foreign key field\n }\n \n // Build filter for related records\n let filter = `${config.foreignKey} = '${String(parentKeyValue).replace(/'/g, \"''\")}'`;\n if (config.filter) {\n filter += ` AND (${config.filter})`;\n }\n \n // Pull related records\n const rv = new RunView();\n const result = await rv.RunView({\n EntityName: config.entity,\n ExtraFilter: filter\n }, getSystemUser());\n \n if (!result.Success) {\n this.warn(`Failed to pull related ${config.entity}: ${result.ErrorMessage}`);\n continue;\n }\n \n // Process each related record\n const relatedRecords: RecordData[] = [];\n for (const relatedRecord of result.Results) {\n const recordData: RecordData = {\n fields: {}\n };\n \n // Process fields, omitting the foreign key since it will be set via @parent\n for (const [fieldName, fieldValue] of Object.entries(relatedRecord)) {\n // Skip internal fields\n if (fieldName.startsWith('__mj_')) {\n continue;\n }\n \n // Convert foreign key reference to @parent\n if (fieldName === config.foreignKey) {\n const parentFieldName = this.findParentField(parentRecord, parentKeyValue);\n if (parentFieldName) {\n recordData.fields[fieldName] = `@parent:${parentFieldName}`;\n }\n continue;\n }\n \n recordData.fields[fieldName] = fieldValue;\n }\n \n // Pull nested related entities if configured\n if (config.relatedEntities) {\n recordData.relatedEntities = await this.pullRelatedEntities(\n relatedRecord,\n config.relatedEntities,\n syncEngine\n );\n }\n \n relatedRecords.push(recordData);\n }\n \n if (relatedRecords.length > 0) {\n relatedEntities[key] = relatedRecords;\n }\n } catch (error) {\n this.warn(`Error pulling related ${key}: ${error}`);\n }\n }\n \n return relatedEntities;\n }\n \n private findParentField(parentRecord: any, value: any): string | null {\n // Find which field in the parent contains this value\n // Typically this will be the primary key field\n for (const [fieldName, fieldValue] of Object.entries(parentRecord)) {\n if (fieldValue === value && !fieldName.startsWith('__mj_')) {\n return fieldName;\n }\n }\n return null;\n }\n}"]}
@@ -0,0 +1,15 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Push extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ dir: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
7
+ 'dry-run': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
8
+ ci: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
9
+ verbose: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
10
+ };
11
+ run(): Promise<void>;
12
+ private processEntityDirectory;
13
+ private pushRecord;
14
+ private processRelatedEntities;
15
+ }