make the repository layer engine-agnostic (#1127)

DatabaseContext handed every repository a raw better-sqlite3 handle alongside
drizzle, and three of them used it for retention queries built on datetime('now',
?) — a SQLite-only function. That handle is the one thing standing between the
repository layer and a second engine.

Drop it. The two time-based prunes compute their cutoff in JS against the
CURRENT_TIMESTAMP text format, which every engine writes the same way and which
compares correctly as a string; the health-history prune becomes a select of the
rows to keep followed by a NOT IN delete. All three turn async, so their two
callers await them.

Name the dialect rather than repeating a string literal, so adding an engine is
one edit instead of a search.

Tests built their schema through context.sqlite?.exec(). Optional chaining meant
removing the field type-checked cleanly and then silently created no tables, so
the fixture now owns exec() and a raw handle for direct assertions — schema setup
belongs to the test harness, not to the interface repositories consume.

No behaviour change, and no Postgres yet: this only removes the coupling that
would have to be undone first.
This commit is contained in:
ZacharyZcR
2026-07-28 16:59:10 +08:00
committed by GitHub
parent cdb030dff1
commit 64a80f411a
50 changed files with 205 additions and 88 deletions
@@ -1,4 +1,4 @@
import { and, count, desc, eq, inArray, isNull, or } from "drizzle-orm";
import { and, count, desc, eq, inArray, isNull, lt, or } from "drizzle-orm";
import {
alertFirings,
alertRuleChannels,
@@ -7,6 +7,7 @@ import {
notificationChannels,
} from "../db/schema.js";
import type { DatabaseContext } from "./database-context.js";
import { sqlTimestampDaysAgo } from "./sql-timestamp.js";
type AlertRuleRecord = typeof alertRules.$inferSelect;
type NotificationChannelRecord = typeof notificationChannels.$inferSelect;
@@ -411,12 +412,15 @@ export class AlertRepository {
await this.afterWrite();
}
pruneFiringsOlderThan(userId: string, days: number): void {
this.context.sqlite
?.prepare(
"DELETE FROM alert_firings WHERE user_id = ? AND fired_at < datetime('now', ?)",
)
.run(userId, `-${days} days`);
async pruneFiringsOlderThan(userId: string, days: number): Promise<void> {
await this.context.drizzle
.delete(alertFirings)
.where(
and(
eq(alertFirings.userId, userId),
lt(alertFirings.firedAt, sqlTimestampDaysAgo(days)),
),
);
}
async deleteByUserId(userId: string): Promise<{
@@ -1,9 +1,22 @@
import type { BetterSQLite3Database } from "drizzle-orm/better-sqlite3";
import type { Database as BetterSqliteDatabase } from "better-sqlite3";
import type * as schema from "../db/schema.js";
/**
* Engines the repository layer can run against. SQLite is the only one wired up
* today; the alias exists so that adding another is a change in one place
* rather than a hunt for string literals.
*/
export type DatabaseDialect = "sqlite";
/**
* What a repository is allowed to touch.
*
* Deliberately drizzle-only: with no raw driver handle here, no repository can
* reach for engine-specific SQL. Retention queries that previously needed
* `datetime('now', ?)` compute their cutoff in JS instead — see
* ./sql-timestamp.ts.
*/
export interface DatabaseContext {
dialect: "sqlite";
dialect: DatabaseDialect;
drizzle: BetterSQLite3Database<typeof schema>;
sqlite?: BetterSqliteDatabase;
}
+6 -1
View File
@@ -49,7 +49,6 @@ export function createCurrentRepositoryContext(): DatabaseContext {
return {
dialect: "sqlite",
drizzle: getDb(),
sqlite: getSqlite(),
};
}
@@ -59,6 +58,12 @@ export function createCurrentRepositoryWriteHook(
return () => DatabaseSaveTrigger.forceSave(reason);
}
/**
* Raw driver handle for the few synchronous call sites that cannot await —
* getCurrentSettingValue below, and settings reads during startup. Repositories
* must not use this: they take a DatabaseContext, which is drizzle-only.
* Porting to another engine means giving these callers an async path first.
*/
export function getCurrentRepositorySqlite() {
return getSqlite();
}
@@ -1,4 +1,4 @@
import { and, desc, eq } from "drizzle-orm";
import { and, desc, eq, notInArray } from "drizzle-orm";
import { hostHealthChecks, hostHealthHistory } from "../db/schema.js";
import type { DatabaseContext } from "./database-context.js";
@@ -94,7 +94,7 @@ export class HostHealthRepository {
})),
);
this.pruneHistory(userId, hostId, keep);
await this.pruneHistory(userId, hostId, keep);
await this.afterWrite();
return results.length;
}
@@ -141,21 +141,37 @@ export class HostHealthRepository {
};
}
private pruneHistory(userId: string, hostId: number, keep: number): void {
this.context.sqlite
?.prepare(
`DELETE FROM host_health_history
WHERE id IN (
SELECT id FROM host_health_history
WHERE user_id = ? AND host_id = ?
AND id NOT IN (
SELECT id FROM host_health_history
WHERE user_id = ? AND host_id = ?
ORDER BY ts DESC LIMIT ?
)
)`,
)
.run(userId, hostId, userId, hostId, keep);
/** Keeps the newest `keep` rows for the host and drops the rest. */
private async pruneHistory(
userId: string,
hostId: number,
keep: number,
): Promise<void> {
const scope = and(
eq(hostHealthHistory.userId, userId),
eq(hostHealthHistory.hostId, hostId),
);
const retained = await this.context.drizzle
.select({ id: hostHealthHistory.id })
.from(hostHealthHistory)
.where(scope)
.orderBy(desc(hostHealthHistory.ts))
.limit(keep);
// Nothing retained means nothing to keep back, so the scope alone is the
// delete condition.
await this.context.drizzle.delete(hostHealthHistory).where(
retained.length
? and(
scope,
notInArray(
hostHealthHistory.id,
retained.map((row) => row.id),
),
)
: scope,
);
}
private async afterWrite(): Promise<void> {
@@ -1,6 +1,7 @@
import { and, asc, eq, gte, lte } from "drizzle-orm";
import { and, asc, eq, gte, lt, lte } from "drizzle-orm";
import { hostMetricsHistory } from "../db/schema.js";
import type { DatabaseContext } from "./database-context.js";
import { sqlTimestampDaysAgo } from "./sql-timestamp.js";
export type HostMetricsHistoryRecord = typeof hostMetricsHistory.$inferSelect;
@@ -32,12 +33,15 @@ export class HostMetricsHistoryRepository {
await this.afterWrite();
}
pruneOlderThan(hostId: number, retentionDays: number): void {
this.context.sqlite
?.prepare(
"DELETE FROM host_metrics_history WHERE host_id = ? AND ts < datetime('now', ?)",
)
.run(hostId, `-${retentionDays} days`);
async pruneOlderThan(hostId: number, retentionDays: number): Promise<void> {
await this.context.drizzle
.delete(hostMetricsHistory)
.where(
and(
eq(hostMetricsHistory.hostId, hostId),
lt(hostMetricsHistory.ts, sqlTimestampDaysAgo(retentionDays)),
),
);
}
async listRange(
@@ -0,0 +1,20 @@
/**
* Timestamp columns are stored as text defaulting to `CURRENT_TIMESTAMP`, which
* every supported engine writes as `YYYY-MM-DD HH:MM:SS` in UTC. That format
* sorts lexicographically in time order, so retention cutoffs can be plain
* string comparisons.
*
* Computing the cutoff here rather than with `datetime('now', ?)` keeps the
* queries free of engine-specific date functions.
*/
export function sqlTimestampDaysAgo(
days: number,
now: Date = new Date(),
): string {
const cutoff = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
return formatSqlTimestamp(cutoff);
}
export function formatSqlTimestamp(date: Date): string {
return date.toISOString().slice(0, 19).replace("T", " ");
}
+1 -1
View File
@@ -225,7 +225,7 @@ export class AlertEngine {
severity: context.severity,
});
repository.pruneFiringsOlderThan(rule.userId, 30);
await repository.pruneFiringsOlderThan(rule.userId, 30);
} catch (err) {
statsLogger.warn("Failed to write alert firing", {
operation: "alert_firing_insert_error",
+1 -1
View File
@@ -616,7 +616,7 @@ class PollingManager {
});
const retentionDays = this.getRetentionDays();
repository.pruneOlderThan(hostId, retentionDays);
await repository.pruneOlderThan(hostId, retentionDays);
} catch (err) {
statsLogger.warn("Failed to write metrics history", {
operation: "insert_metrics_history",
@@ -17,7 +17,7 @@ describe("AlertRepository", () => {
): Promise<AlertRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -229,7 +229,7 @@ describe("AlertRepository", () => {
expect(unacknowledged.total).toBe(0);
await repo.acknowledgeAllFirings("user-1");
repo.pruneFiringsOlderThan("user-1", 0);
await repo.pruneFiringsOlderThan("user-1", 0);
});
it("loads enabled rules and notification channels for the alert engine", async () => {
@@ -17,7 +17,7 @@ describe("ApiKeyRepository", () => {
}> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -17,7 +17,7 @@ describe("AuditLogRepository", () => {
): Promise<AuditLogRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -17,7 +17,7 @@ describe("C2sTunnelPresetRepository", () => {
): Promise<C2sTunnelPresetRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -17,7 +17,7 @@ describe("CommandHistoryRepository", () => {
): Promise<CommandHistoryRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -17,7 +17,7 @@ describe("DashboardServiceLinkRepository", () => {
): Promise<DashboardServiceLinkRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -17,7 +17,7 @@ describe("DismissedAlertRepository", () => {
): Promise<DismissedAlertRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -17,7 +17,7 @@ describe("FileManagerBookmarkRepository", () => {
): Promise<FileManagerBookmarkRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -17,7 +17,7 @@ describe("HomepageItemRepository", () => {
): Promise<HomepageItemRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -17,7 +17,7 @@ describe("HomepageLayoutRepository", () => {
): Promise<HomepageLayoutRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -27,7 +27,7 @@ describe("HostRepository and CredentialRepository", () => {
}> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -197,7 +197,7 @@ describe("HostRepository and CredentialRepository", () => {
return {
credentials: new CredentialRepository(context, onCredentialWrite),
hosts: new HostRepository(context, onHostWrite),
sqlite: context.sqlite!,
sqlite: adapter.raw,
};
}
@@ -22,7 +22,7 @@ describe("HostFolderRepository", () => {
}> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -170,7 +170,7 @@ describe("HostFolderRepository", () => {
return {
repository: new HostFolderRepository(context, onWrite),
sqlite: context.sqlite!,
sqlite: adapter.raw,
};
}
@@ -17,7 +17,7 @@ describe("HostHealthRepository", () => {
): Promise<HostHealthRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -17,7 +17,7 @@ describe("HostMetricsHistoryRepository", () => {
): Promise<HostMetricsHistoryRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE hosts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
@@ -78,7 +78,7 @@ describe("HostMetricsHistoryRepository", () => {
it("prunes old history for a host only", async () => {
const repo = await createRepository();
repo.pruneOlderThan(1, 1);
await repo.pruneOlderThan(1, 1);
const rows = await repo.listRange(
1,
@@ -17,7 +17,7 @@ describe("HostMetricsPreferenceRepository", () => {
): Promise<HostMetricsPreferenceRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -27,7 +27,7 @@ describe("HostResolutionRepository", () => {
): Promise<HostResolutionRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -17,7 +17,7 @@ describe("NetworkTopologyRepository", () => {
): Promise<NetworkTopologyRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -17,7 +17,7 @@ describe("OpenTabRepository", () => {
): Promise<OpenTabRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -17,7 +17,7 @@ describe("OpksshTokenRepository", () => {
): Promise<OpksshTokenRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -18,7 +18,7 @@ describe("RbacAccessRepository", () => {
): Promise<RbacAccessRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -22,7 +22,7 @@ describe("RecentActivityRepository", () => {
}> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -59,7 +59,7 @@ describe("RecentActivityRepository", () => {
return {
repository: new RecentActivityRepository(context, onWrite),
sqlite: context.sqlite!,
sqlite: adapter.raw,
};
}
@@ -17,7 +17,7 @@ describe("RoleRepository", () => {
): Promise<RoleRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -17,7 +17,7 @@ describe("SessionRecordingRepository", () => {
): Promise<SessionRecordingRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -17,7 +17,7 @@ describe("SessionShareRepository", () => {
): Promise<SessionShareRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -15,7 +15,7 @@ describe("SettingsRepository", () => {
async function createRepository(): Promise<SettingsRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
@@ -22,7 +22,7 @@ describe("SharedHostSecretsRepository", () => {
}> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite!.exec(`
adapter.exec(`
CREATE TABLE host_access (
id INTEGER PRIMARY KEY AUTOINCREMENT,
host_id INTEGER NOT NULL,
@@ -84,7 +84,7 @@ describe("SharedHostSecretsRepository", () => {
return {
repository: new SharedHostSecretsRepository(context, onWrite),
sqlite: context.sqlite!,
sqlite: adapter.raw,
};
}
@@ -20,7 +20,7 @@ describe("SnippetRepository", () => {
}> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE snippets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
@@ -63,7 +63,7 @@ describe("SnippetRepository", () => {
return {
repository: new SnippetRepository(context, onWrite),
sqlite: context.sqlite!,
sqlite: adapter.raw,
};
}
@@ -0,0 +1,36 @@
import { describe, expect, it } from "vitest";
import {
formatSqlTimestamp,
sqlTimestampDaysAgo,
} from "../../../database/repositories/sql-timestamp.js";
describe("sql timestamps", () => {
it("matches the CURRENT_TIMESTAMP text format", () => {
expect(formatSqlTimestamp(new Date("2026-07-28T01:23:45.678Z"))).toBe(
"2026-07-28 01:23:45",
);
});
it("subtracts whole days in UTC", () => {
const now = new Date("2026-07-28T01:23:45.000Z");
expect(sqlTimestampDaysAgo(7, now)).toBe("2026-07-21 01:23:45");
expect(sqlTimestampDaysAgo(30, now)).toBe("2026-06-28 01:23:45");
expect(sqlTimestampDaysAgo(0, now)).toBe("2026-07-28 01:23:45");
});
it("crosses month and year boundaries", () => {
expect(sqlTimestampDaysAgo(1, new Date("2026-01-01T00:00:00.000Z"))).toBe(
"2025-12-31 00:00:00",
);
});
it("stays lexicographically ordered, which is what the cutoff comparison relies on", () => {
const now = new Date("2026-07-28T01:23:45.000Z");
const older = sqlTimestampDaysAgo(30, now);
const newer = sqlTimestampDaysAgo(7, now);
expect(older < newer).toBe(true);
expect(newer < formatSqlTimestamp(now)).toBe(true);
});
});
@@ -17,7 +17,7 @@ describe("SshCredentialUsageRepository", () => {
): Promise<SshCredentialUsageRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -19,8 +19,8 @@ describe("SsoProviderRepository", () => {
): Promise<SsoProviderRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
sqlite = context.sqlite;
context.sqlite?.exec(`
sqlite = adapter.raw;
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -17,7 +17,7 @@ describe("SyncTombstoneRepository", () => {
): Promise<SyncTombstoneRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -102,7 +102,7 @@ describe("SyncTombstoneRepository", () => {
const adapterLocal = new TestSqliteDatabase();
adapter = adapterLocal;
const context = await adapterLocal.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -23,7 +23,7 @@ describe("TermixIdentityCaRepository", () => {
}> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -63,7 +63,7 @@ describe("TermixIdentityCaRepository", () => {
return {
repo: new TermixIdentityCaRepository(context, onWrite),
sqlite: context.sqlite!,
sqlite: adapter.raw,
onWrite,
};
}
@@ -18,7 +18,7 @@ describe("TermixIdentityRepository", () => {
}> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -15,12 +15,31 @@ export class TestSqliteDatabase {
this.context = {
dialect: "sqlite",
drizzle: drizzle(this.sqlite, { schema }),
sqlite: this.sqlite,
};
return this.context;
}
/**
* Schema setup for tests. Lives on the fixture rather than on
* DatabaseContext, which is drizzle-only so that no repository can reach for
* engine-specific SQL.
*/
/** Raw handle for assertions that read the database directly. Tests only. */
get raw(): Database.Database {
if (!this.sqlite) {
throw new Error("connect() must be called before raw access");
}
return this.sqlite;
}
exec(sql: string): void {
if (!this.sqlite) {
throw new Error("connect() must be called before exec()");
}
this.sqlite.exec(sql);
}
async close(): Promise<void> {
if (this.sqlite) {
this.sqlite.close();
@@ -17,7 +17,7 @@ describe("TmuxSessionTagRepository", () => {
): Promise<TmuxSessionTagRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -17,7 +17,7 @@ describe("TransferRecentRepository", () => {
): Promise<TransferRecentRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -17,7 +17,7 @@ describe("TrustedDeviceRepository", () => {
}> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -15,7 +15,7 @@ describe("UserDataExportRepository", () => {
async function createRepository(): Promise<UserDataExportRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -17,7 +17,7 @@ describe("UserPreferenceRepository", () => {
): Promise<UserPreferenceRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -35,7 +35,7 @@ describe("UserRepository and SessionRepository", () => {
}> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -17,7 +17,7 @@ describe("VaultProfileRepository", () => {
): Promise<VaultProfileRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -17,7 +17,7 @@ describe("VaultTokenRepository", () => {
): Promise<VaultTokenRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
adapter.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,