// R-P5 (P5) Apps-Engine & Integrations-Framework — aligned to ENTITY-MODEL // Domäne 4 (AD-20 Pivot-Vorrang). Replaces the stock boilerplate models. // // AD-21: self-contained schema. `tenantId` / `createdByUserId` are BARE String // scalars (no `@relation` to Tenant/User) → zero merge-conflict surface against // core.prisma/accounts.prisma. Intra-module relations (App→DataConnection→ // AppSyncRun→Log/Item) stay inside this file. // // `App` is GLOBAL (a definition/catalog table, no tenantId, no RLS). Everything // tenant-facing (DataConnection + sync rows) is tenant-scoped + RLS // (prisma/rls/apps-engine-isolation.sql, deferred activation like the other // modules). Credentials are stored ENVELOPE-ENCRYPTED via the per-tenant DEK // (R-P5 Phase 1: packages/db/src/crypto/tenant-envelope.server.ts), never plain. // // Scope (AD-26 recut): connection types oauth2 / api_key / rest_api only have a // v1 flow; `windows_client` (KWP) stays in the datamodel but has NO v1 UI/logic. // Concrete app adapters (Gmail/Outlook/AirCall/Mollie) belong to P14/P15/P16/P20, // not here — P5 ships only the generic framework + the sample-data-sync adapter. enum AppCategory { erp communication calendar signature payment ki } enum AppConnectionType { oauth2 api_key rest_api windows_client } enum DataConnectionStatus { pending active degraded broken revoked } enum AppSyncKind { manual scheduled webhook } enum AppSyncStatus { pending running completed partial failed cancelled } enum AppSyncLogSeverity { info success warning error } enum AppSyncItemOperation { created updated deleted skipped } enum AppSyncItemStatus { pending synced failed skipped } enum AppEntityBindingState { active quarantined missing ignored unlinked } /// Global catalog definition of an integratable app (no tenant, no RLS). The /// `connectionSchema` drives the auto-generated credential form; `connectionType` /// selects the connect flow. New apps are registered by the build-time adapter /// scan + this catalog (drift-check warns in the platform admin). model App { id String @id @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt key String @unique title String category AppCategory connectionType AppConnectionType connectionSchema Json @default("{}") phase String? icon String? isActive Boolean @default(true) @@index([category]) } /// A tenant's connection to an App. Multiple per (tenant, app) with a user /// `label` (e.g. three Gmail mailboxes). Credentials live encrypted in /// `credentialsCiphertext`. Disconnect sets `connectionStatus = revoked` (no /// hard-delete → sync history preserved). Soft-delete via `archivedAt`. model DataConnection { id String @id @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt tenantId String // The app's stable catalog `key` (bare scalar per AD-21; matches the adapter // registry + INTEGRATION_CATALOG). App is a global catalog, not an FK target. appId String label String // AES-256-GCM per-tenant DEK envelope (AD-8). Never selected into list VMs. credentialsCiphertext String? connectionStatus DataConnectionStatus @default(pending) lastHealthCheck DateTime? // i18n key describing the current health state (Klartext, M002/M003). healthMessageKey String? tokenExpiresAt DateTime? createdByUserId String? isDemoData Boolean @default(false) archivedAt DateTime? syncRuns AppSyncRun[] entityBindings AppEntityBinding[] @@index([tenantId, appId, archivedAt]) @@index([tenantId, connectionStatus]) @@index([tenantId, tokenExpiresAt]) } /// One sync run for a DataConnection. `summaryText` is the Klartext summary. model AppSyncRun { id String @id @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt tenantId String dataConnectionId String kind AppSyncKind @default(manual) status AppSyncStatus @default(pending) itemsTotal Int @default(0) itemsSucceeded Int @default(0) itemsFailed Int @default(0) summaryText String? startedAt DateTime? completedAt DateTime? createdByUserId String? dataConnection DataConnection @relation(fields: [dataConnectionId], references: [id], onDelete: Cascade) logs AppSyncRunLog[] items AppSyncItem[] @@index([tenantId, dataConnectionId, createdAt(sort: Desc)]) @@index([tenantId, status]) } /// A log line of a sync run (Klartext `messageText` + optional `techDetails`). model AppSyncRunLog { id String @id @default(cuid()) createdAt DateTime @default(now()) tenantId String syncRunId String severity AppSyncLogSeverity @default(info) messageText String techDetails String? syncRun AppSyncRun @relation(fields: [syncRunId], references: [id], onDelete: Cascade) @@index([tenantId, syncRunId]) } /// A single synced object: maps a HiveGrid entity (`entityType`/`entityId`) to an /// external system id (`externalId`). Polymorphic. model AppSyncItem { id String @id @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt tenantId String syncRunId String entityType String entityId String? externalId String operation AppSyncItemOperation @default(created) status AppSyncItemStatus @default(pending) errorText String? syncRun AppSyncRun @relation(fields: [syncRunId], references: [id], onDelete: Cascade) @@index([tenantId, syncRunId]) @@index([tenantId, entityType, entityId]) @@index([tenantId, externalId]) } /// Durable provider-neutral identity mapping. AppSyncItem remains the history /// of one run; this row survives runs, reconnects and process restarts. model AppEntityBinding { id String @id @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt tenantId String dataConnectionId String externalEntityType String localEntityType String externalId String localId String state AppEntityBindingState @default(active) normalizedSnapshot Json fingerprint String overwrittenLocalSnapshot Json? revision Int @default(1) lastSeenAt DateTime? lastSyncedAt DateTime? missingSince DateTime? unlinkedAt DateTime? dataConnection DataConnection @relation(fields: [dataConnectionId], references: [id], onDelete: Cascade) @@unique([tenantId, dataConnectionId, externalEntityType, externalId]) @@unique([tenantId, dataConnectionId, localEntityType, localId]) @@index([tenantId, dataConnectionId, state, lastSyncedAt]) @@index([tenantId, localEntityType, localId, state]) }