Add Flyway migrations V001-V019 for all 18 tables
V001: pgcrypto + citext extensions, trigger_set_updated_at function. V002-V019: tables in FK dependency order per data model v1.1. Spec fixes incorporated: - recipe: added created_at/updated_at (spec says all mutable tables carry audit timestamps, but ERD omitted them) - shopping_list: added household_id FK for HouseholdContext scoping - shopping_list_item: added checked_by FK (API returns checkedBy) - cooking_log: omitted phantom week_plan_slot_id (in FK map but absent from ERD, API, and all journeys) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
-- Extensions
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- gen_random_uuid()
|
||||
CREATE EXTENSION IF NOT EXISTS "citext"; -- case-insensitive text
|
||||
|
||||
-- Trigger function: auto-update updated_at on row change
|
||||
CREATE OR REPLACE FUNCTION trigger_set_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
@@ -0,0 +1,18 @@
|
||||
CREATE TABLE user_account (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email citext NOT NULL UNIQUE,
|
||||
display_name varchar(100) NOT NULL,
|
||||
password_hash varchar(255) NOT NULL,
|
||||
system_role varchar(10) NOT NULL DEFAULT 'user'
|
||||
CHECK (system_role IN ('admin', 'user')),
|
||||
is_active boolean NOT NULL DEFAULT true,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_user_active ON user_account (is_active) WHERE is_active = true;
|
||||
CREATE INDEX idx_user_system_role ON user_account (system_role) WHERE system_role = 'admin';
|
||||
|
||||
CREATE TRIGGER set_user_account_updated_at
|
||||
BEFORE UPDATE ON user_account
|
||||
FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at();
|
||||
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE household (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name varchar(100) NOT NULL,
|
||||
created_by uuid NOT NULL REFERENCES user_account (id) ON DELETE RESTRICT,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE household_member (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
household_id uuid NOT NULL REFERENCES household (id) ON DELETE CASCADE,
|
||||
user_id uuid NOT NULL REFERENCES user_account (id) ON DELETE CASCADE,
|
||||
role varchar(10) NOT NULL CHECK (role IN ('planner', 'member')),
|
||||
joined_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT uq_household_member_user UNIQUE (user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_hm_household ON household_member (household_id);
|
||||
@@ -0,0 +1,8 @@
|
||||
CREATE TABLE household_invite (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
household_id uuid NOT NULL REFERENCES household (id) ON DELETE CASCADE,
|
||||
invite_code varchar(20) NOT NULL UNIQUE,
|
||||
status varchar(10) NOT NULL DEFAULT 'pending'
|
||||
CHECK (status IN ('pending', 'used', 'expired')),
|
||||
expires_at timestamptz NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE ingredient_category (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
household_id uuid NOT NULL REFERENCES household (id) ON DELETE CASCADE,
|
||||
name citext NOT NULL,
|
||||
sort_order smallint NOT NULL DEFAULT 0,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT uq_ingredient_category_name UNIQUE (household_id, name)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_ic_sort ON ingredient_category (household_id, sort_order);
|
||||
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE ingredient (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
household_id uuid NOT NULL REFERENCES household (id) ON DELETE CASCADE,
|
||||
name citext NOT NULL,
|
||||
is_staple boolean NOT NULL DEFAULT false,
|
||||
category_id uuid REFERENCES ingredient_category (id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_ingredient_household ON ingredient (household_id);
|
||||
CREATE INDEX idx_ingredient_name ON ingredient (household_id, name);
|
||||
11
backend/src/main/resources/db/migration/V008__create_tag.sql
Normal file
11
backend/src/main/resources/db/migration/V008__create_tag.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE tag (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
household_id uuid NOT NULL REFERENCES household (id) ON DELETE CASCADE,
|
||||
name citext NOT NULL,
|
||||
tag_type varchar(20) NOT NULL
|
||||
CHECK (tag_type IN ('protein', 'dietary', 'cuisine', 'other')),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT uq_tag_name UNIQUE (household_id, name)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_tag_type ON tag (household_id, tag_type);
|
||||
@@ -0,0 +1,19 @@
|
||||
CREATE TABLE recipe (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
household_id uuid NOT NULL REFERENCES household (id) ON DELETE CASCADE,
|
||||
name varchar(200) NOT NULL,
|
||||
serves smallint NOT NULL CHECK (serves BETWEEN 1 AND 20),
|
||||
cook_time_min smallint NOT NULL CHECK (cook_time_min >= 0),
|
||||
effort varchar(10) NOT NULL CHECK (effort IN ('easy', 'medium', 'hard')),
|
||||
is_child_friendly boolean NOT NULL DEFAULT false,
|
||||
hero_image_url varchar(500),
|
||||
deleted_at timestamptz,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_recipe_household ON recipe (household_id) WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE TRIGGER set_recipe_updated_at
|
||||
BEFORE UPDATE ON recipe
|
||||
FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at();
|
||||
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE recipe_ingredient (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
recipe_id uuid NOT NULL REFERENCES recipe (id) ON DELETE CASCADE,
|
||||
ingredient_id uuid NOT NULL REFERENCES ingredient (id) ON DELETE RESTRICT,
|
||||
quantity numeric(8,2) NOT NULL,
|
||||
unit varchar(20) NOT NULL,
|
||||
sort_order smallint NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX idx_ri_recipe ON recipe_ingredient (recipe_id);
|
||||
CREATE INDEX idx_ri_ingredient ON recipe_ingredient (ingredient_id);
|
||||
@@ -0,0 +1,8 @@
|
||||
CREATE TABLE recipe_step (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
recipe_id uuid NOT NULL REFERENCES recipe (id) ON DELETE CASCADE,
|
||||
step_number smallint NOT NULL,
|
||||
instruction text NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_rs_recipe ON recipe_step (recipe_id, step_number);
|
||||
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE recipe_tag (
|
||||
recipe_id uuid NOT NULL REFERENCES recipe (id) ON DELETE CASCADE,
|
||||
tag_id uuid NOT NULL REFERENCES tag (id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (recipe_id, tag_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_rt_tag ON recipe_tag (tag_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
CREATE TABLE week_plan (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
household_id uuid NOT NULL REFERENCES household (id) ON DELETE CASCADE,
|
||||
week_start date NOT NULL,
|
||||
status varchar(10) NOT NULL DEFAULT 'draft'
|
||||
CHECK (status IN ('draft', 'confirmed')),
|
||||
confirmed_at timestamptz,
|
||||
CONSTRAINT uq_week_plan_week UNIQUE (household_id, week_start)
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
CREATE TABLE week_plan_slot (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
week_plan_id uuid NOT NULL REFERENCES week_plan (id) ON DELETE CASCADE,
|
||||
recipe_id uuid NOT NULL REFERENCES recipe (id) ON DELETE RESTRICT,
|
||||
slot_date date NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_wps_plan ON week_plan_slot (week_plan_id, slot_date);
|
||||
@@ -0,0 +1,14 @@
|
||||
-- Note: cooking_log.week_plan_slot_id from the FK map is intentionally
|
||||
-- omitted — it appears in neither the ERD column list nor the API,
|
||||
-- and no journey uses it. Can be added later if needed.
|
||||
|
||||
CREATE TABLE cooking_log (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
recipe_id uuid NOT NULL REFERENCES recipe (id) ON DELETE RESTRICT,
|
||||
household_id uuid NOT NULL REFERENCES household (id) ON DELETE CASCADE,
|
||||
cooked_on date NOT NULL,
|
||||
cooked_by uuid NOT NULL REFERENCES user_account (id) ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE INDEX idx_cl_household_date ON cooking_log (household_id, cooked_on DESC);
|
||||
CREATE INDEX idx_cl_recipe ON cooking_log (recipe_id);
|
||||
@@ -0,0 +1,14 @@
|
||||
-- Note: household_id added per review — spec only had week_plan_id,
|
||||
-- but HouseholdContext pattern requires direct household scoping.
|
||||
|
||||
CREATE TABLE shopping_list (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
household_id uuid NOT NULL REFERENCES household (id) ON DELETE CASCADE,
|
||||
week_plan_id uuid NOT NULL REFERENCES week_plan (id) ON DELETE RESTRICT,
|
||||
status varchar(10) NOT NULL DEFAULT 'draft'
|
||||
CHECK (status IN ('draft', 'published', 'done')),
|
||||
published_at timestamptz
|
||||
);
|
||||
|
||||
CREATE INDEX idx_sl_household ON shopping_list (household_id);
|
||||
CREATE INDEX idx_sl_week_plan ON shopping_list (week_plan_id);
|
||||
@@ -0,0 +1,16 @@
|
||||
-- Note: checked_by added per review — API returns checkedBy on PATCH
|
||||
-- but the original data model had no such column.
|
||||
|
||||
CREATE TABLE shopping_list_item (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
shopping_list_id uuid NOT NULL REFERENCES shopping_list (id) ON DELETE CASCADE,
|
||||
ingredient_id uuid REFERENCES ingredient (id) ON DELETE SET NULL,
|
||||
custom_name varchar(200),
|
||||
quantity numeric(8,2),
|
||||
unit varchar(20),
|
||||
is_checked boolean NOT NULL DEFAULT false,
|
||||
checked_by uuid REFERENCES user_account (id) ON DELETE SET NULL,
|
||||
source_recipes uuid[]
|
||||
);
|
||||
|
||||
CREATE INDEX idx_sli_list ON shopping_list_item (shopping_list_id);
|
||||
@@ -0,0 +1,14 @@
|
||||
CREATE TABLE pantry_item (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
household_id uuid NOT NULL REFERENCES household (id) ON DELETE CASCADE,
|
||||
ingredient_id uuid REFERENCES ingredient (id) ON DELETE SET NULL,
|
||||
custom_name varchar(200),
|
||||
quantity numeric(8,2),
|
||||
unit varchar(20),
|
||||
best_before date,
|
||||
opened_on date
|
||||
);
|
||||
|
||||
CREATE INDEX idx_pi_household ON pantry_item (household_id);
|
||||
CREATE INDEX idx_pi_expiry ON pantry_item (household_id, best_before)
|
||||
WHERE best_before IS NOT NULL;
|
||||
@@ -0,0 +1,21 @@
|
||||
CREATE TABLE admin_audit_log (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
admin_id uuid NOT NULL REFERENCES user_account (id) ON DELETE RESTRICT,
|
||||
target_user_id uuid NOT NULL REFERENCES user_account (id) ON DELETE RESTRICT,
|
||||
action varchar(30) NOT NULL
|
||||
CHECK (action IN (
|
||||
'create_account', 'update_account', 'reset_password',
|
||||
'deactivate_account', 'reactivate_account', 'change_system_role'
|
||||
)),
|
||||
detail jsonb,
|
||||
ip_address inet,
|
||||
performed_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_aal_target ON admin_audit_log (target_user_id, performed_at DESC);
|
||||
CREATE INDEX idx_aal_admin ON admin_audit_log (admin_id, performed_at DESC);
|
||||
CREATE INDEX idx_aal_action ON admin_audit_log (action, performed_at DESC);
|
||||
|
||||
-- Immutability: prevent accidental updates/deletes on audit log
|
||||
CREATE RULE no_update_audit AS ON UPDATE TO admin_audit_log DO INSTEAD NOTHING;
|
||||
CREATE RULE no_delete_audit AS ON DELETE TO admin_audit_log DO INSTEAD NOTHING;
|
||||
Reference in New Issue
Block a user