Add biome

This commit is contained in:
Endeavorance 2025-04-06 19:28:42 -04:00
parent 09d25c926c
commit 6479d09196
9 changed files with 586 additions and 538 deletions

34
biome.json Normal file
View file

@ -0,0 +1,34 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": false,
"ignore": ["dist"]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"suspicious": {
"noArrayIndexKey": "off"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "double"
}
}
}

BIN
bun.lockb

Binary file not shown.

View file

@ -5,14 +5,13 @@
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"scripts": { "scripts": {
"build": "bun run ./build.ts", "build": "bun run ./build.ts",
"clean": "rm -rf dist" "clean": "rm -rf dist",
"fmt": "biome check --fix"
}, },
"keywords": [], "keywords": [],
"author": "Endeavorance <hello@endeavorance.camp> (https://endeavorance.camp)", "author": "Endeavorance <hello@endeavorance.camp> (https://endeavorance.camp)",
"license": "CC BY-NC-SA 4.0", "license": "CC BY-NC-SA 4.0",
"files": [ "files": ["dist"],
"dist"
],
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "latest",
@ -20,5 +19,8 @@
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5.0.0" "typescript": "^5.0.0"
},
"dependencies": {
"@biomejs/biome": "^1.9.4"
} }
} }

View file

@ -28,7 +28,7 @@ export const ColumnOf = {
type: otherTable.primaryColumnType(), type: otherTable.primaryColumnType(),
references: otherTable.reference(), references: otherTable.reference(),
nullable: false, nullable: false,
cascade cascade,
}; };
}, },
@ -36,7 +36,6 @@ export const ColumnOf = {
Text: { Text: {
type: "TEXT", type: "TEXT",
nullable: true, nullable: true,
}, },
Int: { Int: {
@ -94,7 +93,7 @@ export const ColumnOf = {
return { return {
type: "TEXT", type: "TEXT",
default: defaultValue, default: defaultValue,
} };
}, },
Int(defaultValue: number): ColumnShorthand { Int(defaultValue: number): ColumnShorthand {
@ -109,7 +108,6 @@ export const ColumnOf = {
type: "REAL", type: "REAL",
default: defaultValue, default: defaultValue,
} as const; } as const;
} },
}, },
} as const; } as const;

View file

@ -1,6 +1,13 @@
import { ColumnOf } from "./column-types";
import type { Column, ColumnShorthand, DataType } from "./columns"; import type { Column, ColumnShorthand, DataType } from "./columns";
import { Prequel } from "./prequel"; import { Prequel } from "./prequel";
import { Table } from "./table"; import { Table } from "./table";
import { ColumnOf } from "./column-types";
export { Prequel, Table, ColumnOf, type DataType, type Column, type ColumnShorthand }; export {
Prequel,
Table,
ColumnOf,
type DataType,
type Column,
type ColumnShorthand,
};

View file

@ -4,194 +4,194 @@ import { SchemaError } from "./error";
import { Table } from "./table"; import { Table } from "./table";
interface KVRow { interface KVRow {
kv_id: number; kv_id: number;
key: string; key: string;
value: string; value: string;
} }
/** /**
* Represents a key-value store using Prequel. * Represents a key-value store using Prequel.
*/ */
export class PrequelKVStore { export class PrequelKVStore {
private table: Table<KVRow>; private table: Table<KVRow>;
/** /**
* Creates a new instance of the PrequelKVStore class. * Creates a new instance of the PrequelKVStore class.
* @param pq The Prequel instance. * @param pq The Prequel instance.
* @param tableName The name of the table. * @param tableName The name of the table.
*/ */
constructor(pq: Prequel, tableName: string) { constructor(pq: Prequel, tableName: string) {
this.table = pq.table<KVRow>(tableName, { this.table = pq.table<KVRow>(tableName, {
kv_id: { kv_id: {
type: "INTEGER", type: "INTEGER",
primary: true, primary: true,
autoincrement: true, autoincrement: true,
}, },
key: "TEXT", key: "TEXT",
value: "TEXT", value: "TEXT",
}); });
} }
/** /**
* Inserts or updates a key-value pair in the store. * Inserts or updates a key-value pair in the store.
* @param key The key. * @param key The key.
* @param value The value. * @param value The value.
*/ */
put<T>(key: string, value: T): void { put<T>(key: string, value: T): void {
const lookup = this.table.findOneWhere({ const lookup = this.table.findOneWhere({
key, key,
}); });
if (lookup === null) { if (lookup === null) {
this.table.insertPartial({ this.table.insertPartial({
key, key,
value: JSON.stringify(value), value: JSON.stringify(value),
}); });
} else { } else {
lookup.value = JSON.stringify(value); lookup.value = JSON.stringify(value);
this.table.update(lookup); this.table.update(lookup);
} }
} }
/** /**
* Retrieves the value associated with the specified key from the store. * Retrieves the value associated with the specified key from the store.
* @param key The key. * @param key The key.
* @returns The value associated with the key, or null if the key is not found. * @returns The value associated with the key, or null if the key is not found.
*/ */
get<T>(key: string): T | null { get<T>(key: string): T | null {
const lookup = this.table.findOneWhere({ key }); const lookup = this.table.findOneWhere({ key });
if (lookup === null) { if (lookup === null) {
return null; return null;
} }
return JSON.parse(lookup.value) as T; return JSON.parse(lookup.value) as T;
} }
/** /**
* Checks if a key exists in the table. * Checks if a key exists in the table.
* @param key - The key to check. * @param key - The key to check.
* @returns `true` if the key exists, `false` otherwise. * @returns `true` if the key exists, `false` otherwise.
*/ */
has(key: string): boolean { has(key: string): boolean {
return ( return (
this.table.findOneWhere({ this.table.findOneWhere({
key, key,
}) !== null }) !== null
); );
} }
/** /**
* Get an array of all of the keys in the KV store * Get an array of all of the keys in the KV store
* @returns The keys in the store. * @returns The keys in the store.
*/ */
keys(): string[] { keys(): string[] {
return this.table.findAll().map((row) => row.key); return this.table.findAll().map((row) => row.key);
} }
/** /**
* Get an array of all of the values in the KV store * Get an array of all of the values in the KV store
* @returns The values in the store. * @returns The values in the store.
*/ */
values(): unknown[] { values(): unknown[] {
return this.table.findAll().map((row) => JSON.parse(row.value)); return this.table.findAll().map((row) => JSON.parse(row.value));
} }
} }
/** /**
* Represents a Prequel database instance. * Represents a Prequel database instance.
*/ */
export class Prequel { export class Prequel {
public db: Database; public db: Database;
public kv: PrequelKVStore; public kv: PrequelKVStore;
public tables: Record<string, Table<unknown>> = {}; public tables: Record<string, Table<unknown>> = {};
public query: typeof Database.prototype.query; public query: typeof Database.prototype.query;
public uncachedQuery: typeof Database.prototype.prepare; public uncachedQuery: typeof Database.prototype.prepare;
/** /**
* Creates a new Prequel database instance. * Creates a new Prequel database instance.
* @param filename The filename of the database. Defaults to ":memory:" if not provided. * @param filename The filename of the database. Defaults to ":memory:" if not provided.
*/ */
constructor(filename = ":memory:") { constructor(filename = ":memory:") {
this.db = new Database(filename); this.db = new Database(filename);
this.query = this.db.query.bind(this.db); this.query = this.db.query.bind(this.db);
this.uncachedQuery = this.db.prepare.bind(this.db); this.uncachedQuery = this.db.prepare.bind(this.db);
this.db.exec("PRAGMA foreign_keys=ON"); this.db.exec("PRAGMA foreign_keys=ON");
this.kv = new PrequelKVStore(this, "PrequelManagedKVStore"); this.kv = new PrequelKVStore(this, "PrequelManagedKVStore");
} }
/** /**
* Enables Write-Ahead Logging (WAL) mode for the database. * Enables Write-Ahead Logging (WAL) mode for the database.
*/ */
enableWAL() { enableWAL() {
this.db.exec("PRAGMA journal_mode=WAL;"); this.db.exec("PRAGMA journal_mode=WAL;");
} }
/** /**
* Closes the database connection. * Closes the database connection.
*/ */
close() { close() {
this.db.close(); this.db.close();
} }
/** /**
* Checks if a table with the given name exists in the database. * Checks if a table with the given name exists in the database.
* @param name The name of the table. * @param name The name of the table.
* @returns `true` if the table exists, `false` otherwise. * @returns `true` if the table exists, `false` otherwise.
*/ */
hasTable(name: string): boolean { hasTable(name: string): boolean {
return Object.keys(this.tables).includes(name); return Object.keys(this.tables).includes(name);
} }
/** /**
* Creates a new table in the database. * Creates a new table in the database.
* @param name The name of the table. * @param name The name of the table.
* @param cols The column definitions of the table. * @param cols The column definitions of the table.
* @returns The created table. * @returns The created table.
* @throws {SchemaError} If a table with the same name already exists. * @throws {SchemaError} If a table with the same name already exists.
*/ */
table<RowShape>( table<RowShape>(
name: string, name: string,
cols: ColumnShorthandMap<RowShape>, cols: ColumnShorthandMap<RowShape>,
): Table<RowShape> { ): Table<RowShape> {
if (this.hasTable(name)) { if (this.hasTable(name)) {
throw new SchemaError("Duplicate table name", name); throw new SchemaError("Duplicate table name", name);
} }
const newTable = new Table(this, name, cols); const newTable = new Table(this, name, cols);
this.tables[name] = newTable; this.tables[name] = newTable;
return newTable; return newTable;
} }
/** /**
* Attaches an existing table to the database. * Attaches an existing table to the database.
* @param table The table to attach. * @param table The table to attach.
* @throws {SchemaError} If a table with the same name already exists. * @throws {SchemaError} If a table with the same name already exists.
*/ */
attachTable(table: Table<unknown>) { attachTable(table: Table<unknown>) {
if (this.hasTable(table.name)) { if (this.hasTable(table.name)) {
throw new SchemaError("Duplicate table name", table.name); throw new SchemaError("Duplicate table name", table.name);
} }
this.tables[table.name] = table; this.tables[table.name] = table;
} }
/** /**
* Executes a function within a database transaction. * Executes a function within a database transaction.
* If the function throws an error, the transaction is rolled back. * If the function throws an error, the transaction is rolled back.
* Otherwise, the transaction is committed. * Otherwise, the transaction is committed.
* @param transactionFn The function to execute within the transaction. * @param transactionFn The function to execute within the transaction.
* @returns `true` if the transaction was committed successfully, otherwise `false`. * @returns `true` if the transaction was committed successfully, otherwise `false`.
* @throws The error thrown by the transaction function if the transaction fails. * @throws The error thrown by the transaction function if the transaction fails.
*/ */
transaction<T>(transactionFn: () => T): T { transaction<T>(transactionFn: () => T): T {
try { try {
this.db.exec("BEGIN TRANSACTION;"); this.db.exec("BEGIN TRANSACTION;");
const result: T = transactionFn(); const result: T = transactionFn();
this.db.exec("COMMIT;"); this.db.exec("COMMIT;");
return result; return result;
} catch (error) { } catch (error) {
this.db.exec("ROLLBACK;"); this.db.exec("ROLLBACK;");
throw error; throw error;
} }
} }
} }

View file

@ -37,9 +37,15 @@ function shapeForQuery<T>(obj: Partial<T>): ArbitraryRow {
for (const key of keys) { for (const key of keys) {
const val = obj[key as keyof T]; const val = obj[key as keyof T];
if (typeof val !== "string" && typeof val !== "number" && val !== null) { if (
typeof val === "string" &&
typeof val === "number" &&
typeof val === "boolean" &&
typeof val === "bigint" &&
val !== null
) {
throw new SchemaError( throw new SchemaError(
`Invalid value in query: ${key}: ${val} (type: ${typeof val})`, `Invalid value in query: ${key} -> ${val} (type: ${typeof val})`,
); );
} }
@ -97,7 +103,7 @@ export class Table<RowShape> {
for (const columnName of this._columnNames) { for (const columnName of this._columnNames) {
const columnDefinition = const columnDefinition =
this._columnDefinitions[ this._columnDefinitions[
columnName as keyof typeof this._columnDefinitions columnName as keyof typeof this._columnDefinitions
]; ];
// Identify a primary column besides ROWID if specified // Identify a primary column besides ROWID if specified
@ -160,7 +166,8 @@ export class Table<RowShape> {
} }
public primaryColumnType(): DataType { public primaryColumnType(): DataType {
const primary = this._primaryColumnName as keyof typeof this._columnDefinitions; const primary = this
._primaryColumnName as keyof typeof this._columnDefinitions;
return this._columnDefinitions[primary].type; return this._columnDefinitions[primary].type;
} }

View file

@ -3,143 +3,143 @@ import { SchemaError } from "../src/error";
import { Prequel, Table } from "../src/index"; import { Prequel, Table } from "../src/index";
interface User { interface User {
user_id: number; user_id: number;
name: string; name: string;
} }
test("constructor", () => { test("constructor", () => {
const pq = new Prequel(); const pq = new Prequel();
const users = pq.table<User>("Users", { const users = pq.table<User>("Users", {
user_id: { user_id: {
type: "INTEGER", type: "INTEGER",
primary: true, primary: true,
}, },
name: "TEXT", name: "TEXT",
}); });
users.insertPartial({ name: "Yondu" }); users.insertPartial({ name: "Yondu" });
expect(users.findAll().length).toBe(1); expect(users.findAll().length).toBe(1);
}); });
test(".hasTable() knows if a table exists in the DB", () => { test(".hasTable() knows if a table exists in the DB", () => {
const pq = new Prequel(); const pq = new Prequel();
expect(pq.hasTable("Users")).toBeFalse(); expect(pq.hasTable("Users")).toBeFalse();
expect(pq.hasTable("Notfound")).toBeFalse(); expect(pq.hasTable("Notfound")).toBeFalse();
pq.table<User>("Users", { pq.table<User>("Users", {
user_id: { user_id: {
type: "INTEGER", type: "INTEGER",
primary: true, primary: true,
}, },
name: "TEXT", name: "TEXT",
}); });
expect(pq.hasTable("Users")).toBeTrue(); expect(pq.hasTable("Users")).toBeTrue();
expect(pq.hasTable("Notfound")).toBeFalse(); expect(pq.hasTable("Notfound")).toBeFalse();
}); });
test(".table() creates and attaches a table", () => { test(".table() creates and attaches a table", () => {
const pq = new Prequel(); const pq = new Prequel();
// Already has one table because of KV // Already has one table because of KV
expect(Object.keys(pq.tables)).toHaveLength(1); expect(Object.keys(pq.tables)).toHaveLength(1);
const table = pq.table<User>("Users", { const table = pq.table<User>("Users", {
user_id: { user_id: {
type: "INTEGER", type: "INTEGER",
primary: true, primary: true,
}, },
name: "TEXT", name: "TEXT",
}); });
expect(Object.keys(pq.tables)).toHaveLength(2); expect(Object.keys(pq.tables)).toHaveLength(2);
expect(Object.values(pq.tables)).toContain(table); expect(Object.values(pq.tables)).toContain(table);
}); });
test(".table() throws when creating a duplicate table", () => { test(".table() throws when creating a duplicate table", () => {
const pq = new Prequel(); const pq = new Prequel();
pq.table<User>("Users", { pq.table<User>("Users", {
user_id: { user_id: {
type: "INTEGER", type: "INTEGER",
primary: true, primary: true,
}, },
name: "TEXT", name: "TEXT",
}); });
const oopsie = () => { const oopsie = () => {
pq.table<User>("Users", { pq.table<User>("Users", {
user_id: { user_id: {
type: "INTEGER", type: "INTEGER",
primary: true, primary: true,
}, },
name: "TEXT", name: "TEXT",
}); });
}; };
expect(oopsie).toThrow(SchemaError); expect(oopsie).toThrow(SchemaError);
}); });
test(".attachTable() attaches a table", () => { test(".attachTable() attaches a table", () => {
const pq = new Prequel(); const pq = new Prequel();
const table = new Table<User>(pq.db, "Users", { const table = new Table<User>(pq.db, "Users", {
user_id: { user_id: {
type: "INTEGER", type: "INTEGER",
primary: true, primary: true,
}, },
name: "TEXT", name: "TEXT",
}); });
expect(pq.hasTable("Users")).toBeFalse(); expect(pq.hasTable("Users")).toBeFalse();
pq.attachTable(table); pq.attachTable(table);
expect(pq.hasTable("Users")).toBeTrue(); expect(pq.hasTable("Users")).toBeTrue();
expect(Object.values(pq.tables)).toContain(table); expect(Object.values(pq.tables)).toContain(table);
}); });
test(".attachTable() throws on duplicate table names", () => { test(".attachTable() throws on duplicate table names", () => {
const pq = new Prequel(); const pq = new Prequel();
pq.table<User>("Users", { pq.table<User>("Users", {
user_id: { user_id: {
type: "INTEGER", type: "INTEGER",
primary: true, primary: true,
}, },
name: "TEXT", name: "TEXT",
}); });
const secondTable = new Table<User>(pq.db, "Users", { const secondTable = new Table<User>(pq.db, "Users", {
user_id: { user_id: {
type: "INTEGER", type: "INTEGER",
primary: true, primary: true,
}, },
name: "TEXT", name: "TEXT",
}); });
const doAttachment = () => { const doAttachment = () => {
pq.attachTable(secondTable); pq.attachTable(secondTable);
}; };
expect(doAttachment).toThrow(SchemaError); expect(doAttachment).toThrow(SchemaError);
}); });
test("kv store can set and retrieve arbitrary values", () => { test("kv store can set and retrieve arbitrary values", () => {
const pq = new Prequel(); const pq = new Prequel();
pq.kv.put<number>("answer", 42); pq.kv.put<number>("answer", 42);
pq.kv.put<{ enabled: boolean }>("config", { enabled: true }); pq.kv.put<{ enabled: boolean }>("config", { enabled: true });
pq.kv.put<string[]>("list", ["a", "b", "c"]); pq.kv.put<string[]>("list", ["a", "b", "c"]);
expect(pq.kv.get<number>("answer")).toEqual(42); expect(pq.kv.get<number>("answer")).toEqual(42);
expect(pq.kv.get<{ enabled: boolean }>("config")).toEqual({ enabled: true }); expect(pq.kv.get<{ enabled: boolean }>("config")).toEqual({ enabled: true });
expect(pq.kv.get<string[]>("list")).toEqual(["a", "b", "c"]); expect(pq.kv.get<string[]>("list")).toEqual(["a", "b", "c"]);
}); });
test(".query() creates a cached statement", () => { test(".query() creates a cached statement", () => {
const pq = new Prequel(); const pq = new Prequel();
const query = pq.query("SELECT 1"); const query = pq.query("SELECT 1");
expect(query).toBeDefined(); expect(query).toBeDefined();
expect(query).toBe(pq.query("SELECT 1")); expect(query).toBe(pq.query("SELECT 1"));
}); });

View file

@ -3,414 +3,414 @@ import { expect, test } from "bun:test";
import { Prequel, Table } from "../src/index"; import { Prequel, Table } from "../src/index";
interface UserWithID { interface UserWithID {
user_id: number; user_id: number;
name: string; name: string;
} }
interface UserWithBio { interface UserWithBio {
user_id: number; user_id: number;
bio: { bio: {
name: string; name: string;
age: number; age: number;
}; };
} }
interface Post { interface Post {
post_id: number; post_id: number;
user_id: number; user_id: number;
} }
interface UserWithNullable extends UserWithID { interface UserWithNullable extends UserWithID {
bio: string | null; bio: string | null;
} }
function makeTestTable(): Table<UserWithID> { function makeTestTable(): Table<UserWithID> {
const db = new Database(); const db = new Database();
return new Table<UserWithID>(db, "Users", { return new Table<UserWithID>(db, "Users", {
user_id: { user_id: {
type: "INTEGER", type: "INTEGER",
primary: true, primary: true,
}, },
name: "TEXT", name: "TEXT",
}); });
} }
test("constructor", () => { test("constructor", () => {
const db = new Database(); const db = new Database();
const table = new Table<UserWithID>(db, "Users", { const table = new Table<UserWithID>(db, "Users", {
user_id: { user_id: {
type: "INTEGER", type: "INTEGER",
primary: true, primary: true,
}, },
name: "TEXT", name: "TEXT",
}); });
expect(table.name).toEqual("Users"); expect(table.name).toEqual("Users");
expect(table.db).toBe(db); expect(table.db).toBe(db);
expect(table.columns).toEqual(["user_id", "name"]); expect(table.columns).toEqual(["user_id", "name"]);
}); });
test("constructor -> auto-attach to a prequel isntance", () => { test("constructor -> auto-attach to a prequel isntance", () => {
const db = new Prequel(); const db = new Prequel();
const table = new Table<UserWithID>(db, "Users", { const table = new Table<UserWithID>(db, "Users", {
user_id: { user_id: {
type: "INTEGER", type: "INTEGER",
primary: true, primary: true,
}, },
name: "TEXT", name: "TEXT",
}); });
expect(Object.values(db.tables)).toContain(table); expect(Object.values(db.tables)).toContain(table);
}); });
test(".toString() resolves to the table name for embedding in queries", () => { test(".toString() resolves to the table name for embedding in queries", () => {
const table = makeTestTable(); const table = makeTestTable();
expect(`${table}`).toEqual('"Users"'); expect(`${table}`).toEqual('"Users"');
}); });
test(".reference() creates a default reference to the primary column", () => { test(".reference() creates a default reference to the primary column", () => {
const table = makeTestTable(); const table = makeTestTable();
expect(table.reference()).toEqual("Users(user_id)"); expect(table.reference()).toEqual("Users(user_id)");
}); });
test(".reference() creates a reference to the specified column", () => { test(".reference() creates a reference to the specified column", () => {
const table = makeTestTable(); const table = makeTestTable();
expect(table.reference("name")).toEqual("Users(name)"); expect(table.reference("name")).toEqual("Users(name)");
}); });
test(".insert() inserts a full row", () => { test(".insert() inserts a full row", () => {
const table = makeTestTable(); const table = makeTestTable();
table.insert({ table.insert({
user_id: 100, user_id: 100,
name: "Jack", name: "Jack",
}); });
const lookup = table.db.prepare<UserWithID, number>( const lookup = table.db.prepare<UserWithID, number>(
`SELECT * FROM ${table} WHERE user_id = ?`, `SELECT * FROM ${table} WHERE user_id = ?`,
); );
const found = lookup.get(100); const found = lookup.get(100);
expect(found).toEqual({ expect(found).toEqual({
user_id: 100, user_id: 100,
name: "Jack", name: "Jack",
}); });
}); });
test(".insert() is able to insert nulls to nullable fields", () => { test(".insert() is able to insert nulls to nullable fields", () => {
const table = new Table<UserWithNullable>(new Database(), "Users", { const table = new Table<UserWithNullable>(new Database(), "Users", {
user_id: { user_id: {
type: "INTEGER", type: "INTEGER",
primary: true, primary: true,
}, },
name: "TEXT", name: "TEXT",
bio: { bio: {
type: "TEXT", type: "TEXT",
nullable: true, nullable: true,
}, },
}); });
table.insert({ table.insert({
user_id: 1, user_id: 1,
name: "Jack", name: "Jack",
bio: null, bio: null,
}); });
const lookup = table.db.prepare<UserWithNullable, number>( const lookup = table.db.prepare<UserWithNullable, number>(
`SELECT * FROM ${table} WHERE user_id = ?`, `SELECT * FROM ${table} WHERE user_id = ?`,
); );
const found = lookup.get(1); const found = lookup.get(1);
expect(found?.bio).toBeNull(); expect(found?.bio).toBeNull();
}); });
test(".insertPartial() inserts a partial row", () => { test(".insertPartial() inserts a partial row", () => {
const table = makeTestTable(); const table = makeTestTable();
table.insertPartial({ table.insertPartial({
name: "Jack", name: "Jack",
}); });
const lookup = table.db.prepare<UserWithID, string>( const lookup = table.db.prepare<UserWithID, string>(
`SELECT * FROM ${table} WHERE name = ?`, `SELECT * FROM ${table} WHERE name = ?`,
); );
const found = lookup.get("Jack"); const found = lookup.get("Jack");
expect(found).toEqual({ expect(found).toEqual({
user_id: 1, user_id: 1,
name: "Jack", name: "Jack",
}); });
}); });
test(".insertWithout() inserts a partial row with type safety", () => { test(".insertWithout() inserts a partial row with type safety", () => {
const table = makeTestTable(); const table = makeTestTable();
table.insertWithout<"user_id">({ table.insertWithout<"user_id">({
name: "Jack", name: "Jack",
}); });
const lookup = table.db.prepare<UserWithID, string>( const lookup = table.db.prepare<UserWithID, string>(
`SELECT * FROM ${table} WHERE name = ?`, `SELECT * FROM ${table} WHERE name = ?`,
); );
const found = lookup.get("Jack"); const found = lookup.get("Jack");
expect(found).toEqual({ expect(found).toEqual({
user_id: 1, user_id: 1,
name: "Jack", name: "Jack",
}); });
}); });
test(".count() gets the current number of rows in the table", () => { test(".count() gets the current number of rows in the table", () => {
const table = makeTestTable(); const table = makeTestTable();
table.insertPartial({ name: "Jack" }); table.insertPartial({ name: "Jack" });
table.insertPartial({ name: "Kate" }); table.insertPartial({ name: "Kate" });
table.insertPartial({ name: "Sayid" }); table.insertPartial({ name: "Sayid" });
expect(table.count()).toBe(3); expect(table.count()).toBe(3);
}); });
test(".countWhere() gets the current number of rows that match a condition", () => { test(".countWhere() gets the current number of rows that match a condition", () => {
const table = makeTestTable(); const table = makeTestTable();
table.insertPartial({ name: "Jack" }); table.insertPartial({ name: "Jack" });
table.insertPartial({ name: "Kate" }); table.insertPartial({ name: "Kate" });
table.insertPartial({ name: "Sayid" }); table.insertPartial({ name: "Sayid" });
expect(table.countWhere({ name: "Jack" })).toBe(1); expect(table.countWhere({ name: "Jack" })).toBe(1);
}); });
test(".findAll() returns the full table", () => { test(".findAll() returns the full table", () => {
const table = makeTestTable(); const table = makeTestTable();
table.insertPartial({ name: "Jack" }); table.insertPartial({ name: "Jack" });
table.insertPartial({ name: "Kate" }); table.insertPartial({ name: "Kate" });
table.insertPartial({ name: "Sayid" }); table.insertPartial({ name: "Sayid" });
const allUsers = table.findAll(); const allUsers = table.findAll();
expect(allUsers).toHaveLength(3); expect(allUsers).toHaveLength(3);
expect(allUsers[0]).toEqual({ user_id: 1, name: "Jack" }); expect(allUsers[0]).toEqual({ user_id: 1, name: "Jack" });
expect(allUsers[1]).toEqual({ user_id: 2, name: "Kate" }); expect(allUsers[1]).toEqual({ user_id: 2, name: "Kate" });
expect(allUsers[2]).toEqual({ user_id: 3, name: "Sayid" }); expect(allUsers[2]).toEqual({ user_id: 3, name: "Sayid" });
}); });
test(".query() returns a prepared statement for the table", () => { test(".query() returns a prepared statement for the table", () => {
const table = makeTestTable(); const table = makeTestTable();
const query = table.query("SELECT * FROM Users WHERE name = ?"); const query = table.query("SELECT * FROM Users WHERE name = ?");
expect(query).toBeDefined(); expect(query).toBeDefined();
}); });
test(".findOneWhere() returns a row based on basic criteria", () => { test(".findOneWhere() returns a row based on basic criteria", () => {
const table = makeTestTable(); const table = makeTestTable();
table.insertPartial({ name: "Jack" }); table.insertPartial({ name: "Jack" });
table.insertPartial({ name: "Kate" }); table.insertPartial({ name: "Kate" });
table.insertPartial({ name: "Sayid" }); table.insertPartial({ name: "Sayid" });
const sayid = table.findOneWhere({ name: "Sayid" }); const sayid = table.findOneWhere({ name: "Sayid" });
expect(sayid).toEqual({ user_id: 3, name: "Sayid" }); expect(sayid).toEqual({ user_id: 3, name: "Sayid" });
}); });
test(".findOneWhereOrFail() throws if it finds nothing", () => { test(".findOneWhereOrFail() throws if it finds nothing", () => {
const table = makeTestTable(); const table = makeTestTable();
expect(() => { expect(() => {
table.findOneWhereOrFail({ name: "Sayid" }); table.findOneWhereOrFail({ name: "Sayid" });
}).toThrow(); }).toThrow();
}); });
test(".findAllWhere() returns a row based on basic criteria", () => { test(".findAllWhere() returns a row based on basic criteria", () => {
const table = makeTestTable(); const table = makeTestTable();
table.insertPartial({ name: "Jack" }); table.insertPartial({ name: "Jack" });
table.insertPartial({ name: "Jack" }); table.insertPartial({ name: "Jack" });
table.insertPartial({ name: "Jack" }); table.insertPartial({ name: "Jack" });
const manyJacks = table.findAllWhere({ name: "Jack" }); const manyJacks = table.findAllWhere({ name: "Jack" });
expect(manyJacks).toHaveLength(3); expect(manyJacks).toHaveLength(3);
expect(manyJacks[0]).toEqual({ user_id: 1, name: "Jack" }); expect(manyJacks[0]).toEqual({ user_id: 1, name: "Jack" });
expect(manyJacks[1]).toEqual({ user_id: 2, name: "Jack" }); expect(manyJacks[1]).toEqual({ user_id: 2, name: "Jack" });
expect(manyJacks[2]).toEqual({ user_id: 3, name: "Jack" }); expect(manyJacks[2]).toEqual({ user_id: 3, name: "Jack" });
}); });
test(".findOneById() returns a single element by its primary column", () => { test(".findOneById() returns a single element by its primary column", () => {
const table = makeTestTable(); const table = makeTestTable();
table.insertPartial({ name: "Jack" }); table.insertPartial({ name: "Jack" });
table.insertPartial({ name: "Kate" }); table.insertPartial({ name: "Kate" });
table.insertPartial({ name: "Sayid" }); table.insertPartial({ name: "Sayid" });
const sayid = table.findOneById(3); const sayid = table.findOneById(3);
expect(sayid).toEqual({ user_id: 3, name: "Sayid" }); expect(sayid).toEqual({ user_id: 3, name: "Sayid" });
}); });
test(".findOneById() returns null when nothing was found", () => { test(".findOneById() returns null when nothing was found", () => {
const table = makeTestTable(); const table = makeTestTable();
const nobody = table.findOneById(3); const nobody = table.findOneById(3);
expect(nobody).toBeNull(); expect(nobody).toBeNull();
}); });
test(".findOneByIdOrFail() returns a single element by its primary column", () => { test(".findOneByIdOrFail() returns a single element by its primary column", () => {
const table = makeTestTable(); const table = makeTestTable();
table.insertPartial({ name: "Jack" }); table.insertPartial({ name: "Jack" });
table.insertPartial({ name: "Kate" }); table.insertPartial({ name: "Kate" });
table.insertPartial({ name: "Sayid" }); table.insertPartial({ name: "Sayid" });
const sayid = table.findOneById(3); const sayid = table.findOneById(3);
expect(sayid).toEqual({ user_id: 3, name: "Sayid" }); expect(sayid).toEqual({ user_id: 3, name: "Sayid" });
}); });
test(".findOneByIdOrFail() throws an error when nothing is found", () => { test(".findOneByIdOrFail() throws an error when nothing is found", () => {
const table = makeTestTable(); const table = makeTestTable();
expect(() => { expect(() => {
table.findOneByIdOrFail(3); table.findOneByIdOrFail(3);
}).toThrow(); }).toThrow();
}); });
test(".delete() deletes a full record", () => { test(".delete() deletes a full record", () => {
const table = makeTestTable(); const table = makeTestTable();
table.insertPartial({ name: "Jack" }); table.insertPartial({ name: "Jack" });
table.insertPartial({ name: "Kate" }); table.insertPartial({ name: "Kate" });
table.insertPartial({ name: "Sayid" }); table.insertPartial({ name: "Sayid" });
expect(table.size()).toBe(3); expect(table.size()).toBe(3);
table.delete(table.findOneByIdOrFail(2)); table.delete(table.findOneByIdOrFail(2));
expect(table.size()).toBe(2); expect(table.size()).toBe(2);
expect(table.findAll()).toEqual([ expect(table.findAll()).toEqual([
{ user_id: 1, name: "Jack" }, { user_id: 1, name: "Jack" },
{ user_id: 3, name: "Sayid" }, { user_id: 3, name: "Sayid" },
]); ]);
}); });
test(".deleteById() deletes the element with the given primary value", () => { test(".deleteById() deletes the element with the given primary value", () => {
const table = makeTestTable(); const table = makeTestTable();
table.insertPartial({ name: "Jack" }); table.insertPartial({ name: "Jack" });
table.insertPartial({ name: "Kate" }); table.insertPartial({ name: "Kate" });
table.insertPartial({ name: "Sayid" }); table.insertPartial({ name: "Sayid" });
expect(table.size()).toBe(3); expect(table.size()).toBe(3);
table.deleteById(2); table.deleteById(2);
expect(table.size()).toBe(2); expect(table.size()).toBe(2);
expect(table.findAll()).toEqual([ expect(table.findAll()).toEqual([
{ user_id: 1, name: "Jack" }, { user_id: 1, name: "Jack" },
{ user_id: 3, name: "Sayid" }, { user_id: 3, name: "Sayid" },
]); ]);
}); });
test(".deleteWhere() deletes all rows that match", () => { test(".deleteWhere() deletes all rows that match", () => {
const table = makeTestTable(); const table = makeTestTable();
table.insertPartial({ name: "Jack" }); table.insertPartial({ name: "Jack" });
table.insertPartial({ name: "Jack" }); table.insertPartial({ name: "Jack" });
table.insertPartial({ name: "Sayid" }); table.insertPartial({ name: "Sayid" });
expect(table.size()).toBe(3); expect(table.size()).toBe(3);
table.deleteWhere({ name: "Jack" }); table.deleteWhere({ name: "Jack" });
expect(table.size()).toBe(1); expect(table.size()).toBe(1);
expect(table.findAll()).toEqual([{ user_id: 3, name: "Sayid" }]); expect(table.findAll()).toEqual([{ user_id: 3, name: "Sayid" }]);
}); });
test(".deleteById() respects when references are set to cascade", () => { test(".deleteById() respects when references are set to cascade", () => {
const pq = new Prequel(); const pq = new Prequel();
const users = pq.table<UserWithID>("Users", { const users = pq.table<UserWithID>("Users", {
user_id: { user_id: {
type: "INTEGER", type: "INTEGER",
primary: true, primary: true,
}, },
name: "TEXT", name: "TEXT",
}); });
const posts = pq.table<Post>("Posts", { const posts = pq.table<Post>("Posts", {
post_id: { post_id: {
type: "INTEGER", type: "INTEGER",
primary: true, primary: true,
}, },
user_id: { user_id: {
type: "INTEGER", type: "INTEGER",
references: users.reference(), references: users.reference(),
cascade: true, cascade: true,
}, },
}); });
const user = users.insertPartial({ name: "Bob" }); const user = users.insertPartial({ name: "Bob" });
posts.insertPartial({ posts.insertPartial({
user_id: user.user_id, user_id: user.user_id,
}); });
users.deleteById(user.user_id); users.deleteById(user.user_id);
expect(posts.size()).toBe(0); expect(posts.size()).toBe(0);
}); });
test(".deleteAll() deletes all rows in the table", () => { test(".deleteAll() deletes all rows in the table", () => {
const table = makeTestTable(); const table = makeTestTable();
table.insertPartial({ name: "Jack" }); table.insertPartial({ name: "Jack" });
table.insertPartial({ name: "Kate" }); table.insertPartial({ name: "Kate" });
table.insertPartial({ name: "Sayid" }); table.insertPartial({ name: "Sayid" });
expect(table.count()).toBe(3); expect(table.count()).toBe(3);
table.deleteAll(); table.deleteAll();
expect(table.count()).toBe(0); expect(table.count()).toBe(0);
}); });
test(".update() updates a full record", () => { test(".update() updates a full record", () => {
const table = makeTestTable(); const table = makeTestTable();
const locke = table.insertPartial({ name: "Locke" }); const locke = table.insertPartial({ name: "Locke" });
table.insertPartial({ name: "Kate" }); table.insertPartial({ name: "Kate" });
table.insertPartial({ name: "Sayid" }); table.insertPartial({ name: "Sayid" });
locke.name = "Jeremy Bentham"; locke.name = "Jeremy Bentham";
table.update(locke); table.update(locke);
const newLocke = table.findOneById(1); const newLocke = table.findOneById(1);
expect(newLocke?.name).toEqual("Jeremy Bentham"); expect(newLocke?.name).toEqual("Jeremy Bentham");
}); });
test(".update() throws if no primary column is set besides ROWID", () => { test(".update() throws if no primary column is set besides ROWID", () => {
const db = new Database(); const db = new Database();
const table = new Table<UserWithID>(db, "Users", { const table = new Table<UserWithID>(db, "Users", {
user_id: "INTEGER", user_id: "INTEGER",
name: "TEXT", name: "TEXT",
}); });
const user = table.insert({ user_id: 1, name: "User" }); const user = table.insert({ user_id: 1, name: "User" });
const oops = () => { const oops = () => {
table.update(user); table.update(user);
}; };
expect(oops).toThrow(/primary column/); expect(oops).toThrow(/primary column/);
}); });
test(".updateWhere() updates rows based on the condition", () => { test(".updateWhere() updates rows based on the condition", () => {
const db = new Database(); const db = new Database();
const table = new Table<UserWithID>(db, "Users", { const table = new Table<UserWithID>(db, "Users", {
user_id: "INTEGER", user_id: "INTEGER",
name: "TEXT", name: "TEXT",
}); });
table.insert({ user_id: 1, name: "Jack" }); table.insert({ user_id: 1, name: "Jack" });
table.insert({ user_id: 2, name: "Kate" }); table.insert({ user_id: 2, name: "Kate" });
table.insert({ user_id: 3, name: "Sayid" }); table.insert({ user_id: 3, name: "Sayid" });
table.updateWhere({ name: "Sawyer" }, { user_id: 2 }); table.updateWhere({ name: "Sawyer" }, { user_id: 2 });
expect(table.findOneById(2)?.name).toEqual("Sawyer"); expect(table.findOneById(2)?.name).toEqual("Sawyer");
}); });
test(".updateAll() updates all rows in the table", () => { test(".updateAll() updates all rows in the table", () => {
const db = new Database(); const db = new Database();
const table = new Table<UserWithID>(db, "Users", { const table = new Table<UserWithID>(db, "Users", {
user_id: "INTEGER", user_id: "INTEGER",
name: "TEXT", name: "TEXT",
}); });
table.insert({ user_id: 1, name: "Jack" }); table.insert({ user_id: 1, name: "Jack" });
table.insert({ user_id: 2, name: "Kate" }); table.insert({ user_id: 2, name: "Kate" });
table.insert({ user_id: 3, name: "Sayid" }); table.insert({ user_id: 3, name: "Sayid" });
table.updateAll({ name: "Sawyer" }); table.updateAll({ name: "Sawyer" });
expect(table.findAll().map((u) => u.name)).toEqual([ expect(table.findAll().map((u) => u.name)).toEqual([
"Sawyer", "Sawyer",
"Sawyer", "Sawyer",
"Sawyer", "Sawyer",
]); ]);
}); });