426 lines
11 KiB
TypeScript
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();
|
|
}
|
|
},
|
|
};
|