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:
231
drizzle/0000_peaceful_amazoness.sql
Normal file
231
drizzle/0000_peaceful_amazoness.sql
Normal 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");
|
||||
122
drizzle/0001_rls_policies.sql
Normal file
122
drizzle/0001_rls_policies.sql
Normal 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;
|
||||
1860
drizzle/meta/0000_snapshot.json
Normal file
1860
drizzle/meta/0000_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
13
drizzle/meta/_journal.json
Normal file
13
drizzle/meta/_journal.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1775682934077,
|
||||
"tag": "0000_peaceful_amazoness",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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 };
|
||||
|
||||
@@ -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<Record<string, unknown>>(),
|
||||
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<Record<string, unknown>>(),
|
||||
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<string[]>().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<string[]>().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<string[]>().default([]),
|
||||
/** Keywords / Schlagworte for search */
|
||||
keywords: jsonb("keywords").$type<string[]>().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<Record<string, unknown>>(),
|
||||
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<string[]>().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<Record<string, unknown>>(),
|
||||
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<Record<string, unknown>>(),
|
||||
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<Record<string, unknown>>(),
|
||||
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] }),
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user