feat: vollständiges Datenmodell für Normen und Entscheidungen (AIIA-15)

Replaces placeholder schema with full legal data model:
- Normen with temporal versioning (valid_from/valid_to), source rank hierarchy,
  immutable version chains, and norm-to-norm cross-references
- Entscheidungen with structured metadata (Aktenzeichen, Gremium, Leitsatz,
  Tenor, Tatbestand, Entscheidungsgründe), decision-norm links with Stichtag
- NV Bühne Fachgruppen and BSchGO Arbitration Tribunals
- Cases, Analyses, and DSGVO Audit Log
- Mandantentrennung via tenant_id + PostgreSQL Row-Level Security policies
- Initial Drizzle migration and RLS migration

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
CTO (LegalAI)
2026-04-08 21:16:40 +00:00
parent 1d61b2ad8a
commit a7245001ad
6 changed files with 2745 additions and 43 deletions

View File

@@ -0,0 +1,231 @@
CREATE TYPE "public"."analysis_mode" AS ENUM('gutachten', 'entscheidung', 'vergleich', 'risiko');--> statement-breakpoint
CREATE TYPE "public"."analysis_status" AS ENUM('draft', 'in_progress', 'completed', 'archived');--> statement-breakpoint
CREATE TYPE "public"."decision_type" AS ENUM('schiedsspruch', 'urteil', 'beschluss', 'vergleich', 'einstweilige_verfuegung');--> statement-breakpoint
CREATE TYPE "public"."legal_domain" AS ENUM('buehnenrecht', 'arbeitsrecht', 'tarifrecht', 'urheberrecht', 'sozialrecht', 'vertragsrecht', 'prozessrecht');--> statement-breakpoint
CREATE TYPE "public"."norm_type" AS ENUM('gesetz', 'tarifvertrag', 'schiedsordnung', 'verordnung', 'satzung', 'richtlinie');--> statement-breakpoint
CREATE TYPE "public"."source_rank" AS ENUM('gesetz', 'tarif', 'schiedsordnung', 'praxis', 'kommentar');--> statement-breakpoint
CREATE TYPE "public"."user_role" AS ENUM('admin', 'attorney', 'paralegal', 'viewer');--> statement-breakpoint
CREATE TABLE "analyses" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"tenant_id" uuid NOT NULL,
"case_id" uuid,
"user_id" uuid NOT NULL,
"mode" "analysis_mode" NOT NULL,
"status" "analysis_status" DEFAULT 'draft' NOT NULL,
"title" varchar(500) NOT NULL,
"query" text NOT NULL,
"result" text,
"sources" jsonb,
"ai_provider" varchar(50),
"ai_model" varchar(100),
"token_usage" jsonb,
"metadata" jsonb,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "arbitration_tribunals" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" varchar(255) NOT NULL,
"level" varchar(20) NOT NULL,
"seat" varchar(255),
"bschgo_ref" varchar(100),
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "audit_log" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"tenant_id" uuid NOT NULL,
"user_id" uuid,
"action" varchar(100) NOT NULL,
"entity_type" varchar(100) NOT NULL,
"entity_id" uuid,
"details" jsonb,
"ip_address" varchar(45),
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "cases" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"tenant_id" uuid NOT NULL,
"case_number" varchar(100) NOT NULL,
"title" varchar(500) NOT NULL,
"description" text,
"client_name" varchar(255),
"opposing_party" varchar(255),
"venue" varchar(255),
"fachgruppe_id" uuid,
"domains" jsonb DEFAULT '[]'::jsonb,
"status" varchar(50) DEFAULT 'active' NOT NULL,
"filing_date" date,
"hearing_date" date,
"closed_at" timestamp with time zone,
"metadata" jsonb,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "decision_norms" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"decision_id" uuid NOT NULL,
"norm_id" uuid NOT NULL,
"application_type" varchar(50) DEFAULT 'angewendet' NOT NULL,
"passage" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "decision_references" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"source_decision_id" uuid NOT NULL,
"target_decision_id" uuid NOT NULL,
"reference_type" varchar(50) NOT NULL,
"description" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "decisions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"tenant_id" uuid,
"type" "decision_type" NOT NULL,
"case_reference" varchar(100),
"decision_date" date NOT NULL,
"court" varchar(255) NOT NULL,
"tribunal_id" uuid,
"chamber" varchar(100),
"headnote" text,
"tenor" text,
"facts" text,
"reasoning" text,
"full_text" text,
"domains" jsonb DEFAULT '[]'::jsonb,
"keywords" jsonb DEFAULT '[]'::jsonb,
"publication_source" text,
"is_published" boolean DEFAULT false,
"is_anonymized" boolean DEFAULT false,
"metadata" jsonb,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "norm_instruments" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"tenant_id" uuid,
"type" "norm_type" NOT NULL,
"source_rank" "source_rank" NOT NULL,
"abbreviation" varchar(50) NOT NULL,
"full_title" text NOT NULL,
"enacted_at" date,
"issuing_body" varchar(255),
"citation" text,
"metadata" jsonb,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "norm_references" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"source_norm_id" uuid NOT NULL,
"target_norm_id" uuid NOT NULL,
"reference_type" varchar(50) DEFAULT 'verweist_auf' NOT NULL,
"description" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "norms" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"tenant_id" uuid,
"instrument_id" uuid NOT NULL,
"paragraph" varchar(50) NOT NULL,
"subsection" varchar(100),
"title" varchar(500),
"body" text NOT NULL,
"valid_from" date NOT NULL,
"valid_to" date,
"previous_version_id" uuid,
"version_number" integer DEFAULT 1 NOT NULL,
"domains" jsonb DEFAULT '[]'::jsonb,
"notes" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "nv_buehne_fachgruppen" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" varchar(255) NOT NULL,
"abbreviation" varchar(20),
"section_ref" varchar(100),
"description" text,
"special_provisions" jsonb DEFAULT '[]'::jsonb,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "tenants" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" varchar(255) NOT NULL,
"slug" varchar(100) NOT NULL,
"address" text,
"phone" varchar(50),
"email" varchar(255),
"retention_days" integer DEFAULT 3650,
"settings" jsonb,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "tenants_slug_unique" UNIQUE("slug")
);
--> statement-breakpoint
CREATE TABLE "users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"tenant_id" uuid NOT NULL,
"email" varchar(255) NOT NULL,
"name" varchar(255) NOT NULL,
"role" "user_role" DEFAULT 'viewer' NOT NULL,
"password_hash" text,
"email_verified_at" timestamp with time zone,
"last_login_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "analyses" ADD CONSTRAINT "analyses_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "analyses" ADD CONSTRAINT "analyses_case_id_cases_id_fk" FOREIGN KEY ("case_id") REFERENCES "public"."cases"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "analyses" ADD CONSTRAINT "analyses_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "audit_log" ADD CONSTRAINT "audit_log_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "audit_log" ADD CONSTRAINT "audit_log_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "cases" ADD CONSTRAINT "cases_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "cases" ADD CONSTRAINT "cases_fachgruppe_id_nv_buehne_fachgruppen_id_fk" FOREIGN KEY ("fachgruppe_id") REFERENCES "public"."nv_buehne_fachgruppen"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "decision_norms" ADD CONSTRAINT "decision_norms_decision_id_decisions_id_fk" FOREIGN KEY ("decision_id") REFERENCES "public"."decisions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "decision_norms" ADD CONSTRAINT "decision_norms_norm_id_norms_id_fk" FOREIGN KEY ("norm_id") REFERENCES "public"."norms"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "decision_references" ADD CONSTRAINT "decision_references_source_decision_id_decisions_id_fk" FOREIGN KEY ("source_decision_id") REFERENCES "public"."decisions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "decision_references" ADD CONSTRAINT "decision_references_target_decision_id_decisions_id_fk" FOREIGN KEY ("target_decision_id") REFERENCES "public"."decisions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "decisions" ADD CONSTRAINT "decisions_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "decisions" ADD CONSTRAINT "decisions_tribunal_id_arbitration_tribunals_id_fk" FOREIGN KEY ("tribunal_id") REFERENCES "public"."arbitration_tribunals"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "norm_instruments" ADD CONSTRAINT "norm_instruments_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "norm_references" ADD CONSTRAINT "norm_references_source_norm_id_norms_id_fk" FOREIGN KEY ("source_norm_id") REFERENCES "public"."norms"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "norm_references" ADD CONSTRAINT "norm_references_target_norm_id_norms_id_fk" FOREIGN KEY ("target_norm_id") REFERENCES "public"."norms"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "norms" ADD CONSTRAINT "norms_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "norms" ADD CONSTRAINT "norms_instrument_id_norm_instruments_id_fk" FOREIGN KEY ("instrument_id") REFERENCES "public"."norm_instruments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "norms" ADD CONSTRAINT "norms_previous_version_id_norms_id_fk" FOREIGN KEY ("previous_version_id") REFERENCES "public"."norms"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "users" ADD CONSTRAINT "users_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "analyses_tenant_idx" ON "analyses" USING btree ("tenant_id");--> statement-breakpoint
CREATE INDEX "analyses_case_idx" ON "analyses" USING btree ("case_id");--> statement-breakpoint
CREATE INDEX "analyses_mode_idx" ON "analyses" USING btree ("mode");--> statement-breakpoint
CREATE INDEX "audit_log_tenant_idx" ON "audit_log" USING btree ("tenant_id");--> statement-breakpoint
CREATE INDEX "audit_log_entity_idx" ON "audit_log" USING btree ("entity_type","entity_id");--> statement-breakpoint
CREATE INDEX "audit_log_created_idx" ON "audit_log" USING btree ("created_at");--> statement-breakpoint
CREATE UNIQUE INDEX "cases_tenant_number_idx" ON "cases" USING btree ("tenant_id","case_number");--> statement-breakpoint
CREATE INDEX "cases_tenant_idx" ON "cases" USING btree ("tenant_id");--> statement-breakpoint
CREATE INDEX "cases_status_idx" ON "cases" USING btree ("status");--> statement-breakpoint
CREATE UNIQUE INDEX "decision_norms_unique_idx" ON "decision_norms" USING btree ("decision_id","norm_id");--> statement-breakpoint
CREATE UNIQUE INDEX "decision_refs_unique_idx" ON "decision_references" USING btree ("source_decision_id","target_decision_id","reference_type");--> statement-breakpoint
CREATE INDEX "decisions_type_idx" ON "decisions" USING btree ("type");--> statement-breakpoint
CREATE INDEX "decisions_date_idx" ON "decisions" USING btree ("decision_date");--> statement-breakpoint
CREATE INDEX "decisions_court_idx" ON "decisions" USING btree ("court");--> statement-breakpoint
CREATE INDEX "decisions_tenant_idx" ON "decisions" USING btree ("tenant_id");--> statement-breakpoint
CREATE INDEX "decisions_case_ref_idx" ON "decisions" USING btree ("case_reference");--> statement-breakpoint
CREATE INDEX "norm_instruments_type_idx" ON "norm_instruments" USING btree ("type");--> statement-breakpoint
CREATE INDEX "norm_instruments_tenant_idx" ON "norm_instruments" USING btree ("tenant_id");--> statement-breakpoint
CREATE UNIQUE INDEX "norm_refs_unique_idx" ON "norm_references" USING btree ("source_norm_id","target_norm_id","reference_type");--> statement-breakpoint
CREATE INDEX "norms_instrument_idx" ON "norms" USING btree ("instrument_id");--> statement-breakpoint
CREATE INDEX "norms_paragraph_idx" ON "norms" USING btree ("instrument_id","paragraph");--> statement-breakpoint
CREATE INDEX "norms_valid_from_idx" ON "norms" USING btree ("valid_from");--> statement-breakpoint
CREATE INDEX "norms_tenant_idx" ON "norms" USING btree ("tenant_id");--> statement-breakpoint
CREATE UNIQUE INDEX "users_tenant_email_idx" ON "users" USING btree ("tenant_id","email");

View File

@@ -0,0 +1,122 @@
-- ============================================================
-- Row-Level Security (RLS) for Mandantentrennung
-- ============================================================
-- Every tenant-scoped table gets RLS policies.
-- The application sets current_setting('app.tenant_id') via
-- middleware before executing any query.
-- ============================================================
-- Enable RLS on all tenant-scoped tables
ALTER TABLE tenants ENABLE ROW LEVEL SECURITY;
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE norm_instruments ENABLE ROW LEVEL SECURITY;
ALTER TABLE norms ENABLE ROW LEVEL SECURITY;
ALTER TABLE decisions ENABLE ROW LEVEL SECURITY;
ALTER TABLE cases ENABLE ROW LEVEL SECURITY;
ALTER TABLE analyses ENABLE ROW LEVEL SECURITY;
ALTER TABLE audit_log ENABLE ROW LEVEL SECURITY;
-- ============================================================
-- Tenants: users can only see their own tenant
-- ============================================================
CREATE POLICY tenant_isolation ON tenants
USING (id = current_setting('app.tenant_id', true)::uuid);
-- ============================================================
-- Users: scoped to tenant
-- ============================================================
CREATE POLICY users_tenant_isolation ON users
USING (tenant_id = current_setting('app.tenant_id', true)::uuid);
-- ============================================================
-- Norm Instruments: tenant-scoped OR shared (tenant_id IS NULL)
-- Shared norm instruments (system reference data) are visible
-- to all tenants but only writable by superusers.
-- ============================================================
CREATE POLICY norm_instruments_read ON norm_instruments
FOR SELECT
USING (
tenant_id IS NULL
OR tenant_id = current_setting('app.tenant_id', true)::uuid
);
CREATE POLICY norm_instruments_write ON norm_instruments
FOR ALL
USING (tenant_id = current_setting('app.tenant_id', true)::uuid)
WITH CHECK (tenant_id = current_setting('app.tenant_id', true)::uuid);
-- ============================================================
-- Norms: follow instrument visibility (tenant-scoped OR shared)
-- ============================================================
CREATE POLICY norms_read ON norms
FOR SELECT
USING (
tenant_id IS NULL
OR tenant_id = current_setting('app.tenant_id', true)::uuid
);
CREATE POLICY norms_write ON norms
FOR ALL
USING (tenant_id = current_setting('app.tenant_id', true)::uuid)
WITH CHECK (tenant_id = current_setting('app.tenant_id', true)::uuid);
-- ============================================================
-- Decisions: tenant-scoped OR shared (published/anonymized)
-- ============================================================
CREATE POLICY decisions_read ON decisions
FOR SELECT
USING (
tenant_id IS NULL
OR tenant_id = current_setting('app.tenant_id', true)::uuid
OR (is_published = true AND is_anonymized = true)
);
CREATE POLICY decisions_write ON decisions
FOR ALL
USING (tenant_id = current_setting('app.tenant_id', true)::uuid)
WITH CHECK (tenant_id = current_setting('app.tenant_id', true)::uuid);
-- ============================================================
-- Cases: strictly tenant-scoped (contains client PII)
-- ============================================================
CREATE POLICY cases_tenant_isolation ON cases
USING (tenant_id = current_setting('app.tenant_id', true)::uuid);
-- ============================================================
-- Analyses: strictly tenant-scoped
-- ============================================================
CREATE POLICY analyses_tenant_isolation ON analyses
USING (tenant_id = current_setting('app.tenant_id', true)::uuid);
-- ============================================================
-- Audit Log: strictly tenant-scoped
-- ============================================================
CREATE POLICY audit_log_tenant_isolation ON audit_log
USING (tenant_id = current_setting('app.tenant_id', true)::uuid);
-- ============================================================
-- Application role setup
-- ============================================================
-- The app connects as 'legalai_app' which has RLS enforced.
-- Migrations and seed data run as the superuser which bypasses RLS.
DO $$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'legalai_app') THEN
CREATE ROLE legalai_app LOGIN;
END IF;
END
$$;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO legalai_app;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO legalai_app;
-- Force RLS for the app role (even table owners are subject to policies)
ALTER TABLE tenants FORCE ROW LEVEL SECURITY;
ALTER TABLE users FORCE ROW LEVEL SECURITY;
ALTER TABLE norm_instruments FORCE ROW LEVEL SECURITY;
ALTER TABLE norms FORCE ROW LEVEL SECURITY;
ALTER TABLE decisions FORCE ROW LEVEL SECURITY;
ALTER TABLE cases FORCE ROW LEVEL SECURITY;
ALTER TABLE analyses FORCE ROW LEVEL SECURITY;
ALTER TABLE audit_log FORCE ROW LEVEL SECURITY;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1775682934077,
"tag": "0000_peaceful_amazoness",
"breakpoints": true
}
]
}