Initial release: collaborative trip planner
Multi-user trip planning web app in a single Docker container. Mullvad-style token accounts, trip sharing via join codes, day-by-day calendar with typed entries (activity, hotel, travel, flight, rental car, immigration, note), multi-leg flight segments with bundled IATA airport dataset, Leaflet/OSM map with per-leg great-circle km (air vs ground), rough km-driven vs rental included-km comparison, cost splitting with settle-up suggestions, flip-clock departure countdown. Node 20 + Express + SQLite (WAL, additive migrations), vanilla JS SPA, 44 API tests.
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
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',
|
||||
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,
|
||||
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,
|
||||
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' },
|
||||
];
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
return db;
|
||||
}
|
||||
Reference in New Issue
Block a user