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
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
-- 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();
|
||||
Reference in New Issue
Block a user