Prepare for DB migartions

This commit is contained in:
2025-03-29 20:46:35 +01:00
parent eea2eac109
commit d46e9a3faf

View File

@@ -1,5 +1,6 @@
import { Database as BunSqliteDatabase } from "bun:sqlite"; import { Database as BunSqliteDatabase } from "bun:sqlite";
import { AttachmentId, PieceId, RepertoireId, RequestId, SessionId, Sha256_Bin, UserId } from "common"; import { AttachmentId, PieceId, RepertoireId, RequestId, SessionId, Sha256_Bin, UserId } from "common";
import { HashSet } from "effect";
import { ColumnType, CompiledQuery, CreateTableBuilder, Kysely, Selectable } from "kysely"; import { ColumnType, CompiledQuery, CreateTableBuilder, Kysely, Selectable } from "kysely";
import { BunSqliteDialect } from "kysely-bun-sqlite"; import { BunSqliteDialect } from "kysely-bun-sqlite";
@@ -15,9 +16,11 @@ export interface Database {
Attachment: AttachmentTable; Attachment: AttachmentTable;
File: FileTable; File: FileTable;
Piece: PieceTable; Piece: PieceTable;
Option: OptionTable;
Repertoire: RepertoireTable; Repertoire: RepertoireTable;
RepertoireEntry: RepertoireEntryTable; RepertoireEntry: RepertoireEntryTable;
Session: SessionTable; Session: SessionTable;
sqlite_schema: SqliteSchemaTable;
} }
export interface SystemInformation { export interface SystemInformation {
@@ -52,6 +55,11 @@ export interface FileTable {
data: ColumnType<Uint8Array, Uint8Array, never>; data: ColumnType<Uint8Array, Uint8Array, never>;
} }
export interface OptionTable {
key: ColumnType<string, string, never>;
value: string;
}
export interface PieceData { export interface PieceData {
name: string; name: string;
composer: string | null; composer: string | null;
@@ -91,13 +99,23 @@ export interface SessionTable extends SessionData {
expiresAt: string; expiresAt: string;
} }
export interface SqliteSchemaTable {
type: ColumnType<string, never, never>;
name: ColumnType<string, never, never>;
tbl_name: ColumnType<string, never, never>;
rootpage: ColumnType<number, never, never>;
sql: ColumnType<string | null, never, never>;
}
export type AccessLog = Selectable<AccessLogTable>; export type AccessLog = Selectable<AccessLogTable>;
export type Attachment = Selectable<AttachmentTable>; export type Attachment = Selectable<AttachmentTable>;
export type File = Selectable<FileTable>; export type File = Selectable<FileTable>;
export type Option = Selectable<OptionTable>;
export type Piece = Selectable<PieceTable>; export type Piece = Selectable<PieceTable>;
export type Repertoire = Selectable<RepertoireTable>; export type Repertoire = Selectable<RepertoireTable>;
export type RepertoireEntry = Selectable<RepertoireEntryTable>; export type RepertoireEntry = Selectable<RepertoireEntryTable>;
export type Session = Selectable<SessionTable>; export type Session = Selectable<SessionTable>;
export type SqliteSchema = Selectable<SqliteSchemaTable>;
function systemInformation<TB extends string, C extends string>(schema: CreateTableBuilder<TB, C>) { function systemInformation<TB extends string, C extends string>(schema: CreateTableBuilder<TB, C>) {
return schema return schema
@@ -115,112 +133,133 @@ export async function initDatabase(filename: string = "db.sqlite3"): Promise<Kys
await db.executeQuery(CompiledQuery.raw("PRAGMA foreign_keys = ON")); await db.executeQuery(CompiledQuery.raw("PRAGMA foreign_keys = ON"));
await db.schema const tables = await db
.createTable("AccessLog") .selectFrom("sqlite_schema")
.ifNotExists() .select("name")
.addColumn("requestId", "text", (c) => c.notNull().primaryKey()) .where("type", "=", "table")
.addColumn("timestamp", "text", (c) => c.notNull()) .execute()
.addColumn("method", "text", (c) => c.notNull()) .then((tables) => HashSet.make(...tables.map(({ name }) => name)));
.addColumn("pathname", "text", (c) => c.notNull())
.addColumn("query", "text", (c) => c.notNull())
.addColumn("ip", "text")
.execute();
await db.schema if (!HashSet.has(tables, "Option")) {
.createIndex("AccessLog_timestamp") await db.schema
.ifNotExists() .createTable("AccessLog")
.on("AccessLog") .ifNotExists()
.column("timestamp") .addColumn("requestId", "text", (c) => c.notNull().primaryKey())
.execute(); .addColumn("timestamp", "text", (c) => c.notNull())
.addColumn("method", "text", (c) => c.notNull())
.addColumn("pathname", "text", (c) => c.notNull())
.addColumn("query", "text", (c) => c.notNull())
.addColumn("ip", "text")
.execute();
await db.schema await db.schema
.createTable("File") .createIndex("AccessLog_timestamp")
.ifNotExists() .ifNotExists()
.addColumn("sha256", "blob", (c) => c.notNull().primaryKey()) .on("AccessLog")
.addColumn("data", "blob", (c) => c.notNull()) .column("timestamp")
.execute(); .execute();
await db.schema await db.schema
.createTable("User") .createTable("File")
.ifNotExists() .ifNotExists()
.addColumn("userId", "text", (c) => c.notNull().primaryKey()) .addColumn("sha256", "blob", (c) => c.notNull().primaryKey())
.addColumn("username", "text", (c) => c.notNull().unique()) .addColumn("data", "blob", (c) => c.notNull())
.addColumn("password", "text", (c) => c.notNull()) .execute();
.addColumn("admin", "boolean", (c) => c.notNull())
.execute();
await db.schema await db.schema
.createTable("Piece") .createTable("User")
.ifNotExists() .ifNotExists()
.addColumn("pieceId", "text", (c) => c.notNull().primaryKey()) .addColumn("userId", "text", (c) => c.notNull().primaryKey())
.addColumn("name", "text", (c) => c.notNull()) .addColumn("username", "text", (c) => c.notNull().unique())
.addColumn("composer", "text") .addColumn("password", "text", (c) => c.notNull())
.addColumn("lyricist", "text") .addColumn("admin", "boolean", (c) => c.notNull())
.addColumn("arranger", "text") .execute();
.$call(systemInformation)
.execute();
await db.schema await db.schema
.createIndex("Piece_name_composer_arranger") .createTable("Piece")
.ifNotExists() .ifNotExists()
.on("Piece") .addColumn("pieceId", "text", (c) => c.notNull().primaryKey())
.columns(["name", "composer", "arranger"]) .addColumn("name", "text", (c) => c.notNull())
.execute(); .addColumn("composer", "text")
.addColumn("lyricist", "text")
.addColumn("arranger", "text")
.$call(systemInformation)
.execute();
await db.schema await db.schema
.createTable("Repertoire") .createIndex("Piece_name_composer_arranger")
.ifNotExists() .ifNotExists()
.addColumn("repertoireId", "text", (c) => c.notNull().primaryKey()) .on("Piece")
.addColumn("name", "text", (c) => c.notNull()) .columns(["name", "composer", "arranger"])
.$call(systemInformation) .execute();
.execute();
await db.schema await db.schema
.createIndex("Repertoire_name") .createTable("Repertoire")
.ifNotExists() .ifNotExists()
.on("Repertoire") .addColumn("repertoireId", "text", (c) => c.notNull().primaryKey())
.column("name") .addColumn("name", "text", (c) => c.notNull())
.execute(); .$call(systemInformation)
.execute();
await db.schema await db.schema
.createTable("RepertoireEntry") .createIndex("Repertoire_name")
.ifNotExists() .ifNotExists()
.addColumn("repertoireId", "text", (c) => c.notNull().references("Repertoire.repertoireId").onDelete("cascade").onUpdate("cascade")) .on("Repertoire")
.addColumn("order", "integer", (c) => c.notNull()) .column("name")
.addColumn("pieceId", "text", (c) => c.notNull().references("Piece.pieceId").onDelete("restrict").onUpdate("restrict")) .execute();
.addPrimaryKeyConstraint("pk_RepertoireEntry", ["repertoireId", "order"])
.execute();
await db.schema await db.schema
.createTable("Session") .createTable("RepertoireEntry")
.ifNotExists() .ifNotExists()
.addColumn("sessionId", "text", (c) => c.notNull().primaryKey()) .addColumn("repertoireId", "text", (c) => c.notNull().references("Repertoire.repertoireId").onDelete("cascade").onUpdate("cascade"))
.addColumn("state", "text") .addColumn("order", "integer", (c) => c.notNull())
.addColumn("codeVerifier", "text") .addColumn("pieceId", "text", (c) => c.notNull().references("Piece.pieceId").onDelete("restrict").onUpdate("restrict"))
.addColumn("accessToken", "text") .addPrimaryKeyConstraint("pk_RepertoireEntry", ["repertoireId", "order"])
.addColumn("idToken", "text") .execute();
.addColumn("refreshToken", "text")
.addColumn("external", "boolean")
.addColumn("expiresAt", "text", (c) => c.notNull())
.execute();
await db.schema await db.schema
.createTable("Attachment") .createTable("Session")
.ifNotExists() .ifNotExists()
.addColumn("attachmentId", "text", (c) => c.notNull().primaryKey()) .addColumn("sessionId", "text", (c) => c.notNull().primaryKey())
.addColumn("pieceId", "text", (c) => c.notNull().references("Piece.pieceId").onDelete("cascade").onUpdate("cascade")) .addColumn("state", "text")
.addColumn("sha256", "blob", (c) => c.notNull().references("File.sha256").onDelete("restrict").onUpdate("restrict")) .addColumn("codeVerifier", "text")
.addColumn("filename", "text", (c) => c.notNull()) .addColumn("accessToken", "text")
.addColumn("mediaType", "text", (c) => c.notNull()) .addColumn("idToken", "text")
.$call(systemInformation) .addColumn("refreshToken", "text")
.execute(); .addColumn("external", "boolean")
.addColumn("expiresAt", "text", (c) => c.notNull())
.execute();
await db.schema await db.schema
.createIndex("Attachment_pieceId_filename") .createTable("Attachment")
.ifNotExists() .ifNotExists()
.on("Attachment") .addColumn("attachmentId", "text", (c) => c.notNull().primaryKey())
.columns(["pieceId", "filename"]) .addColumn("pieceId", "text", (c) => c.notNull().references("Piece.pieceId").onDelete("cascade").onUpdate("cascade"))
.execute(); .addColumn("sha256", "blob", (c) => c.notNull().references("File.sha256").onDelete("restrict").onUpdate("restrict"))
.addColumn("filename", "text", (c) => c.notNull())
.addColumn("mediaType", "text", (c) => c.notNull())
.$call(systemInformation)
.execute();
await db.schema
.createIndex("Attachment_pieceId_filename")
.ifNotExists()
.on("Attachment")
.columns(["pieceId", "filename"])
.execute();
await db.schema
.createTable("Option")
.ifNotExists()
.addColumn("key", "text", (c) => c.notNull().primaryKey())
.addColumn("value", "text", (c) => c.notNull())
.execute();
await db
.insertInto("Option")
.values({ key: "database_version", value: "0" })
.execute();
}
return db; return db;
} }