@flusys/nestjs-core 7.1.0 → 8.0.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 +2 -2
- package/config/index.d.ts +1 -1
- package/constants/index.d.ts +1 -1
- package/docs/docs.config.d.ts +1 -1
- package/docs/index.d.ts +1 -1
- package/fesm/229.js +729 -0
- package/fesm/config/index.js +126 -1
- package/fesm/constants/index.js +29 -1
- package/fesm/docs/index.js +288 -1
- package/fesm/index.js +124 -5
- package/fesm/interfaces/index.js +18 -4
- package/fesm/migration/cli.js +19 -2
- package/fesm/migration/index.js +45 -3
- package/fesm/runtime.js +85 -0
- package/fesm/utils/index.js +131 -1
- package/index.d.ts +5 -5
- package/interfaces/app-config.interfaces.d.ts +1 -1
- package/interfaces/index.d.ts +4 -4
- package/interfaces/migration.interface.d.ts +2 -2
- package/migration/cli.d.ts +0 -1
- package/migration/datasource.factory.d.ts +1 -1
- package/migration/index.d.ts +3 -3
- package/migration/migration.cli.d.ts +1 -1
- package/migration/migration.runner.d.ts +1 -1
- package/package.json +24 -32
- package/utils/datasource-config.builder.d.ts +2 -2
- package/utils/index.d.ts +1 -1
- package/cjs/config/env-config.service.js +0 -162
- package/cjs/config/env-config.service.spec.js +0 -325
- package/cjs/config/index.js +0 -18
- package/cjs/constants/database.constants.js +0 -20
- package/cjs/constants/index.js +0 -18
- package/cjs/docs/docs.config.js +0 -276
- package/cjs/docs/docs.config.spec.js +0 -923
- package/cjs/docs/index.js +0 -18
- package/cjs/index.js +0 -23
- package/cjs/interfaces/app-config.interfaces.js +0 -4
- package/cjs/interfaces/base-entity.interface.js +0 -9
- package/cjs/interfaces/database.interface.js +0 -4
- package/cjs/interfaces/index.js +0 -21
- package/cjs/interfaces/migration.interface.js +0 -4
- package/cjs/migration/cli.js +0 -10
- package/cjs/migration/datasource.factory.js +0 -43
- package/cjs/migration/datasource.factory.spec.js +0 -113
- package/cjs/migration/index.js +0 -45
- package/cjs/migration/migration.cli.js +0 -231
- package/cjs/migration/migration.cli.spec.js +0 -293
- package/cjs/migration/migration.runner.js +0 -276
- package/cjs/migration/migration.runner.spec.js +0 -268
- package/cjs/utils/datasource-config.builder.js +0 -103
- package/cjs/utils/datasource-config.builder.spec.js +0 -232
- package/cjs/utils/index.js +0 -18
- package/fesm/config/env-config.service.js +0 -112
- package/fesm/config/env-config.service.spec.js +0 -321
- package/fesm/constants/database.constants.js +0 -2
- package/fesm/docs/docs.config.js +0 -258
- package/fesm/docs/docs.config.spec.js +0 -919
- package/fesm/interfaces/app-config.interfaces.js +0 -1
- package/fesm/interfaces/base-entity.interface.js +0 -6
- package/fesm/interfaces/database.interface.js +0 -1
- package/fesm/interfaces/migration.interface.js +0 -1
- package/fesm/migration/datasource.factory.js +0 -25
- package/fesm/migration/datasource.factory.spec.js +0 -109
- package/fesm/migration/migration.cli.js +0 -180
- package/fesm/migration/migration.cli.spec.js +0 -289
- package/fesm/migration/migration.runner.js +0 -202
- package/fesm/migration/migration.runner.spec.js +0 -264
- package/fesm/utils/datasource-config.builder.js +0 -84
- 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
|
-
});
|