@flusys/nestjs-core 6.0.2 → 6.1.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.
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ const _datasourcefactory = require("./datasource.factory");
6
+ const mockInitialize = jest.fn();
7
+ const dataSourceCtor = jest.fn().mockImplementation((options)=>({
8
+ options,
9
+ initialize: mockInitialize
10
+ }));
11
+ jest.mock('typeorm', ()=>({
12
+ DataSource: jest.fn().mockImplementation((options)=>dataSourceCtor(options))
13
+ }));
14
+ jest.mock('typeorm-naming-strategies', ()=>({
15
+ SnakeNamingStrategy: jest.fn().mockImplementation(()=>({
16
+ __brand: 'SnakeNamingStrategy'
17
+ }))
18
+ }));
19
+ describe('datasource.factory', ()=>{
20
+ const baseConfig = {
21
+ defaultDatabaseConfig: {
22
+ type: 'postgres',
23
+ host: 'localhost',
24
+ port: 5432,
25
+ username: 'root',
26
+ password: 'pw',
27
+ database: 'flusys_default'
28
+ },
29
+ migrationsPath: 'migrations',
30
+ entities: [
31
+ Object
32
+ ]
33
+ };
34
+ beforeEach(()=>{
35
+ jest.clearAllMocks();
36
+ mockInitialize.mockResolvedValue(undefined);
37
+ });
38
+ describe('createMigrationDataSource', ()=>{
39
+ it('should build a DataSource with the resolved connection and a snake naming strategy', ()=>{
40
+ const ds = (0, _datasourcefactory.createMigrationDataSource)({
41
+ config: baseConfig
42
+ });
43
+ expect(dataSourceCtor).toHaveBeenCalledWith(expect.objectContaining({
44
+ type: 'postgres',
45
+ host: 'localhost',
46
+ database: 'flusys_default',
47
+ synchronize: false,
48
+ migrations: [
49
+ 'migrations/default/*{.ts,.js}'
50
+ ]
51
+ }));
52
+ expect(ds.options.namingStrategy).toEqual({
53
+ __brand: 'SnakeNamingStrategy'
54
+ });
55
+ });
56
+ it('should resolve tenant-specific database and migrations path', ()=>{
57
+ const config = {
58
+ ...baseConfig,
59
+ tenants: [
60
+ {
61
+ id: 'tenant-a',
62
+ database: 'db_a'
63
+ }
64
+ ]
65
+ };
66
+ (0, _datasourcefactory.createMigrationDataSource)({
67
+ config,
68
+ tenantId: 'tenant-a'
69
+ });
70
+ expect(dataSourceCtor).toHaveBeenCalledWith(expect.objectContaining({
71
+ database: 'db_a',
72
+ migrations: [
73
+ 'migrations/tenant-a/*{.ts,.js}'
74
+ ]
75
+ }));
76
+ });
77
+ it('should resolve entities via an entity resolver function when provided', ()=>{
78
+ const resolver = jest.fn().mockReturnValue([
79
+ Number,
80
+ String
81
+ ]);
82
+ const config = {
83
+ ...baseConfig,
84
+ entities: resolver
85
+ };
86
+ (0, _datasourcefactory.createMigrationDataSource)({
87
+ config
88
+ });
89
+ expect(resolver).toHaveBeenCalled();
90
+ expect(dataSourceCtor).toHaveBeenCalledWith(expect.objectContaining({
91
+ entities: [
92
+ Number,
93
+ String
94
+ ]
95
+ }));
96
+ });
97
+ });
98
+ describe('initializeDataSource', ()=>{
99
+ it('should create and initialize the DataSource', async ()=>{
100
+ const ds = await (0, _datasourcefactory.initializeDataSource)({
101
+ config: baseConfig
102
+ });
103
+ expect(mockInitialize).toHaveBeenCalledTimes(1);
104
+ expect(ds).toBeDefined();
105
+ });
106
+ it('should propagate initialization errors', async ()=>{
107
+ mockInitialize.mockRejectedValue(new Error('connection refused'));
108
+ await expect((0, _datasourcefactory.initializeDataSource)({
109
+ config: baseConfig
110
+ })).rejects.toThrow('connection refused');
111
+ });
112
+ });
113
+ });
@@ -0,0 +1,293 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ const _migrationcli = require("./migration.cli");
6
+ const mockGetTenantId = jest.fn();
7
+ const mockTryGetValue = jest.fn();
8
+ const mockIsProduction = jest.fn();
9
+ jest.mock('../config', ()=>({
10
+ envConfig: {
11
+ getTenantId: (...args)=>mockGetTenantId(...args),
12
+ tryGetValue: (...args)=>mockTryGetValue(...args),
13
+ isProduction: (...args)=>mockIsProduction(...args)
14
+ }
15
+ }));
16
+ const mockGetDatabaseForTenant = jest.fn();
17
+ const mockGetMigrationsFolderPath = jest.fn();
18
+ jest.mock('../utils', ()=>({
19
+ getDatabaseForTenant: (...args)=>mockGetDatabaseForTenant(...args),
20
+ getMigrationsFolderPath: (...args)=>mockGetMigrationsFolderPath(...args)
21
+ }));
22
+ const mockGenerateMigration = jest.fn();
23
+ const mockRunMigrations = jest.fn();
24
+ const mockRevertMigration = jest.fn();
25
+ const mockResetDatabase = jest.fn();
26
+ const mockMigrationStatus = jest.fn();
27
+ const mockRunForAllTenants = jest.fn();
28
+ jest.mock('./migration.runner', ()=>({
29
+ generateMigration: (...args)=>mockGenerateMigration(...args),
30
+ runMigrations: (...args)=>mockRunMigrations(...args),
31
+ revertMigration: (...args)=>mockRevertMigration(...args),
32
+ resetDatabase: (...args)=>mockResetDatabase(...args),
33
+ migrationStatus: (...args)=>mockMigrationStatus(...args),
34
+ runForAllTenants: (...args)=>mockRunForAllTenants(...args)
35
+ }));
36
+ describe('runMigrationCli', ()=>{
37
+ const baseConfig = {
38
+ defaultDatabaseConfig: {
39
+ type: 'postgres',
40
+ database: 'flusys_default'
41
+ },
42
+ migrationsPath: 'migrations',
43
+ entities: []
44
+ };
45
+ let logSpy;
46
+ let errorSpy;
47
+ let exitSpy;
48
+ let originalArgv;
49
+ beforeEach(()=>{
50
+ jest.clearAllMocks();
51
+ originalArgv = process.argv;
52
+ logSpy = jest.spyOn(console, 'log').mockImplementation(()=>undefined);
53
+ errorSpy = jest.spyOn(console, 'error').mockImplementation(()=>undefined);
54
+ exitSpy = jest.spyOn(process, 'exit').mockImplementation(()=>{
55
+ throw new Error('process.exit called');
56
+ });
57
+ mockGetTenantId.mockReturnValue(null);
58
+ mockTryGetValue.mockReturnValue('');
59
+ mockIsProduction.mockReturnValue(false);
60
+ mockGetDatabaseForTenant.mockReturnValue({
61
+ database: 'flusys_default'
62
+ });
63
+ mockGetMigrationsFolderPath.mockReturnValue('migrations/default');
64
+ });
65
+ afterEach(()=>{
66
+ process.argv = originalArgv;
67
+ logSpy.mockRestore();
68
+ errorSpy.mockRestore();
69
+ exitSpy.mockRestore();
70
+ });
71
+ function setArgv(...args) {
72
+ process.argv = [
73
+ 'node',
74
+ 'cli.js',
75
+ ...args
76
+ ];
77
+ }
78
+ it('should print help and return when no command is given', async ()=>{
79
+ setArgv();
80
+ await (0, _migrationcli.runMigrationCli)(baseConfig);
81
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Migration CLI'));
82
+ expect(mockRunMigrations).not.toHaveBeenCalled();
83
+ });
84
+ it('should run migrations for the default tenant', async ()=>{
85
+ setArgv('run');
86
+ mockRunMigrations.mockResolvedValue({
87
+ success: true,
88
+ migrationsRun: []
89
+ });
90
+ await (0, _migrationcli.runMigrationCli)(baseConfig);
91
+ expect(mockRunMigrations).toHaveBeenCalledWith(baseConfig, undefined);
92
+ expect(exitSpy).not.toHaveBeenCalled();
93
+ });
94
+ it('should exit with code 1 when run fails', async ()=>{
95
+ setArgv('run');
96
+ mockRunMigrations.mockResolvedValue({
97
+ success: false,
98
+ error: 'boom'
99
+ });
100
+ await expect((0, _migrationcli.runMigrationCli)(baseConfig)).rejects.toThrow('process.exit called');
101
+ expect(exitSpy).toHaveBeenCalledWith(1);
102
+ });
103
+ it('should revert the last migration', async ()=>{
104
+ setArgv('revert');
105
+ mockRevertMigration.mockResolvedValue({
106
+ success: true
107
+ });
108
+ await (0, _migrationcli.runMigrationCli)(baseConfig);
109
+ expect(mockRevertMigration).toHaveBeenCalledWith(baseConfig, undefined);
110
+ });
111
+ it('should exit with code 1 when revert fails', async ()=>{
112
+ setArgv('revert');
113
+ mockRevertMigration.mockResolvedValue({
114
+ success: false,
115
+ error: 'nothing to revert'
116
+ });
117
+ await expect((0, _migrationcli.runMigrationCli)(baseConfig)).rejects.toThrow('process.exit called');
118
+ expect(exitSpy).toHaveBeenCalledWith(1);
119
+ });
120
+ it('should report migration status', async ()=>{
121
+ setArgv('status');
122
+ mockMigrationStatus.mockResolvedValue({
123
+ success: true,
124
+ migrationsRun: [
125
+ 'Up to date'
126
+ ]
127
+ });
128
+ await (0, _migrationcli.runMigrationCli)(baseConfig);
129
+ expect(mockMigrationStatus).toHaveBeenCalledWith(baseConfig, undefined);
130
+ });
131
+ it('should generate a migration using the --name flag', async ()=>{
132
+ setArgv('generate', '--name=AddProducts');
133
+ mockGenerateMigration.mockResolvedValue(undefined);
134
+ await (0, _migrationcli.runMigrationCli)(baseConfig);
135
+ expect(mockGenerateMigration).toHaveBeenCalledWith(baseConfig, 'AddProducts', undefined, undefined);
136
+ });
137
+ it('should prefer npm_config_name over --name', async ()=>{
138
+ setArgv('generate', '--name=Ignored');
139
+ mockTryGetValue.mockImplementation((key)=>key === 'npm_config_name' ? 'FromNpm' : '');
140
+ mockGenerateMigration.mockResolvedValue(undefined);
141
+ await (0, _migrationcli.runMigrationCli)(baseConfig);
142
+ expect(mockGenerateMigration).toHaveBeenCalledWith(baseConfig, 'FromNpm', undefined, undefined);
143
+ });
144
+ it('should exit with code 1 when generate is missing a name', async ()=>{
145
+ setArgv('generate');
146
+ await expect((0, _migrationcli.runMigrationCli)(baseConfig)).rejects.toThrow('process.exit called');
147
+ expect(exitSpy).toHaveBeenCalledWith(1);
148
+ expect(mockGenerateMigration).not.toHaveBeenCalled();
149
+ });
150
+ it('should pass the resolved tenant id through to sub-commands', async ()=>{
151
+ setArgv('run');
152
+ mockGetTenantId.mockReturnValue('tenant-a');
153
+ mockRunMigrations.mockResolvedValue({
154
+ success: true,
155
+ migrationsRun: []
156
+ });
157
+ await (0, _migrationcli.runMigrationCli)(baseConfig);
158
+ expect(mockRunMigrations).toHaveBeenCalledWith(baseConfig, 'tenant-a');
159
+ });
160
+ it('should reject reset in production without --force', async ()=>{
161
+ setArgv('reset');
162
+ mockIsProduction.mockReturnValue(true);
163
+ await expect((0, _migrationcli.runMigrationCli)(baseConfig)).rejects.toThrow('process.exit called');
164
+ expect(exitSpy).toHaveBeenCalledWith(1);
165
+ expect(mockResetDatabase).not.toHaveBeenCalled();
166
+ });
167
+ it('should allow reset in production when --force is passed', async ()=>{
168
+ setArgv('reset', '--force');
169
+ mockIsProduction.mockReturnValue(true);
170
+ mockResetDatabase.mockResolvedValue({
171
+ success: true
172
+ });
173
+ mockGenerateMigration.mockResolvedValue(undefined);
174
+ mockRunMigrations.mockResolvedValue({
175
+ success: true,
176
+ migrationsRun: []
177
+ });
178
+ await (0, _migrationcli.runMigrationCli)(baseConfig);
179
+ expect(mockResetDatabase).toHaveBeenCalledWith(baseConfig, undefined);
180
+ expect(mockGenerateMigration).toHaveBeenCalledWith(baseConfig, 'Init', undefined, undefined);
181
+ expect(mockRunMigrations).toHaveBeenCalledWith(baseConfig, undefined);
182
+ });
183
+ it('should use provided migration name as the reset init migration name', async ()=>{
184
+ setArgv('reset', '--name=InitialSetup');
185
+ mockResetDatabase.mockResolvedValue({
186
+ success: true
187
+ });
188
+ mockGenerateMigration.mockResolvedValue(undefined);
189
+ mockRunMigrations.mockResolvedValue({
190
+ success: true,
191
+ migrationsRun: []
192
+ });
193
+ await (0, _migrationcli.runMigrationCli)(baseConfig);
194
+ expect(mockGenerateMigration).toHaveBeenCalledWith(baseConfig, 'InitialSetup', undefined, undefined);
195
+ });
196
+ it('should exit with code 1 when reset fails', async ()=>{
197
+ setArgv('reset');
198
+ mockResetDatabase.mockResolvedValue({
199
+ success: false,
200
+ error: 'cannot drop'
201
+ });
202
+ await expect((0, _migrationcli.runMigrationCli)(baseConfig)).rejects.toThrow('process.exit called');
203
+ expect(exitSpy).toHaveBeenCalledWith(1);
204
+ expect(mockGenerateMigration).not.toHaveBeenCalled();
205
+ });
206
+ it('should reject unknown commands', async ()=>{
207
+ setArgv('frobnicate');
208
+ await expect((0, _migrationcli.runMigrationCli)(baseConfig)).rejects.toThrow('process.exit called');
209
+ expect(exitSpy).toHaveBeenCalledWith(1);
210
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Unknown command'));
211
+ });
212
+ describe(':all commands', ()=>{
213
+ it('should run for all tenants when multi-tenant mode is enabled', async ()=>{
214
+ setArgv('run:all');
215
+ const multiTenantConfig = {
216
+ ...baseConfig,
217
+ bootstrapAppConfig: {
218
+ databaseMode: 'multi-tenant'
219
+ }
220
+ };
221
+ mockRunForAllTenants.mockResolvedValue([
222
+ {
223
+ success: true
224
+ }
225
+ ]);
226
+ await (0, _migrationcli.runMigrationCli)(multiTenantConfig);
227
+ expect(mockRunForAllTenants).toHaveBeenCalledWith(multiTenantConfig, 'run');
228
+ });
229
+ it('should reject :all commands when database mode is single', async ()=>{
230
+ setArgv('run:all');
231
+ const singleConfig = {
232
+ ...baseConfig,
233
+ bootstrapAppConfig: {
234
+ databaseMode: 'single'
235
+ }
236
+ };
237
+ await expect((0, _migrationcli.runMigrationCli)(singleConfig)).rejects.toThrow('process.exit called');
238
+ expect(exitSpy).toHaveBeenCalledWith(1);
239
+ expect(mockRunForAllTenants).not.toHaveBeenCalled();
240
+ });
241
+ it('should exit with code 1 when any tenant fails during :all commands', async ()=>{
242
+ setArgv('status:all');
243
+ mockRunForAllTenants.mockResolvedValue([
244
+ {
245
+ success: true
246
+ },
247
+ {
248
+ success: false
249
+ }
250
+ ]);
251
+ await expect((0, _migrationcli.runMigrationCli)(baseConfig)).rejects.toThrow('process.exit called');
252
+ expect(exitSpy).toHaveBeenCalledWith(1);
253
+ });
254
+ it('should parse the action prefix from the :all command', async ()=>{
255
+ setArgv('revert:all');
256
+ mockRunForAllTenants.mockResolvedValue([
257
+ {
258
+ success: true
259
+ }
260
+ ]);
261
+ await (0, _migrationcli.runMigrationCli)(baseConfig);
262
+ expect(mockRunForAllTenants).toHaveBeenCalledWith(baseConfig, 'revert');
263
+ });
264
+ });
265
+ describe('config resolution', ()=>{
266
+ it('should load config from an explicit string path argument', async ()=>{
267
+ setArgv('status');
268
+ const loadedConfig = {
269
+ ...baseConfig,
270
+ migrationsPath: 'custom-migrations'
271
+ };
272
+ jest.doMock(require('path').resolve(process.cwd(), 'custom-config.ts'), ()=>({
273
+ migrationConfig: loadedConfig
274
+ }), {
275
+ virtual: true
276
+ });
277
+ mockMigrationStatus.mockResolvedValue({
278
+ success: true,
279
+ migrationsRun: [
280
+ 'Up to date'
281
+ ]
282
+ });
283
+ await (0, _migrationcli.runMigrationCli)('custom-config.ts');
284
+ expect(mockMigrationStatus).toHaveBeenCalledWith(loadedConfig, undefined);
285
+ });
286
+ it('should exit with code 1 and show help when no config can be resolved', async ()=>{
287
+ setArgv('status');
288
+ await expect((0, _migrationcli.runMigrationCli)()).rejects.toThrow('process.exit called');
289
+ expect(exitSpy).toHaveBeenCalledWith(1);
290
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Config not found'));
291
+ });
292
+ });
293
+ });
@@ -0,0 +1,268 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ const _migrationrunner = require("./migration.runner");
6
+ const _datasourcefactory = require("./datasource.factory");
7
+ const mockExistsSync = jest.fn();
8
+ const mockMkdirSync = jest.fn();
9
+ const mockReaddirSync = jest.fn();
10
+ const mockRmSync = jest.fn();
11
+ const mockExecSync = jest.fn();
12
+ jest.mock('fs', ()=>({
13
+ existsSync: (...args)=>mockExistsSync(...args),
14
+ mkdirSync: (...args)=>mockMkdirSync(...args),
15
+ readdirSync: (...args)=>mockReaddirSync(...args),
16
+ rmSync: (...args)=>mockRmSync(...args)
17
+ }));
18
+ jest.mock('child_process', ()=>({
19
+ execSync: (...args)=>mockExecSync(...args)
20
+ }));
21
+ jest.mock('../config', ()=>({
22
+ envConfig: {
23
+ getEnv: jest.fn().mockReturnValue({
24
+ NODE_ENV: 'test'
25
+ })
26
+ }
27
+ }));
28
+ const mockRunMigrations = jest.fn();
29
+ const mockUndoLastMigration = jest.fn();
30
+ const mockShowMigrations = jest.fn();
31
+ const mockDestroy = jest.fn();
32
+ const mockDsInitialize = jest.fn();
33
+ const mockAdminInitialize = jest.fn();
34
+ const mockAdminQuery = jest.fn();
35
+ const mockAdminDestroy = jest.fn();
36
+ jest.mock('./datasource.factory', ()=>({
37
+ initializeDataSource: jest.fn()
38
+ }));
39
+ jest.mock('typeorm', ()=>({
40
+ DataSource: jest.fn().mockImplementation(()=>({
41
+ initialize: mockAdminInitialize,
42
+ query: mockAdminQuery,
43
+ destroy: mockAdminDestroy
44
+ }))
45
+ }));
46
+ describe('migration.runner', ()=>{
47
+ const baseConfig = {
48
+ defaultDatabaseConfig: {
49
+ type: 'postgres',
50
+ database: 'flusys_default'
51
+ },
52
+ migrationsPath: 'migrations',
53
+ entities: []
54
+ };
55
+ let logSpy;
56
+ let errorSpy;
57
+ beforeEach(()=>{
58
+ jest.clearAllMocks();
59
+ logSpy = jest.spyOn(console, 'log').mockImplementation(()=>undefined);
60
+ errorSpy = jest.spyOn(console, 'error').mockImplementation(()=>undefined);
61
+ _datasourcefactory.initializeDataSource.mockResolvedValue({
62
+ runMigrations: mockRunMigrations,
63
+ undoLastMigration: mockUndoLastMigration,
64
+ showMigrations: mockShowMigrations,
65
+ destroy: mockDestroy,
66
+ initialize: mockDsInitialize
67
+ });
68
+ });
69
+ afterEach(()=>{
70
+ logSpy.mockRestore();
71
+ errorSpy.mockRestore();
72
+ });
73
+ describe('ensureMigrationsFolder', ()=>{
74
+ it('should create the folder when it does not exist', ()=>{
75
+ mockExistsSync.mockReturnValue(false);
76
+ const folder = (0, _migrationrunner.ensureMigrationsFolder)('migrations', 'tenant-a');
77
+ expect(folder).toBe('migrations/tenant-a');
78
+ expect(mockMkdirSync).toHaveBeenCalledWith('migrations/tenant-a', {
79
+ recursive: true
80
+ });
81
+ });
82
+ it('should not create the folder when it already exists', ()=>{
83
+ mockExistsSync.mockReturnValue(true);
84
+ (0, _migrationrunner.ensureMigrationsFolder)('migrations');
85
+ expect(mockMkdirSync).not.toHaveBeenCalled();
86
+ });
87
+ });
88
+ describe('generateMigration', ()=>{
89
+ it('should shell out to the typeorm CLI with the resolved datasource path', async ()=>{
90
+ mockExistsSync.mockImplementation((p)=>p.includes('flusysnest'));
91
+ await (0, _migrationrunner.generateMigration)(baseConfig, 'AddProductTable', 'tenant-a');
92
+ expect(mockExecSync).toHaveBeenCalledTimes(1);
93
+ const [command, options] = mockExecSync.mock.calls[0];
94
+ expect(command).toContain('migration:generate');
95
+ expect(command).toContain('migrations/tenant-a/AddProductTable');
96
+ expect(command).toContain('projects/flusysnest/src/persistence/migration.config.ts');
97
+ expect(options.env.TENANT_ID).toBe('tenant-a');
98
+ });
99
+ it('should use an explicit datasource path when provided', async ()=>{
100
+ mockExistsSync.mockReturnValue(false);
101
+ await (0, _migrationrunner.generateMigration)(baseConfig, 'AddProductTable', undefined, 'custom/datasource.ts');
102
+ const [command] = mockExecSync.mock.calls[0];
103
+ expect(command).toContain('-d "custom/datasource.ts"');
104
+ });
105
+ it('should throw when no datasource file can be found and none is specified', async ()=>{
106
+ mockExistsSync.mockReturnValue(false);
107
+ await expect((0, _migrationrunner.generateMigration)(baseConfig, 'AddProductTable')).rejects.toThrow('Could not find datasource file. Specify with --datasource=<path>');
108
+ });
109
+ });
110
+ describe('runMigrations', ()=>{
111
+ it('should report success with the list of executed migrations', async ()=>{
112
+ mockRunMigrations.mockResolvedValue([
113
+ {
114
+ name: 'M1'
115
+ },
116
+ {
117
+ name: 'M2'
118
+ }
119
+ ]);
120
+ const result = await (0, _migrationrunner.runMigrations)(baseConfig);
121
+ expect(result).toEqual({
122
+ tenantId: undefined,
123
+ database: 'flusys_default',
124
+ success: true,
125
+ migrationsRun: [
126
+ 'M1',
127
+ 'M2'
128
+ ]
129
+ });
130
+ expect(mockDestroy).toHaveBeenCalled();
131
+ });
132
+ it('should report no pending migrations when none run', async ()=>{
133
+ mockRunMigrations.mockResolvedValue([]);
134
+ const result = await (0, _migrationrunner.runMigrations)(baseConfig);
135
+ expect(result.migrationsRun).toEqual([]);
136
+ expect(result.success).toBe(true);
137
+ });
138
+ it('should capture errors and mark the result as failed', async ()=>{
139
+ _datasourcefactory.initializeDataSource.mockRejectedValue(new Error('connection refused'));
140
+ const result = await (0, _migrationrunner.runMigrations)(baseConfig, 'tenant-a');
141
+ expect(result).toEqual({
142
+ tenantId: 'tenant-a',
143
+ database: 'flusys_default',
144
+ success: false,
145
+ error: 'connection refused'
146
+ });
147
+ expect(errorSpy).toHaveBeenCalled();
148
+ });
149
+ });
150
+ describe('revertMigration', ()=>{
151
+ it('should undo the last migration and close the connection', async ()=>{
152
+ const result = await (0, _migrationrunner.revertMigration)(baseConfig);
153
+ expect(mockUndoLastMigration).toHaveBeenCalled();
154
+ expect(mockDestroy).toHaveBeenCalled();
155
+ expect(result.success).toBe(true);
156
+ });
157
+ it('should capture revert errors', async ()=>{
158
+ mockUndoLastMigration.mockRejectedValue(new Error('no migrations to revert'));
159
+ const result = await (0, _migrationrunner.revertMigration)(baseConfig);
160
+ expect(result.success).toBe(false);
161
+ expect(result.error).toBe('no migrations to revert');
162
+ });
163
+ });
164
+ describe('migrationStatus', ()=>{
165
+ it('should report pending status', async ()=>{
166
+ mockShowMigrations.mockResolvedValue(true);
167
+ const result = await (0, _migrationrunner.migrationStatus)(baseConfig);
168
+ expect(result.migrationsRun).toEqual([
169
+ 'Has pending'
170
+ ]);
171
+ });
172
+ it('should report up-to-date status', async ()=>{
173
+ mockShowMigrations.mockResolvedValue(false);
174
+ const result = await (0, _migrationrunner.migrationStatus)(baseConfig);
175
+ expect(result.migrationsRun).toEqual([
176
+ 'Up to date'
177
+ ]);
178
+ });
179
+ });
180
+ describe('resetDatabase', ()=>{
181
+ it('should remove migration files then drop/recreate a postgres database', async ()=>{
182
+ mockExistsSync.mockReturnValue(true);
183
+ mockReaddirSync.mockReturnValue([
184
+ '1-init.ts',
185
+ '2-add.js',
186
+ 'readme.md'
187
+ ]);
188
+ mockAdminQuery.mockResolvedValue(undefined);
189
+ const config = {
190
+ defaultDatabaseConfig: {
191
+ type: 'postgres',
192
+ host: 'localhost',
193
+ port: 5432,
194
+ username: 'root',
195
+ password: 'pw',
196
+ database: 'flusys_default'
197
+ },
198
+ migrationsPath: 'migrations',
199
+ entities: []
200
+ };
201
+ const result = await (0, _migrationrunner.resetDatabase)(config);
202
+ expect(mockRmSync).toHaveBeenCalledTimes(2);
203
+ expect(mockAdminQuery).toHaveBeenCalledWith(expect.stringContaining('DROP DATABASE IF EXISTS "flusys_default"'));
204
+ expect(mockAdminDestroy).toHaveBeenCalled();
205
+ expect(result.success).toBe(true);
206
+ });
207
+ it('should delete the sqlite file directly for sqlite databases', async ()=>{
208
+ mockExistsSync.mockReturnValue(true);
209
+ mockReaddirSync.mockReturnValue([]);
210
+ const config = {
211
+ defaultDatabaseConfig: {
212
+ type: 'sqlite',
213
+ database: 'flusys.sqlite'
214
+ },
215
+ migrationsPath: 'migrations',
216
+ entities: []
217
+ };
218
+ const result = await (0, _migrationrunner.resetDatabase)(config);
219
+ expect(mockRmSync).toHaveBeenCalledWith('flusys.sqlite');
220
+ expect(mockAdminInitialize).not.toHaveBeenCalled();
221
+ expect(result.success).toBe(true);
222
+ });
223
+ it('should fail for unsupported database types', async ()=>{
224
+ mockExistsSync.mockReturnValue(false);
225
+ const config = {
226
+ defaultDatabaseConfig: {
227
+ type: 'mssql',
228
+ database: 'flusys'
229
+ },
230
+ migrationsPath: 'migrations',
231
+ entities: []
232
+ };
233
+ const result = await (0, _migrationrunner.resetDatabase)(config);
234
+ expect(result.success).toBe(false);
235
+ expect(result.error).toContain("Database reset is not supported for type 'mssql'");
236
+ });
237
+ });
238
+ describe('runForAllTenants', ()=>{
239
+ it('should run the given command across all configured tenants and summarize results', async ()=>{
240
+ mockRunMigrations.mockResolvedValue([
241
+ {
242
+ name: 'M1'
243
+ }
244
+ ]);
245
+ const config = {
246
+ ...baseConfig,
247
+ tenants: [
248
+ {
249
+ id: 'tenant-a',
250
+ database: 'db_a'
251
+ },
252
+ {
253
+ id: 'tenant-b',
254
+ database: 'db_b'
255
+ }
256
+ ]
257
+ };
258
+ const results = await (0, _migrationrunner.runForAllTenants)(config, 'run');
259
+ expect(results).toHaveLength(2);
260
+ expect(results.every((r)=>r.success)).toBe(true);
261
+ expect(_datasourcefactory.initializeDataSource).toHaveBeenCalledTimes(2);
262
+ });
263
+ it('should return an empty result set when no tenants are configured', async ()=>{
264
+ const results = await (0, _migrationrunner.runForAllTenants)(baseConfig, 'status');
265
+ expect(results).toEqual([]);
266
+ });
267
+ });
268
+ });