better formatting

This commit is contained in:
Hellings 2026-10-23 09:12:49 -04:00
parent b3b9b40d3a
commit 8ada5294c4
3 changed files with 97 additions and 36 deletions

View file

@ -1,19 +1,39 @@
import type { TableClass } from "muddle/contracts/typegen/orm/index.ts";
import { getTableMetadata } from "./decorators/Table.ts";
export interface SchemaColumn { name: string; type: string; nullable: boolean }
export interface SchemaTable { name: string; columns: SchemaColumn[] }
export interface Migration { up: string[]; down: string[] }
export interface SchemaColumn {
name: string;
type: string;
nullable: boolean;
}
export interface SchemaTable {
name: string;
columns: SchemaColumn[];
}
export interface Migration {
up: string[];
down: string[];
}
export function schemaOf(tables: TableClass[]): SchemaTable[] {
return tables.map((table) => {
const metadata = getTableMetadata(table);
if (!metadata) throw new Error(`Missing @Table metadata for ${table.name}`);
return { name: metadata.name, columns: [...metadata.fields.values()].map((field) => ({ name: field.column, type: field.type, nullable: field.nullable })) };
return {
name: metadata.name,
columns: [...metadata.fields.values()].map((field) => ({
name: field.column,
type: field.type,
nullable: field.nullable,
})),
};
});
}
export function diffSchema(previous: SchemaTable[], next: SchemaTable[]): Migration {
export function diffSchema(
previous: SchemaTable[],
next: SchemaTable[],
): Migration {
const oldTables = new Map(previous.map((table) => [table.name, table]));
const nextTables = new Map(next.map((table) => [table.name, table]));
const up: string[] = [];
@ -21,19 +41,27 @@ export function diffSchema(previous: SchemaTable[], next: SchemaTable[]): Migrat
for (const table of next) {
const old = oldTables.get(table.name);
if (!old) {
up.push(`CREATE TABLE ${table.name} (${table.columns.map(columnSql).join(", ")});`);
up.push(
`CREATE TABLE ${table.name} (${table.columns.map(columnSql).join(", ")});`,
);
down.unshift(`DROP TABLE ${table.name};`);
continue;
}
const oldColumns = new Map(old.columns.map((column) => [column.name, column]));
for (const column of table.columns) if (!oldColumns.has(column.name)) {
const oldColumns = new Map(
old.columns.map((column) => [column.name, column]),
);
for (const column of table.columns)
if (!oldColumns.has(column.name)) {
up.push(`ALTER TABLE ${table.name} ADD COLUMN ${columnSql(column)};`);
down.unshift(`ALTER TABLE ${table.name} DROP COLUMN ${column.name};`);
}
}
for (const table of previous) if (!nextTables.has(table.name)) {
for (const table of previous)
if (!nextTables.has(table.name)) {
up.push(`DROP TABLE ${table.name};`);
down.unshift(`CREATE TABLE ${table.name} (${table.columns.map(columnSql).join(", ")});`);
down.unshift(
`CREATE TABLE ${table.name} (${table.columns.map(columnSql).join(", ")});`,
);
}
return { up, down };
}

View file

@ -3,33 +3,66 @@ import type { SqlDriver } from "muddle/contracts/typegen/orm/index.ts";
/** Persistent sessions and atomic OAuth transaction consumption for SQLite, D1 and Postgres. */
export class DatabaseSessionStore implements SessionStore {
private constructor(private readonly driver: SqlDriver, private readonly table: string) {}
private constructor(
private readonly driver: SqlDriver,
private readonly table: string,
) {}
static async create(driver: SqlDriver, table = "muddle_sessions"): Promise<DatabaseSessionStore> {
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table)) throw new Error("Invalid session table name");
await driver.execute(`CREATE TABLE IF NOT EXISTS "${table}" (id TEXT PRIMARY KEY, value TEXT NOT NULL, expires BIGINT NOT NULL)`);
static async create(
driver: SqlDriver,
table = "muddle_sessions",
): Promise<DatabaseSessionStore> {
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table))
throw new Error("Invalid session table name");
await driver.execute(
`CREATE TABLE IF NOT EXISTS "${table}" (id TEXT PRIMARY KEY, value TEXT NOT NULL, expires BIGINT NOT NULL)`,
);
return new DatabaseSessionStore(driver, table);
}
async get(key: string): Promise<SessionRecord | undefined> {
const rows = await this.driver.query<{ value: string; expires: number }>(`SELECT value, expires FROM "${this.table}" WHERE id = ${this.driver.placeholder(1)}`, [key]);
const rows = await this.driver.query<{ value: string; expires: number }>(
`SELECT value, expires FROM "${this.table}" WHERE id = ${this.driver.placeholder(1)}`,
[key],
);
const row = rows[0];
if (!row || Number(row.expires) <= Date.now()) { await this.delete(key); return undefined; }
if (!row || Number(row.expires) <= Date.now()) {
await this.delete(key);
return undefined;
}
return { value: JSON.parse(row.value), expires: Number(row.expires) };
}
async set(key: string, record: SessionRecord): Promise<void> {
const args = [key, JSON.stringify(record.value), record.expires];
await this.driver.execute(`INSERT INTO "${this.table}" (id, value, expires) VALUES (${args.map((_, index) => this.driver.placeholder(index + 1)).join(", ")}) ON CONFLICT (id) DO UPDATE SET value = excluded.value, expires = excluded.expires`, args);
await this.driver.execute(
`INSERT INTO "${this.table}" (id, value, expires) VALUES (${args.map((_, index) => this.driver.placeholder(index + 1)).join(", ")}) ON CONFLICT (id) DO UPDATE SET value = excluded.value, expires = excluded.expires`,
args,
);
}
async delete(key: string): Promise<void> { await this.driver.execute(`DELETE FROM "${this.table}" WHERE id = ${this.driver.placeholder(1)}`, [key]); }
async delete(key: string): Promise<void> {
await this.driver.execute(
`DELETE FROM "${this.table}" WHERE id = ${this.driver.placeholder(1)}`,
[key],
);
}
async consume(key: string): Promise<SessionRecord | undefined> {
const rows = await this.driver.query<{ value: string; expires: number }>(`DELETE FROM "${this.table}" WHERE id = ${this.driver.placeholder(1)} RETURNING value, expires`, [key]);
const rows = await this.driver.query<{ value: string; expires: number }>(
`DELETE FROM "${this.table}" WHERE id = ${this.driver.placeholder(1)} RETURNING value, expires`,
[key],
);
const row = rows[0];
return row && Number(row.expires) > Date.now() ? { value: JSON.parse(row.value), expires: Number(row.expires) } : undefined;
return row && Number(row.expires) > Date.now()
? { value: JSON.parse(row.value), expires: Number(row.expires) }
: undefined;
}
async prune(): Promise<void> { await this.driver.execute(`DELETE FROM "${this.table}" WHERE expires <= ${this.driver.placeholder(1)}`, [Date.now()]); }
async prune(): Promise<void> {
await this.driver.execute(
`DELETE FROM "${this.table}" WHERE expires <= ${this.driver.placeholder(1)}`,
[Date.now()],
);
}
}