@memberjunction/cli 3.2.0 → 3.4.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 CHANGED
@@ -114,6 +114,34 @@ mj codegen [--skipdb]
114
114
  Options:
115
115
  - `--skipdb`: Skip database migration before running code generation
116
116
 
117
+ #### `mj codegen manifest`
118
+
119
+ Generates a class registrations manifest to prevent tree-shaking of `@RegisterClass` decorated classes. The tool walks the app's transitive dependency tree and produces a tailored import manifest.
120
+
121
+ ```bash
122
+ mj codegen manifest [--output <path>] [--appDir <path>] [--filter <class>] [--quiet]
123
+ ```
124
+
125
+ Options:
126
+ - `--output, -o <path>`: Output manifest file path (default: `./src/generated/class-registrations-manifest.ts`)
127
+ - `--appDir, -a <path>`: App directory containing `package.json` (default: current directory)
128
+ - `--filter, -f <class>`: Only include classes with this base class (can be repeated)
129
+ - `--quiet, -q`: Suppress progress output
130
+
131
+ Examples:
132
+ ```bash
133
+ # Generate manifest for the current app
134
+ mj codegen manifest --output ./src/generated/class-registrations-manifest.ts
135
+
136
+ # Generate for a specific app directory
137
+ mj codegen manifest --appDir ./packages/MJAPI --output ./packages/MJAPI/src/generated/class-registrations-manifest.ts
138
+
139
+ # Only include engine and action classes
140
+ mj codegen manifest --filter BaseEngine --filter BaseAction
141
+ ```
142
+
143
+ The generated manifest imports all packages in the app's dependency tree that contain `@RegisterClass` decorators, ensuring the class factory system works correctly even with aggressive tree-shaking. See the [CodeGenLib README](../CodeGenLib/README.md) for programmatic usage.
144
+
117
145
  #### `mj migrate`
118
146
 
119
147
  Applies database migrations to update your MemberJunction schema to the latest version.
@@ -0,0 +1,12 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class CodeGenManifest extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ output: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
7
+ appDir: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
8
+ filter: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
9
+ quiet: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
10
+ };
11
+ run(): Promise<void>;
12
+ }
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const codegen_lib_1 = require("@memberjunction/codegen-lib");
5
+ class CodeGenManifest extends core_1.Command {
6
+ static description = 'Generate a class registrations manifest to prevent tree-shaking of @RegisterClass decorated classes';
7
+ static examples = [
8
+ `<%= config.bin %> <%= command.id %> --output ./src/generated/class-registrations-manifest.ts`,
9
+ `<%= config.bin %> <%= command.id %> --appDir ./packages/MJAPI --output ./packages/MJAPI/src/generated/class-registrations-manifest.ts`,
10
+ `<%= config.bin %> <%= command.id %> --filter BaseEngine --filter BaseAction`,
11
+ ];
12
+ static flags = {
13
+ output: core_1.Flags.string({
14
+ char: 'o',
15
+ description: 'Output manifest file path',
16
+ default: './src/generated/class-registrations-manifest.ts',
17
+ }),
18
+ appDir: core_1.Flags.string({
19
+ char: 'a',
20
+ description: 'App directory containing package.json (defaults to current directory)',
21
+ }),
22
+ filter: core_1.Flags.string({
23
+ char: 'f',
24
+ description: 'Only include classes with this base class (can be repeated)',
25
+ multiple: true,
26
+ }),
27
+ quiet: core_1.Flags.boolean({
28
+ char: 'q',
29
+ description: 'Suppress progress output',
30
+ default: false,
31
+ }),
32
+ };
33
+ async run() {
34
+ const { flags } = await this.parse(CodeGenManifest);
35
+ const result = await (0, codegen_lib_1.generateClassRegistrationsManifest)({
36
+ outputPath: flags.output,
37
+ appDir: flags.appDir || process.cwd(),
38
+ verbose: !flags.quiet,
39
+ filterBaseClasses: flags.filter && flags.filter.length > 0 ? flags.filter : undefined,
40
+ });
41
+ if (!result.success) {
42
+ this.error(`Manifest generation failed:\n${result.errors.map(e => ` - ${e}`).join('\n')}`);
43
+ }
44
+ if (!flags.quiet) {
45
+ this.log('');
46
+ this.log('Manifest generated successfully:');
47
+ this.log(` Dependencies walked: ${result.totalDepsWalked}`);
48
+ this.log(` Packages with @RegisterClass: ${result.packages.length}`);
49
+ this.log(` Total classes: ${result.classes.length}`);
50
+ this.log(` Output: ${result.outputPath}`);
51
+ }
52
+ }
53
+ }
54
+ exports.default = CodeGenManifest;
@@ -5,6 +5,8 @@ export default class Migrate extends Command {
5
5
  static flags: {
6
6
  verbose: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
7
7
  tag: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
8
+ schema: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
9
+ dir: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
8
10
  };
9
11
  run(): Promise<void>;
10
12
  private analyzeFlywayError;
@@ -11,21 +11,28 @@ class Migrate extends core_1.Command {
11
11
  static description = 'Migrate MemberJunction database to latest version';
12
12
  static examples = [
13
13
  `<%= config.bin %> <%= command.id %>
14
+ `,
15
+ `<%= config.bin %> <%= command.id %> --schema __BCSaaS --dir ./migrations/v1
16
+ `,
17
+ `<%= config.bin %> <%= command.id %> --schema __BCSaaS --tag v1.0.0
14
18
  `,
15
19
  ];
16
20
  static flags = {
17
21
  verbose: core_1.Flags.boolean({ char: 'v', description: 'Enable additional logging' }),
18
22
  tag: core_1.Flags.string({ char: 't', description: 'Version tag to use for running remote migrations' }),
23
+ schema: core_1.Flags.string({ char: 's', description: 'Target schema (overrides coreSchema from config)' }),
24
+ dir: core_1.Flags.string({ description: 'Migration source directory (overrides migrationsLocation from config)' }),
19
25
  };
20
26
  async run() {
21
27
  const { flags } = await this.parse(Migrate);
22
28
  const config = (0, config_1.getValidatedConfig)();
23
- const flywayConfig = await (0, config_1.getFlywayConfig)(config, flags.tag);
29
+ const targetSchema = flags.schema || config.coreSchema;
30
+ const flywayConfig = await (0, config_1.getFlywayConfig)(config, flags.tag, flags.schema, flags.dir);
24
31
  const flyway = new node_flyway_1.Flyway(flywayConfig);
25
32
  if (flags.verbose) {
26
33
  this.log(`Connecting to ${flywayConfig.url}`);
27
34
  this.log(`Database Connection: ${config.dbHost}, ${config.dbDatabase}, User: ${flywayConfig.user}`);
28
- this.log(`Migrating ${config.coreSchema} schema using migrations from:\n\t- ${flywayConfig.migrationLocations.join('\n\t- ')}\n`);
35
+ this.log(`Migrating ${targetSchema} schema using migrations from:\n\t- ${flywayConfig.migrationLocations.join('\n\t- ')}\n`);
29
36
  this.log(`Flyway config settings: baselineVersion: ${config.baselineVersion}, baselineMigrate: ${config.baselineOnMigrate}\n`);
30
37
  }
31
38
  if (flags.tag) {
@@ -43,15 +50,9 @@ class Migrate extends core_1.Command {
43
50
  }
44
51
  }
45
52
  else if (isParseError) {
46
- // Parse error - could be SQL error or connection issue
47
- // Run Flyway CLI directly to get the actual error
48
- spinner.fail();
49
- this.logToStderr('\n❌ Migration failed - unable to parse Flyway response.');
50
- this.logToStderr(`Execution time: ${result.additionalDetails.executionTime / 1000}s\n`);
51
- this.logToStderr('🔍 Running diagnostic to identify the actual error...');
52
- const dbInfo = `${config.dbHost}:${config.dbPort}/${config.dbDatabase}`;
53
- this.logToStderr(` Database: ${dbInfo}`);
54
- this.logToStderr(` User: ${config.codeGenLogin}\n`);
53
+ // Parse error - could be SQL error or connection issue, or just a node-flyway bug
54
+ // Run Flyway CLI directly to determine if migration actually succeeded
55
+ spinner.text = 'Verifying migration status...';
55
56
  try {
56
57
  const { spawnSync } = require('child_process');
57
58
  const path = require('path');
@@ -62,13 +63,27 @@ class Migrate extends core_1.Command {
62
63
  const flywayExePath = path.join(flywayDir, flywayExeName);
63
64
  // Use spawnSync to avoid shell interpretation of special characters in password
64
65
  const jdbcUrl = `jdbc:sqlserver://${config.dbHost}:${config.dbPort};databaseName=${config.dbDatabase};trustServerCertificate=${config.dbTrustServerCertificate}`;
65
- // Build common args
66
+ // Build common args - use targetSchema instead of coreSchema
67
+ // This ensures diagnostic validates the schema being migrated, not the core MJ schema
66
68
  const baseArgs = [
67
69
  `-url=${jdbcUrl}`,
68
70
  `-user=${config.codeGenLogin}`,
69
71
  `-password=${config.codeGenPassword}`,
70
- `-schemas=${config.coreSchema}`
72
+ `-schemas=${targetSchema}`
71
73
  ];
74
+ // Add placeholder arguments from config
75
+ // Read directly from config.SQLOutput.schemaPlaceholders since node-flyway doesn't use them
76
+ const schemaPlaceholders = config.SQLOutput?.schemaPlaceholders;
77
+ if (schemaPlaceholders && schemaPlaceholders.length > 0) {
78
+ schemaPlaceholders.forEach(({ schema, placeholder }) => {
79
+ const cleanPlaceholder = placeholder.replace(/^\$\{|\}$/g, '');
80
+ // Skip Flyway built-in placeholders
81
+ if (cleanPlaceholder.startsWith('flyway:'))
82
+ return;
83
+ // Flyway CLI format: -placeholders.PLACEHOLDER_NAME=value
84
+ baseArgs.push(`-placeholders.${cleanPlaceholder}=${schema}`);
85
+ });
86
+ }
72
87
  // Convert relative migration paths to absolute paths
73
88
  const absoluteMigrationPaths = flywayConfig.migrationLocations.map((loc) => {
74
89
  // Remove 'filesystem:' prefix if present
@@ -76,50 +91,50 @@ class Migrate extends core_1.Command {
76
91
  // Convert to absolute path if relative
77
92
  return path.isAbsolute(cleanLoc) ? loc : `filesystem:${path.resolve(cleanLoc)}`;
78
93
  });
79
- // First try validate to catch checksum mismatches
80
- const validateArgs = [
94
+ // Skip validation and go straight to migrate
95
+ // node-flyway already tried and failed, so we just need to run the actual migration
96
+ const migrateArgs = [
81
97
  ...baseArgs,
98
+ `-baselineVersion=${config.baselineVersion}`,
99
+ `-baselineOnMigrate=${config.baselineOnMigrate}`,
82
100
  `-locations=${absoluteMigrationPaths.join(',')}`,
83
- 'validate'
101
+ 'migrate'
84
102
  ];
85
- const validateResult = spawnSync(flywayExePath, validateArgs, { encoding: 'utf8' });
86
- const validateOutput = validateResult.stderr || validateResult.stdout || '';
87
- // Check if validation failed
88
- if (validateResult.status !== 0 || validateOutput.toLowerCase().includes('validate failed')) {
89
- this.analyzeFlywayError(validateOutput, config);
103
+ const migrateResult = spawnSync(flywayExePath, migrateArgs, { encoding: 'utf8' });
104
+ const migrateOutput = migrateResult.stderr || migrateResult.stdout || '';
105
+ // Check if output contains error messages even if exit code is 0
106
+ // Exclude SQL Server informational messages and recompilation warnings
107
+ const hasErrorsInOutput = (migrateOutput.toLowerCase().includes('error') &&
108
+ !migrateOutput.includes('Error Code: 0') && // Informational messages
109
+ !migrateOutput.includes('Error Code: 15070')) || // Recompilation warnings
110
+ migrateOutput.toLowerCase().includes('incorrect syntax') ||
111
+ migrateOutput.toLowerCase().includes('must be the only statement');
112
+ if (migrateResult.status === 0 && !hasErrorsInOutput) {
113
+ // Migration actually succeeded - don't throw error
114
+ spinner.succeed('Migrations complete');
115
+ this.log(`Execution time: ${result.additionalDetails.executionTime / 1000}s`);
116
+ if (flags.verbose) {
117
+ this.logToStderr('\n💡 Note: Migration succeeded but node-flyway had trouble parsing the response.');
118
+ this.logToStderr(' This is a known issue with node-flyway and does not affect migration success.\n');
119
+ }
120
+ return;
121
+ }
122
+ else if (migrateResult.status === 0 && hasErrorsInOutput) {
123
+ // Exit code was 0 but output contains errors - SQL script likely has error handling
124
+ spinner.fail();
125
+ this.logToStderr('\n⚠️ Migration completed but errors were detected in output:\n');
126
+ this.analyzeFlywayError(migrateOutput, config);
90
127
  }
91
128
  else {
92
- // Validation passed, try migrate to see the actual SQL error
93
- const migrateArgs = [
94
- ...baseArgs,
95
- `-baselineVersion=${config.baselineVersion}`,
96
- `-baselineOnMigrate=${config.baselineOnMigrate}`,
97
- `-locations=${absoluteMigrationPaths.join(',')}`,
98
- 'migrate'
99
- ];
100
- const migrateResult = spawnSync(flywayExePath, migrateArgs, { encoding: 'utf8' });
101
- const migrateOutput = migrateResult.stderr || migrateResult.stdout || '';
102
- // Check if output contains error messages even if exit code is 0
103
- const hasErrorsInOutput = migrateOutput.toLowerCase().includes('error') ||
104
- migrateOutput.toLowerCase().includes('incorrect syntax') ||
105
- migrateOutput.toLowerCase().includes('must be the only statement');
106
- if (migrateResult.status === 0 && !hasErrorsInOutput) {
107
- this.logToStderr('✓ Migration executed successfully (Flyway CLI reports success)');
108
- this.logToStderr(' The issue was with node-flyway response parsing only\n');
109
- }
110
- else if (migrateResult.status === 0 && hasErrorsInOutput) {
111
- // Exit code was 0 but output contains errors - SQL script likely has error handling
112
- this.logToStderr('⚠️ Migration completed but errors were detected in output:\n');
113
- this.analyzeFlywayError(migrateOutput, config);
114
- }
115
- else {
116
- // Migration failed with non-zero exit code
117
- this.analyzeFlywayError(migrateOutput, config);
118
- }
129
+ // Migration failed with non-zero exit code
130
+ spinner.fail();
131
+ this.logToStderr('\n❌ Migration failed:\n');
132
+ this.analyzeFlywayError(migrateOutput, config);
119
133
  }
120
134
  }
121
135
  catch (err) {
122
- this.logToStderr(`❌ Error running diagnostic: ${err.message || err}\n`);
136
+ spinner.fail();
137
+ this.logToStderr(`\n❌ Error running diagnostic: ${err.message || err}\n`);
123
138
  }
124
139
  this.error('Migration failed - see diagnostic information above');
125
140
  }
package/dist/config.d.ts CHANGED
@@ -14,6 +14,40 @@ declare const mjConfigSchema: z.ZodObject<{
14
14
  mjRepoUrl: z.ZodCatch<z.ZodString>;
15
15
  baselineVersion: z.ZodDefault<z.ZodOptional<z.ZodString>>;
16
16
  baselineOnMigrate: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
17
+ SQLOutput: z.ZodOptional<z.ZodObject<{
18
+ schemaPlaceholders: z.ZodOptional<z.ZodArray<z.ZodObject<{
19
+ schema: z.ZodString;
20
+ placeholder: z.ZodString;
21
+ }, "strip", z.ZodTypeAny, {
22
+ schema: string;
23
+ placeholder: string;
24
+ }, {
25
+ schema: string;
26
+ placeholder: string;
27
+ }>, "many">>;
28
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
29
+ schemaPlaceholders: z.ZodOptional<z.ZodArray<z.ZodObject<{
30
+ schema: z.ZodString;
31
+ placeholder: z.ZodString;
32
+ }, "strip", z.ZodTypeAny, {
33
+ schema: string;
34
+ placeholder: string;
35
+ }, {
36
+ schema: string;
37
+ placeholder: string;
38
+ }>, "many">>;
39
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
40
+ schemaPlaceholders: z.ZodOptional<z.ZodArray<z.ZodObject<{
41
+ schema: z.ZodString;
42
+ placeholder: z.ZodString;
43
+ }, "strip", z.ZodTypeAny, {
44
+ schema: string;
45
+ placeholder: string;
46
+ }, {
47
+ schema: string;
48
+ placeholder: string;
49
+ }>, "many">>;
50
+ }, z.ZodTypeAny, "passthrough">>>;
17
51
  }, "strip", z.ZodTypeAny, {
18
52
  dbHost: string;
19
53
  dbDatabase: string;
@@ -27,6 +61,18 @@ declare const mjConfigSchema: z.ZodObject<{
27
61
  mjRepoUrl: string;
28
62
  baselineVersion: string;
29
63
  baselineOnMigrate: boolean;
64
+ SQLOutput?: z.objectOutputType<{
65
+ schemaPlaceholders: z.ZodOptional<z.ZodArray<z.ZodObject<{
66
+ schema: z.ZodString;
67
+ placeholder: z.ZodString;
68
+ }, "strip", z.ZodTypeAny, {
69
+ schema: string;
70
+ placeholder: string;
71
+ }, {
72
+ schema: string;
73
+ placeholder: string;
74
+ }>, "many">>;
75
+ }, z.ZodTypeAny, "passthrough"> | undefined;
30
76
  }, {
31
77
  dbDatabase: string;
32
78
  codeGenLogin: string;
@@ -40,6 +86,18 @@ declare const mjConfigSchema: z.ZodObject<{
40
86
  mjRepoUrl?: unknown;
41
87
  baselineVersion?: string | undefined;
42
88
  baselineOnMigrate?: boolean | undefined;
89
+ SQLOutput?: z.objectInputType<{
90
+ schemaPlaceholders: z.ZodOptional<z.ZodArray<z.ZodObject<{
91
+ schema: z.ZodString;
92
+ placeholder: z.ZodString;
93
+ }, "strip", z.ZodTypeAny, {
94
+ schema: string;
95
+ placeholder: string;
96
+ }, {
97
+ schema: string;
98
+ placeholder: string;
99
+ }>, "many">>;
100
+ }, z.ZodTypeAny, "passthrough"> | undefined;
43
101
  }>;
44
102
  export declare const config: {
45
103
  dbHost: string;
@@ -54,6 +112,18 @@ export declare const config: {
54
112
  mjRepoUrl: string;
55
113
  baselineVersion: string;
56
114
  baselineOnMigrate: boolean;
115
+ SQLOutput?: z.objectOutputType<{
116
+ schemaPlaceholders: z.ZodOptional<z.ZodArray<z.ZodObject<{
117
+ schema: z.ZodString;
118
+ placeholder: z.ZodString;
119
+ }, "strip", z.ZodTypeAny, {
120
+ schema: string;
121
+ placeholder: string;
122
+ }, {
123
+ schema: string;
124
+ placeholder: string;
125
+ }>, "many">>;
126
+ }, z.ZodTypeAny, "passthrough"> | undefined;
57
127
  } | undefined;
58
128
  /**
59
129
  * Get validated config for commands that require database connection.
@@ -72,5 +142,5 @@ export declare const getOptionalConfig: () => Partial<MJConfig> | undefined;
72
142
  */
73
143
  export declare const updatedConfig: () => MJConfig | undefined;
74
144
  export declare const createFlywayUrl: (mjConfig: MJConfig) => string;
75
- export declare const getFlywayConfig: (mjConfig: MJConfig, tag?: string) => Promise<FlywayConfig>;
145
+ export declare const getFlywayConfig: (mjConfig: MJConfig, tag?: string, schema?: string, dir?: string) => Promise<FlywayConfig>;
76
146
  export {};
package/dist/config.js CHANGED
@@ -16,7 +16,7 @@ const DEFAULT_CLI_CONFIG = {
16
16
  dbHost: process.env.DB_HOST ?? 'localhost',
17
17
  dbPort: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 1433,
18
18
  dbDatabase: process.env.DB_DATABASE ?? '',
19
- dbTrustServerCertificate: process.env.DB_TRUST_SERVER_CERTIFICATE === 'true' || process.env.DB_TRUST_SERVER_CERTIFICATE === '1',
19
+ dbTrustServerCertificate: (0, config_1.parseBooleanEnv)(process.env.DB_TRUST_SERVER_CERTIFICATE),
20
20
  codeGenLogin: process.env.CODEGEN_DB_USERNAME ?? '',
21
21
  codeGenPassword: process.env.CODEGEN_DB_PASSWORD ?? '',
22
22
  coreSchema: '__mj',
@@ -37,6 +37,11 @@ const mergedConfig = searchResult?.config
37
37
  const result = searchResult
38
38
  ? { ...searchResult, config: mergedConfig }
39
39
  : { config: mergedConfig, filepath: '', isEmpty: false };
40
+ // Schema placeholder configuration for cross-schema references
41
+ const schemaPlaceholderSchema = zod_1.z.object({
42
+ schema: zod_1.z.string(),
43
+ placeholder: zod_1.z.string(),
44
+ });
40
45
  // Schema for database-dependent config (required fields)
41
46
  const mjConfigSchema = zod_1.z.object({
42
47
  dbHost: zod_1.z.string().default('localhost'),
@@ -51,6 +56,9 @@ const mjConfigSchema = zod_1.z.object({
51
56
  mjRepoUrl: zod_1.z.string().url().catch(MJ_REPO_URL),
52
57
  baselineVersion: zod_1.z.string().optional().default('202601122300'),
53
58
  baselineOnMigrate: zod_1.z.boolean().optional().default(true),
59
+ SQLOutput: zod_1.z.object({
60
+ schemaPlaceholders: zod_1.z.array(schemaPlaceholderSchema).optional(),
61
+ }).passthrough().optional(),
54
62
  });
55
63
  // Schema for non-database commands (all fields optional)
56
64
  const mjConfigSchemaOptional = zod_1.z.object({
@@ -66,6 +74,9 @@ const mjConfigSchemaOptional = zod_1.z.object({
66
74
  mjRepoUrl: zod_1.z.string().url().catch(MJ_REPO_URL),
67
75
  baselineVersion: zod_1.z.string().optional().default('202601122300'),
68
76
  baselineOnMigrate: zod_1.z.boolean().optional().default(true),
77
+ SQLOutput: zod_1.z.object({
78
+ schemaPlaceholders: zod_1.z.array(schemaPlaceholderSchema).optional(),
79
+ }).passthrough().optional(),
69
80
  });
70
81
  // Don't validate at module load - let commands decide when they need validated config
71
82
  exports.config = result?.config;
@@ -111,8 +122,12 @@ const createFlywayUrl = (mjConfig) => {
111
122
  return `jdbc:sqlserver://${mjConfig.dbHost}:${mjConfig.dbPort}; databaseName=${mjConfig.dbDatabase}${mjConfig.dbTrustServerCertificate ? '; trustServerCertificate=true' : ''}`;
112
123
  };
113
124
  exports.createFlywayUrl = createFlywayUrl;
114
- const getFlywayConfig = async (mjConfig, tag) => {
125
+ const getFlywayConfig = async (mjConfig, tag, schema, dir) => {
126
+ const targetSchema = schema || mjConfig.coreSchema;
115
127
  let location = mjConfig.migrationsLocation;
128
+ if (dir && !tag) {
129
+ location = dir.startsWith('filesystem:') ? dir : `filesystem:${dir}`;
130
+ }
116
131
  if (tag) {
117
132
  // when tag is set, we want to fetch migrations from the github repo using the tag specified
118
133
  // we save those to a tmp dir and set that tmp dir as the migration location
@@ -122,18 +137,58 @@ const getFlywayConfig = async (mjConfig, tag) => {
122
137
  await git.clone(mjConfig.mjRepoUrl, tmp, ['--sparse', '--depth=1', '--branch', branch]);
123
138
  await git.raw(['sparse-checkout', 'set', 'migrations']);
124
139
  location = `filesystem:${tmp}`;
140
+ if (dir) {
141
+ const subPath = dir.replace(/^filesystem:/, '').replace(/^\.\//, '');
142
+ location = `filesystem:${tmp}/${subPath}`;
143
+ }
144
+ }
145
+ // Build advanced config - only include properties with defined values
146
+ const advancedConfig = {
147
+ schemas: [targetSchema],
148
+ };
149
+ // Only add cleanDisabled if explicitly set to false
150
+ if (mjConfig.cleanDisabled === false) {
151
+ advancedConfig.cleanDisabled = false;
152
+ }
153
+ // Enable custom placeholders for cross-schema references (e.g., BCSaaS → MJ)
154
+ // Uses schemaPlaceholders from mj.config.cjs SQLOutput configuration if available
155
+ // Falls back to legacy behavior (mjSchema placeholder) for backward compatibility
156
+ // NOTE: The Map entries are swapped (value, key) due to a bug in node-flyway's Map.forEach iteration
157
+ const schemaPlaceholders = mjConfig.SQLOutput?.schemaPlaceholders;
158
+ if (schemaPlaceholders && schemaPlaceholders.length > 0) {
159
+ // Use schemaPlaceholders from config (new behavior - supports BCSaaS and other extensions)
160
+ // NOTE: node-flyway seems to hang when placeHolders Map is provided
161
+ // Placeholders will be applied by the diagnostic Flyway CLI call instead
162
+ // advancedConfig.placeHolderReplacement = true;
163
+ // const placeholderMap = new Map();
164
+ //
165
+ // schemaPlaceholders.forEach(({ schema: schemaName, placeholder }) => {
166
+ // const cleanPlaceholder = placeholder.replace(/^\$\{|\}$/g, '');
167
+ // if (cleanPlaceholder.startsWith('flyway:')) return;
168
+ // placeholderMap.set(cleanPlaceholder, schemaName);
169
+ // });
170
+ //
171
+ // if (placeholderMap.size > 0) {
172
+ // advancedConfig.placeHolders = placeholderMap;
173
+ // }
174
+ }
175
+ else if (schema && schema !== mjConfig.coreSchema) {
176
+ // Legacy behavior: Add mjSchema placeholder for non-core schemas
177
+ advancedConfig.placeHolderReplacement = true;
178
+ // Map('mjSchema' => '__mj') generates -placeholders.mjSchema=__mj
179
+ advancedConfig.placeHolders = new Map([['mjSchema', mjConfig.coreSchema]]);
125
180
  }
181
+ // Merge additional required properties into advancedConfig
182
+ advancedConfig.baselineVersion = mjConfig.baselineVersion;
183
+ advancedConfig.baselineOnMigrate = mjConfig.baselineOnMigrate;
126
184
  return {
127
185
  url: (0, exports.createFlywayUrl)(mjConfig),
128
186
  user: mjConfig.codeGenLogin,
129
187
  password: mjConfig.codeGenPassword,
188
+ // Note: Flyway uses the first schema in advanced.schemas as the default schema
189
+ // Setting both defaultSchema and schemas causes issues due to node-flyway's filtering logic
130
190
  migrationLocations: [location],
131
- advanced: {
132
- schemas: [mjConfig.coreSchema],
133
- cleanDisabled: mjConfig.cleanDisabled === false ? false : undefined,
134
- baselineVersion: mjConfig.baselineVersion,
135
- baselineOnMigrate: mjConfig.baselineOnMigrate,
136
- },
191
+ advanced: advancedConfig,
137
192
  };
138
193
  };
139
194
  exports.getFlywayConfig = getFlywayConfig;
@@ -157,6 +157,65 @@
157
157
  "index.js"
158
158
  ]
159
159
  },
160
+ "codegen:manifest": {
161
+ "aliases": [],
162
+ "args": {},
163
+ "description": "Generate a class registrations manifest to prevent tree-shaking of @RegisterClass decorated classes",
164
+ "examples": [
165
+ "<%= config.bin %> <%= command.id %> --output ./src/generated/class-registrations-manifest.ts",
166
+ "<%= config.bin %> <%= command.id %> --appDir ./packages/MJAPI --output ./packages/MJAPI/src/generated/class-registrations-manifest.ts",
167
+ "<%= config.bin %> <%= command.id %> --filter BaseEngine --filter BaseAction"
168
+ ],
169
+ "flags": {
170
+ "output": {
171
+ "char": "o",
172
+ "description": "Output manifest file path",
173
+ "name": "output",
174
+ "default": "./src/generated/class-registrations-manifest.ts",
175
+ "hasDynamicHelp": false,
176
+ "multiple": false,
177
+ "type": "option"
178
+ },
179
+ "appDir": {
180
+ "char": "a",
181
+ "description": "App directory containing package.json (defaults to current directory)",
182
+ "name": "appDir",
183
+ "hasDynamicHelp": false,
184
+ "multiple": false,
185
+ "type": "option"
186
+ },
187
+ "filter": {
188
+ "char": "f",
189
+ "description": "Only include classes with this base class (can be repeated)",
190
+ "name": "filter",
191
+ "hasDynamicHelp": false,
192
+ "multiple": true,
193
+ "type": "option"
194
+ },
195
+ "quiet": {
196
+ "char": "q",
197
+ "description": "Suppress progress output",
198
+ "name": "quiet",
199
+ "allowNo": false,
200
+ "type": "boolean"
201
+ }
202
+ },
203
+ "hasDynamicHelp": false,
204
+ "hiddenAliases": [],
205
+ "id": "codegen:manifest",
206
+ "pluginAlias": "@memberjunction/cli",
207
+ "pluginName": "@memberjunction/cli",
208
+ "pluginType": "core",
209
+ "strict": true,
210
+ "enableJsonFlag": false,
211
+ "isESM": false,
212
+ "relativePath": [
213
+ "dist",
214
+ "commands",
215
+ "codegen",
216
+ "manifest.js"
217
+ ]
218
+ },
160
219
  "dbdoc:analyze": {
161
220
  "aliases": [],
162
221
  "args": {},
@@ -594,12 +653,14 @@
594
653
  "status.js"
595
654
  ]
596
655
  },
597
- "install": {
656
+ "migrate": {
598
657
  "aliases": [],
599
658
  "args": {},
600
- "description": "Install MemberJunction",
659
+ "description": "Migrate MemberJunction database to latest version",
601
660
  "examples": [
602
- "<%= config.bin %> <%= command.id %>\n"
661
+ "<%= config.bin %> <%= command.id %>\n",
662
+ "<%= config.bin %> <%= command.id %> --schema __BCSaaS --dir ./migrations/v1\n",
663
+ "<%= config.bin %> <%= command.id %> --schema __BCSaaS --tag v1.0.0\n"
603
664
  ],
604
665
  "flags": {
605
666
  "verbose": {
@@ -608,11 +669,34 @@
608
669
  "name": "verbose",
609
670
  "allowNo": false,
610
671
  "type": "boolean"
672
+ },
673
+ "tag": {
674
+ "char": "t",
675
+ "description": "Version tag to use for running remote migrations",
676
+ "name": "tag",
677
+ "hasDynamicHelp": false,
678
+ "multiple": false,
679
+ "type": "option"
680
+ },
681
+ "schema": {
682
+ "char": "s",
683
+ "description": "Target schema (overrides coreSchema from config)",
684
+ "name": "schema",
685
+ "hasDynamicHelp": false,
686
+ "multiple": false,
687
+ "type": "option"
688
+ },
689
+ "dir": {
690
+ "description": "Migration source directory (overrides migrationsLocation from config)",
691
+ "name": "dir",
692
+ "hasDynamicHelp": false,
693
+ "multiple": false,
694
+ "type": "option"
611
695
  }
612
696
  },
613
697
  "hasDynamicHelp": false,
614
698
  "hiddenAliases": [],
615
- "id": "install",
699
+ "id": "migrate",
616
700
  "pluginAlias": "@memberjunction/cli",
617
701
  "pluginName": "@memberjunction/cli",
618
702
  "pluginType": "core",
@@ -622,14 +706,14 @@
622
706
  "relativePath": [
623
707
  "dist",
624
708
  "commands",
625
- "install",
709
+ "migrate",
626
710
  "index.js"
627
711
  ]
628
712
  },
629
- "migrate": {
713
+ "install": {
630
714
  "aliases": [],
631
715
  "args": {},
632
- "description": "Migrate MemberJunction database to latest version",
716
+ "description": "Install MemberJunction",
633
717
  "examples": [
634
718
  "<%= config.bin %> <%= command.id %>\n"
635
719
  ],
@@ -640,19 +724,11 @@
640
724
  "name": "verbose",
641
725
  "allowNo": false,
642
726
  "type": "boolean"
643
- },
644
- "tag": {
645
- "char": "t",
646
- "description": "Version tag to use for running remote migrations",
647
- "name": "tag",
648
- "hasDynamicHelp": false,
649
- "multiple": false,
650
- "type": "option"
651
727
  }
652
728
  },
653
729
  "hasDynamicHelp": false,
654
730
  "hiddenAliases": [],
655
- "id": "migrate",
731
+ "id": "install",
656
732
  "pluginAlias": "@memberjunction/cli",
657
733
  "pluginName": "@memberjunction/cli",
658
734
  "pluginType": "core",
@@ -662,7 +738,7 @@
662
738
  "relativePath": [
663
739
  "dist",
664
740
  "commands",
665
- "migrate",
741
+ "install",
666
742
  "index.js"
667
743
  ]
668
744
  },
@@ -2500,5 +2576,5 @@
2500
2576
  ]
2501
2577
  }
2502
2578
  },
2503
- "version": "3.2.0"
2579
+ "version": "3.4.0"
2504
2580
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memberjunction/cli",
3
- "version": "3.2.0",
3
+ "version": "3.4.0",
4
4
  "description": "MemberJunction command line tools",
5
5
  "keywords": [
6
6
  "oclif"
@@ -51,15 +51,15 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@inquirer/prompts": "^5.0.1",
54
- "@memberjunction/ai-cli": "3.2.0",
55
- "@memberjunction/codegen-lib": "3.2.0",
56
- "@memberjunction/config": "3.2.0",
57
- "@memberjunction/core": "3.2.0",
58
- "@memberjunction/db-auto-doc": "3.2.0",
59
- "@memberjunction/metadata-sync": "3.2.0",
60
- "@memberjunction/query-gen": "3.2.0",
61
- "@memberjunction/sqlserver-dataprovider": "3.2.0",
62
- "@memberjunction/testing-cli": "3.2.0",
54
+ "@memberjunction/ai-cli": "3.4.0",
55
+ "@memberjunction/codegen-lib": "3.4.0",
56
+ "@memberjunction/config": "3.4.0",
57
+ "@memberjunction/core": "3.4.0",
58
+ "@memberjunction/db-auto-doc": "3.4.0",
59
+ "@memberjunction/metadata-sync": "3.4.0",
60
+ "@memberjunction/query-gen": "3.4.0",
61
+ "@memberjunction/sqlserver-dataprovider": "3.4.0",
62
+ "@memberjunction/testing-cli": "3.4.0",
63
63
  "@oclif/core": "^3",
64
64
  "@oclif/plugin-help": "^6",
65
65
  "@oclif/plugin-version": "^2.0.17",