@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,202 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'child_process';
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
import { DataSource } from 'typeorm';
|
|
5
|
-
import { envConfig } from '../config';
|
|
6
|
-
import { getActiveTenants, getDatabaseForTenant, getMigrationsFolderPath, resolveDatabaseConnection } from '../utils';
|
|
7
|
-
import { initializeDataSource } from './datasource.factory';
|
|
8
|
-
function getLabel(tenantId) {
|
|
9
|
-
return tenantId ? `[${tenantId}]` : '[default]';
|
|
10
|
-
}
|
|
11
|
-
async function withDataSource(config, tenantId, operation) {
|
|
12
|
-
const { database } = getDatabaseForTenant(config, tenantId);
|
|
13
|
-
const label = getLabel(tenantId);
|
|
14
|
-
try {
|
|
15
|
-
const ds = await initializeDataSource({
|
|
16
|
-
config,
|
|
17
|
-
tenantId
|
|
18
|
-
});
|
|
19
|
-
const partial = await operation(ds, label);
|
|
20
|
-
await ds.destroy();
|
|
21
|
-
return {
|
|
22
|
-
tenantId,
|
|
23
|
-
database,
|
|
24
|
-
success: true,
|
|
25
|
-
...partial
|
|
26
|
-
};
|
|
27
|
-
} catch (error) {
|
|
28
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
29
|
-
console.error(`${label} ✗ Failed: ${errorMessage}`);
|
|
30
|
-
return {
|
|
31
|
-
tenantId,
|
|
32
|
-
database,
|
|
33
|
-
success: false,
|
|
34
|
-
error: errorMessage
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
export function ensureMigrationsFolder(basePath, tenantId) {
|
|
39
|
-
const folder = getMigrationsFolderPath(basePath, tenantId);
|
|
40
|
-
if (!fs.existsSync(folder)) {
|
|
41
|
-
fs.mkdirSync(folder, {
|
|
42
|
-
recursive: true
|
|
43
|
-
});
|
|
44
|
-
console.log(`📁 Created: ${folder}`);
|
|
45
|
-
}
|
|
46
|
-
return folder;
|
|
47
|
-
}
|
|
48
|
-
export async function generateMigration(config, name, tenantId, datasourcePath) {
|
|
49
|
-
const label = tenantId ? `[${tenantId}]` : '[default]';
|
|
50
|
-
const folder = ensureMigrationsFolder(config.migrationsPath, tenantId);
|
|
51
|
-
console.log(`${label} Generating migration: ${name}`);
|
|
52
|
-
console.log(`${label} Target folder: ${folder}`);
|
|
53
|
-
const dsPath = datasourcePath || findDatasourcePath();
|
|
54
|
-
const migrationFile = path.join(folder, name);
|
|
55
|
-
const command = `ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli migration:generate "${migrationFile}" -d "${dsPath}"`;
|
|
56
|
-
execSync(command, {
|
|
57
|
-
stdio: 'inherit',
|
|
58
|
-
env: {
|
|
59
|
-
...envConfig.getEnv(),
|
|
60
|
-
TENANT_ID: tenantId || ''
|
|
61
|
-
},
|
|
62
|
-
cwd: process.cwd()
|
|
63
|
-
});
|
|
64
|
-
console.log(`${label} ✓ Generated!`);
|
|
65
|
-
}
|
|
66
|
-
function findDatasourcePath() {
|
|
67
|
-
const possiblePaths = [
|
|
68
|
-
'src/persistence/migration.config.ts',
|
|
69
|
-
'projects/flusysnest/src/persistence/migration.config.ts'
|
|
70
|
-
];
|
|
71
|
-
for (const p of possiblePaths){
|
|
72
|
-
if (fs.existsSync(path.join(process.cwd(), p))) {
|
|
73
|
-
return p;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
throw new Error('Could not find datasource file. Specify with --datasource=<path>');
|
|
77
|
-
}
|
|
78
|
-
export async function runMigrations(config, tenantId) {
|
|
79
|
-
return withDataSource(config, tenantId, async (ds, label)=>{
|
|
80
|
-
const migrations = await ds.runMigrations();
|
|
81
|
-
if (migrations.length === 0) {
|
|
82
|
-
console.log(`${label} No pending migrations.`);
|
|
83
|
-
} else {
|
|
84
|
-
console.log(`${label} ✓ Executed ${migrations.length} migration(s):`);
|
|
85
|
-
migrations.forEach((m)=>console.log(` - ${m.name}`));
|
|
86
|
-
}
|
|
87
|
-
return {
|
|
88
|
-
migrationsRun: migrations.map((m)=>m.name)
|
|
89
|
-
};
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
export async function revertMigration(config, tenantId) {
|
|
93
|
-
return withDataSource(config, tenantId, async (ds, label)=>{
|
|
94
|
-
await ds.undoLastMigration();
|
|
95
|
-
console.log(`${label} ✓ Reverted!`);
|
|
96
|
-
return {};
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
function clearMigrationFiles(basePath, tenantId, label) {
|
|
100
|
-
const folder = getMigrationsFolderPath(basePath, tenantId);
|
|
101
|
-
if (!fs.existsSync(folder)) return;
|
|
102
|
-
const files = fs.readdirSync(folder).filter((f)=>f.endsWith('.ts') || f.endsWith('.js'));
|
|
103
|
-
files.forEach((file)=>fs.rmSync(path.join(folder, file)));
|
|
104
|
-
console.log(`${label} ✓ Removed ${files.length} migration file(s) from ${folder}`);
|
|
105
|
-
}
|
|
106
|
-
async function dropAndRecreateDatabase(dbConfig, label) {
|
|
107
|
-
const { type, database } = dbConfig;
|
|
108
|
-
if (type === 'sqlite') {
|
|
109
|
-
if (database && fs.existsSync(database)) fs.rmSync(database);
|
|
110
|
-
console.log(`${label} ✓ Deleted sqlite file`);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
if (type !== 'mysql' && type !== 'mariadb' && type !== 'postgres') {
|
|
114
|
-
throw new Error(`Database reset is not supported for type '${type}'`);
|
|
115
|
-
}
|
|
116
|
-
const adminDs = new DataSource({
|
|
117
|
-
type,
|
|
118
|
-
host: dbConfig.host,
|
|
119
|
-
port: dbConfig.port,
|
|
120
|
-
username: dbConfig.username,
|
|
121
|
-
password: dbConfig.password,
|
|
122
|
-
database: type === 'postgres' ? 'postgres' : undefined
|
|
123
|
-
});
|
|
124
|
-
await adminDs.initialize();
|
|
125
|
-
try {
|
|
126
|
-
if (type === 'postgres') {
|
|
127
|
-
await adminDs.query(`SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()`, [
|
|
128
|
-
database
|
|
129
|
-
]);
|
|
130
|
-
await adminDs.query(`DROP DATABASE IF EXISTS "${database}"`);
|
|
131
|
-
await adminDs.query(`CREATE DATABASE "${database}"`);
|
|
132
|
-
} else {
|
|
133
|
-
await adminDs.query(`DROP DATABASE IF EXISTS \`${database}\``);
|
|
134
|
-
await adminDs.query(`CREATE DATABASE \`${database}\` CHARACTER SET utf8mb4`);
|
|
135
|
-
}
|
|
136
|
-
console.log(`${label} ✓ Database '${database}' dropped and recreated`);
|
|
137
|
-
} finally{
|
|
138
|
-
await adminDs.destroy();
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
export async function resetDatabase(config, tenantId) {
|
|
142
|
-
const { database } = getDatabaseForTenant(config, tenantId);
|
|
143
|
-
const label = getLabel(tenantId);
|
|
144
|
-
try {
|
|
145
|
-
clearMigrationFiles(config.migrationsPath, tenantId, label);
|
|
146
|
-
const dbConfig = resolveDatabaseConnection(config, tenantId);
|
|
147
|
-
await dropAndRecreateDatabase(dbConfig, label);
|
|
148
|
-
return {
|
|
149
|
-
tenantId,
|
|
150
|
-
database,
|
|
151
|
-
success: true
|
|
152
|
-
};
|
|
153
|
-
} catch (error) {
|
|
154
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
155
|
-
console.error(`${label} ✗ Failed: ${errorMessage}`);
|
|
156
|
-
return {
|
|
157
|
-
tenantId,
|
|
158
|
-
database,
|
|
159
|
-
success: false,
|
|
160
|
-
error: errorMessage
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
export async function migrationStatus(config, tenantId) {
|
|
165
|
-
return withDataSource(config, tenantId, async (ds, label)=>{
|
|
166
|
-
const hasPending = await ds.showMigrations();
|
|
167
|
-
console.log(`${label} Pending: ${hasPending ? 'Yes' : 'No'}`);
|
|
168
|
-
return {
|
|
169
|
-
migrationsRun: [
|
|
170
|
-
hasPending ? 'Has pending' : 'Up to date'
|
|
171
|
-
]
|
|
172
|
-
};
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
export async function runForAllTenants(config, command) {
|
|
176
|
-
const tenants = getActiveTenants(config);
|
|
177
|
-
console.log(`\n🔄 Running '${command}' for ${tenants.length} tenant(s)...\n`);
|
|
178
|
-
const results = [];
|
|
179
|
-
for (const tenant of tenants){
|
|
180
|
-
let result;
|
|
181
|
-
switch(command){
|
|
182
|
-
case 'run':
|
|
183
|
-
result = await runMigrations(config, tenant.id);
|
|
184
|
-
break;
|
|
185
|
-
case 'revert':
|
|
186
|
-
result = await revertMigration(config, tenant.id);
|
|
187
|
-
break;
|
|
188
|
-
case 'status':
|
|
189
|
-
result = await migrationStatus(config, tenant.id);
|
|
190
|
-
break;
|
|
191
|
-
}
|
|
192
|
-
results.push(result);
|
|
193
|
-
console.log('');
|
|
194
|
-
}
|
|
195
|
-
console.log('\n=== Summary ===');
|
|
196
|
-
results.forEach((r)=>{
|
|
197
|
-
const icon = r.success ? '✓' : '✗';
|
|
198
|
-
const msg = r.error ? ` - ${r.error}` : '';
|
|
199
|
-
console.log(`${icon} ${r.tenantId || 'default'}${msg}`);
|
|
200
|
-
});
|
|
201
|
-
return results;
|
|
202
|
-
}
|
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
const mockExistsSync = jest.fn();
|
|
2
|
-
const mockMkdirSync = jest.fn();
|
|
3
|
-
const mockReaddirSync = jest.fn();
|
|
4
|
-
const mockRmSync = jest.fn();
|
|
5
|
-
const mockExecSync = jest.fn();
|
|
6
|
-
jest.mock('fs', ()=>({
|
|
7
|
-
existsSync: (...args)=>mockExistsSync(...args),
|
|
8
|
-
mkdirSync: (...args)=>mockMkdirSync(...args),
|
|
9
|
-
readdirSync: (...args)=>mockReaddirSync(...args),
|
|
10
|
-
rmSync: (...args)=>mockRmSync(...args)
|
|
11
|
-
}));
|
|
12
|
-
jest.mock('child_process', ()=>({
|
|
13
|
-
execSync: (...args)=>mockExecSync(...args)
|
|
14
|
-
}));
|
|
15
|
-
jest.mock('../config', ()=>({
|
|
16
|
-
envConfig: {
|
|
17
|
-
getEnv: jest.fn().mockReturnValue({
|
|
18
|
-
NODE_ENV: 'test'
|
|
19
|
-
})
|
|
20
|
-
}
|
|
21
|
-
}));
|
|
22
|
-
const mockRunMigrations = jest.fn();
|
|
23
|
-
const mockUndoLastMigration = jest.fn();
|
|
24
|
-
const mockShowMigrations = jest.fn();
|
|
25
|
-
const mockDestroy = jest.fn();
|
|
26
|
-
const mockDsInitialize = jest.fn();
|
|
27
|
-
const mockAdminInitialize = jest.fn();
|
|
28
|
-
const mockAdminQuery = jest.fn();
|
|
29
|
-
const mockAdminDestroy = jest.fn();
|
|
30
|
-
jest.mock('./datasource.factory', ()=>({
|
|
31
|
-
initializeDataSource: jest.fn()
|
|
32
|
-
}));
|
|
33
|
-
jest.mock('typeorm', ()=>({
|
|
34
|
-
DataSource: jest.fn().mockImplementation(()=>({
|
|
35
|
-
initialize: mockAdminInitialize,
|
|
36
|
-
query: mockAdminQuery,
|
|
37
|
-
destroy: mockAdminDestroy
|
|
38
|
-
}))
|
|
39
|
-
}));
|
|
40
|
-
import { ensureMigrationsFolder, generateMigration, runMigrations, revertMigration, resetDatabase, migrationStatus, runForAllTenants } from './migration.runner';
|
|
41
|
-
import { initializeDataSource } from './datasource.factory';
|
|
42
|
-
describe('migration.runner', ()=>{
|
|
43
|
-
const baseConfig = {
|
|
44
|
-
defaultDatabaseConfig: {
|
|
45
|
-
type: 'postgres',
|
|
46
|
-
database: 'flusys_default'
|
|
47
|
-
},
|
|
48
|
-
migrationsPath: 'migrations',
|
|
49
|
-
entities: []
|
|
50
|
-
};
|
|
51
|
-
let logSpy;
|
|
52
|
-
let errorSpy;
|
|
53
|
-
beforeEach(()=>{
|
|
54
|
-
jest.clearAllMocks();
|
|
55
|
-
logSpy = jest.spyOn(console, 'log').mockImplementation(()=>undefined);
|
|
56
|
-
errorSpy = jest.spyOn(console, 'error').mockImplementation(()=>undefined);
|
|
57
|
-
initializeDataSource.mockResolvedValue({
|
|
58
|
-
runMigrations: mockRunMigrations,
|
|
59
|
-
undoLastMigration: mockUndoLastMigration,
|
|
60
|
-
showMigrations: mockShowMigrations,
|
|
61
|
-
destroy: mockDestroy,
|
|
62
|
-
initialize: mockDsInitialize
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
afterEach(()=>{
|
|
66
|
-
logSpy.mockRestore();
|
|
67
|
-
errorSpy.mockRestore();
|
|
68
|
-
});
|
|
69
|
-
describe('ensureMigrationsFolder', ()=>{
|
|
70
|
-
it('should create the folder when it does not exist', ()=>{
|
|
71
|
-
mockExistsSync.mockReturnValue(false);
|
|
72
|
-
const folder = ensureMigrationsFolder('migrations', 'tenant-a');
|
|
73
|
-
expect(folder).toBe('migrations/tenant-a');
|
|
74
|
-
expect(mockMkdirSync).toHaveBeenCalledWith('migrations/tenant-a', {
|
|
75
|
-
recursive: true
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
it('should not create the folder when it already exists', ()=>{
|
|
79
|
-
mockExistsSync.mockReturnValue(true);
|
|
80
|
-
ensureMigrationsFolder('migrations');
|
|
81
|
-
expect(mockMkdirSync).not.toHaveBeenCalled();
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
describe('generateMigration', ()=>{
|
|
85
|
-
it('should shell out to the typeorm CLI with the resolved datasource path', async ()=>{
|
|
86
|
-
mockExistsSync.mockImplementation((p)=>p.includes('flusysnest'));
|
|
87
|
-
await generateMigration(baseConfig, 'AddProductTable', 'tenant-a');
|
|
88
|
-
expect(mockExecSync).toHaveBeenCalledTimes(1);
|
|
89
|
-
const [command, options] = mockExecSync.mock.calls[0];
|
|
90
|
-
expect(command).toContain('migration:generate');
|
|
91
|
-
expect(command).toContain('migrations/tenant-a/AddProductTable');
|
|
92
|
-
expect(command).toContain('projects/flusysnest/src/persistence/migration.config.ts');
|
|
93
|
-
expect(options.env.TENANT_ID).toBe('tenant-a');
|
|
94
|
-
});
|
|
95
|
-
it('should use an explicit datasource path when provided', async ()=>{
|
|
96
|
-
mockExistsSync.mockReturnValue(false);
|
|
97
|
-
await generateMigration(baseConfig, 'AddProductTable', undefined, 'custom/datasource.ts');
|
|
98
|
-
const [command] = mockExecSync.mock.calls[0];
|
|
99
|
-
expect(command).toContain('-d "custom/datasource.ts"');
|
|
100
|
-
});
|
|
101
|
-
it('should throw when no datasource file can be found and none is specified', async ()=>{
|
|
102
|
-
mockExistsSync.mockReturnValue(false);
|
|
103
|
-
await expect(generateMigration(baseConfig, 'AddProductTable')).rejects.toThrow('Could not find datasource file. Specify with --datasource=<path>');
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
describe('runMigrations', ()=>{
|
|
107
|
-
it('should report success with the list of executed migrations', async ()=>{
|
|
108
|
-
mockRunMigrations.mockResolvedValue([
|
|
109
|
-
{
|
|
110
|
-
name: 'M1'
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
name: 'M2'
|
|
114
|
-
}
|
|
115
|
-
]);
|
|
116
|
-
const result = await runMigrations(baseConfig);
|
|
117
|
-
expect(result).toEqual({
|
|
118
|
-
tenantId: undefined,
|
|
119
|
-
database: 'flusys_default',
|
|
120
|
-
success: true,
|
|
121
|
-
migrationsRun: [
|
|
122
|
-
'M1',
|
|
123
|
-
'M2'
|
|
124
|
-
]
|
|
125
|
-
});
|
|
126
|
-
expect(mockDestroy).toHaveBeenCalled();
|
|
127
|
-
});
|
|
128
|
-
it('should report no pending migrations when none run', async ()=>{
|
|
129
|
-
mockRunMigrations.mockResolvedValue([]);
|
|
130
|
-
const result = await runMigrations(baseConfig);
|
|
131
|
-
expect(result.migrationsRun).toEqual([]);
|
|
132
|
-
expect(result.success).toBe(true);
|
|
133
|
-
});
|
|
134
|
-
it('should capture errors and mark the result as failed', async ()=>{
|
|
135
|
-
initializeDataSource.mockRejectedValue(new Error('connection refused'));
|
|
136
|
-
const result = await runMigrations(baseConfig, 'tenant-a');
|
|
137
|
-
expect(result).toEqual({
|
|
138
|
-
tenantId: 'tenant-a',
|
|
139
|
-
database: 'flusys_default',
|
|
140
|
-
success: false,
|
|
141
|
-
error: 'connection refused'
|
|
142
|
-
});
|
|
143
|
-
expect(errorSpy).toHaveBeenCalled();
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
describe('revertMigration', ()=>{
|
|
147
|
-
it('should undo the last migration and close the connection', async ()=>{
|
|
148
|
-
const result = await revertMigration(baseConfig);
|
|
149
|
-
expect(mockUndoLastMigration).toHaveBeenCalled();
|
|
150
|
-
expect(mockDestroy).toHaveBeenCalled();
|
|
151
|
-
expect(result.success).toBe(true);
|
|
152
|
-
});
|
|
153
|
-
it('should capture revert errors', async ()=>{
|
|
154
|
-
mockUndoLastMigration.mockRejectedValue(new Error('no migrations to revert'));
|
|
155
|
-
const result = await revertMigration(baseConfig);
|
|
156
|
-
expect(result.success).toBe(false);
|
|
157
|
-
expect(result.error).toBe('no migrations to revert');
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
describe('migrationStatus', ()=>{
|
|
161
|
-
it('should report pending status', async ()=>{
|
|
162
|
-
mockShowMigrations.mockResolvedValue(true);
|
|
163
|
-
const result = await migrationStatus(baseConfig);
|
|
164
|
-
expect(result.migrationsRun).toEqual([
|
|
165
|
-
'Has pending'
|
|
166
|
-
]);
|
|
167
|
-
});
|
|
168
|
-
it('should report up-to-date status', async ()=>{
|
|
169
|
-
mockShowMigrations.mockResolvedValue(false);
|
|
170
|
-
const result = await migrationStatus(baseConfig);
|
|
171
|
-
expect(result.migrationsRun).toEqual([
|
|
172
|
-
'Up to date'
|
|
173
|
-
]);
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
describe('resetDatabase', ()=>{
|
|
177
|
-
it('should remove migration files then drop/recreate a postgres database', async ()=>{
|
|
178
|
-
mockExistsSync.mockReturnValue(true);
|
|
179
|
-
mockReaddirSync.mockReturnValue([
|
|
180
|
-
'1-init.ts',
|
|
181
|
-
'2-add.js',
|
|
182
|
-
'readme.md'
|
|
183
|
-
]);
|
|
184
|
-
mockAdminQuery.mockResolvedValue(undefined);
|
|
185
|
-
const config = {
|
|
186
|
-
defaultDatabaseConfig: {
|
|
187
|
-
type: 'postgres',
|
|
188
|
-
host: 'localhost',
|
|
189
|
-
port: 5432,
|
|
190
|
-
username: 'root',
|
|
191
|
-
password: 'pw',
|
|
192
|
-
database: 'flusys_default'
|
|
193
|
-
},
|
|
194
|
-
migrationsPath: 'migrations',
|
|
195
|
-
entities: []
|
|
196
|
-
};
|
|
197
|
-
const result = await resetDatabase(config);
|
|
198
|
-
expect(mockRmSync).toHaveBeenCalledTimes(2);
|
|
199
|
-
expect(mockAdminQuery).toHaveBeenCalledWith(expect.stringContaining('DROP DATABASE IF EXISTS "flusys_default"'));
|
|
200
|
-
expect(mockAdminDestroy).toHaveBeenCalled();
|
|
201
|
-
expect(result.success).toBe(true);
|
|
202
|
-
});
|
|
203
|
-
it('should delete the sqlite file directly for sqlite databases', async ()=>{
|
|
204
|
-
mockExistsSync.mockReturnValue(true);
|
|
205
|
-
mockReaddirSync.mockReturnValue([]);
|
|
206
|
-
const config = {
|
|
207
|
-
defaultDatabaseConfig: {
|
|
208
|
-
type: 'sqlite',
|
|
209
|
-
database: 'flusys.sqlite'
|
|
210
|
-
},
|
|
211
|
-
migrationsPath: 'migrations',
|
|
212
|
-
entities: []
|
|
213
|
-
};
|
|
214
|
-
const result = await resetDatabase(config);
|
|
215
|
-
expect(mockRmSync).toHaveBeenCalledWith('flusys.sqlite');
|
|
216
|
-
expect(mockAdminInitialize).not.toHaveBeenCalled();
|
|
217
|
-
expect(result.success).toBe(true);
|
|
218
|
-
});
|
|
219
|
-
it('should fail for unsupported database types', async ()=>{
|
|
220
|
-
mockExistsSync.mockReturnValue(false);
|
|
221
|
-
const config = {
|
|
222
|
-
defaultDatabaseConfig: {
|
|
223
|
-
type: 'mssql',
|
|
224
|
-
database: 'flusys'
|
|
225
|
-
},
|
|
226
|
-
migrationsPath: 'migrations',
|
|
227
|
-
entities: []
|
|
228
|
-
};
|
|
229
|
-
const result = await resetDatabase(config);
|
|
230
|
-
expect(result.success).toBe(false);
|
|
231
|
-
expect(result.error).toContain("Database reset is not supported for type 'mssql'");
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
describe('runForAllTenants', ()=>{
|
|
235
|
-
it('should run the given command across all configured tenants and summarize results', async ()=>{
|
|
236
|
-
mockRunMigrations.mockResolvedValue([
|
|
237
|
-
{
|
|
238
|
-
name: 'M1'
|
|
239
|
-
}
|
|
240
|
-
]);
|
|
241
|
-
const config = {
|
|
242
|
-
...baseConfig,
|
|
243
|
-
tenants: [
|
|
244
|
-
{
|
|
245
|
-
id: 'tenant-a',
|
|
246
|
-
database: 'db_a'
|
|
247
|
-
},
|
|
248
|
-
{
|
|
249
|
-
id: 'tenant-b',
|
|
250
|
-
database: 'db_b'
|
|
251
|
-
}
|
|
252
|
-
]
|
|
253
|
-
};
|
|
254
|
-
const results = await runForAllTenants(config, 'run');
|
|
255
|
-
expect(results).toHaveLength(2);
|
|
256
|
-
expect(results.every((r)=>r.success)).toBe(true);
|
|
257
|
-
expect(initializeDataSource).toHaveBeenCalledTimes(2);
|
|
258
|
-
});
|
|
259
|
-
it('should return an empty result set when no tenants are configured', async ()=>{
|
|
260
|
-
const results = await runForAllTenants(baseConfig, 'status');
|
|
261
|
-
expect(results).toEqual([]);
|
|
262
|
-
});
|
|
263
|
-
});
|
|
264
|
-
});
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Build DataSource options from database config
|
|
3
|
-
*/ export function buildDataSourceOptions(config, entities, migrationsPath, migrationsTableName = 'migrations') {
|
|
4
|
-
return {
|
|
5
|
-
type: config.type,
|
|
6
|
-
host: config.host,
|
|
7
|
-
port: config.port,
|
|
8
|
-
username: config.username,
|
|
9
|
-
password: config.password,
|
|
10
|
-
database: config.database || '',
|
|
11
|
-
entities,
|
|
12
|
-
migrations: [
|
|
13
|
-
migrationsPath
|
|
14
|
-
],
|
|
15
|
-
migrationsTableName,
|
|
16
|
-
synchronize: false
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Get migration folder path for a tenant
|
|
21
|
-
*/ export function getMigrationsFolderPath(basePath, tenantId) {
|
|
22
|
-
const folder = tenantId || 'default';
|
|
23
|
-
return `${basePath}/${folder}`;
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Get migrations glob pattern for a tenant
|
|
27
|
-
*/ export function getMigrationsGlobPattern(basePath, tenantId) {
|
|
28
|
-
return `${getMigrationsFolderPath(basePath, tenantId)}/*{.ts,.js}`;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Get database and config for a tenant (or default)
|
|
32
|
-
*/ export function getDatabaseForTenant(config, tenantId) {
|
|
33
|
-
if (tenantId && config.tenants) {
|
|
34
|
-
const tenant = config.tenants.find((t)=>t.id === tenantId);
|
|
35
|
-
if (!tenant) {
|
|
36
|
-
throw new Error(`Tenant '${tenantId}' not found. Available: ${config.tenants.map((t)=>t.id).join(', ')}`);
|
|
37
|
-
}
|
|
38
|
-
return {
|
|
39
|
-
database: tenant.database,
|
|
40
|
-
tenant,
|
|
41
|
-
tenantConfig: tenant
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
// Default config - use bootstrapAppConfig for feature flags
|
|
45
|
-
const defaultTenantConfig = {
|
|
46
|
-
id: 'default',
|
|
47
|
-
database: config.defaultDatabaseConfig.database || 'default',
|
|
48
|
-
enableCompanyFeature: config.bootstrapAppConfig?.enableCompanyFeature ?? false,
|
|
49
|
-
permissionMode: config.bootstrapAppConfig?.permissionMode ?? 'FULL'
|
|
50
|
-
};
|
|
51
|
-
return {
|
|
52
|
-
database: defaultTenantConfig.database,
|
|
53
|
-
tenantConfig: defaultTenantConfig
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Resolve full connection details for a tenant.
|
|
58
|
-
*/ export function resolveDatabaseConnection(config, tenantId) {
|
|
59
|
-
const { database, tenant } = getDatabaseForTenant(config, tenantId);
|
|
60
|
-
return tenant ? {
|
|
61
|
-
...config.defaultDatabaseConfig,
|
|
62
|
-
host: tenant.host ?? config.defaultDatabaseConfig.host,
|
|
63
|
-
port: tenant.port ?? config.defaultDatabaseConfig.port,
|
|
64
|
-
username: tenant.username ?? config.defaultDatabaseConfig.username,
|
|
65
|
-
password: tenant.password ?? config.defaultDatabaseConfig.password,
|
|
66
|
-
database: tenant.database
|
|
67
|
-
} : {
|
|
68
|
-
...config.defaultDatabaseConfig,
|
|
69
|
-
database
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Resolve entities based on tenant config
|
|
74
|
-
*/ export function resolveEntities(config, tenantConfig) {
|
|
75
|
-
if (typeof config.entities === 'function') {
|
|
76
|
-
return config.entities(tenantConfig);
|
|
77
|
-
}
|
|
78
|
-
return config.entities;
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Get active tenants from config
|
|
82
|
-
*/ export function getActiveTenants(config) {
|
|
83
|
-
return config.tenants || [];
|
|
84
|
-
}
|