Improved typings

This commit is contained in:
Endeavorance 2025-04-18 10:53:57 -04:00
parent 98a5b9c0e1
commit 45ece62045
2 changed files with 29 additions and 45 deletions

View file

@ -2,6 +2,15 @@ type RedefineValueTypes<ShapeToRedefine, NewValueType> = {
[K in keyof ShapeToRedefine]: NewValueType; [K in keyof ShapeToRedefine]: NewValueType;
}; };
/** Types that may appear in a row */
export type ColumnValue =
| string
| number
| boolean
| bigint
| NodeJS.TypedArray
| null;
/** Types that define the data type of a column in the DB */ /** Types that define the data type of a column in the DB */
export type DataType = "TEXT" | "INTEGER" | "REAL" | "BLOB" | "NULL"; export type DataType = "TEXT" | "INTEGER" | "REAL" | "BLOB" | "NULL";

View file

@ -1,7 +1,8 @@
import type { Database, Statement } from "bun:sqlite"; import type { Database, SQLQueryBindings, Statement } from "bun:sqlite";
import { import {
type ColumnMap, type ColumnMap,
type ColumnShorthandMap, type ColumnShorthandMap,
type ColumnValue,
type DataType, type DataType,
generateColumnDefinitionSQL, generateColumnDefinitionSQL,
normalizeColumns, normalizeColumns,
@ -11,19 +12,6 @@ import { Prequel } from "./prequel";
export type ExtractRowShape<T> = T extends Table<infer R> ? R : never; export type ExtractRowShape<T> = T extends Table<infer R> ? R : never;
/** Types that may appear in a row */
type ColumnValue = string | number | boolean | bigint | null;
function isColumnValue(val: unknown): val is ColumnValue {
return (
typeof val === "string" ||
typeof val === "number" ||
typeof val === "boolean" ||
typeof val === "bigint" ||
val === null
);
}
export class PrequelIDNotFoundError extends Error { export class PrequelIDNotFoundError extends Error {
constructor(tableName: string, id: ColumnValue) { constructor(tableName: string, id: ColumnValue) {
super(`ID ${id} not found in ${tableName}`); super(`ID ${id} not found in ${tableName}`);
@ -36,23 +24,10 @@ export class PrequelEmptyResultsError extends Error {
} }
} }
/** An arbitrarily shaped Row */ function shapeForQuery<T>(obj: Partial<T>): SQLQueryBindings {
type ArbitraryRow = Record<string, ColumnValue>; const asInsertDataShape: SQLQueryBindings = {};
function shapeForQuery<T>(obj: Partial<T>): ArbitraryRow {
const asInsertDataShape: ArbitraryRow = {};
const keys = Object.keys(obj);
for (const key of keys) {
const val = obj[key as keyof T];
if (val !== undefined && !isColumnValue(val)) {
throw new SchemaError(
`Invalid value in query: ${key} -> ${val} (type: ${typeof val})`,
);
}
for (const [key, val] of Object.entries(obj)) {
const asQueryKey = `$${key}`; const asQueryKey = `$${key}`;
asInsertDataShape[asQueryKey] = val as ColumnValue; asInsertDataShape[asQueryKey] = val as ColumnValue;
} }
@ -73,11 +48,11 @@ export class Table<RowShape> {
private _createTableSQL: string; private _createTableSQL: string;
private _primaryColumnName = "ROWID"; private _primaryColumnName = "ROWID";
private _primaryColumnType: DataType | null = null; private _primaryColumnType: DataType | null = null;
private _insertQuery: Statement<RowShape, [ArbitraryRow]>; private _insertQuery: Statement<RowShape, [SQLQueryBindings]>;
private _updateQuery: Statement<RowShape, [ArbitraryRow]>; private _updateQuery: Statement<RowShape, [SQLQueryBindings]>;
private _findQuery: Statement<RowShape, [string]>; private _findQuery: Statement<RowShape, [string]>;
private _deleteQuery: Statement<void, [string]>; private _deleteQuery: Statement<void, [string]>;
private _deleteRowQuery: Statement<void, [ArbitraryRow]>; private _deleteRowQuery: Statement<void, [SQLQueryBindings]>;
private _sizeQuery: Statement<{ count: number }, []>; private _sizeQuery: Statement<{ count: number }, []>;
private _truncateQuery: Statement<void, []>; private _truncateQuery: Statement<void, []>;
@ -141,11 +116,11 @@ export class Table<RowShape> {
this.db.exec(this._createTableSQL); this.db.exec(this._createTableSQL);
// Prepare common queries // Prepare common queries
this._insertQuery = this.db.query<RowShape, ArbitraryRow>( this._insertQuery = this.db.query<RowShape, SQLQueryBindings>(
`INSERT INTO "${name}" (${allCols}) VALUES (${allColVars}) RETURNING *;`, `INSERT INTO "${name}" (${allCols}) VALUES (${allColVars}) RETURNING *;`,
); );
this._updateQuery = this.db.query<RowShape, ArbitraryRow>( this._updateQuery = this.db.query<RowShape, SQLQueryBindings>(
`UPDATE "${name}" SET ${updateColumnSQL} WHERE "${this._primaryColumnName}" = $${this._primaryColumnName} RETURNING *;`, `UPDATE "${name}" SET ${updateColumnSQL} WHERE "${this._primaryColumnName}" = $${this._primaryColumnName} RETURNING *;`,
); );
@ -161,7 +136,7 @@ export class Table<RowShape> {
`SELECT count(*) as count FROM ${name}`, `SELECT count(*) as count FROM ${name}`,
); );
this._deleteRowQuery = this.db.query<void, ArbitraryRow>( this._deleteRowQuery = this.db.query<void, SQLQueryBindings>(
`DELETE FROM "${name}" WHERE "${this._primaryColumnName}" = $${this._primaryColumnName};`, `DELETE FROM "${name}" WHERE "${this._primaryColumnName}" = $${this._primaryColumnName};`,
); );
@ -229,7 +204,7 @@ export class Table<RowShape> {
* @typeParam RowShape - The shape of the row in the table. * @typeParam RowShape - The shape of the row in the table.
*/ */
public insertPartial(data: Partial<RowShape>): RowShape { public insertPartial(data: Partial<RowShape>): RowShape {
const asInsertDataShape: ArbitraryRow = shapeForQuery<RowShape>(data); const asInsertDataShape: SQLQueryBindings = shapeForQuery<RowShape>(data);
const keys = Object.keys(data); const keys = Object.keys(data);
const colNames = keys.join(", "); const colNames = keys.join(", ");
const varNames = keys.map((keyName) => `$${keyName}`); const varNames = keys.map((keyName) => `$${keyName}`);
@ -261,7 +236,7 @@ export class Table<RowShape> {
); );
} }
const asInsertDataShape: ArbitraryRow = shapeForQuery<RowShape>(data); const asInsertDataShape: SQLQueryBindings = shapeForQuery<RowShape>(data);
return this._updateQuery.get(asInsertDataShape); return this._updateQuery.get(asInsertDataShape);
} }
@ -284,7 +259,7 @@ export class Table<RowShape> {
const whereParts = whereKeys.map((key) => `"${key}" = $${key}`); const whereParts = whereKeys.map((key) => `"${key}" = $${key}`);
const whereClause = whereParts.join(" AND "); const whereClause = whereParts.join(" AND ");
const query = this._db.prepare<RowShape, ArbitraryRow>( const query = this._db.prepare<RowShape, SQLQueryBindings>(
`UPDATE ${this} SET ${setClause} WHERE ${whereClause} RETURNING *;`, `UPDATE ${this} SET ${setClause} WHERE ${whereClause} RETURNING *;`,
); );
@ -308,7 +283,7 @@ export class Table<RowShape> {
const setParts = keys.map((key) => `"${key}" = $${key}`); const setParts = keys.map((key) => `"${key}" = $${key}`);
const setClause = setParts.join(", "); const setClause = setParts.join(", ");
const query = this._db.prepare<RowShape, ArbitraryRow>( const query = this._db.prepare<RowShape, SQLQueryBindings>(
`UPDATE ${this} SET ${setClause} RETURNING *;`, `UPDATE ${this} SET ${setClause} RETURNING *;`,
); );
@ -345,7 +320,7 @@ export class Table<RowShape> {
const keys = Object.keys(conditions); const keys = Object.keys(conditions);
const whereParts = keys.map((key) => `"${key}" = $${key}`); const whereParts = keys.map((key) => `"${key}" = $${key}`);
const whereClause = whereParts.join(" AND "); const whereClause = whereParts.join(" AND ");
const query = this._db.prepare<{ count: number }, ArbitraryRow>( const query = this._db.prepare<{ count: number }, SQLQueryBindings>(
`SELECT count(*) as count FROM ${this} WHERE ${whereClause};`, `SELECT count(*) as count FROM ${this} WHERE ${whereClause};`,
); );
@ -401,7 +376,7 @@ export class Table<RowShape> {
const keys = Object.keys(conditions); const keys = Object.keys(conditions);
const whereParts = keys.map((key) => `"${key}" = $${key}`); const whereParts = keys.map((key) => `"${key}" = $${key}`);
const whereClause = whereParts.join(" AND "); const whereClause = whereParts.join(" AND ");
const query = this._db.prepare<RowShape, ArbitraryRow>( const query = this._db.prepare<RowShape, SQLQueryBindings>(
`SELECT * FROM ${this} WHERE ${whereClause};`, `SELECT * FROM ${this} WHERE ${whereClause};`,
); );
@ -459,7 +434,7 @@ export class Table<RowShape> {
} }
} }
const query = this._db.prepare<RowShape, ArbitraryRow>( const query = this._db.prepare<RowShape, SQLQueryBindings>(
`SELECT * FROM ${this} WHERE ${whereClause} ${optionParts.join(" ")};`, `SELECT * FROM ${this} WHERE ${whereClause} ${optionParts.join(" ")};`,
); );
@ -480,7 +455,7 @@ export class Table<RowShape> {
); );
} }
const asDeleteDataShape: ArbitraryRow = shapeForQuery<RowShape>(data); const asDeleteDataShape: SQLQueryBindings = shapeForQuery<RowShape>(data);
this._deleteRowQuery.run(asDeleteDataShape); this._deleteRowQuery.run(asDeleteDataShape);
} }
@ -509,7 +484,7 @@ export class Table<RowShape> {
const keys = Object.keys(conditions); const keys = Object.keys(conditions);
const whereParts = keys.map((key) => `"${key}" = $${key}`); const whereParts = keys.map((key) => `"${key}" = $${key}`);
const whereClause = whereParts.join(" AND "); const whereClause = whereParts.join(" AND ");
const query = this._db.prepare<void, ArbitraryRow>( const query = this._db.prepare<void, SQLQueryBindings>(
`DELETE FROM ${this} WHERE ${whereClause};`, `DELETE FROM ${this} WHERE ${whereClause};`,
); );