diff --git a/package.json b/package.json index 1ea59a8..e0c0144 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { - "name": "@muddle/database", - "type": "module", - "exports": "./index.ts", - "version": "0.1.0", - "peerDependencies": { - "muddle": "*" - }, - "dependencies": { - "pg": "^8.23.1" - } + "name": "@muddle/database", + "type": "module", + "exports": "./index.ts", + "version": "0.1.0", + "peerDependencies": { + "muddle": "*" + }, + "dependencies": { + "pg": "^8.23.1" + } } diff --git a/src/migrations.ts b/src/migrations.ts index 0666974..7304ea7 100644 --- a/src/migrations.ts +++ b/src/migrations.ts @@ -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,20 +41,28 @@ 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)) { - up.push(`ALTER TABLE ${table.name} ADD COLUMN ${columnSql(column)};`); - down.unshift(`ALTER TABLE ${table.name} DROP COLUMN ${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)) { + up.push(`DROP TABLE ${table.name};`); + down.unshift( + `CREATE TABLE ${table.name} (${table.columns.map(columnSql).join(", ")});`, + ); } - } - 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(", ")});`); - } return { up, down }; } diff --git a/src/sessions.ts b/src/sessions.ts index f4e8898..b6edc45 100644 --- a/src/sessions.ts +++ b/src/sessions.ts @@ -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 { - 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 { + 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 { - 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 { 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 { await this.driver.execute(`DELETE FROM "${this.table}" WHERE id = ${this.driver.placeholder(1)}`, [key]); } + async delete(key: string): Promise { + await this.driver.execute( + `DELETE FROM "${this.table}" WHERE id = ${this.driver.placeholder(1)}`, + [key], + ); + } async consume(key: string): Promise { - 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 { await this.driver.execute(`DELETE FROM "${this.table}" WHERE expires <= ${this.driver.placeholder(1)}`, [Date.now()]); } + async prune(): Promise { + await this.driver.execute( + `DELETE FROM "${this.table}" WHERE expires <= ${this.driver.placeholder(1)}`, + [Date.now()], + ); + } }