525 lines
23 KiB
SQL
525 lines
23 KiB
SQL
-- 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);
|