Files
buildfor_life_ops/drizzle/0002_phase1_constraints_and_search.sql
grabowski b59904fdae Phases 1-5 + rooms/floors, accounts, custom types, users, notifications
Data model
- Properties, rooms (+optional floors), assets (typed custom fields + Zod
  runtime validator + move history), documents (polymorphic scope)
- Projects -> work packages -> tasks -> subtasks
- Decision events (scoped to project/property/asset/work_package)
- Checklist templates + instances, maintenance schedules (time + usage) with
  auto-materialized checklists on event recording
- Wiki (global + per-project) with revisions + tsvector FTS
- Property accounts (utility/meter numbers by kind)
- Notifications table + per-user channel prefs

Infra
- RBAC guards (requireCompany / requireAdmin)
- Storage abstraction: LocalDiskStorage (HMAC signed URLs) + S3Storage
  behind the same interface, switchable via STORAGE_BACKEND
- CSV export for assets / maintenance / decisions
- QR labels: /api/qr SVG endpoint + printable /assets/[id]/label
- Notifications: in-app + SMTP (own server via nodemailer) + Matrix
  (Client-Server API, per-company room) with opt-in per user
- Company switcher + auto-select first company on login

UI
- Topbar: bell with unread count, theme toggle, name, Sign Out (flat)
- Sidebar: main nav + dedicated Admin section (Asset types, Users, Company)
- Nested-route tabs on property / project / asset detail pages
- Admin UIs for users (invite, role, reset pw, deactivate) and company
  settings (default currency, Matrix room id)
- Custom asset type creation + field-def editor with immutable key/type
  guard and auto-deprecate when removing a field still referenced

Graph
- graphify-out/ committed: GRAPH_REPORT.md, graph.html, graph.json
2026-04-23 15:18:11 +07:00

113 lines
4.8 KiB
PL/PgSQL

-- Phase 1 follow-up: integrity constraints, FTS, GIN, partial FK,
-- and updated_at + asset_types.schema_version triggers.
-- Drizzle-kit can't express any of these from the TS schema, so they live here.
-- ---------------------------------------------------------------------------
-- 1. assets.current_container_kind XOR location FK
-- ---------------------------------------------------------------------------
ALTER TABLE "assets" ADD CONSTRAINT "assets_location_xor" CHECK (
(current_container_kind = 'project' AND current_project_id IS NOT NULL AND current_property_id IS NULL)
OR
(current_container_kind = 'property' AND current_property_id IS NOT NULL AND current_project_id IS NULL)
);
--> statement-breakpoint
ALTER TABLE "asset_location_history" ADD CONSTRAINT "alh_to_xor" CHECK (
(to_kind = 'project' AND to_project_id IS NOT NULL AND to_property_id IS NULL)
OR
(to_kind = 'property' AND to_property_id IS NOT NULL AND to_project_id IS NULL)
);
--> statement-breakpoint
-- ---------------------------------------------------------------------------
-- 2. Self-referential FK on asset_types.parent_id
-- ---------------------------------------------------------------------------
ALTER TABLE "asset_types" ADD CONSTRAINT "asset_types_parent_id_fk"
FOREIGN KEY ("parent_id") REFERENCES "asset_types"("id") ON DELETE SET NULL;
--> statement-breakpoint
-- ---------------------------------------------------------------------------
-- 3. JSONB GIN index on assets.custom_fields
-- ---------------------------------------------------------------------------
CREATE INDEX "assets_custom_fields_gin" ON "assets" USING GIN ("custom_fields" jsonb_path_ops);
--> statement-breakpoint
-- ---------------------------------------------------------------------------
-- 4. Full-text search on assets (tsvector + trigger + GIN)
-- ---------------------------------------------------------------------------
ALTER TABLE "assets" ALTER COLUMN "search_tsv" TYPE tsvector USING NULL::tsvector;
--> statement-breakpoint
CREATE OR REPLACE FUNCTION assets_tsv_trigger() RETURNS trigger AS $$
BEGIN
NEW.search_tsv :=
setweight(to_tsvector('simple', coalesce(NEW.name, '')), 'A') ||
setweight(to_tsvector('simple', coalesce(NEW.tag, '')), 'B') ||
setweight(to_tsvector('simple', coalesce(NEW.serial_number, '')), 'B') ||
setweight(to_tsvector('simple', coalesce(NEW.manufacturer, '')), 'C') ||
setweight(to_tsvector('simple', coalesce(NEW.model, '')), 'C');
RETURN NEW;
END $$ LANGUAGE plpgsql;
--> statement-breakpoint
CREATE TRIGGER assets_tsv_upd BEFORE INSERT OR UPDATE
ON "assets" FOR EACH ROW EXECUTE FUNCTION assets_tsv_trigger();
--> statement-breakpoint
CREATE INDEX "assets_search_tsv_gin" ON "assets" USING GIN ("search_tsv");
--> statement-breakpoint
-- ---------------------------------------------------------------------------
-- 5. Generic updated_at trigger, attached to every Phase-0/1 table that has it
-- ---------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION set_updated_at() RETURNS trigger AS $$
BEGIN
NEW.updated_at := now();
RETURN NEW;
END $$ LANGUAGE plpgsql;
--> statement-breakpoint
CREATE TRIGGER companies_set_updated_at BEFORE UPDATE ON "companies"
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
--> statement-breakpoint
CREATE TRIGGER users_set_updated_at BEFORE UPDATE ON "users"
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
--> statement-breakpoint
CREATE TRIGGER properties_set_updated_at BEFORE UPDATE ON "properties"
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
--> statement-breakpoint
CREATE TRIGGER asset_types_set_updated_at BEFORE UPDATE ON "asset_types"
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
--> statement-breakpoint
CREATE TRIGGER asset_field_defs_set_updated_at BEFORE UPDATE ON "asset_field_defs"
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
--> statement-breakpoint
CREATE TRIGGER assets_set_updated_at BEFORE UPDATE ON "assets"
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
--> statement-breakpoint
-- ---------------------------------------------------------------------------
-- 6. Bump asset_types.schema_version when its field defs change
-- (cache key for the runtime Zod validator)
-- ---------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION bump_asset_type_schema_version() RETURNS trigger AS $$
DECLARE
target_id uuid;
BEGIN
IF TG_OP = 'DELETE' THEN
target_id := OLD.asset_type_id;
ELSE
target_id := NEW.asset_type_id;
END IF;
UPDATE asset_types
SET schema_version = schema_version + 1,
updated_at = now()
WHERE id = target_id;
RETURN NULL;
END $$ LANGUAGE plpgsql;
--> statement-breakpoint
CREATE TRIGGER asset_field_defs_bump_version
AFTER INSERT OR UPDATE OR DELETE ON "asset_field_defs"
FOR EACH ROW EXECUTE FUNCTION bump_asset_type_schema_version();