@@ -0,0 +1,49 @@
|
||||
DROP TABLE plant_status_history;
|
||||
DROP TABLE application_settings;
|
||||
DROP TABLE journal_attachments;
|
||||
DROP TABLE journal_entry_tags;
|
||||
DROP TABLE journal_entries;
|
||||
DROP TABLE entity_image_history;
|
||||
|
||||
ALTER TABLE locations DROP COLUMN image_id;
|
||||
ALTER TABLE plants DROP COLUMN image_id;
|
||||
ALTER TABLE species DROP COLUMN image_id;
|
||||
ALTER TABLE gardens DROP COLUMN image_id;
|
||||
DROP TABLE images;
|
||||
|
||||
DROP TABLE species_tags;
|
||||
DROP TABLE plant_tags;
|
||||
DROP TABLE task_tags;
|
||||
DROP TABLE tags;
|
||||
DROP TABLE task_priorities;
|
||||
DROP TABLE task_template_opt_outs;
|
||||
DROP TABLE tasks;
|
||||
DROP TABLE plant_locations;
|
||||
DROP TABLE plants;
|
||||
DROP TABLE locations;
|
||||
DROP TABLE species_task_templates;
|
||||
DROP TABLE care_instructions;
|
||||
DROP TABLE species;
|
||||
DROP TABLE species_categories;
|
||||
DROP TABLE garden_role_permission_overrides;
|
||||
DROP TABLE garden_invites;
|
||||
DROP TABLE garden_members;
|
||||
|
||||
ALTER TABLE users DROP CONSTRAINT users_application_role_fkey;
|
||||
DROP TABLE role_permissions;
|
||||
DROP TABLE roles;
|
||||
DROP TABLE gardens;
|
||||
DROP TABLE user_email_changes;
|
||||
DROP TABLE tokens;
|
||||
DROP TABLE sessions;
|
||||
DROP TABLE users;
|
||||
|
||||
DROP TYPE task_template_origin;
|
||||
DROP TYPE task_trigger_type;
|
||||
DROP TYPE care_instruction_status;
|
||||
DROP TYPE plant_status;
|
||||
DROP TYPE plant_lifecycle;
|
||||
DROP TYPE soil_reaction;
|
||||
DROP TYPE soil_condition;
|
||||
DROP TYPE sun_exposure;
|
||||
DROP EXTENSION citext;
|
||||
@@ -0,0 +1,524 @@
|
||||
-- Extensions and domain types
|
||||
CREATE EXTENSION IF NOT EXISTS citext;
|
||||
|
||||
CREATE TYPE sun_exposure AS ENUM ('sunny', 'partial_shade', 'shade');
|
||||
CREATE TYPE soil_condition AS ENUM ('dry', 'moist', 'boggy');
|
||||
CREATE TYPE soil_reaction AS ENUM ('alkaline', 'acidic', 'neutral');
|
||||
CREATE TYPE plant_lifecycle AS ENUM ('annual', 'biennial', 'perennial');
|
||||
CREATE TYPE plant_status AS ENUM ('alive', 'dead', 'removed', 'infested', 'harvested');
|
||||
CREATE TYPE care_instruction_status AS ENUM ('good', 'bad', 'untested', 'testing', 'planned');
|
||||
CREATE TYPE task_trigger_type AS ENUM (
|
||||
'month_of_year',
|
||||
'relative_to_planting',
|
||||
'relative_to_last_task',
|
||||
'relative_to_sowing',
|
||||
'relative_to_harvest',
|
||||
'relative_to_species_planting'
|
||||
);
|
||||
CREATE TYPE task_template_origin AS ENUM (
|
||||
'manual',
|
||||
'season_sowing',
|
||||
'season_planting',
|
||||
'season_harvest'
|
||||
);
|
||||
|
||||
-- Authentication and users
|
||||
CREATE TABLE users (
|
||||
id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
name text NOT NULL,
|
||||
email citext NOT NULL UNIQUE,
|
||||
password_hash bytea NOT NULL,
|
||||
activated boolean NOT NULL,
|
||||
application_role text NOT NULL DEFAULT 'application:user',
|
||||
color text NOT NULL DEFAULT (ARRAY['#d95f02','#1b9e77','#7570b3','#e7298a','#66a61e','#e6ab02','#a6761d','#1f78b4'])[1 + floor(random() * 8)::int],
|
||||
deleted_at timestamptz,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
version integer NOT NULL DEFAULT 1,
|
||||
CONSTRAINT users_color_format CHECK (color ~ '^#[0-9A-Fa-f]{6}$')
|
||||
);
|
||||
|
||||
CREATE TABLE sessions (
|
||||
token text PRIMARY KEY,
|
||||
data bytea NOT NULL,
|
||||
expiry timestamptz NOT NULL
|
||||
);
|
||||
CREATE INDEX sessions_expiry_idx ON sessions (expiry);
|
||||
|
||||
CREATE TABLE tokens (
|
||||
hash bytea PRIMARY KEY,
|
||||
user_id bigint NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
expiry timestamptz NOT NULL,
|
||||
scope text NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE user_email_changes (
|
||||
user_id bigint PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||
email citext NOT NULL UNIQUE,
|
||||
token_hash bytea NOT NULL UNIQUE,
|
||||
expires_at timestamptz NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- Gardens, roles, and permissions
|
||||
CREATE TABLE gardens (
|
||||
id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
name text NOT NULL,
|
||||
description text NOT NULL DEFAULT '',
|
||||
image_data text NOT NULL DEFAULT '',
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
version integer NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
CREATE TABLE roles (
|
||||
name text PRIMARY KEY,
|
||||
scope text NOT NULL CHECK (scope IN ('application', 'garden')),
|
||||
label text NOT NULL,
|
||||
system boolean NOT NULL DEFAULT false,
|
||||
garden_id bigint REFERENCES gardens(id) ON DELETE CASCADE,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT roles_scope_garden_check CHECK (
|
||||
(scope = 'application' AND garden_id IS NULL) OR scope = 'garden'
|
||||
)
|
||||
);
|
||||
CREATE INDEX roles_garden_id_idx ON roles(garden_id) WHERE garden_id IS NOT NULL;
|
||||
|
||||
CREATE TABLE role_permissions (
|
||||
role_name text NOT NULL REFERENCES roles(name) ON DELETE CASCADE,
|
||||
permission text NOT NULL,
|
||||
PRIMARY KEY (role_name, permission)
|
||||
);
|
||||
|
||||
ALTER TABLE users
|
||||
ADD CONSTRAINT users_application_role_fkey
|
||||
FOREIGN KEY (application_role) REFERENCES roles(name);
|
||||
|
||||
CREATE TABLE garden_members (
|
||||
garden_id bigint NOT NULL REFERENCES gardens(id) ON DELETE CASCADE,
|
||||
user_id bigint NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
role text NOT NULL DEFAULT 'member' REFERENCES roles(name),
|
||||
joined_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (garden_id, user_id)
|
||||
);
|
||||
CREATE INDEX garden_members_user_id_idx ON garden_members (user_id);
|
||||
|
||||
CREATE TABLE garden_invites (
|
||||
id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
garden_id bigint NOT NULL REFERENCES gardens(id) ON DELETE CASCADE,
|
||||
email citext NOT NULL,
|
||||
role text NOT NULL DEFAULT 'member' REFERENCES roles(name),
|
||||
token_hash bytea NOT NULL UNIQUE,
|
||||
invited_by bigint NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
expires_at timestamptz NOT NULL,
|
||||
accepted_at timestamptz,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE UNIQUE INDEX garden_invites_pending_email_unique
|
||||
ON garden_invites (garden_id, email) WHERE accepted_at IS NULL;
|
||||
CREATE INDEX garden_invites_garden_idx ON garden_invites (garden_id, created_at);
|
||||
|
||||
CREATE TABLE garden_role_permission_overrides (
|
||||
garden_id bigint NOT NULL REFERENCES gardens(id) ON DELETE CASCADE,
|
||||
role_name text NOT NULL REFERENCES roles(name) ON DELETE CASCADE,
|
||||
permission text NOT NULL,
|
||||
granted boolean NOT NULL,
|
||||
PRIMARY KEY (garden_id, role_name, permission)
|
||||
);
|
||||
|
||||
-- Species catalogue and care instructions
|
||||
CREATE TABLE species_categories (
|
||||
id bigserial PRIMARY KEY,
|
||||
name text NOT NULL,
|
||||
sort_order integer NOT NULL DEFAULT 0,
|
||||
active boolean NOT NULL DEFAULT true,
|
||||
lifecycle plant_lifecycle,
|
||||
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
version integer NOT NULL DEFAULT 1
|
||||
);
|
||||
CREATE UNIQUE INDEX species_categories_name_key ON species_categories (lower(name));
|
||||
|
||||
CREATE TABLE species (
|
||||
id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
garden_id bigint REFERENCES gardens(id) ON DELETE CASCADE,
|
||||
category_id bigint REFERENCES species_categories(id) ON DELETE RESTRICT,
|
||||
common_name text NOT NULL,
|
||||
cultivar text NOT NULL DEFAULT '',
|
||||
botanical_name text NOT NULL DEFAULT '',
|
||||
sun_exposure sun_exposure,
|
||||
soil_condition soil_condition,
|
||||
soil_reaction soil_reaction,
|
||||
winter_protection text,
|
||||
spacing_cm integer,
|
||||
height_cm integer,
|
||||
sow_month_from smallint CHECK (sow_month_from BETWEEN 1 AND 12),
|
||||
sow_day_from smallint CHECK (sow_day_from BETWEEN 1 AND 31),
|
||||
sow_month_to smallint CHECK (sow_month_to BETWEEN 1 AND 12),
|
||||
sow_day_to smallint CHECK (sow_day_to BETWEEN 1 AND 31),
|
||||
planting_month_from smallint CHECK (planting_month_from BETWEEN 1 AND 12),
|
||||
planting_day_from smallint CHECK (planting_day_from BETWEEN 1 AND 31),
|
||||
planting_month_to smallint CHECK (planting_month_to BETWEEN 1 AND 12),
|
||||
planting_day_to smallint CHECK (planting_day_to BETWEEN 1 AND 31),
|
||||
harvest_month_from smallint CHECK (harvest_month_from BETWEEN 1 AND 12),
|
||||
harvest_day_from smallint CHECK (harvest_day_from BETWEEN 1 AND 31),
|
||||
harvest_month_to smallint CHECK (harvest_month_to BETWEEN 1 AND 12),
|
||||
harvest_day_to smallint CHECK (harvest_day_to BETWEEN 1 AND 31),
|
||||
notes text NOT NULL DEFAULT '',
|
||||
attributes jsonb NOT NULL DEFAULT '{}',
|
||||
image_data text NOT NULL DEFAULT '',
|
||||
created_by bigint REFERENCES users(id),
|
||||
updated_by bigint REFERENCES users(id),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
version integer NOT NULL DEFAULT 1
|
||||
);
|
||||
CREATE UNIQUE INDEX species_global_unique
|
||||
ON species (common_name, cultivar) WHERE garden_id IS NULL;
|
||||
CREATE UNIQUE INDEX species_garden_unique
|
||||
ON species (garden_id, common_name, cultivar) WHERE garden_id IS NOT NULL;
|
||||
|
||||
CREATE TABLE care_instructions (
|
||||
id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
species_id bigint NOT NULL REFERENCES species(id) ON DELETE CASCADE,
|
||||
text text NOT NULL,
|
||||
status care_instruction_status NOT NULL DEFAULT 'untested',
|
||||
created_by bigint NOT NULL REFERENCES users(id),
|
||||
updated_by bigint NOT NULL REFERENCES users(id),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
version integer NOT NULL DEFAULT 1
|
||||
);
|
||||
CREATE INDEX care_instructions_species ON care_instructions(species_id);
|
||||
|
||||
CREATE TABLE species_task_templates (
|
||||
id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
species_id bigint NOT NULL REFERENCES species(id) ON DELETE CASCADE,
|
||||
title text NOT NULL,
|
||||
description text NOT NULL DEFAULT '',
|
||||
trigger_type task_trigger_type NOT NULL,
|
||||
month_from smallint CHECK (month_from BETWEEN 1 AND 12),
|
||||
day_from smallint CHECK (day_from BETWEEN 1 AND 31),
|
||||
month_to smallint CHECK (month_to BETWEEN 1 AND 12),
|
||||
day_to smallint CHECK (day_to BETWEEN 1 AND 31),
|
||||
offset_days_from integer,
|
||||
offset_days_to integer,
|
||||
interval_days smallint,
|
||||
trigger_offset integer NOT NULL DEFAULT 0 CHECK (trigger_offset >= 0),
|
||||
trigger_offset_unit text NOT NULL DEFAULT 'day' CHECK (trigger_offset_unit IN ('day', 'week', 'month')),
|
||||
duration integer NOT NULL DEFAULT 0 CHECK (duration >= 0),
|
||||
duration_unit text NOT NULL DEFAULT 'day' CHECK (duration_unit IN ('day', 'week', 'month')),
|
||||
recurrence text NOT NULL DEFAULT '' CHECK (recurrence IN ('', 'daily', 'weekly', 'monthly', 'yearly')),
|
||||
recurrence_interval integer NOT NULL DEFAULT 1 CHECK (recurrence_interval > 0),
|
||||
origin task_template_origin NOT NULL DEFAULT 'manual',
|
||||
priority smallint NOT NULL DEFAULT 0,
|
||||
active boolean NOT NULL DEFAULT true,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
version integer NOT NULL DEFAULT 1,
|
||||
CHECK (offset_days_from IS NULL OR offset_days_to IS NULL OR offset_days_from <= offset_days_to)
|
||||
);
|
||||
CREATE INDEX species_task_templates_species_id_idx
|
||||
ON species_task_templates (species_id) WHERE active = true;
|
||||
CREATE UNIQUE INDEX species_task_templates_derived_origin_key
|
||||
ON species_task_templates (species_id, origin) WHERE origin <> 'manual';
|
||||
|
||||
-- Locations, plants, and tasks
|
||||
CREATE TABLE locations (
|
||||
id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
garden_id bigint NOT NULL REFERENCES gardens(id) ON DELETE CASCADE,
|
||||
parent_id bigint REFERENCES locations(id) ON DELETE SET NULL,
|
||||
name text NOT NULL,
|
||||
description text NOT NULL DEFAULT '',
|
||||
kind text NOT NULL DEFAULT '',
|
||||
area_sqm numeric(10,2),
|
||||
sun_exposure sun_exposure,
|
||||
soil_condition soil_condition,
|
||||
soil_reaction soil_reaction,
|
||||
attributes jsonb NOT NULL DEFAULT '{}',
|
||||
image_data text NOT NULL DEFAULT '',
|
||||
created_by bigint REFERENCES users(id),
|
||||
updated_by bigint REFERENCES users(id),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
version integer NOT NULL DEFAULT 1
|
||||
);
|
||||
CREATE INDEX locations_garden_id_idx ON locations (garden_id);
|
||||
CREATE INDEX locations_parent_id_idx ON locations (parent_id);
|
||||
|
||||
CREATE TABLE plants (
|
||||
id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
garden_id bigint NOT NULL REFERENCES gardens(id) ON DELETE CASCADE,
|
||||
species_id bigint REFERENCES species(id) ON DELETE SET NULL,
|
||||
name text NOT NULL,
|
||||
notes text NOT NULL DEFAULT '',
|
||||
acquired_at date,
|
||||
status plant_status NOT NULL DEFAULT 'alive',
|
||||
removed_at date,
|
||||
attributes jsonb NOT NULL DEFAULT '{}',
|
||||
image_data text NOT NULL DEFAULT '',
|
||||
created_by bigint REFERENCES users(id),
|
||||
updated_by bigint REFERENCES users(id),
|
||||
planted_by bigint REFERENCES users(id),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
version integer NOT NULL DEFAULT 1
|
||||
);
|
||||
CREATE INDEX plants_garden_id_idx ON plants (garden_id) WHERE status = 'alive';
|
||||
|
||||
CREATE TABLE plant_locations (
|
||||
id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
plant_id bigint NOT NULL REFERENCES plants(id) ON DELETE CASCADE,
|
||||
location_id bigint NOT NULL REFERENCES locations(id) ON DELETE CASCADE,
|
||||
quantity integer NOT NULL DEFAULT 1,
|
||||
planted_at date,
|
||||
removed_at date,
|
||||
notes text NOT NULL DEFAULT '',
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
version integer NOT NULL DEFAULT 1
|
||||
);
|
||||
CREATE UNIQUE INDEX plant_location_active
|
||||
ON plant_locations (plant_id, location_id) WHERE removed_at IS NULL;
|
||||
CREATE INDEX plant_locations_location_id_idx
|
||||
ON plant_locations (location_id) WHERE removed_at IS NULL;
|
||||
|
||||
CREATE TABLE tasks (
|
||||
id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
garden_id bigint NOT NULL REFERENCES gardens(id) ON DELETE CASCADE,
|
||||
plant_id bigint REFERENCES plants(id) ON DELETE CASCADE,
|
||||
location_id bigint REFERENCES locations(id) ON DELETE CASCADE,
|
||||
template_id bigint REFERENCES species_task_templates(id) ON DELETE SET NULL,
|
||||
title text NOT NULL,
|
||||
description text NOT NULL DEFAULT '',
|
||||
due_at_start timestamptz,
|
||||
due_at_end timestamptz,
|
||||
generated_for date,
|
||||
completed_at timestamptz,
|
||||
completed_by bigint REFERENCES users(id),
|
||||
priority smallint NOT NULL DEFAULT 0,
|
||||
active boolean NOT NULL DEFAULT true,
|
||||
recurrence text NOT NULL DEFAULT '' CHECK (recurrence IN ('', 'daily', 'weekly', 'monthly', 'yearly')),
|
||||
recurrence_interval integer NOT NULL DEFAULT 1 CHECK (recurrence_interval > 0),
|
||||
repeat_from_id bigint REFERENCES tasks(id) ON DELETE SET NULL,
|
||||
plant_status_on_completion plant_status,
|
||||
created_by bigint NOT NULL REFERENCES users(id),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
version integer NOT NULL DEFAULT 1,
|
||||
CHECK (due_at_start IS NULL OR due_at_end IS NULL OR due_at_start <= due_at_end),
|
||||
CONSTRAINT tasks_completion_user_check CHECK (
|
||||
(completed_at IS NULL AND completed_by IS NULL) OR
|
||||
(completed_at IS NOT NULL AND completed_by IS NOT NULL)
|
||||
)
|
||||
);
|
||||
CREATE INDEX tasks_garden_id_due_at_end_idx
|
||||
ON tasks (garden_id, due_at_end) WHERE completed_at IS NULL;
|
||||
CREATE INDEX tasks_garden_id_due_at_start_idx
|
||||
ON tasks (garden_id, due_at_start) WHERE completed_at IS NULL;
|
||||
CREATE INDEX tasks_plant_id_idx ON tasks (plant_id) WHERE plant_id IS NOT NULL;
|
||||
CREATE UNIQUE INDEX tasks_repeat_from_unique
|
||||
ON tasks (repeat_from_id) WHERE repeat_from_id IS NOT NULL;
|
||||
CREATE UNIQUE INDEX tasks_template_slot_unique
|
||||
ON tasks (plant_id, template_id, generated_for) WHERE template_id IS NOT NULL;
|
||||
|
||||
CREATE TABLE task_template_opt_outs (
|
||||
plant_id bigint NOT NULL REFERENCES plants(id) ON DELETE CASCADE,
|
||||
template_id bigint NOT NULL REFERENCES species_task_templates(id) ON DELETE CASCADE,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (plant_id, template_id)
|
||||
);
|
||||
|
||||
CREATE TABLE task_priorities (
|
||||
id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
name text NOT NULL UNIQUE,
|
||||
value smallint NOT NULL UNIQUE CHECK (value BETWEEN -100 AND 100),
|
||||
sort_order integer NOT NULL DEFAULT 0,
|
||||
active boolean NOT NULL DEFAULT true,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
version integer NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
-- Tags
|
||||
CREATE TABLE tags (
|
||||
id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
garden_id bigint REFERENCES gardens(id) ON DELETE CASCADE,
|
||||
name text NOT NULL,
|
||||
UNIQUE (garden_id, name)
|
||||
);
|
||||
CREATE UNIQUE INDEX tags_global_name_key ON tags(name) WHERE garden_id IS NULL;
|
||||
|
||||
CREATE TABLE task_tags (
|
||||
task_id bigint NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
||||
tag_id bigint NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (task_id, tag_id)
|
||||
);
|
||||
CREATE TABLE plant_tags (
|
||||
plant_id bigint NOT NULL REFERENCES plants(id) ON DELETE CASCADE,
|
||||
tag_id bigint NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (plant_id, tag_id)
|
||||
);
|
||||
CREATE TABLE species_tags (
|
||||
species_id bigint NOT NULL REFERENCES species(id) ON DELETE CASCADE,
|
||||
tag_id bigint NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (species_id, tag_id)
|
||||
);
|
||||
|
||||
-- Image library
|
||||
CREATE TABLE images (
|
||||
id bigserial PRIMARY KEY,
|
||||
garden_id bigint NOT NULL REFERENCES gardens(id) ON DELETE CASCADE,
|
||||
file_name text NOT NULL DEFAULT '',
|
||||
media_type text NOT NULL,
|
||||
data bytea NOT NULL,
|
||||
size bigint NOT NULL,
|
||||
checksum text NOT NULL,
|
||||
source text NOT NULL DEFAULT 'upload',
|
||||
created_by bigint REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX images_garden_created_idx ON images(garden_id, created_at DESC, id DESC);
|
||||
CREATE INDEX images_garden_checksum_idx ON images(garden_id, checksum);
|
||||
|
||||
ALTER TABLE gardens ADD COLUMN image_id bigint REFERENCES images(id) ON DELETE SET NULL;
|
||||
ALTER TABLE species ADD COLUMN image_id bigint REFERENCES images(id) ON DELETE SET NULL;
|
||||
ALTER TABLE plants ADD COLUMN image_id bigint REFERENCES images(id) ON DELETE SET NULL;
|
||||
ALTER TABLE locations ADD COLUMN image_id bigint REFERENCES images(id) ON DELETE SET NULL;
|
||||
|
||||
CREATE TABLE entity_image_history (
|
||||
id bigserial PRIMARY KEY,
|
||||
garden_id bigint NOT NULL REFERENCES gardens(id) ON DELETE CASCADE,
|
||||
entity_type text NOT NULL CHECK (entity_type IN ('garden', 'species', 'plant', 'location')),
|
||||
entity_id bigint NOT NULL,
|
||||
previous_image_id bigint REFERENCES images(id) ON DELETE SET NULL,
|
||||
image_id bigint REFERENCES images(id) ON DELETE SET NULL,
|
||||
changed_by bigint REFERENCES users(id) ON DELETE SET NULL,
|
||||
changed_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX entity_image_history_entity_idx
|
||||
ON entity_image_history(garden_id, entity_type, entity_id, changed_at DESC);
|
||||
|
||||
-- Journal
|
||||
CREATE TABLE journal_entries (
|
||||
id bigserial PRIMARY KEY,
|
||||
garden_id bigint NOT NULL REFERENCES gardens(id) ON DELETE CASCADE,
|
||||
author_id bigint NOT NULL REFERENCES users(id),
|
||||
entry_type text NOT NULL DEFAULT 'journal' CHECK (entry_type IN ('journal', 'pinboard')),
|
||||
title text NOT NULL,
|
||||
body text NOT NULL DEFAULT '',
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
version integer NOT NULL DEFAULT 1
|
||||
);
|
||||
CREATE INDEX journal_entries_garden_created_idx
|
||||
ON journal_entries(garden_id, created_at DESC, id DESC);
|
||||
CREATE INDEX journal_entries_garden_type_created_idx
|
||||
ON journal_entries(garden_id, entry_type, created_at DESC, id DESC);
|
||||
|
||||
CREATE TABLE journal_entry_tags (
|
||||
journal_entry_id bigint NOT NULL REFERENCES journal_entries(id) ON DELETE CASCADE,
|
||||
tag_id bigint NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (journal_entry_id, tag_id)
|
||||
);
|
||||
|
||||
CREATE TABLE journal_attachments (
|
||||
id bigserial PRIMARY KEY,
|
||||
journal_entry_id bigint NOT NULL REFERENCES journal_entries(id) ON DELETE CASCADE,
|
||||
image_id bigint REFERENCES images(id) ON DELETE RESTRICT,
|
||||
file_name text NOT NULL,
|
||||
media_type text NOT NULL,
|
||||
data bytea NOT NULL,
|
||||
size bigint NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX journal_attachments_entry_idx ON journal_attachments(journal_entry_id, id);
|
||||
|
||||
-- Application settings and lifecycle history
|
||||
CREATE TABLE application_settings (
|
||||
singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
|
||||
lifecycle_status_enabled boolean NOT NULL DEFAULT false,
|
||||
lifecycle_removal_month smallint NOT NULL DEFAULT 12 CHECK (lifecycle_removal_month BETWEEN 1 AND 12),
|
||||
lifecycle_removal_day smallint NOT NULL DEFAULT 1 CHECK (lifecycle_removal_day BETWEEN 1 AND 31),
|
||||
timezone text NOT NULL DEFAULT 'Europe/Berlin',
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
version integer NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
CREATE TABLE plant_status_history (
|
||||
id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
plant_id bigint NOT NULL REFERENCES plants(id) ON DELETE CASCADE,
|
||||
from_status plant_status NOT NULL,
|
||||
to_status plant_status NOT NULL,
|
||||
reason text NOT NULL,
|
||||
effective_at date NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX plant_status_history_plant_id_idx
|
||||
ON plant_status_history (plant_id, created_at DESC);
|
||||
|
||||
-- Initial reference data
|
||||
INSERT INTO roles (name, scope, label, system) VALUES
|
||||
('application:user', 'application', 'Nutzer', true),
|
||||
('application:admin', 'application', 'Administrator', true),
|
||||
('owner', 'garden', 'Eigentümer', true),
|
||||
('admin', 'garden', 'Administrator', true),
|
||||
('member', 'garden', 'Mitglied', true),
|
||||
('worker', 'garden', 'Mitarbeiter', true),
|
||||
('viewer', 'garden', 'Leser', true);
|
||||
|
||||
INSERT INTO role_permissions (role_name, permission) VALUES
|
||||
('application:user', 'gardens:create'),
|
||||
('application:admin', 'gardens:create'),
|
||||
('application:admin', 'global_species:write'),
|
||||
('application:admin', 'users:manage'),
|
||||
('application:admin', 'application_settings:write'),
|
||||
('application:admin', 'roles:manage'),
|
||||
('owner', '*'),
|
||||
('admin', 'garden:*'),
|
||||
('member', 'garden:read'),
|
||||
('member', 'content:write'),
|
||||
('member', 'plants:create'),
|
||||
('member', 'plants:read:own'),
|
||||
('member', 'plants:read:other'),
|
||||
('member', 'plants:update:own'),
|
||||
('member', 'plants:delete:own'),
|
||||
('member', 'locations:create'),
|
||||
('member', 'locations:read:own'),
|
||||
('member', 'locations:read:other'),
|
||||
('member', 'locations:update:own'),
|
||||
('member', 'locations:delete:own'),
|
||||
('member', 'tasks:create'),
|
||||
('member', 'tasks:read:own'),
|
||||
('member', 'tasks:read:other'),
|
||||
('member', 'tasks:update:own'),
|
||||
('member', 'tasks:delete:own'),
|
||||
('member', 'tasks:complete:own'),
|
||||
('member', 'tasks:complete:other'),
|
||||
('worker', 'garden:read'),
|
||||
('worker', 'tasks:read:own'),
|
||||
('worker', 'tasks:read:other'),
|
||||
('worker', 'tasks:complete:own'),
|
||||
('worker', 'tasks:complete:other'),
|
||||
('viewer', 'garden:read'),
|
||||
('viewer', 'plants:read:own'),
|
||||
('viewer', 'plants:read:other'),
|
||||
('viewer', 'locations:read:own'),
|
||||
('viewer', 'locations:read:other'),
|
||||
('viewer', 'tasks:read:own'),
|
||||
('viewer', 'tasks:read:other');
|
||||
|
||||
INSERT INTO task_priorities (name, value, sort_order) VALUES
|
||||
('Niedrig', -5, 10),
|
||||
('Normal', 0, 20),
|
||||
('Erhöht', 3, 30),
|
||||
('Hoch', 5, 40);
|
||||
|
||||
INSERT INTO application_settings (singleton) VALUES (true);
|
||||
|
||||
INSERT INTO species_categories (name, sort_order) VALUES
|
||||
('Gehölz', 10),
|
||||
('Gemüse', 20),
|
||||
('Kraut', 30),
|
||||
('Obst', 40),
|
||||
('Staude', 50);
|
||||
Reference in New Issue
Block a user