128 lines
4.2 KiB
TypeScript
128 lines
4.2 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import {
|
|
batch,
|
|
createDb,
|
|
createRepository,
|
|
databaseHealth,
|
|
retryTransaction,
|
|
} from "../src/index.ts";
|
|
import type { Driver, Row } from "../src/index.ts";
|
|
|
|
function memoryDriver(): Driver {
|
|
const rows: Row[] = [{ id: 1, name: "One" }];
|
|
return {
|
|
dialect: "sqlite",
|
|
async query(sql) {
|
|
if (/COUNT/.test(sql)) return [{ count: rows.length }];
|
|
if (/SELECT 1 AS healthy/.test(sql)) return [{ healthy: 1 }];
|
|
return rows;
|
|
},
|
|
async exec() {
|
|
return { changes: 1, lastInsertId: 2 };
|
|
},
|
|
async transaction(callback) {
|
|
return callback(this);
|
|
},
|
|
close() {},
|
|
};
|
|
}
|
|
|
|
describe("database helper kit", () => {
|
|
test("provides repository CRUD helpers and health checks", async () => {
|
|
const db = createDb(memoryDriver());
|
|
const repository = createRepository<{ id: number; name: string }>(db, {
|
|
table: "items",
|
|
allowedColumns: ["name"],
|
|
});
|
|
expect((await repository.find(1))?.name).toBe("One");
|
|
expect(await repository.count()).toBe(1);
|
|
expect((await databaseHealth(db)).ok).toBe(true);
|
|
expect(batch([1, 2, 3], 2)).toEqual([[1, 2], [3]]);
|
|
});
|
|
|
|
test("retries transaction callbacks with bounded attempts", async () => {
|
|
const db = createDb(memoryDriver());
|
|
let calls = 0;
|
|
const value = await retryTransaction(
|
|
db,
|
|
async () => {
|
|
calls += 1;
|
|
if (calls < 2) throw new Error("retry");
|
|
return "done";
|
|
},
|
|
{ attempts: 2, baseDelayMs: 0, shouldRetry: () => true },
|
|
);
|
|
expect(value).toBe("done");
|
|
expect(calls).toBe(2);
|
|
});
|
|
|
|
test("uses dialect-aware placeholders and bounded list options", async () => {
|
|
const queries: string[] = [];
|
|
const driver = memoryDriver();
|
|
driver.dialect = "postgres";
|
|
const originalQuery = driver.query;
|
|
driver.query = async (sql, params) => {
|
|
queries.push(sql);
|
|
return originalQuery.call(driver, sql, params);
|
|
};
|
|
const originalExec = driver.exec;
|
|
driver.exec = async (sql, params) => {
|
|
queries.push(sql);
|
|
return originalExec.call(driver, sql, params);
|
|
};
|
|
const repository = createRepository<{ id: number; name: string }>(createDb(driver), {
|
|
table: "items",
|
|
allowedColumns: ["name"],
|
|
maxListLimit: 50,
|
|
});
|
|
await repository.find(1);
|
|
await repository.create({ name: "Two" });
|
|
await repository.update(1, { name: "Changed" });
|
|
await repository.all({ limit: 10, offset: 5, orderBy: "name", direction: "desc" });
|
|
expect(queries.some((sql) => sql.includes("id = $1"))).toBe(true);
|
|
expect(queries.some((sql) => sql.includes("VALUES ($1)"))).toBe(true);
|
|
expect(queries.some((sql) => sql.includes("LIMIT $1 OFFSET $2"))).toBe(true);
|
|
});
|
|
|
|
test("automatically applies an immutable tenant scope to every repository operation", async () => {
|
|
const calls: Array<{ sql: string; params: unknown[] }> = [];
|
|
const driver: Driver = {
|
|
dialect: "sqlite",
|
|
async query(sql, params = []) {
|
|
calls.push({ sql, params: [...params] });
|
|
return /COUNT/.test(sql) ? [{ count: 0 }] : [];
|
|
},
|
|
async exec(sql, params = []) {
|
|
calls.push({ sql, params: [...params] });
|
|
return { changes: 1 };
|
|
},
|
|
async transaction(callback) {
|
|
return callback(this);
|
|
},
|
|
close() {},
|
|
};
|
|
type RecordRow = { id: number; name: string; tenant_id: string };
|
|
const repository = createRepository<RecordRow>(createDb(driver), {
|
|
table: "records",
|
|
allowedColumns: ["name"],
|
|
scope: { column: "tenant_id", value: "acme" },
|
|
});
|
|
await repository.all();
|
|
await repository.find(1);
|
|
await repository.create({ name: "A", tenant_id: "other" });
|
|
await repository.update(1, { name: "B" });
|
|
await repository.remove(1);
|
|
await repository.count();
|
|
expect(calls.every((call) => call.sql.includes("tenant_id"))).toBe(true);
|
|
expect(calls.every((call) => call.params.includes("acme"))).toBe(true);
|
|
expect(calls.some((call) => call.params.includes("other"))).toBe(false);
|
|
});
|
|
|
|
test("rejects unsafe repository identifiers", () => {
|
|
const db = createDb(memoryDriver());
|
|
expect(() => createRepository(db, { table: "items; DROP TABLE items" })).toThrow(
|
|
"Unsafe SQL identifier",
|
|
);
|
|
});
|
|
});
|