grocery-app/packages/db/migrations/zz_group_invites_and_join_policies.sql
Nico 77ae5be445
All checks were successful
Build & Deploy Costco Grocery List / build (push) Successful in 1m10s
Build & Deploy Costco Grocery List / verify-images (push) Successful in 3s
Build & Deploy Costco Grocery List / deploy (push) Successful in 11s
Build & Deploy Costco Grocery List / notify (push) Successful in 1s
refactor
2026-02-22 01:27:03 -08:00

166 lines
4.4 KiB
PL/PgSQL

BEGIN;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'group_join_policy') THEN
CREATE TYPE group_join_policy AS ENUM (
'NOT_ACCEPTING',
'AUTO_ACCEPT',
'APPROVAL_REQUIRED'
);
END IF;
END
$$;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'group_join_request_status') THEN
CREATE TYPE group_join_request_status AS ENUM (
'PENDING',
'APPROVED',
'DENIED',
'CANCELED'
);
END IF;
END
$$;
CREATE TABLE IF NOT EXISTS group_settings (
group_id INTEGER PRIMARY KEY REFERENCES households(id) ON DELETE CASCADE,
join_policy group_join_policy NOT NULL DEFAULT 'NOT_ACCEPTING',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
INSERT INTO group_settings (group_id, join_policy)
SELECT h.id, 'NOT_ACCEPTING'::group_join_policy
FROM households h
WHERE NOT EXISTS (
SELECT 1 FROM group_settings gs WHERE gs.group_id = h.id
);
CREATE TABLE IF NOT EXISTS group_join_requests (
id SERIAL PRIMARY KEY,
group_id INTEGER NOT NULL REFERENCES households(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
status group_join_request_status NOT NULL DEFAULT 'PENDING',
decided_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
decided_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_group_join_requests_pending
ON group_join_requests(group_id, user_id)
WHERE status = 'PENDING';
CREATE INDEX IF NOT EXISTS idx_group_join_requests_group
ON group_join_requests(group_id);
CREATE TABLE IF NOT EXISTS group_invite_links (
id SERIAL PRIMARY KEY,
group_id INTEGER NOT NULL REFERENCES households(id) ON DELETE CASCADE,
created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token VARCHAR(64) NOT NULL UNIQUE,
policy group_join_policy NOT NULL DEFAULT 'NOT_ACCEPTING',
single_use BOOLEAN NOT NULL DEFAULT FALSE,
expires_at TIMESTAMPTZ NOT NULL,
used_at TIMESTAMPTZ,
revoked_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_group_invite_links_group_id
ON group_invite_links(group_id);
CREATE TABLE IF NOT EXISTS group_audit_log (
id SERIAL PRIMARY KEY,
group_id INTEGER NOT NULL REFERENCES households(id) ON DELETE CASCADE,
actor_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
actor_role VARCHAR(20),
event_type VARCHAR(100) NOT NULL,
request_id VARCHAR(128) NOT NULL,
ip INET,
user_agent TEXT,
success BOOLEAN NOT NULL DEFAULT TRUE,
error_code VARCHAR(100),
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_group_audit_group_created
ON group_audit_log(group_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_group_audit_request_id
ON group_audit_log(request_id);
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_indexes
WHERE schemaname = current_schema()
AND tablename = 'households'
AND indexdef ILIKE 'CREATE UNIQUE INDEX%'
AND indexdef ILIKE '%(invite_code)%'
) THEN
CREATE UNIQUE INDEX idx_households_invite_code_unique
ON households(invite_code);
END IF;
END
$$;
ALTER TABLE household_members
DROP CONSTRAINT IF EXISTS household_members_role_check;
UPDATE household_members
SET role = 'member'
WHERE role = 'user';
WITH ranked_admins AS (
SELECT
hm.id,
ROW_NUMBER() OVER (
PARTITION BY hm.household_id
ORDER BY hm.joined_at ASC, hm.id ASC
) AS admin_rank
FROM household_members hm
WHERE hm.role = 'admin'
)
UPDATE household_members hm
SET role = CASE
WHEN ra.admin_rank = 1 THEN 'owner'
ELSE 'admin'
END
FROM ranked_admins ra
WHERE hm.id = ra.id;
WITH ownerless_households AS (
SELECT h.id AS household_id
FROM households h
WHERE NOT EXISTS (
SELECT 1
FROM household_members hm
WHERE hm.household_id = h.id
AND hm.role = 'owner'
)
),
first_member AS (
SELECT DISTINCT ON (hm.household_id)
hm.id,
hm.household_id
FROM household_members hm
JOIN ownerless_households oh ON oh.household_id = hm.household_id
ORDER BY hm.household_id, hm.joined_at ASC, hm.id ASC
)
UPDATE household_members hm
SET role = 'owner'
FROM first_member fm
WHERE hm.id = fm.id;
ALTER TABLE household_members
ADD CONSTRAINT household_members_role_check
CHECK (role IN ('owner', 'admin', 'member'));
COMMIT;