first commit
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Database models — the single source of truth for a table's shape.
|
||||
*
|
||||
* A model defined with `table()` + the `v` column builder drives (1) DDL for
|
||||
* migrations, (2) coercion/validation of DB rows into typed objects
|
||||
* (`model.parse`), and later (3) the types the sqlc-style query generator emits.
|
||||
* Column types are dialect-neutral; each adapter maps them to its own SQL types.
|
||||
*/
|
||||
|
||||
export type BaseType = "id" | "text" | "int" | "real" | "bool" | "timestamp" | "json";
|
||||
|
||||
export interface ColumnDef {
|
||||
type: BaseType;
|
||||
/** NOT NULL unless `.optional()` was called. Ids are implicitly not-null. */
|
||||
notNull: boolean;
|
||||
primaryKey: boolean;
|
||||
autoIncrement: boolean;
|
||||
unique: boolean;
|
||||
/** Literal default, or the sentinel "now" for a current-timestamp default. */
|
||||
default?: unknown;
|
||||
references?: { table: string; column: string };
|
||||
}
|
||||
|
||||
/** A fluent column definition. Chain modifiers, then hand it to `table()`. */
|
||||
export class Column {
|
||||
readonly def: ColumnDef;
|
||||
constructor(type: BaseType, overrides: Partial<ColumnDef> = {}) {
|
||||
this.def = {
|
||||
type,
|
||||
notNull: true,
|
||||
primaryKey: false,
|
||||
autoIncrement: false,
|
||||
unique: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
optional(): this {
|
||||
this.def.notNull = false;
|
||||
return this;
|
||||
}
|
||||
unique(): this {
|
||||
this.def.unique = true;
|
||||
return this;
|
||||
}
|
||||
default(value: unknown): this {
|
||||
this.def.default = value;
|
||||
return this;
|
||||
}
|
||||
primaryKey(): this {
|
||||
this.def.primaryKey = true;
|
||||
return this;
|
||||
}
|
||||
references(table: string, column = "id"): this {
|
||||
this.def.references = { table, column };
|
||||
return this;
|
||||
}
|
||||
/** Coerce a raw DB value into its JS type (used by `model.parse`). */
|
||||
coerce(raw: unknown): unknown {
|
||||
if (raw === null || raw === undefined) return this.def.notNull ? raw : null;
|
||||
switch (this.def.type) {
|
||||
case "id":
|
||||
case "int":
|
||||
return typeof raw === "bigint" ? Number(raw) : Number(raw);
|
||||
case "real":
|
||||
return Number(raw);
|
||||
case "bool":
|
||||
return raw === true || raw === 1 || raw === "1" || raw === "true";
|
||||
case "timestamp":
|
||||
return raw instanceof Date ? raw : new Date(raw as string | number);
|
||||
case "json":
|
||||
return typeof raw === "string" ? safeJson(raw) : raw;
|
||||
default:
|
||||
return String(raw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function safeJson(value: string): unknown {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/** Column builders. `v.id()` is an auto-increment primary key. */
|
||||
export const v = {
|
||||
id: () => new Column("id", { primaryKey: true, autoIncrement: true }),
|
||||
text: () => new Column("text"),
|
||||
string: () => new Column("text"),
|
||||
int: () => new Column("int"),
|
||||
number: () => new Column("real"),
|
||||
real: () => new Column("real"),
|
||||
bool: () => new Column("bool"),
|
||||
boolean: () => new Column("bool"),
|
||||
timestamp: () => new Column("timestamp"),
|
||||
json: () => new Column("json"),
|
||||
};
|
||||
|
||||
export type Columns = Record<string, Column>;
|
||||
|
||||
export interface Model<T = Record<string, unknown>> {
|
||||
name: string;
|
||||
columns: Columns;
|
||||
/** Coerce a raw DB row into a typed object (unknown columns pass through). */
|
||||
parse(row: Record<string, unknown>): T;
|
||||
/** Column definitions, for migrations and the query generator. */
|
||||
describe(): Record<string, ColumnDef>;
|
||||
}
|
||||
|
||||
/** Define a table model from a name and a map of columns. */
|
||||
export function table<T = Record<string, unknown>>(name: string, columns: Columns): Model<T> {
|
||||
return {
|
||||
name,
|
||||
columns,
|
||||
parse(row) {
|
||||
const out: Record<string, unknown> = { ...row };
|
||||
for (const [key, column] of Object.entries(columns)) {
|
||||
if (key in row) out[key] = column.coerce(row[key]);
|
||||
}
|
||||
return out as T;
|
||||
},
|
||||
describe() {
|
||||
const out: Record<string, ColumnDef> = {};
|
||||
for (const [key, column] of Object.entries(columns)) out[key] = column.def;
|
||||
return out;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user