better formatting
This commit is contained in:
parent
b3b9b40d3a
commit
8ada5294c4
3 changed files with 97 additions and 36 deletions
|
|
@ -1,19 +1,39 @@
|
||||||
import type { TableClass } from "muddle/contracts/typegen/orm/index.ts";
|
import type { TableClass } from "muddle/contracts/typegen/orm/index.ts";
|
||||||
import { getTableMetadata } from "./decorators/Table.ts";
|
import { getTableMetadata } from "./decorators/Table.ts";
|
||||||
|
|
||||||
export interface SchemaColumn { name: string; type: string; nullable: boolean }
|
export interface SchemaColumn {
|
||||||
export interface SchemaTable { name: string; columns: SchemaColumn[] }
|
name: string;
|
||||||
export interface Migration { up: string[]; down: 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[] {
|
export function schemaOf(tables: TableClass[]): SchemaTable[] {
|
||||||
return tables.map((table) => {
|
return tables.map((table) => {
|
||||||
const metadata = getTableMetadata(table);
|
const metadata = getTableMetadata(table);
|
||||||
if (!metadata) throw new Error(`Missing @Table metadata for ${table.name}`);
|
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 oldTables = new Map(previous.map((table) => [table.name, table]));
|
||||||
const nextTables = new Map(next.map((table) => [table.name, table]));
|
const nextTables = new Map(next.map((table) => [table.name, table]));
|
||||||
const up: string[] = [];
|
const up: string[] = [];
|
||||||
|
|
@ -21,19 +41,27 @@ export function diffSchema(previous: SchemaTable[], next: SchemaTable[]): Migrat
|
||||||
for (const table of next) {
|
for (const table of next) {
|
||||||
const old = oldTables.get(table.name);
|
const old = oldTables.get(table.name);
|
||||||
if (!old) {
|
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};`);
|
down.unshift(`DROP TABLE ${table.name};`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const oldColumns = new Map(old.columns.map((column) => [column.name, column]));
|
const oldColumns = new Map(
|
||||||
for (const column of table.columns) if (!oldColumns.has(column.name)) {
|
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)};`);
|
up.push(`ALTER TABLE ${table.name} ADD COLUMN ${columnSql(column)};`);
|
||||||
down.unshift(`ALTER TABLE ${table.name} DROP COLUMN ${column.name};`);
|
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};`);
|
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 };
|
return { up, down };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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. */
|
/** Persistent sessions and atomic OAuth transaction consumption for SQLite, D1 and Postgres. */
|
||||||
export class DatabaseSessionStore implements SessionStore {
|
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> {
|
static async create(
|
||||||
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table)) throw new Error("Invalid session table name");
|
driver: SqlDriver,
|
||||||
await driver.execute(`CREATE TABLE IF NOT EXISTS "${table}" (id TEXT PRIMARY KEY, value TEXT NOT NULL, expires BIGINT NOT NULL)`);
|
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);
|
return new DatabaseSessionStore(driver, table);
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(key: string): Promise<SessionRecord | undefined> {
|
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];
|
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) };
|
return { value: JSON.parse(row.value), expires: Number(row.expires) };
|
||||||
}
|
}
|
||||||
|
|
||||||
async set(key: string, record: SessionRecord): Promise<void> {
|
async set(key: string, record: SessionRecord): Promise<void> {
|
||||||
const args = [key, JSON.stringify(record.value), record.expires];
|
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> {
|
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];
|
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()],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue