@flusys/nestjs-core 7.0.2 → 8.0.0-beta.1

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.
Files changed (70) hide show
  1. package/README.md +2 -2
  2. package/config/index.d.ts +1 -1
  3. package/constants/index.d.ts +1 -1
  4. package/docs/docs.config.d.ts +1 -1
  5. package/docs/index.d.ts +1 -1
  6. package/fesm/229.js +729 -0
  7. package/fesm/config/index.js +126 -1
  8. package/fesm/constants/index.js +29 -1
  9. package/fesm/docs/index.js +288 -1
  10. package/fesm/index.js +124 -5
  11. package/fesm/interfaces/index.js +18 -4
  12. package/fesm/migration/cli.js +19 -2
  13. package/fesm/migration/index.js +45 -3
  14. package/fesm/runtime.js +85 -0
  15. package/fesm/utils/index.js +131 -1
  16. package/index.d.ts +5 -5
  17. package/interfaces/app-config.interfaces.d.ts +2 -1
  18. package/interfaces/database.interface.d.ts +1 -0
  19. package/interfaces/index.d.ts +4 -4
  20. package/interfaces/migration.interface.d.ts +2 -2
  21. package/migration/cli.d.ts +0 -1
  22. package/migration/datasource.factory.d.ts +1 -1
  23. package/migration/index.d.ts +3 -3
  24. package/migration/migration.cli.d.ts +1 -1
  25. package/migration/migration.runner.d.ts +1 -1
  26. package/package.json +25 -33
  27. package/utils/datasource-config.builder.d.ts +2 -2
  28. package/utils/index.d.ts +1 -1
  29. package/cjs/config/env-config.service.js +0 -162
  30. package/cjs/config/env-config.service.spec.js +0 -325
  31. package/cjs/config/index.js +0 -18
  32. package/cjs/constants/database.constants.js +0 -20
  33. package/cjs/constants/index.js +0 -18
  34. package/cjs/docs/docs.config.js +0 -276
  35. package/cjs/docs/docs.config.spec.js +0 -923
  36. package/cjs/docs/index.js +0 -18
  37. package/cjs/index.js +0 -23
  38. package/cjs/interfaces/app-config.interfaces.js +0 -4
  39. package/cjs/interfaces/base-entity.interface.js +0 -9
  40. package/cjs/interfaces/database.interface.js +0 -4
  41. package/cjs/interfaces/index.js +0 -21
  42. package/cjs/interfaces/migration.interface.js +0 -4
  43. package/cjs/migration/cli.js +0 -10
  44. package/cjs/migration/datasource.factory.js +0 -43
  45. package/cjs/migration/datasource.factory.spec.js +0 -113
  46. package/cjs/migration/index.js +0 -45
  47. package/cjs/migration/migration.cli.js +0 -231
  48. package/cjs/migration/migration.cli.spec.js +0 -293
  49. package/cjs/migration/migration.runner.js +0 -276
  50. package/cjs/migration/migration.runner.spec.js +0 -268
  51. package/cjs/utils/datasource-config.builder.js +0 -103
  52. package/cjs/utils/datasource-config.builder.spec.js +0 -232
  53. package/cjs/utils/index.js +0 -18
  54. package/fesm/config/env-config.service.js +0 -112
  55. package/fesm/config/env-config.service.spec.js +0 -321
  56. package/fesm/constants/database.constants.js +0 -2
  57. package/fesm/docs/docs.config.js +0 -258
  58. package/fesm/docs/docs.config.spec.js +0 -919
  59. package/fesm/interfaces/app-config.interfaces.js +0 -1
  60. package/fesm/interfaces/base-entity.interface.js +0 -6
  61. package/fesm/interfaces/database.interface.js +0 -1
  62. package/fesm/interfaces/migration.interface.js +0 -1
  63. package/fesm/migration/datasource.factory.js +0 -25
  64. package/fesm/migration/datasource.factory.spec.js +0 -109
  65. package/fesm/migration/migration.cli.js +0 -180
  66. package/fesm/migration/migration.cli.spec.js +0 -289
  67. package/fesm/migration/migration.runner.js +0 -202
  68. package/fesm/migration/migration.runner.spec.js +0 -264
  69. package/fesm/utils/datasource-config.builder.js +0 -84
  70. package/fesm/utils/datasource-config.builder.spec.js +0 -228
@@ -1 +0,0 @@
1
- /** DataSource service options for multi-tenant datasource */ export { };
@@ -1,6 +0,0 @@
1
- /**
2
- * Entity Contract Interfaces
3
- *
4
- * These interfaces define the standard fields that entities should have.
5
- * Use them as contracts when implementing new entities.
6
- */ /** Base entity with UUID, timestamps, and soft delete */ /** Entity with company relation (for multi-company support) */ export { };
@@ -1 +0,0 @@
1
- export { };
@@ -1 +0,0 @@
1
- export { };
@@ -1,25 +0,0 @@
1
- import { DataSource } from 'typeorm';
2
- import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
3
- import { buildDataSourceOptions, getDatabaseForTenant, getMigrationsGlobPattern, resolveDatabaseConnection, resolveEntities } from '../utils/datasource-config.builder';
4
- export function createMigrationDataSource(options) {
5
- const { config, tenantId } = options;
6
- const { database, tenant, tenantConfig } = getDatabaseForTenant(config, tenantId);
7
- console.log(`\n📦 Database: ${database}${tenant ? ` [${tenant.name}]` : ' [default]'}`);
8
- console.log(`📁 Migrations: migrations/${tenantId || 'default'}/`);
9
- if (config.bootstrapAppConfig?.databaseMode === 'multi-tenant' && !tenantId && config.tenants && config.tenants.length > 0) {
10
- console.log(`\n💡 Multi-tenant mode. Available tenants:`);
11
- config.tenants.forEach((t)=>console.log(` - TENANT_ID=${t.id} → ${t.database}`));
12
- }
13
- const dbConfig = resolveDatabaseConnection(config, tenantId);
14
- const entities = resolveEntities(config, tenantConfig);
15
- const dsOptions = buildDataSourceOptions(dbConfig, entities, getMigrationsGlobPattern(config.migrationsPath, tenantId), config.migrationsTableName);
16
- return new DataSource({
17
- ...dsOptions,
18
- namingStrategy: new SnakeNamingStrategy()
19
- });
20
- }
21
- export async function initializeDataSource(options) {
22
- const ds = createMigrationDataSource(options);
23
- await ds.initialize();
24
- return ds;
25
- }
@@ -1,109 +0,0 @@
1
- const mockInitialize = jest.fn();
2
- const dataSourceCtor = jest.fn().mockImplementation((options)=>({
3
- options,
4
- initialize: mockInitialize
5
- }));
6
- jest.mock('typeorm', ()=>({
7
- DataSource: jest.fn().mockImplementation((options)=>dataSourceCtor(options))
8
- }));
9
- jest.mock('typeorm-naming-strategies', ()=>({
10
- SnakeNamingStrategy: jest.fn().mockImplementation(()=>({
11
- __brand: 'SnakeNamingStrategy'
12
- }))
13
- }));
14
- import { createMigrationDataSource, initializeDataSource } from './datasource.factory';
15
- describe('datasource.factory', ()=>{
16
- const baseConfig = {
17
- defaultDatabaseConfig: {
18
- type: 'postgres',
19
- host: 'localhost',
20
- port: 5432,
21
- username: 'root',
22
- password: 'pw',
23
- database: 'flusys_default'
24
- },
25
- migrationsPath: 'migrations',
26
- entities: [
27
- Object
28
- ]
29
- };
30
- beforeEach(()=>{
31
- jest.clearAllMocks();
32
- mockInitialize.mockResolvedValue(undefined);
33
- });
34
- describe('createMigrationDataSource', ()=>{
35
- it('should build a DataSource with the resolved connection and a snake naming strategy', ()=>{
36
- const ds = createMigrationDataSource({
37
- config: baseConfig
38
- });
39
- expect(dataSourceCtor).toHaveBeenCalledWith(expect.objectContaining({
40
- type: 'postgres',
41
- host: 'localhost',
42
- database: 'flusys_default',
43
- synchronize: false,
44
- migrations: [
45
- 'migrations/default/*{.ts,.js}'
46
- ]
47
- }));
48
- expect(ds.options.namingStrategy).toEqual({
49
- __brand: 'SnakeNamingStrategy'
50
- });
51
- });
52
- it('should resolve tenant-specific database and migrations path', ()=>{
53
- const config = {
54
- ...baseConfig,
55
- tenants: [
56
- {
57
- id: 'tenant-a',
58
- database: 'db_a'
59
- }
60
- ]
61
- };
62
- createMigrationDataSource({
63
- config,
64
- tenantId: 'tenant-a'
65
- });
66
- expect(dataSourceCtor).toHaveBeenCalledWith(expect.objectContaining({
67
- database: 'db_a',
68
- migrations: [
69
- 'migrations/tenant-a/*{.ts,.js}'
70
- ]
71
- }));
72
- });
73
- it('should resolve entities via an entity resolver function when provided', ()=>{
74
- const resolver = jest.fn().mockReturnValue([
75
- Number,
76
- String
77
- ]);
78
- const config = {
79
- ...baseConfig,
80
- entities: resolver
81
- };
82
- createMigrationDataSource({
83
- config
84
- });
85
- expect(resolver).toHaveBeenCalled();
86
- expect(dataSourceCtor).toHaveBeenCalledWith(expect.objectContaining({
87
- entities: [
88
- Number,
89
- String
90
- ]
91
- }));
92
- });
93
- });
94
- describe('initializeDataSource', ()=>{
95
- it('should create and initialize the DataSource', async ()=>{
96
- const ds = await initializeDataSource({
97
- config: baseConfig
98
- });
99
- expect(mockInitialize).toHaveBeenCalledTimes(1);
100
- expect(ds).toBeDefined();
101
- });
102
- it('should propagate initialization errors', async ()=>{
103
- mockInitialize.mockRejectedValue(new Error('connection refused'));
104
- await expect(initializeDataSource({
105
- config: baseConfig
106
- })).rejects.toThrow('connection refused');
107
- });
108
- });
109
- });
@@ -1,180 +0,0 @@
1
- import * as path from 'path';
2
- import { envConfig } from '../config';
3
- import { getDatabaseForTenant, getMigrationsFolderPath } from '../utils';
4
- import { generateMigration, migrationStatus, resetDatabase, revertMigration, runForAllTenants, runMigrations } from './migration.runner';
5
- function showHelp() {
6
- console.log(`
7
- Migration CLI
8
-
9
- Usage:
10
- npm run migration <command> [options]
11
-
12
- Commands:
13
- generate --name=<Name> Generate migration
14
- run Run pending migrations
15
- revert Revert last migration
16
- reset Drop db, wipe migration files, recreate db, generate + run initial migration (dev only)
17
- status Show migration status
18
- run:all Run for all tenants
19
- revert:all Revert for all tenants
20
- status:all Status for all tenants
21
-
22
- Options:
23
- --config=<path> Migration config file (default: src/persistence/migration.config.ts)
24
- --name=<Name> Migration name (for generate command)
25
- --force Required to run 'reset' when MODE is not 'dev'
26
-
27
- Environment:
28
- TENANT_ID Target tenant (default: uses default database)
29
- MIGRATION_CONFIG Default config path (overrides auto-detection)
30
-
31
- Examples:
32
- npm run migration generate --name=CreateUsers
33
- npm run migration -- generate --name=CreateUsers --config=src/persistence/migration.config.ts
34
- TENANT_ID=tenant1 npm run migration run
35
- npm run migration run:all
36
- npm run migration reset
37
- `);
38
- }
39
- function parseArgs() {
40
- const args = process.argv.slice(2);
41
- let command;
42
- let name;
43
- let configPath;
44
- let datasourcePath;
45
- let force = false;
46
- for(let i = 0; i < args.length; i++){
47
- const arg = args[i];
48
- if (arg.startsWith('--name=')) {
49
- name = arg.split('=')[1];
50
- } else if (arg.startsWith('--config=')) {
51
- configPath = arg.split('=')[1];
52
- } else if (arg.startsWith('--datasource=')) {
53
- datasourcePath = arg.split('=')[1];
54
- } else if (arg === '--force') {
55
- force = true;
56
- } else if (!arg.startsWith('-') && !command) {
57
- command = arg;
58
- }
59
- }
60
- return {
61
- command,
62
- name,
63
- configPath,
64
- datasourcePath,
65
- force
66
- };
67
- }
68
- async function loadConfig(configPath) {
69
- const absolutePath = path.isAbsolute(configPath) ? configPath : path.resolve(process.cwd(), configPath);
70
- // Use require() for CommonJS compatibility with ts-node
71
- // eslint-disable-next-line @typescript-eslint/no-require-imports
72
- const module = require(absolutePath);
73
- return module.migrationConfig || module.default?.migrationConfig || module;
74
- }
75
- export async function runMigrationCli(configOrPath) {
76
- const { command, name, configPath: argConfigPath, datasourcePath, force } = parseArgs();
77
- const tenantId = envConfig.getTenantId() ?? undefined;
78
- // npm passes --name=X as npm_config_name
79
- const migrationName = envConfig.tryGetValue('npm_config_name') || envConfig.tryGetValue('MIGRATION_NAME') || name;
80
- // Resolve config
81
- let config;
82
- let dsPath = datasourcePath;
83
- if (typeof configOrPath === 'string') {
84
- config = await loadConfig(configOrPath);
85
- dsPath = dsPath || configOrPath;
86
- } else if (configOrPath) {
87
- config = configOrPath;
88
- } else if (argConfigPath) {
89
- config = await loadConfig(argConfigPath);
90
- dsPath = dsPath || argConfigPath;
91
- } else {
92
- // Try paths in order: env var, then common defaults
93
- const envConfigPath = envConfig.tryGetValue('MIGRATION_CONFIG');
94
- const defaultPaths = [
95
- envConfigPath,
96
- 'src/persistence/migration.config.ts',
97
- 'src/persistence/migration.config.js',
98
- 'persistence/migration.config.ts',
99
- 'persistence/migration.config.js'
100
- ].filter(Boolean);
101
- let loaded = false;
102
- for (const tryPath of defaultPaths){
103
- try {
104
- config = await loadConfig(tryPath);
105
- dsPath = dsPath || tryPath;
106
- loaded = true;
107
- break;
108
- } catch {
109
- // Try next path
110
- }
111
- }
112
- if (!loaded) {
113
- console.error('❌ Config not found. Use --config=<path> or set MIGRATION_CONFIG env var');
114
- console.error(' Tried paths:', defaultPaths.join(', '));
115
- showHelp();
116
- process.exit(1);
117
- }
118
- }
119
- if (!command) {
120
- showHelp();
121
- return;
122
- }
123
- // Multi-tenant batch commands
124
- if (command.endsWith(':all')) {
125
- if (config.bootstrapAppConfig?.databaseMode === 'single') {
126
- console.error('❌ Multi-tenant mode not enabled');
127
- process.exit(1);
128
- }
129
- const action = command.split(':')[0];
130
- const results = await runForAllTenants(config, action);
131
- if (results.some((r)=>!r.success)) process.exit(1);
132
- return;
133
- }
134
- // Show target info
135
- const { database } = getDatabaseForTenant(config, tenantId);
136
- const folder = getMigrationsFolderPath(config.migrationsPath, tenantId);
137
- console.log(`\n📦 Database: ${database}`);
138
- console.log(`📁 Migrations: ${path.relative(process.cwd(), folder)}`);
139
- switch(command){
140
- case 'generate':
141
- if (!migrationName) {
142
- console.error('❌ Migration name required. Use --name=<Name>');
143
- process.exit(1);
144
- }
145
- await generateMigration(config, migrationName, tenantId, dsPath);
146
- break;
147
- case 'run':
148
- {
149
- const result = await runMigrations(config, tenantId);
150
- if (!result.success) process.exit(1);
151
- break;
152
- }
153
- case 'revert':
154
- {
155
- const result = await revertMigration(config, tenantId);
156
- if (!result.success) process.exit(1);
157
- break;
158
- }
159
- case 'reset':
160
- {
161
- if (envConfig.isProduction() && !force) {
162
- console.error('❌ Refusing to reset database outside dev mode. Pass --force to override.');
163
- process.exit(1);
164
- }
165
- const result = await resetDatabase(config, tenantId);
166
- if (!result.success) process.exit(1);
167
- await generateMigration(config, migrationName || 'Init', tenantId, dsPath);
168
- const runResult = await runMigrations(config, tenantId);
169
- if (!runResult.success) process.exit(1);
170
- break;
171
- }
172
- case 'status':
173
- await migrationStatus(config, tenantId);
174
- break;
175
- default:
176
- console.error(`❌ Unknown command: ${command}`);
177
- showHelp();
178
- process.exit(1);
179
- }
180
- }
@@ -1,289 +0,0 @@
1
- const mockGetTenantId = jest.fn();
2
- const mockTryGetValue = jest.fn();
3
- const mockIsProduction = jest.fn();
4
- jest.mock('../config', ()=>({
5
- envConfig: {
6
- getTenantId: (...args)=>mockGetTenantId(...args),
7
- tryGetValue: (...args)=>mockTryGetValue(...args),
8
- isProduction: (...args)=>mockIsProduction(...args)
9
- }
10
- }));
11
- const mockGetDatabaseForTenant = jest.fn();
12
- const mockGetMigrationsFolderPath = jest.fn();
13
- jest.mock('../utils', ()=>({
14
- getDatabaseForTenant: (...args)=>mockGetDatabaseForTenant(...args),
15
- getMigrationsFolderPath: (...args)=>mockGetMigrationsFolderPath(...args)
16
- }));
17
- const mockGenerateMigration = jest.fn();
18
- const mockRunMigrations = jest.fn();
19
- const mockRevertMigration = jest.fn();
20
- const mockResetDatabase = jest.fn();
21
- const mockMigrationStatus = jest.fn();
22
- const mockRunForAllTenants = jest.fn();
23
- jest.mock('./migration.runner', ()=>({
24
- generateMigration: (...args)=>mockGenerateMigration(...args),
25
- runMigrations: (...args)=>mockRunMigrations(...args),
26
- revertMigration: (...args)=>mockRevertMigration(...args),
27
- resetDatabase: (...args)=>mockResetDatabase(...args),
28
- migrationStatus: (...args)=>mockMigrationStatus(...args),
29
- runForAllTenants: (...args)=>mockRunForAllTenants(...args)
30
- }));
31
- import { runMigrationCli } from './migration.cli';
32
- describe('runMigrationCli', ()=>{
33
- const baseConfig = {
34
- defaultDatabaseConfig: {
35
- type: 'postgres',
36
- database: 'flusys_default'
37
- },
38
- migrationsPath: 'migrations',
39
- entities: []
40
- };
41
- let logSpy;
42
- let errorSpy;
43
- let exitSpy;
44
- let originalArgv;
45
- beforeEach(()=>{
46
- jest.clearAllMocks();
47
- originalArgv = process.argv;
48
- logSpy = jest.spyOn(console, 'log').mockImplementation(()=>undefined);
49
- errorSpy = jest.spyOn(console, 'error').mockImplementation(()=>undefined);
50
- exitSpy = jest.spyOn(process, 'exit').mockImplementation(()=>{
51
- throw new Error('process.exit called');
52
- });
53
- mockGetTenantId.mockReturnValue(null);
54
- mockTryGetValue.mockReturnValue('');
55
- mockIsProduction.mockReturnValue(false);
56
- mockGetDatabaseForTenant.mockReturnValue({
57
- database: 'flusys_default'
58
- });
59
- mockGetMigrationsFolderPath.mockReturnValue('migrations/default');
60
- });
61
- afterEach(()=>{
62
- process.argv = originalArgv;
63
- logSpy.mockRestore();
64
- errorSpy.mockRestore();
65
- exitSpy.mockRestore();
66
- });
67
- function setArgv(...args) {
68
- process.argv = [
69
- 'node',
70
- 'cli.js',
71
- ...args
72
- ];
73
- }
74
- it('should print help and return when no command is given', async ()=>{
75
- setArgv();
76
- await runMigrationCli(baseConfig);
77
- expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Migration CLI'));
78
- expect(mockRunMigrations).not.toHaveBeenCalled();
79
- });
80
- it('should run migrations for the default tenant', async ()=>{
81
- setArgv('run');
82
- mockRunMigrations.mockResolvedValue({
83
- success: true,
84
- migrationsRun: []
85
- });
86
- await runMigrationCli(baseConfig);
87
- expect(mockRunMigrations).toHaveBeenCalledWith(baseConfig, undefined);
88
- expect(exitSpy).not.toHaveBeenCalled();
89
- });
90
- it('should exit with code 1 when run fails', async ()=>{
91
- setArgv('run');
92
- mockRunMigrations.mockResolvedValue({
93
- success: false,
94
- error: 'boom'
95
- });
96
- await expect(runMigrationCli(baseConfig)).rejects.toThrow('process.exit called');
97
- expect(exitSpy).toHaveBeenCalledWith(1);
98
- });
99
- it('should revert the last migration', async ()=>{
100
- setArgv('revert');
101
- mockRevertMigration.mockResolvedValue({
102
- success: true
103
- });
104
- await runMigrationCli(baseConfig);
105
- expect(mockRevertMigration).toHaveBeenCalledWith(baseConfig, undefined);
106
- });
107
- it('should exit with code 1 when revert fails', async ()=>{
108
- setArgv('revert');
109
- mockRevertMigration.mockResolvedValue({
110
- success: false,
111
- error: 'nothing to revert'
112
- });
113
- await expect(runMigrationCli(baseConfig)).rejects.toThrow('process.exit called');
114
- expect(exitSpy).toHaveBeenCalledWith(1);
115
- });
116
- it('should report migration status', async ()=>{
117
- setArgv('status');
118
- mockMigrationStatus.mockResolvedValue({
119
- success: true,
120
- migrationsRun: [
121
- 'Up to date'
122
- ]
123
- });
124
- await runMigrationCli(baseConfig);
125
- expect(mockMigrationStatus).toHaveBeenCalledWith(baseConfig, undefined);
126
- });
127
- it('should generate a migration using the --name flag', async ()=>{
128
- setArgv('generate', '--name=AddProducts');
129
- mockGenerateMigration.mockResolvedValue(undefined);
130
- await runMigrationCli(baseConfig);
131
- expect(mockGenerateMigration).toHaveBeenCalledWith(baseConfig, 'AddProducts', undefined, undefined);
132
- });
133
- it('should prefer npm_config_name over --name', async ()=>{
134
- setArgv('generate', '--name=Ignored');
135
- mockTryGetValue.mockImplementation((key)=>key === 'npm_config_name' ? 'FromNpm' : '');
136
- mockGenerateMigration.mockResolvedValue(undefined);
137
- await runMigrationCli(baseConfig);
138
- expect(mockGenerateMigration).toHaveBeenCalledWith(baseConfig, 'FromNpm', undefined, undefined);
139
- });
140
- it('should exit with code 1 when generate is missing a name', async ()=>{
141
- setArgv('generate');
142
- await expect(runMigrationCli(baseConfig)).rejects.toThrow('process.exit called');
143
- expect(exitSpy).toHaveBeenCalledWith(1);
144
- expect(mockGenerateMigration).not.toHaveBeenCalled();
145
- });
146
- it('should pass the resolved tenant id through to sub-commands', async ()=>{
147
- setArgv('run');
148
- mockGetTenantId.mockReturnValue('tenant-a');
149
- mockRunMigrations.mockResolvedValue({
150
- success: true,
151
- migrationsRun: []
152
- });
153
- await runMigrationCli(baseConfig);
154
- expect(mockRunMigrations).toHaveBeenCalledWith(baseConfig, 'tenant-a');
155
- });
156
- it('should reject reset in production without --force', async ()=>{
157
- setArgv('reset');
158
- mockIsProduction.mockReturnValue(true);
159
- await expect(runMigrationCli(baseConfig)).rejects.toThrow('process.exit called');
160
- expect(exitSpy).toHaveBeenCalledWith(1);
161
- expect(mockResetDatabase).not.toHaveBeenCalled();
162
- });
163
- it('should allow reset in production when --force is passed', async ()=>{
164
- setArgv('reset', '--force');
165
- mockIsProduction.mockReturnValue(true);
166
- mockResetDatabase.mockResolvedValue({
167
- success: true
168
- });
169
- mockGenerateMigration.mockResolvedValue(undefined);
170
- mockRunMigrations.mockResolvedValue({
171
- success: true,
172
- migrationsRun: []
173
- });
174
- await runMigrationCli(baseConfig);
175
- expect(mockResetDatabase).toHaveBeenCalledWith(baseConfig, undefined);
176
- expect(mockGenerateMigration).toHaveBeenCalledWith(baseConfig, 'Init', undefined, undefined);
177
- expect(mockRunMigrations).toHaveBeenCalledWith(baseConfig, undefined);
178
- });
179
- it('should use provided migration name as the reset init migration name', async ()=>{
180
- setArgv('reset', '--name=InitialSetup');
181
- mockResetDatabase.mockResolvedValue({
182
- success: true
183
- });
184
- mockGenerateMigration.mockResolvedValue(undefined);
185
- mockRunMigrations.mockResolvedValue({
186
- success: true,
187
- migrationsRun: []
188
- });
189
- await runMigrationCli(baseConfig);
190
- expect(mockGenerateMigration).toHaveBeenCalledWith(baseConfig, 'InitialSetup', undefined, undefined);
191
- });
192
- it('should exit with code 1 when reset fails', async ()=>{
193
- setArgv('reset');
194
- mockResetDatabase.mockResolvedValue({
195
- success: false,
196
- error: 'cannot drop'
197
- });
198
- await expect(runMigrationCli(baseConfig)).rejects.toThrow('process.exit called');
199
- expect(exitSpy).toHaveBeenCalledWith(1);
200
- expect(mockGenerateMigration).not.toHaveBeenCalled();
201
- });
202
- it('should reject unknown commands', async ()=>{
203
- setArgv('frobnicate');
204
- await expect(runMigrationCli(baseConfig)).rejects.toThrow('process.exit called');
205
- expect(exitSpy).toHaveBeenCalledWith(1);
206
- expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Unknown command'));
207
- });
208
- describe(':all commands', ()=>{
209
- it('should run for all tenants when multi-tenant mode is enabled', async ()=>{
210
- setArgv('run:all');
211
- const multiTenantConfig = {
212
- ...baseConfig,
213
- bootstrapAppConfig: {
214
- databaseMode: 'multi-tenant'
215
- }
216
- };
217
- mockRunForAllTenants.mockResolvedValue([
218
- {
219
- success: true
220
- }
221
- ]);
222
- await runMigrationCli(multiTenantConfig);
223
- expect(mockRunForAllTenants).toHaveBeenCalledWith(multiTenantConfig, 'run');
224
- });
225
- it('should reject :all commands when database mode is single', async ()=>{
226
- setArgv('run:all');
227
- const singleConfig = {
228
- ...baseConfig,
229
- bootstrapAppConfig: {
230
- databaseMode: 'single'
231
- }
232
- };
233
- await expect(runMigrationCli(singleConfig)).rejects.toThrow('process.exit called');
234
- expect(exitSpy).toHaveBeenCalledWith(1);
235
- expect(mockRunForAllTenants).not.toHaveBeenCalled();
236
- });
237
- it('should exit with code 1 when any tenant fails during :all commands', async ()=>{
238
- setArgv('status:all');
239
- mockRunForAllTenants.mockResolvedValue([
240
- {
241
- success: true
242
- },
243
- {
244
- success: false
245
- }
246
- ]);
247
- await expect(runMigrationCli(baseConfig)).rejects.toThrow('process.exit called');
248
- expect(exitSpy).toHaveBeenCalledWith(1);
249
- });
250
- it('should parse the action prefix from the :all command', async ()=>{
251
- setArgv('revert:all');
252
- mockRunForAllTenants.mockResolvedValue([
253
- {
254
- success: true
255
- }
256
- ]);
257
- await runMigrationCli(baseConfig);
258
- expect(mockRunForAllTenants).toHaveBeenCalledWith(baseConfig, 'revert');
259
- });
260
- });
261
- describe('config resolution', ()=>{
262
- it('should load config from an explicit string path argument', async ()=>{
263
- setArgv('status');
264
- const loadedConfig = {
265
- ...baseConfig,
266
- migrationsPath: 'custom-migrations'
267
- };
268
- jest.doMock(require('path').resolve(process.cwd(), 'custom-config.ts'), ()=>({
269
- migrationConfig: loadedConfig
270
- }), {
271
- virtual: true
272
- });
273
- mockMigrationStatus.mockResolvedValue({
274
- success: true,
275
- migrationsRun: [
276
- 'Up to date'
277
- ]
278
- });
279
- await runMigrationCli('custom-config.ts');
280
- expect(mockMigrationStatus).toHaveBeenCalledWith(loadedConfig, undefined);
281
- });
282
- it('should exit with code 1 and show help when no config can be resolved', async ()=>{
283
- setArgv('status');
284
- await expect(runMigrationCli()).rejects.toThrow('process.exit called');
285
- expect(exitSpy).toHaveBeenCalledWith(1);
286
- expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Config not found'));
287
- });
288
- });
289
- });