refactor(server): remove DatabaseService.transaction — every transaction runs through UnitOfWork

This commit is contained in:
jubnl
2026-09-22 06:39:38 +02:00
parent a0f50c8316
commit 39a6d991ce
6 changed files with 7 additions and 16 deletions
+3 -2
View File
@@ -914,8 +914,9 @@ cycle seams are folded, nothing on the websocket path imports one, and the
crons all inject now (`*.job.ts` providers on `scheduling/CronRegistrarService`).
1. **Move the SQL** into `<domain>.service.ts` as methods over an injected
`DatabaseService` (`this.db.all<T>/get<T>/run/prepare/transaction`; strict
constructor injection, no `@Optional()`). Preserve every quirk: falsy-coercion
`DatabaseService` (`this.db.all<T>/get<T>/run/prepare`; multi-statement writes go
through `await this.uow.transactional(...)` instead — see the root `CLAUDE.md`;
strict constructor injection, no `@Optional()`). Preserve every quirk: falsy-coercion
defaults (`x || fallback`, never `??`), post-insert/post-update re-selects (no
RETURNING), `COALESCE` semantics. If a controller already wraps the legacy
functions, do not change the service's method surface. The module needs no
@@ -38,11 +38,6 @@ export class DatabaseService {
return this.conn.prepare(sql).run(...params);
}
/** Run `fn` inside a synchronous better-sqlite3 transaction. */
transaction<T>(fn: (conn: Database.Database) => T): T {
return this.conn.transaction(() => fn(this.conn))();
}
// Trip-access helpers delegate to the db/database exports (not this.conn):
// tests vi.mock that module and stub the helpers themselves, so the stubs
// must keep flowing through here.
@@ -19,18 +19,15 @@ describe('DatabaseService (typed query helpers)', () => {
expect(svc.all('SELECT 3 AS three')).toEqual([{ three: 3 }]);
});
it('run + transaction operate on a scratch table', () => {
it('run operates on a scratch table', () => {
svc.run('CREATE TEMP TABLE IF NOT EXISTS _dbsvc_test (n INTEGER)');
svc.run('DELETE FROM _dbsvc_test');
const info = svc.run('INSERT INTO _dbsvc_test (n) VALUES (?)', 41);
expect(info.changes).toBe(1);
const total = svc.transaction((conn) => {
conn.prepare('INSERT INTO _dbsvc_test (n) VALUES (?)').run(1);
return conn.prepare('SELECT SUM(n) AS s FROM _dbsvc_test').get() as { s: number };
});
expect(total.s).toBe(42);
const total = svc.get('SELECT SUM(n) AS s FROM _dbsvc_test') as { s: number };
expect(total.s).toBe(41);
svc.run('DROP TABLE _dbsvc_test');
});
+1 -1
View File
@@ -13,7 +13,7 @@ const routeTestDb = new Database(':memory:');
async function setup() {
const maps = { geocodeQuery: vi.fn(), reverseGeocode: vi.fn().mockResolvedValue({ name: null, address: null }) };
const db = { canAccessTrip: vi.fn(() => ({ user_id: 7 })), get: () => ({ role: 'user' }), transaction: <T>(fn: () => T) => fn() };
const db = { canAccessTrip: vi.fn(() => ({ user_id: 7 })), get: () => ({ role: 'user' }) };
const places = { create: vi.fn((_trip: string, stop: { name: string }) => ({ id: stop.name })), broadcast: vi.fn() };
const assignments = { dayExists: vi.fn(() => true), createAssignment: vi.fn((dayId: number, placeId: string) => ({ dayId, placeId })), broadcast: vi.fn(), reconcile: vi.fn() };
const permissions = { checkPermission: vi.fn(() => true) };
@@ -40,7 +40,6 @@ function setup() {
run: vi.fn((_sql: string, tripId: number, key: string, value: string) =>
tripSettings.set(tripId, { ...tripSettings.get(tripId), [key]: JSON.parse(value) }),
),
transaction: (fn: () => unknown) => fn(),
};
const preferences = new RoadtripPreferencesService(preferenceDb as never, realtime as never, uowStub);
const days = [{ id: 1, day_number: 1, title: null, date: '2026-09-11', default_transport_mode: 'driving' }];
@@ -42,7 +42,6 @@ async function makeService() {
get: <T>(sql: string, ...params: unknown[]) => raw.prepare(sql).get(...params as never[]) as T | undefined,
all: <T>(sql: string, ...params: unknown[]) => raw.prepare(sql).all(...params as never[]) as T[],
run: (sql: string, ...params: unknown[]) => raw.prepare(sql).run(...params as never[]),
transaction: <T>(fn: (conn: unknown) => T) => raw.transaction(() => fn(raw))(),
};
// The realtime side is exercised through the controller and the MCP layer; here the
// service is under test for what it writes, so a broadcast that goes nowhere is right.