feat(db): initial schema
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
27ec24eb4b
commit
da04adb286
5 files changed
+838
-4
No files matched your search
@@ -0,0 +1,392 @@
|
||||
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"]] },
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { Migration } from "kysely/migration";
|
||||
import { initial } from "./0001_initial.ts";
|
||||
|
||||
/** All migrations, keyed by sortable name. Imported statically so bundles need no filesystem. */
|
||||
export const migrations: Record<string, Migration> = {};
|
||||
export const migrations: Record<string, Migration> = {
|
||||
"0001_initial": initial,
|
||||
};
|
||||
Reference in new issue
Block a user