diff options
Diffstat (limited to 'app/lib')
| -rw-r--r-- | app/lib/db.server.ts | 175 |
1 files changed, 107 insertions, 68 deletions
diff --git a/app/lib/db.server.ts b/app/lib/db.server.ts index e35bba6..cb51028 100644 --- a/app/lib/db.server.ts +++ b/app/lib/db.server.ts @@ -47,6 +47,7 @@ function initSchema(db: Database.Database) { order_index INTEGER NOT NULL DEFAULT 0 ); + -- legacy table kept for migration only CREATE TABLE IF NOT EXISTS band_artists ( band_id TEXT NOT NULL REFERENCES bands(id) ON DELETE CASCADE, artist_id TEXT NOT NULL REFERENCES artists(id) ON DELETE CASCADE, @@ -55,6 +56,17 @@ function initSchema(db: Database.Database) { PRIMARY KEY (band_id, artist_id) ); + CREATE TABLE IF NOT EXISTS members ( + id TEXT PRIMARY KEY, + band_id TEXT NOT NULL REFERENCES bands(id) ON DELETE CASCADE, + artist_id TEXT NOT NULL REFERENCES artists(id) ON DELETE CASCADE, + role TEXT, + since TEXT NOT NULL DEFAULT '', + until TEXT NOT NULL DEFAULT '', + note TEXT NOT NULL DEFAULT '', + order_index INTEGER NOT NULL DEFAULT 0 + ); + CREATE TABLE IF NOT EXISTS band_revisions ( id TEXT PRIMARY KEY, band_id TEXT NOT NULL REFERENCES bands(id) ON DELETE CASCADE, @@ -79,16 +91,35 @@ function initSchema(db: Database.Database) { // migrations try { db.exec("ALTER TABLE bands ADD COLUMN description TEXT"); } catch { /* already exists */ } try { db.exec("ALTER TABLE bands ADD COLUMN status TEXT NOT NULL DEFAULT 'active'"); } catch { /* already exists */ } + try { db.exec("ALTER TABLE band_artists ADD COLUMN periods TEXT"); } catch { /* already exists */ } + + // one-time data migration: band_artists → members + try { + const mc = (db.prepare("SELECT COUNT(*) as c FROM members").get() as { c: number }).c; + if (mc === 0) { + const rows = db.prepare("SELECT * FROM band_artists").all() as { + band_id: string; artist_id: string; role: string | null; order_index: number; + }[]; + const insert = db.prepare( + "INSERT INTO members (id, band_id, artist_id, role, since, until, note, order_index) VALUES (?, ?, ?, ?, '', '', '', ?)" + ); + for (const r of rows) { + insert.run(crypto.randomUUID(), r.band_id, r.artist_id, r.role, r.order_index); + } + } + } catch { /* band_artists might not have data */ } db.exec(` CREATE INDEX IF NOT EXISTS idx_artist_links_artist_id ON artist_links(artist_id); - CREATE INDEX IF NOT EXISTS idx_band_artists_band_id ON band_artists(band_id); - CREATE INDEX IF NOT EXISTS idx_band_artists_artist_id ON band_artists(artist_id); + CREATE INDEX IF NOT EXISTS idx_members_band_id ON members(band_id); + CREATE INDEX IF NOT EXISTS idx_members_artist_id ON members(artist_id); CREATE INDEX IF NOT EXISTS idx_band_revisions_band_id ON band_revisions(band_id); CREATE INDEX IF NOT EXISTS idx_artist_revisions_artist_id ON artist_revisions(artist_id); `); } +// ── Interfaces ──────────────────────────────────────────────────────────────── + export interface Band { id: string; slug: string; @@ -122,23 +153,35 @@ export interface ArtistLink { order_index: number; } -export interface BandArtistRow { +export interface MemberRaw { + id: string; band_id: string; artist_id: string; role: string | null; + since: string; + until: string; + note: string; order_index: number; +} + +export interface BandMemberRow extends MemberRaw { artist_name: string; artist_slug: string; } -export interface ArtistBandRow { - band_id: string; - artist_id: string; - role: string | null; +export interface ArtistMemberRow extends MemberRaw { band_name: string; band_slug: string; } +export interface MemberInput { + artist_id: string; + role: string | null; + since: string; + until: string; + note: string; +} + export interface BandRevision { id: string; band_id: string; @@ -194,16 +237,16 @@ export function getBandLinks(bandId: string): BandLink[] { .all(bandId) as BandLink[]; } -export function getBandArtists(bandId: string): BandArtistRow[] { +export function getBandMembers(bandId: string): BandMemberRow[] { return getDb() .prepare( - `SELECT ba.*, a.name AS artist_name, a.slug AS artist_slug - FROM band_artists ba - JOIN artists a ON a.id = ba.artist_id - WHERE ba.band_id = ? - ORDER BY ba.order_index` + `SELECT m.*, a.name AS artist_name, a.slug AS artist_slug + FROM members m + JOIN artists a ON a.id = m.artist_id + WHERE m.band_id = ? + ORDER BY m.order_index` ) - .all(bandId) as BandArtistRow[]; + .all(bandId) as BandMemberRow[]; } export function getBandRevisions(bandId: string): BandRevision[] { @@ -220,7 +263,7 @@ export interface CreateBandInput { description: string | null; status: string; links: { label: string; url: string }[]; - artists: { id: string; role: string | null }[]; + members: MemberInput[]; message: string; ip_address: string; } @@ -236,25 +279,17 @@ export function createBand(input: CreateBandInput): Band { "INSERT INTO band_links (id, band_id, label, url, order_index) VALUES (?, ?, ?, ?, ?)" ).run(crypto.randomUUID(), input.id, link.label, link.url, i); }); - input.artists.forEach((artist, i) => { + input.members.forEach((m, i) => { db.prepare( - "INSERT INTO band_artists (band_id, artist_id, role, order_index) VALUES (?, ?, ?, ?)" - ).run(input.id, artist.id, artist.role, i); + "INSERT INTO members (id, band_id, artist_id, role, since, until, note, order_index) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" + ).run(crypto.randomUUID(), input.id, m.artist_id, m.role, m.since, m.until, m.note, i); }); const band = getBandById(input.id)!; const links = getBandLinks(input.id); - const artists = getBandArtists(input.id); - const snapshot = JSON.stringify({ - name: band.name, - area: band.area, - description: band.description, - status: band.status, - links: links.map((l) => ({ label: l.label, url: l.url })), - artists: artists.map((a) => ({ id: a.artist_id, name: a.artist_name, role: a.role })), - }); + const members = getBandMembers(input.id); db.prepare( "INSERT INTO band_revisions (id, band_id, snapshot, message, ip_address) VALUES (?, ?, ?, ?, ?)" - ).run(crypto.randomUUID(), input.id, snapshot, input.message, input.ip_address); + ).run(crypto.randomUUID(), input.id, buildSnapshot(band, links, members), input.message, input.ip_address); return band; })() as Band; } @@ -266,7 +301,7 @@ export interface UpdateBandInput { description: string | null; status: string; links: { label: string; url: string }[]; - artists: { id: string; role: string | null }[]; + members: MemberInput[]; message: string; ip_address: string; } @@ -283,29 +318,39 @@ export function updateBand(id: string, input: UpdateBandInput): void { "INSERT INTO band_links (id, band_id, label, url, order_index) VALUES (?, ?, ?, ?, ?)" ).run(crypto.randomUUID(), id, link.label, link.url, i); }); - db.prepare("DELETE FROM band_artists WHERE band_id = ?").run(id); - input.artists.forEach((artist, i) => { + db.prepare("DELETE FROM members WHERE band_id = ?").run(id); + input.members.forEach((m, i) => { db.prepare( - "INSERT INTO band_artists (band_id, artist_id, role, order_index) VALUES (?, ?, ?, ?)" - ).run(id, artist.id, artist.role, i); + "INSERT INTO members (id, band_id, artist_id, role, since, until, note, order_index) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" + ).run(crypto.randomUUID(), id, m.artist_id, m.role, m.since, m.until, m.note, i); }); const band = getBandById(id)!; const links = getBandLinks(id); - const artists = getBandArtists(id); - const snapshot = JSON.stringify({ - name: band.name, - area: band.area, - description: band.description, - status: band.status, - links: links.map((l) => ({ label: l.label, url: l.url })), - artists: artists.map((a) => ({ id: a.artist_id, name: a.artist_name, role: a.role })), - }); + const members = getBandMembers(id); db.prepare( "INSERT INTO band_revisions (id, band_id, snapshot, message, ip_address) VALUES (?, ?, ?, ?, ?)" - ).run(crypto.randomUUID(), id, snapshot, input.message, input.ip_address); + ).run(crypto.randomUUID(), id, buildSnapshot(band, links, members), input.message, input.ip_address); })(); } +function buildSnapshot(band: Band, links: BandLink[], members: BandMemberRow[]): string { + return JSON.stringify({ + name: band.name, + area: band.area, + description: band.description, + status: band.status, + links: links.map((l) => ({ label: l.label, url: l.url })), + members: members.map((m) => ({ + artist_id: m.artist_id, + artist_name: m.artist_name, + role: m.role, + since: m.since, + until: m.until, + note: m.note, + })), + }); +} + // ── Artist queries ──────────────────────────────────────────────────────────── export function listArtists(): Artist[] { @@ -326,16 +371,16 @@ export function getArtistLinks(artistId: string): ArtistLink[] { .all(artistId) as ArtistLink[]; } -export function getArtistBands(artistId: string): ArtistBandRow[] { +export function getArtistMembers(artistId: string): ArtistMemberRow[] { return getDb() .prepare( - `SELECT ba.*, b.name AS band_name, b.slug AS band_slug - FROM band_artists ba - JOIN bands b ON b.id = ba.band_id - WHERE ba.artist_id = ? - ORDER BY b.name` + `SELECT m.*, b.name AS band_name, b.slug AS band_slug + FROM members m + JOIN bands b ON b.id = m.band_id + WHERE m.artist_id = ? + ORDER BY b.name, m.order_index` ) - .all(artistId) as ArtistBandRow[]; + .all(artistId) as ArtistMemberRow[]; } export function getArtistRevisions(artistId: string): ArtistRevision[] { @@ -411,21 +456,14 @@ export function updateArtist(id: string, input: UpdateArtistInput): void { // ── Export / Import ─────────────────────────────────────────────────────────── -interface BandArtistRaw { - band_id: string; - artist_id: string; - role: string | null; - order_index: number; -} - export interface DbExport { - version: 1; + version: 3; exported_at: string; bands: Band[]; band_links: BandLink[]; artists: Artist[]; artist_links: ArtistLink[]; - band_artists: BandArtistRaw[]; + members: MemberRaw[]; band_revisions: BandRevision[]; artist_revisions: ArtistRevision[]; } @@ -433,13 +471,13 @@ export interface DbExport { export function exportDb(): DbExport { const db = getDb(); return { - version: 1, + version: 3, exported_at: new Date().toISOString(), bands: db.prepare("SELECT * FROM bands").all() as Band[], band_links: db.prepare("SELECT * FROM band_links ORDER BY band_id, order_index").all() as BandLink[], artists: db.prepare("SELECT * FROM artists").all() as Artist[], artist_links: db.prepare("SELECT * FROM artist_links ORDER BY artist_id, order_index").all() as ArtistLink[], - band_artists: db.prepare("SELECT * FROM band_artists ORDER BY band_id, order_index").all() as BandArtistRaw[], + members: db.prepare("SELECT * FROM members ORDER BY band_id, order_index").all() as MemberRaw[], band_revisions: db.prepare("SELECT * FROM band_revisions ORDER BY created_at").all() as BandRevision[], artist_revisions: db.prepare("SELECT * FROM artist_revisions ORDER BY created_at").all() as ArtistRevision[], }; @@ -450,15 +488,16 @@ export interface ImportResult { artists: number; band_links: number; artist_links: number; - band_artists: number; + members: number; band_revisions: number; artist_revisions: number; } export function importDb(data: DbExport): ImportResult { - if (data.version !== 1) throw new Error("Unsupported export version"); + if (data.version !== 3) throw new Error("Unsupported export version"); const db = getDb(); return db.transaction(() => { + db.prepare("DELETE FROM members").run(); db.prepare("DELETE FROM band_artists").run(); db.prepare("DELETE FROM band_revisions").run(); db.prepare("DELETE FROM artist_revisions").run(); @@ -495,11 +534,11 @@ export function importDb(data: DbExport): ImportResult { insertArtistLink.run(l.id, l.artist_id, l.label, l.url, l.order_index); } - const insertBandArtist = db.prepare( - "INSERT INTO band_artists (band_id, artist_id, role, order_index) VALUES (?, ?, ?, ?)" + const insertMember = db.prepare( + "INSERT INTO members (id, band_id, artist_id, role, since, until, note, order_index) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" ); - for (const ba of data.band_artists) { - insertBandArtist.run(ba.band_id, ba.artist_id, ba.role, ba.order_index); + for (const m of data.members) { + insertMember.run(m.id, m.band_id, m.artist_id, m.role, m.since, m.until, m.note, m.order_index); } const insertBandRev = db.prepare( @@ -521,7 +560,7 @@ export function importDb(data: DbExport): ImportResult { artists: data.artists.length, band_links: data.band_links.length, artist_links: data.artist_links.length, - band_artists: data.band_artists.length, + members: data.members.length, band_revisions: data.band_revisions.length, artist_revisions: data.artist_revisions.length, }; |
