- trip_members.sort_order (per-user, migrated) drives GET /api/trips order - PATCH /api/trips/:id/order updates only the caller's membership row - Trip cards get a drag handle reusing the generalized row-reorder helper; clicking the card still navigates
105 lines
3.9 KiB
JavaScript
105 lines
3.9 KiB
JavaScript
import Database from 'better-sqlite3';
|
|
|
|
const SCHEMA = `
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY,
|
|
token_hash TEXT UNIQUE NOT NULL,
|
|
display_name TEXT NOT NULL,
|
|
created_at TEXT DEFAULT current_timestamp
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS trips (
|
|
id INTEGER PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
start_date TEXT NOT NULL,
|
|
end_date TEXT NOT NULL,
|
|
owner_id INTEGER NOT NULL REFERENCES users(id),
|
|
currency TEXT NOT NULL DEFAULT 'USD',
|
|
join_code TEXT UNIQUE NOT NULL,
|
|
created_at TEXT DEFAULT current_timestamp
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS trip_members (
|
|
trip_id INTEGER NOT NULL REFERENCES trips(id),
|
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
role TEXT NOT NULL DEFAULT 'editor',
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (trip_id, user_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS entries (
|
|
id INTEGER PRIMARY KEY,
|
|
trip_id INTEGER NOT NULL REFERENCES trips(id),
|
|
date TEXT NOT NULL,
|
|
end_date TEXT,
|
|
type TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
details TEXT DEFAULT '',
|
|
start_time TEXT,
|
|
end_time TEXT,
|
|
location_name TEXT,
|
|
lat REAL,
|
|
lng REAL,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
price REAL,
|
|
paid_by INTEGER REFERENCES users(id),
|
|
split_mode TEXT NOT NULL DEFAULT 'equal',
|
|
segments TEXT,
|
|
rental TEXT,
|
|
transport_mode TEXT,
|
|
created_at TEXT DEFAULT current_timestamp
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS entry_participants (
|
|
entry_id INTEGER NOT NULL REFERENCES entries(id),
|
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
PRIMARY KEY (entry_id, user_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_entries_trip ON entries(trip_id, date, sort_order, id);
|
|
CREATE INDEX IF NOT EXISTS idx_members_user ON trip_members(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_participants_entry ON entry_participants(entry_id);
|
|
`;
|
|
|
|
// Columns added after the initial release: CREATE TABLE IF NOT EXISTS never
|
|
// alters existing tables, so databases created by an older version need them
|
|
// backfilled with ALTER TABLE.
|
|
const MIGRATIONS = [
|
|
{ table: 'trips', column: 'currency', ddl: "ALTER TABLE trips ADD COLUMN currency TEXT NOT NULL DEFAULT 'USD'" },
|
|
{ table: 'entries', column: 'price', ddl: 'ALTER TABLE entries ADD COLUMN price REAL' },
|
|
{ table: 'entries', column: 'paid_by', ddl: 'ALTER TABLE entries ADD COLUMN paid_by INTEGER REFERENCES users(id)' },
|
|
{ table: 'entries', column: 'split_mode', ddl: "ALTER TABLE entries ADD COLUMN split_mode TEXT NOT NULL DEFAULT 'equal'" },
|
|
{ table: 'entries', column: 'segments', ddl: 'ALTER TABLE entries ADD COLUMN segments TEXT' },
|
|
{ table: 'entries', column: 'rental', ddl: 'ALTER TABLE entries ADD COLUMN rental TEXT' },
|
|
{ table: 'entries', column: 'end_date', ddl: 'ALTER TABLE entries ADD COLUMN end_date TEXT' },
|
|
{ table: 'entries', column: 'transport_mode', ddl: 'ALTER TABLE entries ADD COLUMN transport_mode TEXT' },
|
|
{ table: 'trip_members', column: 'sort_order', ddl: 'ALTER TABLE trip_members ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0' },
|
|
];
|
|
|
|
function applyMigrations(db) {
|
|
for (const { table, column, ddl } of MIGRATIONS) {
|
|
const columns = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
if (!columns.some((c) => c.name === column)) db.exec(ddl);
|
|
}
|
|
}
|
|
|
|
// Data migrations for the 2026-07 entry-type rework (hotel/travel/immigration
|
|
// removed in favour of stay/transport/activity). Naturally idempotent: after
|
|
// the first run no rows of the legacy types remain, so re-running is a no-op.
|
|
export function applyDataMigrations(db) {
|
|
db.exec("UPDATE entries SET type = 'stay' WHERE type = 'hotel'");
|
|
db.exec("UPDATE entries SET type = 'transport' WHERE type = 'travel'");
|
|
db.exec("UPDATE entries SET type = 'activity', title = '🛂 ' || title WHERE type = 'immigration'");
|
|
}
|
|
|
|
// Open (or create) the SQLite database at dbPath and ensure the schema exists.
|
|
export function openDb(dbPath) {
|
|
const db = new Database(dbPath);
|
|
db.pragma('journal_mode = WAL');
|
|
db.pragma('foreign_keys = ON');
|
|
db.exec(SCHEMA);
|
|
applyMigrations(db);
|
|
applyDataMigrations(db);
|
|
return db;
|
|
}
|