493 lines
20 KiB
TypeScript
493 lines
20 KiB
TypeScript
import { db, type DatabaseClient } from './client.js';
|
|
|
|
export const ensureSchema = async (database: DatabaseClient = db) => {
|
|
await database.query(`
|
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
email VARCHAR(255) NOT NULL UNIQUE,
|
|
password_hash VARCHAR(255),
|
|
name VARCHAR(160) NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS workspaces (
|
|
id INTEGER PRIMARY KEY,
|
|
name VARCHAR(160) NOT NULL DEFAULT 'My Flock',
|
|
workspace_type VARCHAR(16) NOT NULL DEFAULT 'standard',
|
|
billing_email VARCHAR(255),
|
|
billing_plan VARCHAR(32) NOT NULL DEFAULT 'household_basic',
|
|
billing_interval VARCHAR(16) NOT NULL DEFAULT 'monthly',
|
|
subscription_status VARCHAR(32) NOT NULL DEFAULT 'none',
|
|
stripe_customer_id VARCHAR(255),
|
|
stripe_subscription_id VARCHAR(255),
|
|
rescue_verification_status VARCHAR(32) NOT NULL DEFAULT 'not_required',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
ALTER TABLE workspaces
|
|
DROP CONSTRAINT IF EXISTS workspaces_id_check;
|
|
|
|
ALTER TABLE workspaces
|
|
ADD COLUMN IF NOT EXISTS billing_email VARCHAR(255),
|
|
ADD COLUMN IF NOT EXISTS billing_plan VARCHAR(32) NOT NULL DEFAULT 'household_basic',
|
|
ADD COLUMN IF NOT EXISTS billing_interval VARCHAR(16) NOT NULL DEFAULT 'monthly',
|
|
ADD COLUMN IF NOT EXISTS subscription_status VARCHAR(32) NOT NULL DEFAULT 'none',
|
|
ADD COLUMN IF NOT EXISTS stripe_customer_id VARCHAR(255),
|
|
ADD COLUMN IF NOT EXISTS stripe_subscription_id VARCHAR(255),
|
|
ADD COLUMN IF NOT EXISTS rescue_verification_status VARCHAR(32) NOT NULL DEFAULT 'not_required';
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_workspaces_stripe_subscription_id
|
|
ON workspaces (stripe_subscription_id)
|
|
WHERE stripe_subscription_id IS NOT NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_workspaces_stripe_customer_id
|
|
ON workspaces (stripe_customer_id)
|
|
WHERE stripe_customer_id IS NOT NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_workspaces_rescue_status
|
|
ON workspaces (workspace_type, rescue_verification_status, created_at DESC);
|
|
|
|
UPDATE workspaces
|
|
SET subscription_status = 'none'
|
|
WHERE workspace_type = 'standard'
|
|
AND stripe_subscription_id IS NULL
|
|
AND subscription_status = 'active';
|
|
|
|
UPDATE workspaces
|
|
SET rescue_verification_status = 'pending'
|
|
WHERE workspace_type = 'rescue'
|
|
AND rescue_verification_status = 'not_required';
|
|
|
|
CREATE TABLE IF NOT EXISTS workspace_members (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
workspace_id INTEGER NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
|
invite_email VARCHAR(255) NOT NULL,
|
|
name VARCHAR(160) NOT NULL,
|
|
role VARCHAR(16) NOT NULL DEFAULT 'caregiver',
|
|
accepted_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
ALTER TABLE workspace_members
|
|
ADD COLUMN IF NOT EXISTS email VARCHAR(255),
|
|
ADD COLUMN IF NOT EXISTS user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
|
ADD COLUMN IF NOT EXISTS invite_email VARCHAR(255),
|
|
ADD COLUMN IF NOT EXISTS accepted_at TIMESTAMPTZ;
|
|
|
|
UPDATE workspace_members
|
|
SET role = CASE
|
|
WHEN role = 'manager' THEN 'assistant'
|
|
WHEN role = 'staff' THEN 'caregiver'
|
|
ELSE role
|
|
END
|
|
WHERE role IN ('manager', 'staff');
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF EXISTS (
|
|
SELECT 1
|
|
FROM information_schema.columns
|
|
WHERE table_name = 'workspace_members'
|
|
AND column_name = 'email'
|
|
) THEN
|
|
UPDATE workspace_members
|
|
SET invite_email = COALESCE(invite_email, email)
|
|
WHERE invite_email IS NULL;
|
|
END IF;
|
|
END $$;
|
|
|
|
UPDATE workspace_members
|
|
SET invite_email = ''
|
|
WHERE invite_email IS NULL;
|
|
|
|
UPDATE workspace_members
|
|
SET email = invite_email
|
|
WHERE email IS NULL;
|
|
|
|
ALTER TABLE workspace_members
|
|
ALTER COLUMN invite_email SET NOT NULL;
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_workspace_members_workspace_email
|
|
ON workspace_members (workspace_id, invite_email);
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_workspace_members_workspace_user
|
|
ON workspace_members (workspace_id, user_id)
|
|
WHERE user_id IS NOT NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_workspace_members_user_accepted
|
|
ON workspace_members (user_id, accepted_at, workspace_id)
|
|
WHERE user_id IS NOT NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_workspace_members_owner_email
|
|
ON workspace_members (LOWER(COALESCE(invite_email, email)), workspace_id)
|
|
WHERE role = 'owner'
|
|
AND accepted_at IS NOT NULL;
|
|
|
|
CREATE TABLE IF NOT EXISTS auth_sessions (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
active_workspace_id INTEGER NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
token_hash VARCHAR(255) NOT NULL UNIQUE,
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_auth_sessions_created_user
|
|
ON auth_sessions (created_at DESC, user_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS integration_tokens (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
workspace_id INTEGER NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
name VARCHAR(160) NOT NULL,
|
|
token_hash VARCHAR(255) NOT NULL UNIQUE,
|
|
token_prefix VARCHAR(32) NOT NULL,
|
|
scope VARCHAR(16) NOT NULL DEFAULT 'read_write',
|
|
last_used_at TIMESTAMPTZ,
|
|
expires_at TIMESTAMPTZ,
|
|
revoked_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
ALTER TABLE integration_tokens
|
|
ADD COLUMN IF NOT EXISTS workspace_id INTEGER REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
ADD COLUMN IF NOT EXISTS name VARCHAR(160) NOT NULL DEFAULT 'Integration token',
|
|
ADD COLUMN IF NOT EXISTS token_prefix VARCHAR(32),
|
|
ADD COLUMN IF NOT EXISTS scope VARCHAR(16) NOT NULL DEFAULT 'read_write',
|
|
ADD COLUMN IF NOT EXISTS last_used_at TIMESTAMPTZ,
|
|
ADD COLUMN IF NOT EXISTS expires_at TIMESTAMPTZ,
|
|
ADD COLUMN IF NOT EXISTS revoked_at TIMESTAMPTZ;
|
|
|
|
UPDATE integration_tokens
|
|
SET token_prefix = LEFT(token_hash, 12)
|
|
WHERE token_prefix IS NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_integration_tokens_user_workspace
|
|
ON integration_tokens (user_id, workspace_id, created_at DESC);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_integration_tokens_lookup
|
|
ON integration_tokens (token_hash)
|
|
WHERE revoked_at IS NULL;
|
|
|
|
CREATE TABLE IF NOT EXISTS auth_accounts (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
provider_key VARCHAR(32) NOT NULL,
|
|
provider_subject VARCHAR(255) NOT NULL,
|
|
provider_email VARCHAR(255),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_auth_accounts_provider_subject
|
|
ON auth_accounts (provider_key, provider_subject);
|
|
|
|
CREATE TABLE IF NOT EXISTS oauth_states (
|
|
id UUID PRIMARY KEY,
|
|
provider_key VARCHAR(32) NOT NULL,
|
|
code_verifier VARCHAR(255) NOT NULL,
|
|
redirect_to TEXT NOT NULL,
|
|
expires_at TIMESTAMPTZ NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS magic_link_tokens (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
email VARCHAR(255) NOT NULL,
|
|
name VARCHAR(160),
|
|
token_hash VARCHAR(255) NOT NULL UNIQUE,
|
|
redirect_to TEXT NOT NULL,
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_magic_link_tokens_email
|
|
ON magic_link_tokens (email, created_at DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS birds (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
workspace_id INTEGER NOT NULL DEFAULT 1,
|
|
name VARCHAR(120) NOT NULL,
|
|
tag_id VARCHAR(80),
|
|
species VARCHAR(120) NOT NULL,
|
|
motivators VARCHAR(1000),
|
|
demotivators VARCHAR(1000),
|
|
favorite_snack VARCHAR(160),
|
|
gender VARCHAR(16) NOT NULL DEFAULT 'unknown',
|
|
date_of_birth DATE,
|
|
gotcha_day DATE,
|
|
chart_color VARCHAR(7) NOT NULL DEFAULT '#cb3a35',
|
|
photo_data_url TEXT,
|
|
photo_object_key TEXT,
|
|
photo_content_type VARCHAR(80),
|
|
photo_updated_at TIMESTAMPTZ,
|
|
notify_on_dob BOOLEAN NOT NULL DEFAULT FALSE,
|
|
notify_on_gotcha_day BOOLEAN NOT NULL DEFAULT FALSE,
|
|
public_profile_code VARCHAR(32),
|
|
public_profile_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
|
memorialized_at TIMESTAMPTZ,
|
|
memorialized_on DATE,
|
|
memorial_note VARCHAR(1000),
|
|
notify_on_memorial_day BOOLEAN NOT NULL DEFAULT FALSE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
ALTER TABLE birds
|
|
ADD COLUMN IF NOT EXISTS workspace_id INTEGER NOT NULL DEFAULT 1,
|
|
ADD COLUMN IF NOT EXISTS motivators VARCHAR(1000),
|
|
ADD COLUMN IF NOT EXISTS demotivators VARCHAR(1000),
|
|
ADD COLUMN IF NOT EXISTS favorite_snack VARCHAR(160),
|
|
ADD COLUMN IF NOT EXISTS gender VARCHAR(16) NOT NULL DEFAULT 'unknown',
|
|
ADD COLUMN IF NOT EXISTS date_of_birth DATE,
|
|
ADD COLUMN IF NOT EXISTS gotcha_day DATE,
|
|
ADD COLUMN IF NOT EXISTS chart_color VARCHAR(7) NOT NULL DEFAULT '#cb3a35',
|
|
ADD COLUMN IF NOT EXISTS photo_data_url TEXT,
|
|
ADD COLUMN IF NOT EXISTS photo_object_key TEXT,
|
|
ADD COLUMN IF NOT EXISTS photo_content_type VARCHAR(80),
|
|
ADD COLUMN IF NOT EXISTS photo_updated_at TIMESTAMPTZ,
|
|
ADD COLUMN IF NOT EXISTS notify_on_dob BOOLEAN NOT NULL DEFAULT FALSE,
|
|
ADD COLUMN IF NOT EXISTS notify_on_gotcha_day BOOLEAN NOT NULL DEFAULT FALSE,
|
|
ADD COLUMN IF NOT EXISTS public_profile_code VARCHAR(32),
|
|
ADD COLUMN IF NOT EXISTS public_profile_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
|
ADD COLUMN IF NOT EXISTS memorialized_at TIMESTAMPTZ,
|
|
ADD COLUMN IF NOT EXISTS memorialized_on DATE,
|
|
ADD COLUMN IF NOT EXISTS memorial_note VARCHAR(1000),
|
|
ADD COLUMN IF NOT EXISTS notify_on_memorial_day BOOLEAN NOT NULL DEFAULT FALSE;
|
|
|
|
ALTER TABLE birds
|
|
ALTER COLUMN tag_id DROP NOT NULL;
|
|
|
|
UPDATE birds
|
|
SET tag_id = NULL
|
|
WHERE tag_id IS NOT NULL
|
|
AND LOWER(BTRIM(tag_id)) IN ('unknown', 'not recorded', 'n/a', 'na', 'none');
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'birds_workspace_fk') THEN
|
|
ALTER TABLE birds
|
|
ADD CONSTRAINT birds_workspace_fk
|
|
FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE;
|
|
END IF;
|
|
END $$;
|
|
|
|
DELETE FROM workspaces
|
|
WHERE id = 1
|
|
AND name = 'My Flock'
|
|
AND NOT EXISTS (SELECT 1 FROM workspace_members WHERE workspace_members.workspace_id = workspaces.id)
|
|
AND NOT EXISTS (SELECT 1 FROM birds WHERE birds.workspace_id = workspaces.id);
|
|
|
|
ALTER TABLE birds
|
|
DROP CONSTRAINT IF EXISTS birds_tag_id_key;
|
|
|
|
DROP INDEX IF EXISTS idx_birds_workspace_tag_id;
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_birds_workspace_tag_id
|
|
ON birds (workspace_id, LOWER(tag_id))
|
|
WHERE tag_id IS NOT NULL
|
|
AND BTRIM(tag_id) <> ''
|
|
AND LOWER(BTRIM(tag_id)) NOT IN ('unknown', 'not recorded', 'n/a', 'na', 'none');
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_birds_workspace_active_name
|
|
ON birds (workspace_id, name)
|
|
WHERE memorialized_at IS NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_birds_workspace_memorialized
|
|
ON birds (workspace_id, memorialized_on DESC, name)
|
|
WHERE memorialized_at IS NOT NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_birds_tag_lookup_active
|
|
ON birds (LOWER(tag_id), created_at)
|
|
WHERE tag_id IS NOT NULL
|
|
AND BTRIM(tag_id) <> ''
|
|
AND LOWER(BTRIM(tag_id)) NOT IN ('unknown', 'not recorded', 'n/a', 'na', 'none')
|
|
AND memorialized_at IS NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_birds_photo_object_key
|
|
ON birds (photo_object_key)
|
|
WHERE photo_object_key IS NOT NULL;
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_birds_public_profile_code
|
|
ON birds (public_profile_code)
|
|
WHERE public_profile_code IS NOT NULL;
|
|
|
|
CREATE TABLE IF NOT EXISTS pending_bird_transfers (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
bird_id UUID NOT NULL REFERENCES birds(id) ON DELETE CASCADE,
|
|
source_workspace_id INTEGER NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
destination_owner_email VARCHAR(255) NOT NULL,
|
|
requested_by_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
completed_at TIMESTAMPTZ,
|
|
completed_workspace_id INTEGER REFERENCES workspaces(id) ON DELETE SET NULL,
|
|
last_error TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
ALTER TABLE pending_bird_transfers
|
|
ADD COLUMN IF NOT EXISTS completed_at TIMESTAMPTZ,
|
|
ADD COLUMN IF NOT EXISTS completed_workspace_id INTEGER REFERENCES workspaces(id) ON DELETE SET NULL,
|
|
ADD COLUMN IF NOT EXISTS last_error TEXT;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_pending_bird_transfers_destination_email
|
|
ON pending_bird_transfers (LOWER(destination_owner_email), created_at DESC)
|
|
WHERE completed_at IS NULL;
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_pending_bird_transfers_open_bird
|
|
ON pending_bird_transfers (bird_id)
|
|
WHERE completed_at IS NULL;
|
|
|
|
CREATE TABLE IF NOT EXISTS flock_notes (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
workspace_id INTEGER NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
bird_id UUID REFERENCES birds(id) ON DELETE SET NULL,
|
|
title VARCHAR(160) NOT NULL,
|
|
body TEXT NOT NULL,
|
|
created_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_flock_notes_workspace_updated
|
|
ON flock_notes (workspace_id, updated_at DESC);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_flock_notes_bird_updated
|
|
ON flock_notes (bird_id, updated_at DESC)
|
|
WHERE bird_id IS NOT NULL;
|
|
|
|
CREATE TABLE IF NOT EXISTS audit_log_entries (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
workspace_id INTEGER NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
actor_name VARCHAR(160),
|
|
actor_email VARCHAR(255),
|
|
action VARCHAR(80) NOT NULL,
|
|
entity_type VARCHAR(80) NOT NULL,
|
|
entity_id VARCHAR(120),
|
|
entity_name VARCHAR(255),
|
|
details JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_audit_log_entries_workspace_created
|
|
ON audit_log_entries (workspace_id, created_at DESC);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_audit_log_entries_entity
|
|
ON audit_log_entries (workspace_id, entity_type, entity_id, created_at DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS weight_records (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
bird_id UUID NOT NULL REFERENCES birds(id) ON DELETE CASCADE,
|
|
weight_grams NUMERIC(8, 2) NOT NULL CHECK (weight_grams > 0),
|
|
recorded_on DATE NOT NULL,
|
|
notes VARCHAR(280),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE (bird_id, recorded_on)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS vet_visits (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
bird_id UUID NOT NULL REFERENCES birds(id) ON DELETE CASCADE,
|
|
visited_on DATE NOT NULL,
|
|
clinic_name VARCHAR(160) NOT NULL,
|
|
reason VARCHAR(160) NOT NULL,
|
|
notes VARCHAR(1000),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS medications (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
bird_id UUID NOT NULL REFERENCES birds(id) ON DELETE CASCADE,
|
|
name VARCHAR(160) NOT NULL,
|
|
dosage VARCHAR(160) NOT NULL,
|
|
frequency VARCHAR(160) NOT NULL,
|
|
dose_schedule JSONB NOT NULL DEFAULT '[{"key":"dose-1","label":"Dose","time":""}]'::jsonb,
|
|
route VARCHAR(80),
|
|
start_date DATE NOT NULL,
|
|
end_date DATE,
|
|
notes VARCHAR(1000),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
CHECK (end_date IS NULL OR end_date >= start_date)
|
|
);
|
|
|
|
ALTER TABLE medications
|
|
ADD COLUMN IF NOT EXISTS dose_schedule JSONB NOT NULL DEFAULT '[{"key":"dose-1","label":"Dose","time":""}]'::jsonb;
|
|
|
|
CREATE TABLE IF NOT EXISTS bird_milestone_reminder_deliveries (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
bird_id UUID NOT NULL REFERENCES birds(id) ON DELETE CASCADE,
|
|
workspace_id INTEGER NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
reminder_type VARCHAR(24) NOT NULL CHECK (reminder_type IN ('hatch_day', 'gotcha_day', 'memorial_day')),
|
|
reminder_year INTEGER NOT NULL,
|
|
delivered_on DATE NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE (bird_id, reminder_type, reminder_year)
|
|
);
|
|
|
|
ALTER TABLE bird_milestone_reminder_deliveries
|
|
DROP CONSTRAINT IF EXISTS bird_milestone_reminder_deliveries_reminder_type_check;
|
|
|
|
ALTER TABLE bird_milestone_reminder_deliveries
|
|
ADD CONSTRAINT bird_milestone_reminder_deliveries_reminder_type_check
|
|
CHECK (reminder_type IN ('hatch_day', 'gotcha_day', 'memorial_day'));
|
|
|
|
CREATE TABLE IF NOT EXISTS medication_administrations (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
medication_id UUID NOT NULL REFERENCES medications(id) ON DELETE CASCADE,
|
|
bird_id UUID NOT NULL REFERENCES birds(id) ON DELETE CASCADE,
|
|
administered_on DATE NOT NULL,
|
|
administration_slot VARCHAR(80) NOT NULL DEFAULT 'dose-1',
|
|
status VARCHAR(20) NOT NULL CHECK (status IN ('administered', 'missed')),
|
|
notes VARCHAR(500),
|
|
created_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
ALTER TABLE medication_administrations
|
|
ADD COLUMN IF NOT EXISTS administration_slot VARCHAR(80) NOT NULL DEFAULT 'dose-1';
|
|
|
|
ALTER TABLE medication_administrations
|
|
DROP CONSTRAINT IF EXISTS medication_administrations_medication_id_administered_on_key;
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_medication_administrations_unique_slot
|
|
ON medication_administrations (medication_id, administered_on, administration_slot);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_weight_records_bird_recorded_on
|
|
ON weight_records (bird_id, recorded_on DESC);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_vet_visits_bird_visited_on
|
|
ON vet_visits (bird_id, visited_on DESC);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_medications_bird_start_date
|
|
ON medications (bird_id, start_date DESC);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_bird_milestone_reminder_deliveries_workspace
|
|
ON bird_milestone_reminder_deliveries (workspace_id, delivered_on DESC);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_medication_administrations_bird_administered_on
|
|
ON medication_administrations (bird_id, administered_on DESC);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_medication_administrations_medication_date
|
|
ON medication_administrations (medication_id, administered_on DESC, created_at DESC);
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF EXISTS (
|
|
SELECT 1
|
|
FROM information_schema.columns
|
|
WHERE table_name = 'birds'
|
|
AND column_name = 'is_female'
|
|
) THEN
|
|
UPDATE birds
|
|
SET gender = CASE
|
|
WHEN is_female IS TRUE THEN 'female'
|
|
WHEN is_female IS FALSE THEN 'male'
|
|
ELSE gender
|
|
END
|
|
WHERE gender = 'unknown';
|
|
END IF;
|
|
END $$;
|
|
`);
|
|
};
|