release: WRNexusJS 0.8.0
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-02 23:18:51 +05:30
parent 87507edf59
commit 586a6db8ff
625 changed files with 243608 additions and 11210 deletions
+69 -1
View File
@@ -15,7 +15,7 @@ import {
parseQueries,
generateQueriesFile,
} from "../src/index.ts";
import type { Db } from "../src/index.ts";
import type { Db, Driver } from "../src/index.ts";
import { sqlite } from "../src/adapters/sqlite.ts";
import { bunSql } from "../src/adapters/bunsql.ts";
@@ -39,6 +39,39 @@ test("model.parse coerces DB rows to typed values", () => {
expect(row.active).toBe(true);
});
test("database close drains active work, rejects new queries, and is idempotent", async () => {
let release!: () => void;
const pending = new Promise<void>((resolve) => (release = resolve));
let closes = 0;
const driver: Driver = {
dialect: "sqlite",
async query() {
await pending;
return [{ ok: true }];
},
async exec() {
return { changes: 0 };
},
async transaction(fn) {
return fn(this);
},
close() {
closes++;
},
};
const db = createDb(driver);
const query = db.all("SELECT 1");
await Promise.resolve();
const closing = Promise.resolve(db.close());
await expect(db.all("SELECT 2")).rejects.toThrow("WRN-DB-CLOSED");
expect(closes).toBe(0);
release();
expect(await query).toEqual([{ ok: true }]);
await closing;
await db.close();
expect(closes).toBe(1);
});
for (const [label, driver] of [
["bun:sqlite", () => sqlite()],
["Bun.sql/sqlite", () => bunSql("sqlite://:memory:", "sqlite")],
@@ -111,6 +144,41 @@ test("an empty migration set does not touch the database", async () => {
expect(await applyMigrations(db, [])).toEqual([]);
});
test("migration dry-run plans changes without applying schema and honors cancellation", async () => {
const db = createDb(sqlite());
const migrations = [
{ name: "0001_plan", up: "CREATE TABLE planned (id INTEGER)", down: "DROP TABLE planned" },
];
expect(await applyMigrations(db, migrations, { dryRun: true })).toEqual(["0001_plan"]);
expect(
await db.all("SELECT name FROM sqlite_master WHERE type='table' AND name='planned'"),
).toHaveLength(0);
const controller = new AbortController();
controller.abort(new Error("deploy cancelled"));
await expect(applyMigrations(db, migrations, { signal: controller.signal })).rejects.toThrow(
"deploy cancelled",
);
await db.close();
});
test("migration lock rejects a concurrent runner and recovers expired locks", async () => {
const db = createDb(sqlite());
const migrations = [{ name: "0001_lock", up: "CREATE TABLE locked_test (id INTEGER)", down: "" }];
await db.exec(
"CREATE TABLE _wire_migration_locks (name TEXT PRIMARY KEY, owner TEXT NOT NULL, expires_at TEXT NOT NULL)",
);
await db.exec("INSERT INTO _wire_migration_locks (name, owner, expires_at) VALUES (?, ?, ?)", [
"global",
"other",
new Date(Date.now() + 60_000).toISOString(),
]);
await expect(applyMigrations(db, migrations)).rejects.toThrow("WRN-DB-MIGRATION-LOCKED");
await db.exec("UPDATE _wire_migration_locks SET expires_at = ?", [new Date(0).toISOString()]);
expect(await applyMigrations(db, migrations)).toEqual(["0001_lock"]);
await db.close();
});
test("query generator infers params and result types", () => {
const q = parseQueries(
"-- name: GetByEmail :one\nSELECT * FROM users WHERE email = :email;\n" +
+127
View File
@@ -0,0 +1,127 @@
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",
);
});
});
+34
View File
@@ -0,0 +1,34 @@
import { describe, expect, test } from "bun:test";
import { analyzeMigrationSafety } from "../src/index.ts";
describe("expand and contract migration analysis", () => {
test("detects destructive and rollout-unsafe SQL with guidance", () => {
const issues = analyzeMigrationSafety({
name: "0002_breaking",
up: `ALTER TABLE users RENAME COLUMN name TO full_name;
ALTER TABLE users ADD COLUMN tenant_id UUID NOT NULL;
ALTER TABLE users ALTER COLUMN age TYPE BIGINT;
DROP TABLE legacy_users;
CREATE INDEX users_email_idx ON users(email);`,
down: "",
});
expect(issues.map((issue) => issue.code)).toEqual([
"WRN-DB-RENAME",
"WRN-DB-ADD-REQUIRED",
"WRN-DB-TYPE-CHANGE",
"WRN-DB-DROP-TABLE",
"WRN-DB-BLOCKING-INDEX",
]);
expect(issues.every((issue) => issue.recommendation.length > 20)).toBe(true);
});
test("accepts the additive phase of an expand/contract rollout", () => {
expect(
analyzeMigrationSafety({
name: "0002_expand",
up: "ALTER TABLE users ADD COLUMN full_name TEXT;",
down: "",
}),
).toEqual([]);
});
});
+14
View File
@@ -10,6 +10,7 @@ import {
closeDatabases,
} from "../src/index.ts";
import { sqlite } from "../src/adapters/sqlite.ts";
import type { Db } from "../src/index.ts";
test("multi-database registry: default + named connections", async () => {
await closeDatabases(); // isolate from any prior state
@@ -78,3 +79,16 @@ test("closing the registry does not instantiate unused lazy databases", async ()
await closeDatabases();
expect(calls).toBe(0);
});
test("registry closes every database and clears itself when one close fails", async () => {
await closeDatabases();
let secondClosed = false;
const broken = { close: async () => Promise.reject(new Error("close failed")) } as Db;
const healthy = { close: () => void (secondClosed = true) } as Db;
setDb(broken);
registerDb("healthy", healthy);
await expect(closeDatabases()).rejects.toThrow("databases failed to close");
expect(secondClosed).toBe(true);
expect(databaseNames()).toEqual([]);
});