diff --git a/drizzle/0000_peaceful_amazoness.sql b/drizzle/0000_peaceful_amazoness.sql new file mode 100644 index 0000000..e1b1af5 --- /dev/null +++ b/drizzle/0000_peaceful_amazoness.sql @@ -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"); \ No newline at end of file diff --git a/drizzle/0001_rls_policies.sql b/drizzle/0001_rls_policies.sql new file mode 100644 index 0000000..f75ec8e --- /dev/null +++ b/drizzle/0001_rls_policies.sql @@ -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; diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..e66dae8 --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,1860 @@ +{ + "id": "5d49fd29-b748-4be9-a6f3-418dca5d85f3", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.analyses": { + "name": "analyses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "analysis_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "analysis_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "title": { + "name": "title", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true + }, + "query": { + "name": "query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sources": { + "name": "sources", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ai_provider": { + "name": "ai_provider", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "ai_model": { + "name": "ai_model", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "token_usage": { + "name": "token_usage", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "analyses_tenant_idx": { + "name": "analyses_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "analyses_case_idx": { + "name": "analyses_case_idx", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "analyses_mode_idx": { + "name": "analyses_mode_idx", + "columns": [ + { + "expression": "mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "analyses_tenant_id_tenants_id_fk": { + "name": "analyses_tenant_id_tenants_id_fk", + "tableFrom": "analyses", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "analyses_case_id_cases_id_fk": { + "name": "analyses_case_id_cases_id_fk", + "tableFrom": "analyses", + "tableTo": "cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "analyses_user_id_users_id_fk": { + "name": "analyses_user_id_users_id_fk", + "tableFrom": "analyses", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.arbitration_tribunals": { + "name": "arbitration_tribunals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "seat": { + "name": "seat", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "bschgo_ref": { + "name": "bschgo_ref", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "varchar(45)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_tenant_idx": { + "name": "audit_log_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_entity_idx": { + "name": "audit_log_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_created_idx": { + "name": "audit_log_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_tenant_id_tenants_id_fk": { + "name": "audit_log_tenant_id_tenants_id_fk", + "tableFrom": "audit_log", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_log_user_id_users_id_fk": { + "name": "audit_log_user_id_users_id_fk", + "tableFrom": "audit_log", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cases": { + "name": "cases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_number": { + "name": "case_number", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_name": { + "name": "client_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "opposing_party": { + "name": "opposing_party", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "venue": { + "name": "venue", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "fachgruppe_id": { + "name": "fachgruppe_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "domains": { + "name": "domains", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "filing_date": { + "name": "filing_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "hearing_date": { + "name": "hearing_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "cases_tenant_number_idx": { + "name": "cases_tenant_number_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cases_tenant_idx": { + "name": "cases_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cases_status_idx": { + "name": "cases_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cases_tenant_id_tenants_id_fk": { + "name": "cases_tenant_id_tenants_id_fk", + "tableFrom": "cases", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cases_fachgruppe_id_nv_buehne_fachgruppen_id_fk": { + "name": "cases_fachgruppe_id_nv_buehne_fachgruppen_id_fk", + "tableFrom": "cases", + "tableTo": "nv_buehne_fachgruppen", + "columnsFrom": [ + "fachgruppe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.decision_norms": { + "name": "decision_norms", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "decision_id": { + "name": "decision_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "norm_id": { + "name": "norm_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "application_type": { + "name": "application_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'angewendet'" + }, + "passage": { + "name": "passage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "decision_norms_unique_idx": { + "name": "decision_norms_unique_idx", + "columns": [ + { + "expression": "decision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "norm_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decision_norms_decision_id_decisions_id_fk": { + "name": "decision_norms_decision_id_decisions_id_fk", + "tableFrom": "decision_norms", + "tableTo": "decisions", + "columnsFrom": [ + "decision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_norms_norm_id_norms_id_fk": { + "name": "decision_norms_norm_id_norms_id_fk", + "tableFrom": "decision_norms", + "tableTo": "norms", + "columnsFrom": [ + "norm_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.decision_references": { + "name": "decision_references", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "source_decision_id": { + "name": "source_decision_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_decision_id": { + "name": "target_decision_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "reference_type": { + "name": "reference_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "decision_refs_unique_idx": { + "name": "decision_refs_unique_idx", + "columns": [ + { + "expression": "source_decision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_decision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reference_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decision_references_source_decision_id_decisions_id_fk": { + "name": "decision_references_source_decision_id_decisions_id_fk", + "tableFrom": "decision_references", + "tableTo": "decisions", + "columnsFrom": [ + "source_decision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_references_target_decision_id_decisions_id_fk": { + "name": "decision_references_target_decision_id_decisions_id_fk", + "tableFrom": "decision_references", + "tableTo": "decisions", + "columnsFrom": [ + "target_decision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.decisions": { + "name": "decisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "decision_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "case_reference": { + "name": "case_reference", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "decision_date": { + "name": "decision_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "court": { + "name": "court", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tribunal_id": { + "name": "tribunal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chamber": { + "name": "chamber", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "headnote": { + "name": "headnote", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenor": { + "name": "tenor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "facts": { + "name": "facts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reasoning": { + "name": "reasoning", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "full_text": { + "name": "full_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domains": { + "name": "domains", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "keywords": { + "name": "keywords", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "publication_source": { + "name": "publication_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "is_anonymized": { + "name": "is_anonymized", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "decisions_type_idx": { + "name": "decisions_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decisions_date_idx": { + "name": "decisions_date_idx", + "columns": [ + { + "expression": "decision_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decisions_court_idx": { + "name": "decisions_court_idx", + "columns": [ + { + "expression": "court", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decisions_tenant_idx": { + "name": "decisions_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decisions_case_ref_idx": { + "name": "decisions_case_ref_idx", + "columns": [ + { + "expression": "case_reference", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decisions_tenant_id_tenants_id_fk": { + "name": "decisions_tenant_id_tenants_id_fk", + "tableFrom": "decisions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decisions_tribunal_id_arbitration_tribunals_id_fk": { + "name": "decisions_tribunal_id_arbitration_tribunals_id_fk", + "tableFrom": "decisions", + "tableTo": "arbitration_tribunals", + "columnsFrom": [ + "tribunal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.norm_instruments": { + "name": "norm_instruments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "norm_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source_rank": { + "name": "source_rank", + "type": "source_rank", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "abbreviation": { + "name": "abbreviation", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "full_title": { + "name": "full_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enacted_at": { + "name": "enacted_at", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "issuing_body": { + "name": "issuing_body", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "citation": { + "name": "citation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "norm_instruments_type_idx": { + "name": "norm_instruments_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "norm_instruments_tenant_idx": { + "name": "norm_instruments_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "norm_instruments_tenant_id_tenants_id_fk": { + "name": "norm_instruments_tenant_id_tenants_id_fk", + "tableFrom": "norm_instruments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.norm_references": { + "name": "norm_references", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "source_norm_id": { + "name": "source_norm_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_norm_id": { + "name": "target_norm_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "reference_type": { + "name": "reference_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'verweist_auf'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "norm_refs_unique_idx": { + "name": "norm_refs_unique_idx", + "columns": [ + { + "expression": "source_norm_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_norm_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reference_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "norm_references_source_norm_id_norms_id_fk": { + "name": "norm_references_source_norm_id_norms_id_fk", + "tableFrom": "norm_references", + "tableTo": "norms", + "columnsFrom": [ + "source_norm_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "norm_references_target_norm_id_norms_id_fk": { + "name": "norm_references_target_norm_id_norms_id_fk", + "tableFrom": "norm_references", + "tableTo": "norms", + "columnsFrom": [ + "target_norm_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.norms": { + "name": "norms", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "instrument_id": { + "name": "instrument_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "paragraph": { + "name": "paragraph", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "subsection": { + "name": "subsection", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "valid_from": { + "name": "valid_from", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "valid_to": { + "name": "valid_to", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "previous_version_id": { + "name": "previous_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "version_number": { + "name": "version_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "domains": { + "name": "domains", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "norms_instrument_idx": { + "name": "norms_instrument_idx", + "columns": [ + { + "expression": "instrument_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "norms_paragraph_idx": { + "name": "norms_paragraph_idx", + "columns": [ + { + "expression": "instrument_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "paragraph", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "norms_valid_from_idx": { + "name": "norms_valid_from_idx", + "columns": [ + { + "expression": "valid_from", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "norms_tenant_idx": { + "name": "norms_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "norms_tenant_id_tenants_id_fk": { + "name": "norms_tenant_id_tenants_id_fk", + "tableFrom": "norms", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "norms_instrument_id_norm_instruments_id_fk": { + "name": "norms_instrument_id_norm_instruments_id_fk", + "tableFrom": "norms", + "tableTo": "norm_instruments", + "columnsFrom": [ + "instrument_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "norms_previous_version_id_norms_id_fk": { + "name": "norms_previous_version_id_norms_id_fk", + "tableFrom": "norms", + "tableTo": "norms", + "columnsFrom": [ + "previous_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.nv_buehne_fachgruppen": { + "name": "nv_buehne_fachgruppen", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "abbreviation": { + "name": "abbreviation", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "section_ref": { + "name": "section_ref", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "special_provisions": { + "name": "special_provisions", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tenants": { + "name": "tenants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "retention_days": { + "name": "retention_days", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3650 + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tenants_slug_unique": { + "name": "tenants_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "user_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'viewer'" + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified_at": { + "name": "email_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_login_at": { + "name": "last_login_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_tenant_email_idx": { + "name": "users_tenant_email_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_tenant_id_tenants_id_fk": { + "name": "users_tenant_id_tenants_id_fk", + "tableFrom": "users", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.analysis_mode": { + "name": "analysis_mode", + "schema": "public", + "values": [ + "gutachten", + "entscheidung", + "vergleich", + "risiko" + ] + }, + "public.analysis_status": { + "name": "analysis_status", + "schema": "public", + "values": [ + "draft", + "in_progress", + "completed", + "archived" + ] + }, + "public.decision_type": { + "name": "decision_type", + "schema": "public", + "values": [ + "schiedsspruch", + "urteil", + "beschluss", + "vergleich", + "einstweilige_verfuegung" + ] + }, + "public.legal_domain": { + "name": "legal_domain", + "schema": "public", + "values": [ + "buehnenrecht", + "arbeitsrecht", + "tarifrecht", + "urheberrecht", + "sozialrecht", + "vertragsrecht", + "prozessrecht" + ] + }, + "public.norm_type": { + "name": "norm_type", + "schema": "public", + "values": [ + "gesetz", + "tarifvertrag", + "schiedsordnung", + "verordnung", + "satzung", + "richtlinie" + ] + }, + "public.source_rank": { + "name": "source_rank", + "schema": "public", + "values": [ + "gesetz", + "tarif", + "schiedsordnung", + "praxis", + "kommentar" + ] + }, + "public.user_role": { + "name": "user_role", + "schema": "public", + "values": [ + "admin", + "attorney", + "paralegal", + "viewer" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..7a6618b --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1775682934077, + "tag": "0000_peaceful_amazoness", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index e7c9af1..c3ef9e6 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -3,10 +3,11 @@ import { drizzle } from 'drizzle-orm/node-postgres'; import { Pool } from 'pg'; +import * as schema from './schema'; const pool = new Pool({ connectionString: process.env.DATABASE_URL, }); -export const db = drizzle(pool); +export const db = drizzle(pool, { schema }); export { pool }; diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index b5f2f7f..841b72a 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -1,51 +1,526 @@ -// LegalAI Database Schema -// PostgreSQL with Row-Level Security for tenant isolation -// Full data model will be designed by Legal Engineer (AIIA subtask) +import { + pgTable, + pgEnum, + uuid, + text, + varchar, + timestamp, + date, + boolean, + integer, + jsonb, + index, + uniqueIndex, + primaryKey, +} from "drizzle-orm/pg-core"; +import { relations } from "drizzle-orm"; -import { pgTable, uuid, text, timestamp, varchar } from 'drizzle-orm/pg-core'; +// ============================================================ +// Enums +// ============================================================ -// Base tenant table — all tenant-scoped tables reference this -export const tenants = pgTable('tenants', { - id: uuid('id').primaryKey().defaultRandom(), - name: varchar('name', { length: 255 }).notNull(), - createdAt: timestamp('created_at').defaultNow().notNull(), - updatedAt: timestamp('updated_at').defaultNow().notNull(), +/** Quellenrang — legal source hierarchy for norm precedence */ +export const sourceRankEnum = pgEnum("source_rank", [ + "gesetz", // Gesetz (statutory law) — highest + "tarif", // Tarifvertrag (collective agreement, e.g. NV Bühne) + "schiedsordnung", // Schiedsordnung (arbitration rules, e.g. BSchGO) + "praxis", // Bühnenpraxis / Gewohnheitsrecht + "kommentar", // Kommentarliteratur / doctrine +]); + +/** Type of legal norm document */ +export const normTypeEnum = pgEnum("norm_type", [ + "gesetz", // Statute (e.g. ArbGG, KSchG, BGB) + "tarifvertrag", // Collective agreement (e.g. NV Bühne) + "schiedsordnung", // Arbitration rules (e.g. BSchGO) + "verordnung", // Regulation / decree + "satzung", // Bylaws (e.g. GDBA-Satzung) + "richtlinie", // Guideline / directive +]); + +/** Type of court/arbitration decision */ +export const decisionTypeEnum = pgEnum("decision_type", [ + "schiedsspruch", // Arbitration award (Bühnenschiedsgericht) + "urteil", // Court judgment + "beschluss", // Court order / resolution + "vergleich", // Settlement + "einstweilige_verfuegung", // Interim injunction +]); + +/** Legal domain / Rechtsgebiet */ +export const legalDomainEnum = pgEnum("legal_domain", [ + "buehnenrecht", // Stage law (core domain) + "arbeitsrecht", // Employment law + "tarifrecht", // Collective bargaining law + "urheberrecht", // Copyright law + "sozialrecht", // Social security law + "vertragsrecht", // Contract law + "prozessrecht", // Procedural law +]); + +/** Role of a user within a tenant */ +export const userRoleEnum = pgEnum("user_role", [ + "admin", // Tenant administrator (the attorney) + "attorney", // Licensed attorney + "paralegal", // Paralegal / Referendar + "viewer", // Read-only access +]); + +/** Status of an analysis */ +export const analysisStatusEnum = pgEnum("analysis_status", [ + "draft", + "in_progress", + "completed", + "archived", +]); + +/** AI analysis mode */ +export const analysisModeEnum = pgEnum("analysis_mode", [ + "gutachten", // Structured legal opinion + "entscheidung", // Decision proposal based on precedent + "vergleich", // Comparative analysis of norms/decisions + "risiko", // Risk assessment with probability +]); + +// ============================================================ +// Core tables: Tenants & Users +// ============================================================ + +/** Mandanten — law firm or attorney office as tenant */ +export const tenants = pgTable("tenants", { + id: uuid("id").primaryKey().defaultRandom(), + name: varchar("name", { length: 255 }).notNull(), + slug: varchar("slug", { length: 100 }).notNull().unique(), + /** Kanzlei details */ + address: text("address"), + phone: varchar("phone", { length: 50 }), + email: varchar("email", { length: 255 }), + /** DSGVO: data retention policy in days (default 10 years / 3650 days) */ + retentionDays: integer("retention_days").default(3650), + settings: jsonb("settings").$type>(), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), }); -// Users table with tenant association -export const users = pgTable('users', { - id: uuid('id').primaryKey().defaultRandom(), - tenantId: uuid('tenant_id').references(() => tenants.id).notNull(), - email: varchar('email', { length: 255 }).notNull().unique(), - name: varchar('name', { length: 255 }), - role: varchar('role', { length: 50 }).notNull().default('user'), - createdAt: timestamp('created_at').defaultNow().notNull(), - updatedAt: timestamp('updated_at').defaultNow().notNull(), +/** Nutzer — users belong to a tenant */ +export const users = pgTable( + "users", + { + id: uuid("id").primaryKey().defaultRandom(), + tenantId: uuid("tenant_id").notNull().references(() => tenants.id, { onDelete: "cascade" }), + email: varchar("email", { length: 255 }).notNull(), + name: varchar("name", { length: 255 }).notNull(), + role: userRoleEnum("role").notNull().default("viewer"), + passwordHash: text("password_hash"), + emailVerifiedAt: timestamp("email_verified_at", { withTimezone: true }), + lastLoginAt: timestamp("last_login_at", { withTimezone: true }), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), + }, + (t) => [ + uniqueIndex("users_tenant_email_idx").on(t.tenantId, t.email), + ], +); + +// ============================================================ +// Normen (Legal norms with temporal versioning) +// ============================================================ + +/** + * Normenwerke — top-level legal instruments (e.g. "NV Bühne", "ArbGG", "BSchGO"). + * Groups individual norm paragraphs. + */ +export const normInstruments = pgTable( + "norm_instruments", + { + id: uuid("id").primaryKey().defaultRandom(), + tenantId: uuid("tenant_id").references(() => tenants.id, { onDelete: "cascade" }), + /** null tenantId = system-wide (shared reference data) */ + type: normTypeEnum("type").notNull(), + sourceRank: sourceRankEnum("source_rank").notNull(), + /** Official short name, e.g. "NV Bühne", "ArbGG" */ + abbreviation: varchar("abbreviation", { length: 50 }).notNull(), + /** Full title */ + fullTitle: text("full_title").notNull(), + /** Date of enactment / Inkrafttreten */ + enactedAt: date("enacted_at"), + /** Issuing body, e.g. "Deutscher Bundestag", "GDBA/DBV" */ + issuingBody: varchar("issuing_body", { length: 255 }), + /** Official gazette citation / Fundstelle */ + citation: text("citation"), + metadata: jsonb("metadata").$type>(), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), + }, + (t) => [ + index("norm_instruments_type_idx").on(t.type), + index("norm_instruments_tenant_idx").on(t.tenantId), + ], +); + +/** + * Normen — individual norm provisions (paragraphs/articles) with temporal versioning. + * Each row is an immutable version; changes create a new row with previousVersionId. + */ +export const norms = pgTable( + "norms", + { + id: uuid("id").primaryKey().defaultRandom(), + tenantId: uuid("tenant_id").references(() => tenants.id, { onDelete: "cascade" }), + instrumentId: uuid("instrument_id").notNull().references(() => normInstruments.id, { onDelete: "cascade" }), + /** Paragraph/Article designation, e.g. "§ 53", "Art. 12" */ + paragraph: varchar("paragraph", { length: 50 }).notNull(), + /** Optional subsection, e.g. "Abs. 2 S. 3" */ + subsection: varchar("subsection", { length: 100 }), + /** Title/heading of the provision */ + title: varchar("title", { length: 500 }), + /** Full text of the provision (this version) */ + body: text("body").notNull(), + /** Temporal validity window */ + validFrom: date("valid_from").notNull(), + validTo: date("valid_to"), + /** Immutable versioning chain */ + previousVersionId: uuid("previous_version_id").references((): any => norms.id), + /** Version number within this paragraph lineage */ + versionNumber: integer("version_number").notNull().default(1), + /** Legal domains this norm covers */ + domains: jsonb("domains").$type().default([]), + /** Notes / editorial remarks */ + notes: text("notes"), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + }, + (t) => [ + index("norms_instrument_idx").on(t.instrumentId), + index("norms_paragraph_idx").on(t.instrumentId, t.paragraph), + index("norms_valid_from_idx").on(t.validFrom), + index("norms_tenant_idx").on(t.tenantId), + ], +); + +/** + * Norm cross-references — directed links between norms (e.g. NV Bühne § 53 -> ArbGG § 110). + */ +export const normReferences = pgTable( + "norm_references", + { + id: uuid("id").primaryKey().defaultRandom(), + sourceNormId: uuid("source_norm_id").notNull().references(() => norms.id, { onDelete: "cascade" }), + targetNormId: uuid("target_norm_id").notNull().references(() => norms.id, { onDelete: "cascade" }), + /** Type of reference relationship */ + referenceType: varchar("reference_type", { length: 50 }).notNull().default("verweist_auf"), + /** Optional description of the relationship */ + description: text("description"), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + }, + (t) => [ + uniqueIndex("norm_refs_unique_idx").on(t.sourceNormId, t.targetNormId, t.referenceType), + ], +); + +// ============================================================ +// NV Bühne specific: Collective agreement structures +// ============================================================ + +/** + * NV Bühne Fachgruppen — professional groups under the NV Bühne collective agreement. + * (Solo, Chor, Tanz, Bühnentechnik, etc.) + */ +export const nvBuehneFachgruppen = pgTable("nv_buehne_fachgruppen", { + id: uuid("id").primaryKey().defaultRandom(), + name: varchar("name", { length: 255 }).notNull(), + abbreviation: varchar("abbreviation", { length: 20 }), + /** NV Bühne section reference */ + sectionRef: varchar("section_ref", { length: 100 }), + description: text("description"), + /** Applicable special provisions as JSONB array */ + specialProvisions: jsonb("special_provisions").$type().default([]), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), }); -// Placeholder tables — full schema will be defined by Legal Engineer -// See subtask: Datenmodell für Normen und Entscheidungen +// ============================================================ +// BSchGO specific: Arbitration procedure structures +// ============================================================ -export const norms = pgTable('norms', { - id: uuid('id').primaryKey().defaultRandom(), - tenantId: uuid('tenant_id').references(() => tenants.id).notNull(), - title: varchar('title', { length: 500 }).notNull(), - body: text('body'), - quellenRang: varchar('quellen_rang', { length: 50 }).notNull(), - validFrom: timestamp('valid_from').notNull(), - validTo: timestamp('valid_to'), - createdAt: timestamp('created_at').defaultNow().notNull(), - updatedAt: timestamp('updated_at').defaultNow().notNull(), +/** + * Bühnenschiedsgerichte — arbitration tribunals under BSchGO. + * Tracks the specific tribunals (Bezirk, Bundes) and their composition. + */ +export const arbitrationTribunals = pgTable("arbitration_tribunals", { + id: uuid("id").primaryKey().defaultRandom(), + name: varchar("name", { length: 255 }).notNull(), + /** bezirk = regional, bund = federal */ + level: varchar("level", { length: 20 }).notNull(), + /** Seat / location of the tribunal */ + seat: varchar("seat", { length: 255 }), + /** Applicable BSchGO version/section */ + bschgoRef: varchar("bschgo_ref", { length: 100 }), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), }); -export const decisions = pgTable('decisions', { - id: uuid('id').primaryKey().defaultRandom(), - tenantId: uuid('tenant_id').references(() => tenants.id).notNull(), - title: varchar('title', { length: 500 }).notNull(), - body: text('body'), - decisionDate: timestamp('decision_date'), - aktenzeichen: varchar('aktenzeichen', { length: 100 }), - gremium: varchar('gremium', { length: 255 }), - createdAt: timestamp('created_at').defaultNow().notNull(), - updatedAt: timestamp('updated_at').defaultNow().notNull(), -}); +// ============================================================ +// Entscheidungen (Decisions / Rulings) +// ============================================================ + +/** + * Entscheidungen — court judgments, arbitration awards, settlements. + * Core entity for the decision database. + */ +export const decisions = pgTable( + "decisions", + { + id: uuid("id").primaryKey().defaultRandom(), + tenantId: uuid("tenant_id").references(() => tenants.id, { onDelete: "cascade" }), + type: decisionTypeEnum("type").notNull(), + /** Aktenzeichen / case reference number */ + caseReference: varchar("case_reference", { length: 100 }), + /** Date of the decision */ + decisionDate: date("decision_date").notNull(), + /** Court or tribunal that issued the decision */ + court: varchar("court", { length: 255 }).notNull(), + /** Arbitration tribunal reference (for Schiedssprüche) */ + tribunalId: uuid("tribunal_id").references(() => arbitrationTribunals.id), + /** Chamber / Kammer / Senat */ + chamber: varchar("chamber", { length: 100 }), + /** Leitsatz — headnote / guiding principle */ + headnote: text("headnote"), + /** Tenor — operative part of the decision */ + tenor: text("tenor"), + /** Tatbestand — statement of facts */ + facts: text("facts"), + /** Entscheidungsgründe — reasoning */ + reasoning: text("reasoning"), + /** Full text (if available) */ + fullText: text("full_text"), + /** Legal domains covered */ + domains: jsonb("domains").$type().default([]), + /** Keywords / Schlagworte for search */ + keywords: jsonb("keywords").$type().default([]), + /** Publication source / Fundstelle */ + publicationSource: text("publication_source"), + /** Whether the decision is published / anonymized */ + isPublished: boolean("is_published").default(false), + /** DSGVO: anonymization status */ + isAnonymized: boolean("is_anonymized").default(false), + metadata: jsonb("metadata").$type>(), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), + }, + (t) => [ + index("decisions_type_idx").on(t.type), + index("decisions_date_idx").on(t.decisionDate), + index("decisions_court_idx").on(t.court), + index("decisions_tenant_idx").on(t.tenantId), + index("decisions_case_ref_idx").on(t.caseReference), + ], +); + +/** + * Decision-Norm links — which norms were applied in a decision. + * Captures the norm version valid at the time of the decision (Stichtag). + */ +export const decisionNorms = pgTable( + "decision_norms", + { + id: uuid("id").primaryKey().defaultRandom(), + decisionId: uuid("decision_id").notNull().references(() => decisions.id, { onDelete: "cascade" }), + normId: uuid("norm_id").notNull().references(() => norms.id, { onDelete: "cascade" }), + /** How the norm was applied: "angewendet", "zitiert", "ausgelegt", "verworfen" */ + applicationType: varchar("application_type", { length: 50 }).notNull().default("angewendet"), + /** Specific passage in the decision referencing this norm */ + passage: text("passage"), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + }, + (t) => [ + uniqueIndex("decision_norms_unique_idx").on(t.decisionId, t.normId), + ], +); + +/** + * Decision cross-references — links between decisions (Präzedenzfälle, Abweichungen). + */ +export const decisionReferences = pgTable( + "decision_references", + { + id: uuid("id").primaryKey().defaultRandom(), + sourceDecisionId: uuid("source_decision_id").notNull().references(() => decisions.id, { onDelete: "cascade" }), + targetDecisionId: uuid("target_decision_id").notNull().references(() => decisions.id, { onDelete: "cascade" }), + /** Relationship: "bestaetigt", "abweicht", "aufgehoben", "zitiert" */ + referenceType: varchar("reference_type", { length: 50 }).notNull(), + description: text("description"), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + }, + (t) => [ + uniqueIndex("decision_refs_unique_idx").on(t.sourceDecisionId, t.targetDecisionId, t.referenceType), + ], +); + +// ============================================================ +// Fälle (Cases) & Analysen (Analyses) +// ============================================================ + +/** Fälle — client cases managed by the attorney */ +export const cases = pgTable( + "cases", + { + id: uuid("id").primaryKey().defaultRandom(), + tenantId: uuid("tenant_id").notNull().references(() => tenants.id, { onDelete: "cascade" }), + /** Internal case number */ + caseNumber: varchar("case_number", { length: 100 }).notNull(), + title: varchar("title", { length: 500 }).notNull(), + description: text("description"), + /** Client (Mandant) name — may be pseudonymized */ + clientName: varchar("client_name", { length: 255 }), + /** Opposing party */ + opposingParty: varchar("opposing_party", { length: 255 }), + /** Associated theater/venue */ + venue: varchar("venue", { length: 255 }), + /** NV Bühne Fachgruppe if applicable */ + fachgruppeId: uuid("fachgruppe_id").references(() => nvBuehneFachgruppen.id), + domains: jsonb("domains").$type().default([]), + status: varchar("status", { length: 50 }).notNull().default("active"), + /** Key dates */ + filingDate: date("filing_date"), + hearingDate: date("hearing_date"), + closedAt: timestamp("closed_at", { withTimezone: true }), + metadata: jsonb("metadata").$type>(), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), + }, + (t) => [ + uniqueIndex("cases_tenant_number_idx").on(t.tenantId, t.caseNumber), + index("cases_tenant_idx").on(t.tenantId), + index("cases_status_idx").on(t.status), + ], +); + +/** Analysen — AI-assisted legal analyses linked to a case */ +export const analyses = pgTable( + "analyses", + { + id: uuid("id").primaryKey().defaultRandom(), + tenantId: uuid("tenant_id").notNull().references(() => tenants.id, { onDelete: "cascade" }), + caseId: uuid("case_id").references(() => cases.id, { onDelete: "set null" }), + userId: uuid("user_id").notNull().references(() => users.id), + mode: analysisModeEnum("mode").notNull(), + status: analysisStatusEnum("status").notNull().default("draft"), + title: varchar("title", { length: 500 }).notNull(), + /** Input query / legal question */ + query: text("query").notNull(), + /** AI-generated analysis result (markdown) */ + result: text("result"), + /** Source references cited in the analysis */ + sources: jsonb("sources").$type<{ + normIds: string[]; + decisionIds: string[]; + otherSources: string[]; + }>(), + /** AI provider and model used */ + aiProvider: varchar("ai_provider", { length: 50 }), + aiModel: varchar("ai_model", { length: 100 }), + /** Token usage for billing/audit */ + tokenUsage: jsonb("token_usage").$type<{ + inputTokens: number; + outputTokens: number; + }>(), + metadata: jsonb("metadata").$type>(), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), + }, + (t) => [ + index("analyses_tenant_idx").on(t.tenantId), + index("analyses_case_idx").on(t.caseId), + index("analyses_mode_idx").on(t.mode), + ], +); + +// ============================================================ +// DSGVO / Audit +// ============================================================ + +/** Audit log — tracks data access for DSGVO compliance */ +export const auditLog = pgTable( + "audit_log", + { + id: uuid("id").primaryKey().defaultRandom(), + tenantId: uuid("tenant_id").notNull().references(() => tenants.id, { onDelete: "cascade" }), + userId: uuid("user_id").references(() => users.id), + action: varchar("action", { length: 100 }).notNull(), + entityType: varchar("entity_type", { length: 100 }).notNull(), + entityId: uuid("entity_id"), + /** What changed (for update/delete) */ + details: jsonb("details").$type>(), + ipAddress: varchar("ip_address", { length: 45 }), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + }, + (t) => [ + index("audit_log_tenant_idx").on(t.tenantId), + index("audit_log_entity_idx").on(t.entityType, t.entityId), + index("audit_log_created_idx").on(t.createdAt), + ], +); + +// ============================================================ +// Relations (Drizzle ORM relation definitions) +// ============================================================ + +export const tenantsRelations = relations(tenants, ({ many }) => ({ + users: many(users), + cases: many(cases), + analyses: many(analyses), + decisions: many(decisions), + normInstruments: many(normInstruments), +})); + +export const usersRelations = relations(users, ({ one, many }) => ({ + tenant: one(tenants, { fields: [users.tenantId], references: [tenants.id] }), + analyses: many(analyses), +})); + +export const normInstrumentsRelations = relations(normInstruments, ({ one, many }) => ({ + tenant: one(tenants, { fields: [normInstruments.tenantId], references: [tenants.id] }), + norms: many(norms), +})); + +export const normsRelations = relations(norms, ({ one, many }) => ({ + instrument: one(normInstruments, { fields: [norms.instrumentId], references: [normInstruments.id] }), + tenant: one(tenants, { fields: [norms.tenantId], references: [tenants.id] }), + previousVersion: one(norms, { fields: [norms.previousVersionId], references: [norms.id], relationName: "normVersions" }), + outgoingReferences: many(normReferences, { relationName: "sourceNorm" }), + incomingReferences: many(normReferences, { relationName: "targetNorm" }), + decisionNorms: many(decisionNorms), +})); + +export const normReferencesRelations = relations(normReferences, ({ one }) => ({ + sourceNorm: one(norms, { fields: [normReferences.sourceNormId], references: [norms.id], relationName: "sourceNorm" }), + targetNorm: one(norms, { fields: [normReferences.targetNormId], references: [norms.id], relationName: "targetNorm" }), +})); + +export const decisionsRelations = relations(decisions, ({ one, many }) => ({ + tenant: one(tenants, { fields: [decisions.tenantId], references: [tenants.id] }), + tribunal: one(arbitrationTribunals, { fields: [decisions.tribunalId], references: [arbitrationTribunals.id] }), + appliedNorms: many(decisionNorms), + outgoingReferences: many(decisionReferences, { relationName: "sourceDecision" }), + incomingReferences: many(decisionReferences, { relationName: "targetDecision" }), +})); + +export const decisionNormsRelations = relations(decisionNorms, ({ one }) => ({ + decision: one(decisions, { fields: [decisionNorms.decisionId], references: [decisions.id] }), + norm: one(norms, { fields: [decisionNorms.normId], references: [norms.id] }), +})); + +export const decisionReferencesRelations = relations(decisionReferences, ({ one }) => ({ + sourceDecision: one(decisions, { fields: [decisionReferences.sourceDecisionId], references: [decisions.id], relationName: "sourceDecision" }), + targetDecision: one(decisions, { fields: [decisionReferences.targetDecisionId], references: [decisions.id], relationName: "targetDecision" }), +})); + +export const casesRelations = relations(cases, ({ one, many }) => ({ + tenant: one(tenants, { fields: [cases.tenantId], references: [tenants.id] }), + fachgruppe: one(nvBuehneFachgruppen, { fields: [cases.fachgruppeId], references: [nvBuehneFachgruppen.id] }), + analyses: many(analyses), +})); + +export const analysesRelations = relations(analyses, ({ one }) => ({ + tenant: one(tenants, { fields: [analyses.tenantId], references: [tenants.id] }), + case: one(cases, { fields: [analyses.caseId], references: [cases.id] }), + user: one(users, { fields: [analyses.userId], references: [users.id] }), +}));