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:
@@ -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