@memberjunction/metadata-sync 2.47.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.
@@ -38,6 +38,10 @@ const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  /** Global DataSource instance for connection lifecycle management */
40
40
  let globalDataSource = null;
41
+ /** Global provider instance to ensure single initialization */
42
+ let globalProvider = null;
43
+ /** Promise to track ongoing initialization */
44
+ let initializationPromise = null;
41
45
  /**
42
46
  * Initialize a SQLServerDataProvider with the given configuration
43
47
  *
@@ -57,32 +61,45 @@ let globalDataSource = null;
57
61
  * ```
58
62
  */
59
63
  async function initializeProvider(config) {
60
- // Create TypeORM DataSource
61
- const dataSource = new typeorm_1.DataSource({
62
- type: 'mssql',
63
- host: config.dbHost,
64
- port: config.dbPort ? Number(config.dbPort) : 1433,
65
- database: config.dbDatabase,
66
- username: config.dbUsername,
67
- password: config.dbPassword,
68
- synchronize: false,
69
- logging: false,
70
- options: {
71
- encrypt: config.dbEncrypt === 'Y' || config.dbEncrypt === 'true' ||
72
- config.dbHost.includes('.database.windows.net'), // Auto-detect Azure SQL
73
- trustServerCertificate: config.dbTrustServerCertificate === 'Y',
74
- instanceName: config.dbInstanceName
75
- }
76
- });
77
- // Initialize the data source
78
- await dataSource.initialize();
79
- // Store for cleanup
80
- globalDataSource = dataSource;
81
- // Create provider config
82
- const providerConfig = new sqlserver_dataprovider_1.SQLServerProviderConfigData(dataSource, 'system@sync.cli', // Default user for CLI
83
- config.mjCoreSchema || '__mj', 0);
84
- // Use setupSQLServerClient to properly initialize
85
- return await (0, sqlserver_dataprovider_1.setupSQLServerClient)(providerConfig);
64
+ // Return existing provider if already initialized
65
+ if (globalProvider) {
66
+ return globalProvider;
67
+ }
68
+ // Return ongoing initialization if in progress
69
+ if (initializationPromise) {
70
+ return initializationPromise;
71
+ }
72
+ // Start new initialization
73
+ initializationPromise = (async () => {
74
+ // Create TypeORM DataSource
75
+ const dataSource = new typeorm_1.DataSource({
76
+ type: 'mssql',
77
+ host: config.dbHost,
78
+ port: config.dbPort ? Number(config.dbPort) : 1433,
79
+ database: config.dbDatabase,
80
+ username: config.dbUsername,
81
+ password: config.dbPassword,
82
+ synchronize: false,
83
+ logging: false,
84
+ options: {
85
+ encrypt: config.dbEncrypt === 'Y' || config.dbEncrypt === 'true' ||
86
+ config.dbHost.includes('.database.windows.net'), // Auto-detect Azure SQL
87
+ trustServerCertificate: config.dbTrustServerCertificate === 'Y',
88
+ instanceName: config.dbInstanceName
89
+ }
90
+ });
91
+ // Initialize the data source
92
+ await dataSource.initialize();
93
+ // Store for cleanup
94
+ globalDataSource = dataSource;
95
+ // Create provider config
96
+ const providerConfig = new sqlserver_dataprovider_1.SQLServerProviderConfigData(dataSource, 'system@sync.cli', // Default user for CLI
97
+ config.mjCoreSchema || '__mj', 0);
98
+ // Use setupSQLServerClient to properly initialize
99
+ globalProvider = await (0, sqlserver_dataprovider_1.setupSQLServerClient)(providerConfig);
100
+ return globalProvider;
101
+ })();
102
+ return initializationPromise;
86
103
  }
87
104
  exports.initializeProvider = initializeProvider;
88
105
  /**
@@ -107,6 +124,8 @@ async function cleanupProvider() {
107
124
  await globalDataSource.destroy();
108
125
  globalDataSource = null;
109
126
  }
127
+ globalProvider = null;
128
+ initializationPromise = null;
110
129
  }
111
130
  exports.cleanupProvider = cleanupProvider;
112
131
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"provider-utils.js","sourceRoot":"","sources":["../../src/lib/provider-utils.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview Database provider utilities for MetadataSync\n * @module provider-utils\n * \n * This module provides utilities for initializing and managing the database\n * connection, accessing system users, and finding entity directories. It handles\n * the TypeORM DataSource lifecycle and MemberJunction provider initialization.\n */\n\nimport { DataSource } from 'typeorm';\nimport { SQLServerDataProvider, SQLServerProviderConfigData, UserCache, setupSQLServerClient } from '@memberjunction/sqlserver-dataprovider';\nimport type { MJConfig } from '../config';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { UserInfo } from '@memberjunction/core';\n\n/** Global DataSource instance for connection lifecycle management */\nlet globalDataSource: DataSource | null = null;\n\n/**\n * Initialize a SQLServerDataProvider with the given configuration\n * \n * Creates and initializes a TypeORM DataSource for SQL Server, then sets up\n * the MemberJunction SQLServerDataProvider. The connection is stored globally\n * for proper cleanup. Auto-detects Azure SQL databases for encryption settings.\n * \n * @param config - MemberJunction configuration with database connection details\n * @returns Promise resolving to initialized SQLServerDataProvider instance\n * @throws Error if database connection fails\n * \n * @example\n * ```typescript\n * const config = loadMJConfig();\n * const provider = await initializeProvider(config);\n * // Provider is ready for use\n * ```\n */\nexport async function initializeProvider(config: MJConfig): Promise<SQLServerDataProvider> {\n // Create TypeORM DataSource\n const dataSource = new DataSource({\n type: 'mssql',\n host: config.dbHost,\n port: config.dbPort ? Number(config.dbPort) : 1433,\n database: config.dbDatabase,\n username: config.dbUsername,\n password: config.dbPassword,\n synchronize: false,\n logging: false,\n options: {\n encrypt: config.dbEncrypt === 'Y' || config.dbEncrypt === 'true' || \n config.dbHost.includes('.database.windows.net'), // Auto-detect Azure SQL\n trustServerCertificate: config.dbTrustServerCertificate === 'Y',\n instanceName: config.dbInstanceName\n }\n });\n \n // Initialize the data source\n await dataSource.initialize();\n \n // Store for cleanup\n globalDataSource = dataSource;\n \n // Create provider config\n const providerConfig = new SQLServerProviderConfigData(\n dataSource,\n 'system@sync.cli', // Default user for CLI\n config.mjCoreSchema || '__mj',\n 0\n );\n \n // Use setupSQLServerClient to properly initialize\n return await setupSQLServerClient(providerConfig);\n}\n\n/**\n * Clean up the global database connection\n * \n * Destroys the TypeORM DataSource if it exists and is initialized.\n * Should be called when the CLI command completes to ensure proper cleanup.\n * \n * @returns Promise that resolves when cleanup is complete\n * \n * @example\n * ```typescript\n * try {\n * // Do work with database\n * } finally {\n * await cleanupProvider();\n * }\n * ```\n */\nexport async function cleanupProvider(): Promise<void> {\n if (globalDataSource && globalDataSource.isInitialized) {\n await globalDataSource.destroy();\n globalDataSource = null;\n }\n}\n\n/**\n * Get the system user from the UserCache\n * \n * Retrieves the \"System\" user from MemberJunction's UserCache. This user is\n * typically used for CLI operations where no specific user context exists.\n * \n * @returns The System UserInfo object\n * @throws Error if System user is not found in the cache\n * \n * @example\n * ```typescript\n * const systemUser = getSystemUser();\n * const syncEngine = new SyncEngine(systemUser);\n * ```\n */\nexport function getSystemUser(): UserInfo {\n const sysUser = UserCache.Instance.UserByName(\"System\", false);\n if (!sysUser) {\n throw new Error(\"System user not found in cache. Ensure the system user exists in the database.\"); \n }\n return sysUser;\n}\n\n/**\n * Find entity directories at the immediate level only\n * \n * Searches for directories containing .mj-sync.json files, which indicate\n * entity data directories. Only searches immediate subdirectories, not recursive.\n * If a specific directory is provided, only checks that directory.\n * \n * @param dir - Base directory to search from\n * @param specificDir - Optional specific subdirectory name to check\n * @returns Array of absolute directory paths containing .mj-sync.json files\n * \n * @example\n * ```typescript\n * // Find all entity directories\n * const dirs = findEntityDirectories(process.cwd());\n * \n * // Check specific directory\n * const dirs = findEntityDirectories(process.cwd(), 'ai-prompts');\n * ```\n */\nexport function findEntityDirectories(dir: string, specificDir?: string): string[] {\n const results: string[] = [];\n \n // If specific directory is provided, check if it's an entity directory\n if (specificDir) {\n const targetDir = path.isAbsolute(specificDir) ? specificDir : path.join(dir, specificDir);\n if (fs.existsSync(targetDir)) {\n const hasSyncConfig = fs.existsSync(path.join(targetDir, '.mj-sync.json'));\n if (hasSyncConfig) {\n results.push(targetDir);\n }\n }\n return results;\n }\n \n // Otherwise, find all immediate subdirectories with .mj-sync.json\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n \n for (const entry of entries) {\n if (entry.isDirectory() && !entry.name.startsWith('.')) {\n const subDir = path.join(dir, entry.name);\n const hasSyncConfig = fs.existsSync(path.join(subDir, '.mj-sync.json'));\n \n if (hasSyncConfig) {\n results.push(subDir);\n }\n }\n }\n \n return results;\n}"]}
1
+ {"version":3,"file":"provider-utils.js","sourceRoot":"","sources":["../../src/lib/provider-utils.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview Database provider utilities for MetadataSync\n * @module provider-utils\n * \n * This module provides utilities for initializing and managing the database\n * connection, accessing system users, and finding entity directories. It handles\n * the TypeORM DataSource lifecycle and MemberJunction provider initialization.\n */\n\nimport { DataSource } from 'typeorm';\nimport { SQLServerDataProvider, SQLServerProviderConfigData, UserCache, setupSQLServerClient } from '@memberjunction/sqlserver-dataprovider';\nimport type { MJConfig } from '../config';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { UserInfo } from '@memberjunction/core';\nimport { configManager } from './config-manager';\n\n/** Global DataSource instance for connection lifecycle management */\nlet globalDataSource: DataSource | null = null;\n\n/** Global provider instance to ensure single initialization */\nlet globalProvider: SQLServerDataProvider | null = null;\n\n/** Promise to track ongoing initialization */\nlet initializationPromise: Promise<SQLServerDataProvider> | null = null;\n\n/**\n * Initialize a SQLServerDataProvider with the given configuration\n * \n * Creates and initializes a TypeORM DataSource for SQL Server, then sets up\n * the MemberJunction SQLServerDataProvider. The connection is stored globally\n * for proper cleanup. Auto-detects Azure SQL databases for encryption settings.\n * \n * @param config - MemberJunction configuration with database connection details\n * @returns Promise resolving to initialized SQLServerDataProvider instance\n * @throws Error if database connection fails\n * \n * @example\n * ```typescript\n * const config = loadMJConfig();\n * const provider = await initializeProvider(config);\n * // Provider is ready for use\n * ```\n */\nexport async function initializeProvider(config: MJConfig): Promise<SQLServerDataProvider> {\n // Return existing provider if already initialized\n if (globalProvider) {\n return globalProvider;\n }\n \n // Return ongoing initialization if in progress\n if (initializationPromise) {\n return initializationPromise;\n }\n \n // Start new initialization\n initializationPromise = (async () => {\n // Create TypeORM DataSource\n const dataSource = new DataSource({\n type: 'mssql',\n host: config.dbHost,\n port: config.dbPort ? Number(config.dbPort) : 1433,\n database: config.dbDatabase,\n username: config.dbUsername,\n password: config.dbPassword,\n synchronize: false,\n logging: false,\n options: {\n encrypt: config.dbEncrypt === 'Y' || config.dbEncrypt === 'true' || \n config.dbHost.includes('.database.windows.net'), // Auto-detect Azure SQL\n trustServerCertificate: config.dbTrustServerCertificate === 'Y',\n instanceName: config.dbInstanceName\n }\n });\n \n // Initialize the data source\n await dataSource.initialize();\n \n // Store for cleanup\n globalDataSource = dataSource;\n \n // Create provider config\n const providerConfig = new SQLServerProviderConfigData(\n dataSource,\n 'system@sync.cli', // Default user for CLI\n config.mjCoreSchema || '__mj',\n 0\n );\n \n // Use setupSQLServerClient to properly initialize\n globalProvider = await setupSQLServerClient(providerConfig);\n return globalProvider;\n })();\n \n return initializationPromise;\n}\n\n/**\n * Clean up the global database connection\n * \n * Destroys the TypeORM DataSource if it exists and is initialized.\n * Should be called when the CLI command completes to ensure proper cleanup.\n * \n * @returns Promise that resolves when cleanup is complete\n * \n * @example\n * ```typescript\n * try {\n * // Do work with database\n * } finally {\n * await cleanupProvider();\n * }\n * ```\n */\nexport async function cleanupProvider(): Promise<void> {\n if (globalDataSource && globalDataSource.isInitialized) {\n await globalDataSource.destroy();\n globalDataSource = null;\n }\n globalProvider = null;\n initializationPromise = null;\n}\n\n/**\n * Get the system user from the UserCache\n * \n * Retrieves the \"System\" user from MemberJunction's UserCache. This user is\n * typically used for CLI operations where no specific user context exists.\n * \n * @returns The System UserInfo object\n * @throws Error if System user is not found in the cache\n * \n * @example\n * ```typescript\n * const systemUser = getSystemUser();\n * const syncEngine = new SyncEngine(systemUser);\n * ```\n */\nexport function getSystemUser(): UserInfo {\n const sysUser = UserCache.Instance.UserByName(\"System\", false);\n if (!sysUser) {\n throw new Error(\"System user not found in cache. Ensure the system user exists in the database.\"); \n }\n return sysUser;\n}\n\n/**\n * Find entity directories at the immediate level only\n * \n * Searches for directories containing .mj-sync.json files, which indicate\n * entity data directories. Only searches immediate subdirectories, not recursive.\n * If a specific directory is provided, only checks that directory.\n * \n * @param dir - Base directory to search from\n * @param specificDir - Optional specific subdirectory name to check\n * @returns Array of absolute directory paths containing .mj-sync.json files\n * \n * @example\n * ```typescript\n * // Find all entity directories\n * const dirs = findEntityDirectories(process.cwd());\n * \n * // Check specific directory\n * const dirs = findEntityDirectories(process.cwd(), 'ai-prompts');\n * ```\n */\nexport function findEntityDirectories(dir: string, specificDir?: string): string[] {\n const results: string[] = [];\n \n // If specific directory is provided, check if it's an entity directory\n if (specificDir) {\n const targetDir = path.isAbsolute(specificDir) ? specificDir : path.join(dir, specificDir);\n if (fs.existsSync(targetDir)) {\n const hasSyncConfig = fs.existsSync(path.join(targetDir, '.mj-sync.json'));\n if (hasSyncConfig) {\n results.push(targetDir);\n }\n }\n return results;\n }\n \n // Otherwise, find all immediate subdirectories with .mj-sync.json\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n \n for (const entry of entries) {\n if (entry.isDirectory() && !entry.name.startsWith('.')) {\n const subDir = path.join(dir, entry.name);\n const hasSyncConfig = fs.existsSync(path.join(subDir, '.mj-sync.json'));\n \n if (hasSyncConfig) {\n results.push(subDir);\n }\n }\n }\n \n return results;\n}"]}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @fileoverview Singleton manager for shared instances across MetadataSync
3
+ * @module singleton-manager
4
+ *
5
+ * This module ensures that expensive resources like SyncEngine are only
6
+ * initialized once per process, preventing duplicate metadata refreshes
7
+ * and improving performance.
8
+ */
9
+ import { SyncEngine } from './sync-engine';
10
+ import { UserInfo } from '@memberjunction/core';
11
+ /**
12
+ * Get or create a singleton SyncEngine instance
13
+ *
14
+ * Ensures that only one SyncEngine is created and initialized per process,
15
+ * preventing duplicate metadata refreshes that cause the double GetAllMetadata()
16
+ * console output.
17
+ *
18
+ * @param contextUser - The user context for database operations
19
+ * @returns Promise resolving to initialized SyncEngine instance
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * const syncEngine = await getSyncEngine(getSystemUser());
24
+ * // Use syncEngine for operations
25
+ * ```
26
+ */
27
+ export declare function getSyncEngine(contextUser: UserInfo): Promise<SyncEngine>;
28
+ /**
29
+ * Reset the singleton SyncEngine instance
30
+ *
31
+ * Should be called when cleaning up resources to ensure a fresh
32
+ * instance is created on the next request.
33
+ */
34
+ export declare function resetSyncEngine(): void;
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview Singleton manager for shared instances across MetadataSync
4
+ * @module singleton-manager
5
+ *
6
+ * This module ensures that expensive resources like SyncEngine are only
7
+ * initialized once per process, preventing duplicate metadata refreshes
8
+ * and improving performance.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.resetSyncEngine = exports.getSyncEngine = void 0;
12
+ const sync_engine_1 = require("./sync-engine");
13
+ /** Global SyncEngine instance */
14
+ let globalSyncEngine = null;
15
+ /** Promise to track ongoing SyncEngine initialization */
16
+ let syncEngineInitPromise = null;
17
+ /**
18
+ * Get or create a singleton SyncEngine instance
19
+ *
20
+ * Ensures that only one SyncEngine is created and initialized per process,
21
+ * preventing duplicate metadata refreshes that cause the double GetAllMetadata()
22
+ * console output.
23
+ *
24
+ * @param contextUser - The user context for database operations
25
+ * @returns Promise resolving to initialized SyncEngine instance
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * const syncEngine = await getSyncEngine(getSystemUser());
30
+ * // Use syncEngine for operations
31
+ * ```
32
+ */
33
+ async function getSyncEngine(contextUser) {
34
+ // Return existing engine if already initialized
35
+ if (globalSyncEngine) {
36
+ return globalSyncEngine;
37
+ }
38
+ // Return ongoing initialization if in progress
39
+ if (syncEngineInitPromise) {
40
+ return syncEngineInitPromise;
41
+ }
42
+ // Start new initialization
43
+ syncEngineInitPromise = (async () => {
44
+ globalSyncEngine = new sync_engine_1.SyncEngine(contextUser);
45
+ await globalSyncEngine.initialize();
46
+ return globalSyncEngine;
47
+ })();
48
+ return syncEngineInitPromise;
49
+ }
50
+ exports.getSyncEngine = getSyncEngine;
51
+ /**
52
+ * Reset the singleton SyncEngine instance
53
+ *
54
+ * Should be called when cleaning up resources to ensure a fresh
55
+ * instance is created on the next request.
56
+ */
57
+ function resetSyncEngine() {
58
+ globalSyncEngine = null;
59
+ syncEngineInitPromise = null;
60
+ }
61
+ exports.resetSyncEngine = resetSyncEngine;
62
+ //# sourceMappingURL=singleton-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"singleton-manager.js","sourceRoot":"","sources":["../../src/lib/singleton-manager.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;;AAEH,+CAA2C;AAG3C,iCAAiC;AACjC,IAAI,gBAAgB,GAAsB,IAAI,CAAC;AAE/C,yDAAyD;AACzD,IAAI,qBAAqB,GAA+B,IAAI,CAAC;AAE7D;;;;;;;;;;;;;;;GAeG;AACI,KAAK,UAAU,aAAa,CAAC,WAAqB;IACvD,gDAAgD;IAChD,IAAI,gBAAgB,EAAE,CAAC;QACrB,OAAO,gBAAgB,CAAC;IAC1B,CAAC;IAED,+CAA+C;IAC/C,IAAI,qBAAqB,EAAE,CAAC;QAC1B,OAAO,qBAAqB,CAAC;IAC/B,CAAC;IAED,2BAA2B;IAC3B,qBAAqB,GAAG,CAAC,KAAK,IAAI,EAAE;QAClC,gBAAgB,GAAG,IAAI,wBAAU,CAAC,WAAW,CAAC,CAAC;QAC/C,MAAM,gBAAgB,CAAC,UAAU,EAAE,CAAC;QACpC,OAAO,gBAAgB,CAAC;IAC1B,CAAC,CAAC,EAAE,CAAC;IAEL,OAAO,qBAAqB,CAAC;AAC/B,CAAC;AAnBD,sCAmBC;AAED;;;;;GAKG;AACH,SAAgB,eAAe;IAC7B,gBAAgB,GAAG,IAAI,CAAC;IACxB,qBAAqB,GAAG,IAAI,CAAC;AAC/B,CAAC;AAHD,0CAGC","sourcesContent":["/**\n * @fileoverview Singleton manager for shared instances across MetadataSync\n * @module singleton-manager\n * \n * This module ensures that expensive resources like SyncEngine are only\n * initialized once per process, preventing duplicate metadata refreshes\n * and improving performance.\n */\n\nimport { SyncEngine } from './sync-engine';\nimport { UserInfo } from '@memberjunction/core';\n\n/** Global SyncEngine instance */\nlet globalSyncEngine: SyncEngine | null = null;\n\n/** Promise to track ongoing SyncEngine initialization */\nlet syncEngineInitPromise: Promise<SyncEngine> | null = null;\n\n/**\n * Get or create a singleton SyncEngine instance\n * \n * Ensures that only one SyncEngine is created and initialized per process,\n * preventing duplicate metadata refreshes that cause the double GetAllMetadata()\n * console output.\n * \n * @param contextUser - The user context for database operations\n * @returns Promise resolving to initialized SyncEngine instance\n * \n * @example\n * ```typescript\n * const syncEngine = await getSyncEngine(getSystemUser());\n * // Use syncEngine for operations\n * ```\n */\nexport async function getSyncEngine(contextUser: UserInfo): Promise<SyncEngine> {\n // Return existing engine if already initialized\n if (globalSyncEngine) {\n return globalSyncEngine;\n }\n \n // Return ongoing initialization if in progress\n if (syncEngineInitPromise) {\n return syncEngineInitPromise;\n }\n \n // Start new initialization\n syncEngineInitPromise = (async () => {\n globalSyncEngine = new SyncEngine(contextUser);\n await globalSyncEngine.initialize();\n return globalSyncEngine;\n })();\n \n return syncEngineInitPromise;\n}\n\n/**\n * Reset the singleton SyncEngine instance\n * \n * Should be called when cleaning up resources to ensure a fresh\n * instance is created on the next request.\n */\nexport function resetSyncEngine(): void {\n globalSyncEngine = null;\n syncEngineInitPromise = null;\n}"]}
@@ -24,47 +24,47 @@
24
24
  "index.js"
25
25
  ]
26
26
  },
27
- "pull": {
27
+ "push": {
28
28
  "aliases": [],
29
29
  "args": {},
30
- "description": "Pull metadata from database to local files",
30
+ "description": "Push local file changes to the database",
31
31
  "examples": [
32
- "<%= config.bin %> <%= command.id %> --entity=\"AI Prompts\"",
33
- "<%= config.bin %> <%= command.id %> --entity=\"AI Prompts\" --filter=\"CategoryID='customer-service-id'\""
32
+ "<%= config.bin %> <%= command.id %>",
33
+ "<%= config.bin %> <%= command.id %> --dry-run",
34
+ "<%= config.bin %> <%= command.id %> --dir=\"ai-prompts\"",
35
+ "<%= config.bin %> <%= command.id %> --ci"
34
36
  ],
35
37
  "flags": {
36
- "entity": {
37
- "description": "Entity name to pull",
38
- "name": "entity",
39
- "required": true,
40
- "hasDynamicHelp": false,
41
- "multiple": false,
42
- "type": "option"
43
- },
44
- "filter": {
45
- "description": "Additional filter for pulling specific records",
46
- "name": "filter",
38
+ "dir": {
39
+ "description": "Specific entity directory to push",
40
+ "name": "dir",
47
41
  "hasDynamicHelp": false,
48
42
  "multiple": false,
49
43
  "type": "option"
50
44
  },
51
45
  "dry-run": {
52
- "description": "Show what would be pulled without actually pulling",
46
+ "description": "Show what would be pushed without actually pushing",
53
47
  "name": "dry-run",
54
48
  "allowNo": false,
55
49
  "type": "boolean"
56
50
  },
57
- "multi-file": {
58
- "description": "Create a single file with multiple records (provide filename)",
59
- "name": "multi-file",
60
- "hasDynamicHelp": false,
61
- "multiple": false,
62
- "type": "option"
51
+ "ci": {
52
+ "description": "CI mode - no prompts, fail on issues",
53
+ "name": "ci",
54
+ "allowNo": false,
55
+ "type": "boolean"
56
+ },
57
+ "verbose": {
58
+ "char": "v",
59
+ "description": "Show detailed field-level output",
60
+ "name": "verbose",
61
+ "allowNo": false,
62
+ "type": "boolean"
63
63
  }
64
64
  },
65
65
  "hasDynamicHelp": false,
66
66
  "hiddenAliases": [],
67
- "id": "pull",
67
+ "id": "push",
68
68
  "pluginAlias": "@memberjunction/metadata-sync",
69
69
  "pluginName": "@memberjunction/metadata-sync",
70
70
  "pluginType": "core",
@@ -74,43 +74,50 @@
74
74
  "relativePath": [
75
75
  "dist",
76
76
  "commands",
77
- "pull",
77
+ "push",
78
78
  "index.js"
79
79
  ]
80
80
  },
81
- "push": {
81
+ "pull": {
82
82
  "aliases": [],
83
83
  "args": {},
84
- "description": "Push local file changes to the database",
84
+ "description": "Pull metadata from database to local files",
85
85
  "examples": [
86
- "<%= config.bin %> <%= command.id %>",
87
- "<%= config.bin %> <%= command.id %> --dry-run",
88
- "<%= config.bin %> <%= command.id %> --dir=\"ai-prompts\"",
89
- "<%= config.bin %> <%= command.id %> --ci"
86
+ "<%= config.bin %> <%= command.id %> --entity=\"AI Prompts\"",
87
+ "<%= config.bin %> <%= command.id %> --entity=\"AI Prompts\" --filter=\"CategoryID='customer-service-id'\""
90
88
  ],
91
89
  "flags": {
92
- "dir": {
93
- "description": "Specific entity directory to push",
94
- "name": "dir",
90
+ "entity": {
91
+ "description": "Entity name to pull",
92
+ "name": "entity",
93
+ "required": true,
94
+ "hasDynamicHelp": false,
95
+ "multiple": false,
96
+ "type": "option"
97
+ },
98
+ "filter": {
99
+ "description": "Additional filter for pulling specific records",
100
+ "name": "filter",
95
101
  "hasDynamicHelp": false,
96
102
  "multiple": false,
97
103
  "type": "option"
98
104
  },
99
105
  "dry-run": {
100
- "description": "Show what would be pushed without actually pushing",
106
+ "description": "Show what would be pulled without actually pulling",
101
107
  "name": "dry-run",
102
108
  "allowNo": false,
103
109
  "type": "boolean"
104
110
  },
105
- "ci": {
106
- "description": "CI mode - no prompts, fail on issues",
107
- "name": "ci",
108
- "allowNo": false,
109
- "type": "boolean"
111
+ "multi-file": {
112
+ "description": "Create a single file with multiple records (provide filename)",
113
+ "name": "multi-file",
114
+ "hasDynamicHelp": false,
115
+ "multiple": false,
116
+ "type": "option"
110
117
  },
111
118
  "verbose": {
112
119
  "char": "v",
113
- "description": "Show detailed field-level output",
120
+ "description": "Show detailed output",
114
121
  "name": "verbose",
115
122
  "allowNo": false,
116
123
  "type": "boolean"
@@ -118,7 +125,7 @@
118
125
  },
119
126
  "hasDynamicHelp": false,
120
127
  "hiddenAliases": [],
121
- "id": "push",
128
+ "id": "pull",
122
129
  "pluginAlias": "@memberjunction/metadata-sync",
123
130
  "pluginName": "@memberjunction/metadata-sync",
124
131
  "pluginType": "core",
@@ -128,7 +135,7 @@
128
135
  "relativePath": [
129
136
  "dist",
130
137
  "commands",
131
- "push",
138
+ "pull",
132
139
  "index.js"
133
140
  ]
134
141
  },
@@ -199,5 +206,5 @@
199
206
  ]
200
207
  }
201
208
  },
202
- "version": "2.47.0"
209
+ "version": "2.48.0"
203
210
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memberjunction/metadata-sync",
3
- "version": "2.47.0",
3
+ "version": "2.48.0",
4
4
  "description": "MemberJunction metadata synchronization CLI tool",
5
5
  "keywords": [
6
6
  "oclif",
@@ -51,11 +51,11 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@inquirer/prompts": "^5.0.1",
54
- "@memberjunction/core": "2.47.0",
55
- "@memberjunction/core-entities": "2.47.0",
56
- "@memberjunction/core-entities-server": "2.47.0",
57
- "@memberjunction/sqlserver-dataprovider": "2.47.0",
58
- "@memberjunction/graphql-dataprovider": "2.47.0",
54
+ "@memberjunction/core": "2.48.0",
55
+ "@memberjunction/core-entities": "2.48.0",
56
+ "@memberjunction/core-entities-server": "2.48.0",
57
+ "@memberjunction/sqlserver-dataprovider": "2.48.0",
58
+ "@memberjunction/graphql-dataprovider": "2.48.0",
59
59
  "@oclif/core": "^3",
60
60
  "@oclif/plugin-help": "^6",
61
61
  "@oclif/plugin-version": "^2.0.17",