calendar/backend/src/db.js
jtricerolph bf5557d277 Add calendar app — shared events calendar with departments, staff tagging, bank holidays and two-way phone sync
Fastify/Postgres backend + React frontend matching the stack's app conventions, plus
a hand-rolled RFC 4791 CalDAV server (caldav-adapter turned out Koa-only in practice)
so calendars subscribe as genuine two-way sync in Apple/Google/Outlook. v1 scope:
multiple colour-coded calendars, department/staff event tagging via live Workforce
lookups, month/week/day/list views, dashboard, file attachments, activity log, and
an auto-synced UK bank holidays calendar. Verified end-to-end locally against real
Postgres: REST CRUD, CalDAV discovery/PROPFIND/REPORT/PUT/sync-collection, all-day
date handling, system-calendar write protection, and activity logging across both
the web and CalDAV write paths.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 16:36:37 +00:00

106 lines
3.9 KiB
JavaScript

import pg from 'pg'
const { Pool } = pg
export const pool = new Pool({ connectionString: process.env.DATABASE_URL })
export async function initDb() {
await pool.query(`
CREATE TABLE IF NOT EXISTS calendars (
id SERIAL PRIMARY KEY,
slug TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
color TEXT NOT NULL,
is_system BOOLEAN NOT NULL DEFAULT FALSE,
deleted_at TIMESTAMPTZ,
created_by TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS events (
id SERIAL PRIMARY KEY,
calendar_id INT NOT NULL REFERENCES calendars(id),
uid TEXT NOT NULL UNIQUE, -- iCalendar UID, e.g. '<uuid>@calendar.hotelnumberfour.com'
title TEXT NOT NULL,
description TEXT,
location TEXT,
start_at TIMESTAMPTZ NOT NULL,
end_at TIMESTAMPTZ NOT NULL,
all_day BOOLEAN NOT NULL DEFAULT FALSE,
sequence INT NOT NULL DEFAULT 0,
created_by TEXT, -- email
created_by_name TEXT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS events_calendar_idx ON events (calendar_id);
CREATE INDEX IF NOT EXISTS events_start_idx ON events (start_at);
CREATE INDEX IF NOT EXISTS events_deleted_idx ON events (deleted_at);
CREATE TABLE IF NOT EXISTS event_departments (
event_id INT NOT NULL REFERENCES events(id) ON DELETE CASCADE,
department_id TEXT NOT NULL,
department_name TEXT NOT NULL,
PRIMARY KEY (event_id, department_id)
);
CREATE TABLE IF NOT EXISTS event_assignees (
event_id INT NOT NULL REFERENCES events(id) ON DELETE CASCADE,
user_email TEXT NOT NULL,
user_name TEXT,
PRIMARY KEY (event_id, user_email)
);
CREATE TABLE IF NOT EXISTS event_attachments (
id SERIAL PRIMARY KEY,
event_id INT NOT NULL REFERENCES events(id) ON DELETE CASCADE,
filename TEXT NOT NULL,
stored_path TEXT NOT NULL, -- relative path under uploads dir
mime_type TEXT,
size_bytes INT,
uploaded_by TEXT, -- email
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS caldav_credentials (
id SERIAL PRIMARY KEY,
user_email TEXT NOT NULL,
username TEXT NOT NULL UNIQUE, -- generated, e.g. '<localpart>-<random6>'
password_hash TEXT NOT NULL, -- bcrypt
label TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_used_at TIMESTAMPTZ
);
CREATE TABLE IF NOT EXISTS activity_log (
id SERIAL PRIMARY KEY,
actor_email TEXT,
actor_name TEXT,
action TEXT NOT NULL, -- created | updated | deleted
entity_type TEXT NOT NULL, -- event | calendar | attachment | caldav_credential
entity_id INT,
calendar_id INT,
summary TEXT NOT NULL,
details JSONB,
source TEXT NOT NULL DEFAULT 'web', -- web | caldav
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS activity_log_calendar_idx ON activity_log (calendar_id, created_at DESC);
CREATE INDEX IF NOT EXISTS activity_log_entity_idx ON activity_log (entity_type, entity_id);
`)
await seedDefaults()
}
async function seedDefaults() {
await pool.query(
`INSERT INTO calendars (slug, name, color, is_system)
VALUES ('bank-holidays', 'Bank Holidays', '#64748b', TRUE)
ON CONFLICT (slug) DO NOTHING`
)
await pool.query(
`INSERT INTO calendars (slug, name, color, is_system)
VALUES ('general', 'All Staff', '#c9a84c', FALSE)
ON CONFLICT (slug) DO NOTHING`
)
}