mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 14:06:27 +00:00
0a4c6fbace
db/database.ts opened the connection as a side effect of being imported, so the moment anything reached it the database was open. index.ts had to defer loading the app until a first-boot restore was done, and nothing in the container decided when the connection opened or closed. The module now opens the connection on first use and opens nothing at import. A connection closed on purpose (a restore, the shutdown) stays closed until it is reopened, as before. The new DatabaseLifecycle provider owns the decisions: buildApp() opens through it, which also binds the ORM to later swaps (the hook attachOrm used to register) and runs the schema bootstrap in the same order as before, and the shutdown closes through it. Its file property is the one resolved database path. The module keeps every export, so the test suites that replace it with vi.mock keep working; buildDbMock gains the new openDb.
125 lines
4.1 KiB
TypeScript
125 lines
4.1 KiB
TypeScript
/**
|
|
* DatabaseLifecycle: the provider that owns when the core connection opens,
|
|
* closes and reopens.
|
|
*
|
|
* The connection module and the schema bootstrap are mocked: what is pinned
|
|
* here is the order the provider drives them in (the boot order the legacy
|
|
* baseline depends on) and that the ORM is rebuilt around a reopened handle.
|
|
* The real open, migrate and reopen run in tests/integration/database-lifecycle.test.ts.
|
|
*/
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import type { MikroORM } from '@mikro-orm/core';
|
|
|
|
const { dbMock, ormMock, calls } = vi.hoisted(() => {
|
|
const calls: string[] = [];
|
|
return {
|
|
calls,
|
|
dbMock: {
|
|
getRawConnection: vi.fn(() => {
|
|
calls.push('getRawConnection');
|
|
return {};
|
|
}),
|
|
registerReinitializeHook: vi.fn((_hook: () => Promise<void>) => {
|
|
calls.push('registerReinitializeHook');
|
|
}),
|
|
closeDb: vi.fn(() => {
|
|
calls.push('closeDb');
|
|
}),
|
|
reinitialize: vi.fn(async () => {
|
|
calls.push('reinitialize');
|
|
}),
|
|
},
|
|
ormMock: {
|
|
runSchemaBootstrap: vi.fn(async (_orm: unknown, opts?: { snapshot?: boolean }) => {
|
|
calls.push(`runSchemaBootstrap:${JSON.stringify(opts ?? {})}`);
|
|
}),
|
|
},
|
|
};
|
|
});
|
|
|
|
vi.mock('../../../../src/db/database', () => dbMock);
|
|
vi.mock('../../../../src/db/orm', () => ormMock);
|
|
|
|
import { DatabaseLifecycle } from '../../../../src/nest/database/database-lifecycle.service';
|
|
|
|
function ormStub() {
|
|
const connection = {
|
|
close: vi.fn(async (force?: boolean) => {
|
|
calls.push(`connection.close:${String(force)}`);
|
|
}),
|
|
connect: vi.fn(async () => {
|
|
calls.push('connection.connect');
|
|
}),
|
|
};
|
|
return { orm: { em: { getConnection: () => connection } } as unknown as MikroORM, connection };
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
calls.length = 0;
|
|
});
|
|
|
|
describe('DatabaseLifecycle', () => {
|
|
it('DBLIFE-001: open() opens the connection, binds the ORM to later swaps, then brings the schema to head', async () => {
|
|
const { orm } = ormStub();
|
|
|
|
await new DatabaseLifecycle(orm).open();
|
|
|
|
expect(calls).toEqual(['getRawConnection', 'registerReinitializeHook', 'runSchemaBootstrap:{}']);
|
|
expect(ormMock.runSchemaBootstrap).toHaveBeenCalledWith(orm);
|
|
});
|
|
|
|
it('DBLIFE-002: the hook open() registers rebuilds the ORM client and migrates the new file without a pre-migrate copy', async () => {
|
|
const { orm, connection } = ormStub();
|
|
await new DatabaseLifecycle(orm).open();
|
|
const hook = dbMock.registerReinitializeHook.mock.calls[0][0];
|
|
calls.length = 0;
|
|
|
|
await hook();
|
|
|
|
expect(connection.close).toHaveBeenCalledWith(true);
|
|
expect(calls).toEqual(['connection.close:true', 'connection.connect', 'runSchemaBootstrap:{"snapshot":false}']);
|
|
});
|
|
|
|
it('DBLIFE-003: close() and reopen() go through the connection module', async () => {
|
|
const lifecycle = new DatabaseLifecycle(ormStub().orm);
|
|
|
|
lifecycle.close();
|
|
await lifecycle.reopen();
|
|
|
|
expect(calls).toEqual(['closeDb', 'reinitialize']);
|
|
});
|
|
|
|
it('DBLIFE-004: a reopen that fails is not swallowed', async () => {
|
|
dbMock.reinitialize.mockRejectedValueOnce(new Error('database is locked'));
|
|
|
|
await expect(new DatabaseLifecycle(ormStub().orm).reopen()).rejects.toThrow('database is locked');
|
|
});
|
|
|
|
describe('file', () => {
|
|
let dir: string | null = null;
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
if (dir) fs.rmSync(dir, { recursive: true, force: true });
|
|
dir = null;
|
|
});
|
|
|
|
it('DBLIFE-005: is the in-memory database under test', () => {
|
|
expect(new DatabaseLifecycle(ormStub().orm).file).toBe(':memory:');
|
|
});
|
|
|
|
it('DBLIFE-006: is TREK_DB_FILE when it is set, the one path the connection, the backup and the restore share', () => {
|
|
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'trek-lifecycle-'));
|
|
const file = path.join(dir, 'nested', 'custom-name.db');
|
|
vi.stubEnv('NODE_ENV', 'production');
|
|
vi.stubEnv('TREK_DB_FILE', file);
|
|
|
|
expect(new DatabaseLifecycle(ormStub().orm).file).toBe(file);
|
|
});
|
|
});
|
|
});
|