Files
STTracker/packages/db/src/migrations/0001_initial.ts
T

426 lines
11 KiB
TypeScript

import type { Kysely } from "kysely";
import type { Migration } from "kysely/migration";
/**
* Column kinds: `t` text, `i` integer, `r` real, `fk:<table>` text reference. Not null by default;
* a trailing `?` makes it nullable. Only portable types, so the same code runs on SQLite and Postgres.
*/
type Kind = string;
type Spec = Array<[name: string, kind: Kind]>;
async function create(
db: Kysely<any>,
name: string,
cols: Spec,
opts: { unique?: string[][]; indexes?: string[][] } = {},
): Promise<void> {
let b = db.schema.createTable(name).addColumn("id", "text", (c) => c.primaryKey());
const fkCols: string[] = [];
for (const [col, kind] of cols) {
const nullable = kind.endsWith("?");
const base = nullable ? kind.slice(0, -1) : kind;
const isFk = base.startsWith("fk:");
const type = base === "i" ? "integer" : base === "r" ? "real" : "text";
if (isFk) fkCols.push(col);
b = b.addColumn(col, type, (c) => {
let x = c;
if (isFk) x = x.references(`${base.slice(3)}.id`);
return nullable ? x : x.notNull();
});
}
for (const u of opts.unique ?? []) b = b.addUniqueConstraint(`${name}_${u.join("_")}_uq`, u as Array<"id">);
await b.execute();
const indexes = [...fkCols.map((c) => [c]), ...(opts.indexes ?? [])];
for (const cols of indexes) {
await db.schema
.createIndex(`${name}_${cols.join("_")}_idx`)
.on(name)
.columns(cols as Array<"id">)
.execute();
}
}
export const initial: Migration = {
async up(db: Kysely<any>) {
await create(
db,
"users",
[
["username", "t"],
["display_name", "t"],
["password_hash", "t"],
["role", "t"],
["preferences", "t"],
["created_at", "t"],
["updated_at", "t"],
],
{ unique: [["username"]] },
);
await create(
db,
"sessions",
[
["user_id", "fk:users"],
["token_hash", "t"],
["expires_at", "t"],
["created_at", "t"],
],
{ unique: [["token_hash"]] },
);
await create(
db,
"invites",
[
["code_hash", "t"],
["created_by", "fk:users"],
["used_by", "fk:users?"],
["expires_at", "t"],
["created_at", "t"],
],
{ unique: [["code_hash"]] },
);
await create(
db,
"app_tokens",
[
["user_id", "fk:users"],
["name", "t"],
["token_hash", "t"],
["scopes", "t"],
["last_used_at", "t?"],
["revoked_at", "t?"],
["created_at", "t"],
],
{ unique: [["token_hash"]] },
);
// works.parent_id is a self reference; works.cover_attachment_id and merged_into_id stay
// plain columns (attachments is created later; merges may point at any work).
await create(
db,
"works",
[
["media_type", "t"],
["title", "t"],
["original_title", "t"],
["year", "i?"],
["release_date", "t?"],
["creators", "t"],
["description", "t"],
["cover_url", "t"],
["cover_attachment_id", "t?"],
["metadata", "t"],
["parent_id", "fk:works?"],
["position", "t?"],
["owner_user_id", "fk:users?"],
["merged_into_id", "t?"],
["created_at", "t"],
["updated_at", "t"],
],
{ indexes: [["merged_into_id"], ["media_type"]] },
);
await create(
db,
"external_ids",
[
["work_id", "fk:works"],
["namespace", "t"],
["value", "t"],
],
{ unique: [["namespace", "value"]] },
);
await create(
db,
"work_aliases",
[
["user_id", "fk:users"],
["from_work_id", "fk:works"],
["to_work_id", "fk:works"],
],
{ unique: [["user_id", "from_work_id"]] },
);
await create(db, "work_history", [
["work_id", "fk:works"],
["before", "t"],
["after", "t"],
["actor", "t"],
["created_at", "t"],
]);
await create(
db,
"entries",
[
["user_id", "fk:users"],
["work_id", "fk:works"],
["status", "t"],
["rating", "i?"],
["review", "t?"],
["private_notes", "t?"],
["favorite", "i"],
["tags", "t"],
["custom_fields", "t"],
["visibility", "t"],
["overrides", "t"],
["media_type_override", "t?"],
["field_sources", "t"],
["created_at", "t"],
["updated_at", "t"],
["deleted_at", "t?"],
],
{ unique: [["user_id", "work_id"]] },
);
await create(db, "engagements", [
["entry_id", "fk:entries"],
["started_at", "t?"],
["started_precision", "t"],
["finished_at", "t?"],
["finished_precision", "t"],
["progress", "t"],
["platform", "t?"],
["rating", "i?"],
["notes", "t?"],
["custom_fields", "t"],
["field_sources", "t"],
["created_at", "t"],
["updated_at", "t"],
["deleted_at", "t?"],
]);
await create(
db,
"source_connections",
[
["user_id", "fk:users"],
["plugin_id", "t"],
["name", "t"],
["config", "t"],
["credentials_enc", "t?"],
["schedule_minutes", "i?"],
["cursor", "t?"],
["status", "t"],
["last_run_at", "t?"],
["created_at", "t"],
],
);
await create(
db,
"engagement_sources",
[
["engagement_id", "fk:engagements"],
["connection_id", "fk:source_connections"],
["record_key", "t"],
],
{ unique: [["connection_id", "record_key"]] },
);
await create(
db,
"listens",
[
["user_id", "fk:users"],
["listened_at", "t"],
["artist", "t"],
["track", "t"],
["album", "t?"],
["duration_ms", "i?"],
["mbids", "t"],
["work_id", "fk:works?"],
["source", "t"],
["source_key", "t"],
["created_at", "t"],
],
{ unique: [["user_id", "source", "source_key"]], indexes: [["user_id", "listened_at"]] },
);
await create(db, "lists", [
["user_id", "fk:users"],
["name", "t"],
["description", "t"],
["kind", "t"],
["visibility", "t"],
["rating", "i?"],
["review", "t?"],
["created_at", "t"],
["updated_at", "t"],
["deleted_at", "t?"],
]);
await create(db, "list_items", [
["list_id", "fk:lists"],
["work_id", "fk:works"],
["position", "i"],
["note", "t?"],
]);
await create(db, "custom_field_definitions", [
["user_id", "fk:users"],
["name", "t"],
["type", "t"],
["options", "t"],
["applies_to", "t"],
["level", "t"],
["created_at", "t"],
]);
await create(db, "copies", [
["user_id", "fk:users"],
["work_id", "fk:works?"],
["title_hint", "t?"],
["format", "t?"],
["acquired_from", "t?"],
["acquired_at", "t?"],
["acquired_precision", "t"],
["price_cents", "i?"],
["currency", "t?"],
["notes", "t?"],
["custom_fields", "t"],
["source_ref", "t?"],
["created_at", "t"],
["updated_at", "t"],
["deleted_at", "t?"],
]);
await create(
db,
"attachments",
[
["user_id", "fk:users"],
["sha256", "t"],
["mime", "t"],
["size", "i"],
["filename", "t"],
["caption", "t?"],
["taken_at", "t?"],
["visibility", "t?"],
["created_at", "t"],
],
{ indexes: [["sha256"]] },
);
await create(
db,
"attachment_links",
[
["attachment_id", "fk:attachments"],
["entity", "t"],
["entity_id", "t"],
],
{ indexes: [["entity", "entity_id"]] },
);
await create(
db,
"user_history",
[
["user_id", "fk:users"],
["entity", "t"],
["entity_id", "t"],
["before", "t"],
["after", "t"],
["actor", "t"],
["created_at", "t"],
],
{ indexes: [["entity", "entity_id"]] },
);
await create(db, "match_suggestions", [
["user_id", "fk:users"],
["work_id", "fk:works"],
["candidate_work_id", "fk:works"],
["score", "r"],
["status", "t"],
["created_at", "t"],
]);
await create(db, "source_runs", [
["connection_id", "fk:source_connections"],
["mode", "t"],
["started_at", "t"],
["finished_at", "t?"],
["status", "t"],
["counts", "t"],
["log", "t"],
["error", "t?"],
]);
await create(
db,
"source_records",
[
["connection_id", "fk:source_connections"],
["key", "t"],
["kind", "t"],
["current_hash", "t"],
["first_seen", "t"],
["last_seen", "t"],
["deleted_at", "t?"],
["merged_into_key", "t?"],
["target_entity", "t?"],
["target_id", "t?"],
],
{ unique: [["connection_id", "key"]], indexes: [["target_entity", "target_id"]] },
);
await create(db, "source_record_versions", [
["record_id", "fk:source_records"],
["hash", "t"],
["raw_gz", "t"],
["seen_at", "t"],
]);
await create(
db,
"change_events",
[
["user_id", "fk:users"],
["connection_id", "fk:source_connections?"],
["run_id", "fk:source_runs?"],
["record_id", "fk:source_records?"],
["type", "t"],
["target_entity", "t"],
["target_id", "t?"],
["payload", "t"],
["status", "t"],
["created_at", "t"],
["resolved_at", "t?"],
],
{ indexes: [["user_id", "status"]] },
);
await create(
db,
"jobs",
[
["type", "t"],
["payload", "t"],
["run_at", "t"],
["status", "t"],
["attempts", "i"],
["last_error", "t?"],
["locked_at", "t?"],
["lock_key", "t?"],
["created_at", "t"],
],
{ indexes: [["status", "run_at"], ["lock_key"]] },
);
},
async down(db: Kysely<any>) {
for (const table of [
"jobs",
"change_events",
"source_record_versions",
"source_records",
"source_runs",
"match_suggestions",
"user_history",
"attachment_links",
"attachments",
"copies",
"custom_field_definitions",
"list_items",
"lists",
"listens",
"engagement_sources",
"source_connections",
"engagements",
"entries",
"work_history",
"work_aliases",
"external_ids",
"works",
"app_tokens",
"invites",
"sessions",
"users",
]) {
await db.schema.dropTable(table).execute();
}
},
};