forked from bai/curriculum-project-hub
feat: add organization tenant model
This commit is contained in:
+146
-70
@@ -25,6 +25,54 @@ datasource db {
|
||||
|
||||
// --- Platform identity ---------------------------------------------------
|
||||
|
||||
/// ADR-0020: SaaS tenant root. Every project and team belongs to exactly one
|
||||
/// organization; platform operator access remains outside project roles.
|
||||
model Organization {
|
||||
id String @id @default(cuid())
|
||||
slug String @unique
|
||||
name String
|
||||
status OrganizationStatus @default(ACTIVE)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
memberships OrganizationMembership[]
|
||||
projects Project[]
|
||||
teams Team[]
|
||||
externalDirectoryConnections ExternalDirectoryConnection[]
|
||||
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
enum OrganizationStatus {
|
||||
ACTIVE
|
||||
SUSPENDED
|
||||
ARCHIVED
|
||||
}
|
||||
|
||||
/// Org-scoped platform role. Distinct from project PermissionRole and from
|
||||
/// global PlatformRoleAssignment, which is reserved for SaaS/platform control.
|
||||
model OrganizationMembership {
|
||||
id String @id @default(cuid())
|
||||
organizationId String
|
||||
userId String
|
||||
role OrganizationMemberRole @default(MEMBER)
|
||||
createdAt DateTime @default(now())
|
||||
revokedAt DateTime?
|
||||
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([organizationId, userId, revokedAt])
|
||||
@@index([organizationId, revokedAt])
|
||||
@@index([userId, revokedAt])
|
||||
}
|
||||
|
||||
enum OrganizationMemberRole {
|
||||
OWNER
|
||||
ADMIN
|
||||
MEMBER
|
||||
}
|
||||
|
||||
/// A Feishu user known to the Hub. principal sub-typology is OPEN (ADR-0004).
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
@@ -34,16 +82,17 @@ model User {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
platformRoles PlatformRoleAssignment[]
|
||||
createdProjects Project[] @relation("projectCreator")
|
||||
requestedRuns AgentRun[] @relation("runRequester")
|
||||
heldLocks ProjectAgentLock[] @relation("lockHolder")
|
||||
feishuBindings ProjectGroupBinding[] @relation("bindingCreator")
|
||||
teamMemberships TeamMembership[]
|
||||
platformRoles PlatformRoleAssignment[]
|
||||
organizationMemberships OrganizationMembership[]
|
||||
createdProjects Project[] @relation("projectCreator")
|
||||
requestedRuns AgentRun[] @relation("runRequester")
|
||||
heldLocks ProjectAgentLock[] @relation("lockHolder")
|
||||
feishuBindings ProjectGroupBinding[] @relation("bindingCreator")
|
||||
teamMemberships TeamMembership[]
|
||||
externalPrincipalMemberships ExternalPrincipalMembership[]
|
||||
permissionGrants PermissionGrant[] @relation("grantCreator")
|
||||
roleTriggerGrants RoleTriggerGrant[] @relation("roleGrantCreator")
|
||||
auditEntries AuditEntry[] @relation("auditActor")
|
||||
permissionGrants PermissionGrant[] @relation("grantCreator")
|
||||
roleTriggerGrants RoleTriggerGrant[] @relation("roleGrantCreator")
|
||||
auditEntries AuditEntry[] @relation("auditActor")
|
||||
}
|
||||
|
||||
/// Platform-level role (admin/teacher). Distinct from ADR-0004 PermissionRole.
|
||||
@@ -80,27 +129,30 @@ enum PrincipalType {
|
||||
|
||||
/// Hub-managed teacher team. A team is a first-class permission principal.
|
||||
model Team {
|
||||
id String @id @default(cuid())
|
||||
slug String @unique
|
||||
name String
|
||||
description String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
archivedAt DateTime?
|
||||
id String @id @default(cuid())
|
||||
organizationId String
|
||||
slug String
|
||||
name String
|
||||
description String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
archivedAt DateTime?
|
||||
|
||||
memberships TeamMembership[]
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
memberships TeamMembership[]
|
||||
externalBindings TeamExternalBinding[]
|
||||
|
||||
@@index([archivedAt])
|
||||
@@index([organizationId, archivedAt])
|
||||
@@index([organizationId, slug, archivedAt])
|
||||
}
|
||||
|
||||
/// Direct Hub team membership. External Feishu groups can also map to teams via
|
||||
/// TeamExternalBinding; both sources are resolved at authorization time.
|
||||
model TeamMembership {
|
||||
id String @id @default(cuid())
|
||||
id String @id @default(cuid())
|
||||
teamId String
|
||||
userId String
|
||||
createdAt DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
revokedAt DateTime?
|
||||
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
@@ -129,27 +181,50 @@ model TeamExternalBinding {
|
||||
}
|
||||
|
||||
/// Locally synchronized Feishu external principal membership.
|
||||
model ExternalDirectoryConnection {
|
||||
id String @id @default(cuid())
|
||||
organizationId String
|
||||
provider String
|
||||
providerTenantId String?
|
||||
source String
|
||||
status String @default("ACTIVE")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
revokedAt DateTime?
|
||||
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
memberships ExternalPrincipalMembership[]
|
||||
|
||||
@@unique([organizationId, provider, source])
|
||||
@@index([organizationId, revokedAt])
|
||||
@@index([provider, providerTenantId])
|
||||
}
|
||||
|
||||
/// Locally synchronized Feishu external principal membership, scoped through
|
||||
/// an organization-owned ExternalDirectoryConnection.
|
||||
model ExternalPrincipalMembership {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
connectionId String
|
||||
principalType PrincipalType
|
||||
principalId String
|
||||
source String
|
||||
syncedAt DateTime @default(now())
|
||||
revokedAt DateTime?
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
connection ExternalDirectoryConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([userId, principalType, principalId, source, revokedAt])
|
||||
@@unique([userId, principalType, principalId, connectionId, revokedAt])
|
||||
@@index([userId, revokedAt])
|
||||
@@index([connectionId, revokedAt])
|
||||
@@index([principalType, principalId, revokedAt])
|
||||
@@index([source, syncedAt])
|
||||
}
|
||||
|
||||
// --- Project & Feishu binding (ADR-0001) ---------------------------------
|
||||
|
||||
model Project {
|
||||
id String @id @default(cuid())
|
||||
organizationId String
|
||||
name String
|
||||
workspaceDir String
|
||||
createdByUserId String?
|
||||
@@ -157,15 +232,17 @@ model Project {
|
||||
updatedAt DateTime @updatedAt
|
||||
archivedAt DateTime?
|
||||
|
||||
createdBy User? @relation("projectCreator", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||
groupBinding ProjectGroupBinding?
|
||||
agentSessions AgentSession[]
|
||||
agentRuns AgentRun[]
|
||||
agentLock ProjectAgentLock?
|
||||
roleTriggerGrants RoleTriggerGrant[] @relation("projectRoleGrants")
|
||||
auditEntries AuditEntry[] @relation("projectAudit")
|
||||
fileChanges AgentFileChange[] @relation("projectFileChanges")
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
createdBy User? @relation("projectCreator", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||
groupBinding ProjectGroupBinding?
|
||||
agentSessions AgentSession[]
|
||||
agentRuns AgentRun[]
|
||||
agentLock ProjectAgentLock?
|
||||
roleTriggerGrants RoleTriggerGrant[] @relation("projectRoleGrants")
|
||||
auditEntries AuditEntry[] @relation("projectAudit")
|
||||
fileChanges AgentFileChange[] @relation("projectFileChanges")
|
||||
|
||||
@@index([organizationId, archivedAt])
|
||||
@@index([archivedAt])
|
||||
}
|
||||
|
||||
@@ -206,8 +283,8 @@ model AgentSession {
|
||||
updatedAt DateTime @updatedAt
|
||||
archivedAt DateTime?
|
||||
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
runs AgentRun[]
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
runs AgentRun[]
|
||||
messages AgentMessage[] @relation("sessionMessages")
|
||||
|
||||
@@index([projectId, archivedAt])
|
||||
@@ -235,12 +312,12 @@ enum AgentEntrypoint {
|
||||
}
|
||||
|
||||
model AgentRun {
|
||||
id String @id @default(cuid())
|
||||
id String @id @default(cuid())
|
||||
projectId String
|
||||
sessionId String?
|
||||
requestedByUserId String?
|
||||
entrypoint AgentEntrypoint
|
||||
status AgentRunStatus @default(ACTIVE)
|
||||
status AgentRunStatus @default(ACTIVE)
|
||||
prompt String
|
||||
model String
|
||||
provider String
|
||||
@@ -248,21 +325,21 @@ model AgentRun {
|
||||
inputTokens Int?
|
||||
outputTokens Int?
|
||||
/// Provider/gateway-reported cost in USD. Null means this run has no trusted cost fact.
|
||||
costUsd Decimal? @db.Decimal(18, 8)
|
||||
costUsd Decimal? @db.Decimal(18, 8)
|
||||
/// Semantic source of costUsd, e.g. provider_reported. Not a pricing-estimate fallback.
|
||||
costSource String?
|
||||
metadata Json
|
||||
error String?
|
||||
startedAt DateTime @default(now())
|
||||
startedAt DateTime @default(now())
|
||||
finishedAt DateTime?
|
||||
updatedAt DateTime @updatedAt
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
session AgentSession? @relation(fields: [sessionId], references: [id], onDelete: SetNull)
|
||||
requestedBy User? @relation("runRequester", fields: [requestedByUserId], references: [id], onDelete: SetNull)
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
session AgentSession? @relation(fields: [sessionId], references: [id], onDelete: SetNull)
|
||||
requestedBy User? @relation("runRequester", fields: [requestedByUserId], references: [id], onDelete: SetNull)
|
||||
projectLock ProjectAgentLock?
|
||||
messages AgentMessage[] @relation("runMessages")
|
||||
fileChanges AgentFileChange[] @relation("runFileChanges")
|
||||
messages AgentMessage[] @relation("runMessages")
|
||||
fileChanges AgentFileChange[] @relation("runFileChanges")
|
||||
|
||||
@@index([projectId, status])
|
||||
@@index([projectId, finishedAt])
|
||||
@@ -276,16 +353,16 @@ model AgentRun {
|
||||
/// a run holds at most one lock. WellFormed (holder is non-terminal) is an
|
||||
/// app-level invariant checked on read/write, not a DB constraint.
|
||||
model ProjectAgentLock {
|
||||
projectId String @id
|
||||
runId String @unique
|
||||
projectId String @id
|
||||
runId String @unique
|
||||
holderUserId String?
|
||||
acquiredAt DateTime @default(now())
|
||||
acquiredAt DateTime @default(now())
|
||||
heartbeatAt DateTime?
|
||||
expiresAt DateTime?
|
||||
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
run AgentRun @relation(fields: [runId], references: [id], onDelete: Cascade)
|
||||
holder User? @relation("lockHolder", fields: [holderUserId], references: [id], onDelete: SetNull)
|
||||
run AgentRun @relation(fields: [runId], references: [id], onDelete: Cascade)
|
||||
holder User? @relation("lockHolder", fields: [holderUserId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([expiresAt])
|
||||
}
|
||||
@@ -315,14 +392,14 @@ enum PermissionResourceType {
|
||||
/// `principalType/principalId` so user/team/Feishu principals compose through
|
||||
/// the same authorization path.
|
||||
model PermissionGrant {
|
||||
id String @id @default(cuid())
|
||||
id String @id @default(cuid())
|
||||
resourceType PermissionResourceType
|
||||
resourceId String
|
||||
principalType PrincipalType
|
||||
principalId String
|
||||
role PermissionRole
|
||||
createdByUserId String?
|
||||
createdAt DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
revokedAt DateTime?
|
||||
|
||||
createdBy User? @relation("grantCreator", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||
@@ -335,7 +412,7 @@ model PermissionGrant {
|
||||
/// opaque string column each. ADR-0019 pins `agentTrigger` values used by the
|
||||
/// authorizer: ROLE, MANAGE_ONLY, DISABLED.
|
||||
model PermissionSettings {
|
||||
id String @id @default(cuid())
|
||||
id String @id @default(cuid())
|
||||
resourceType PermissionResourceType
|
||||
resourceId String
|
||||
externalShare String
|
||||
@@ -344,14 +421,13 @@ model PermissionSettings {
|
||||
collaboratorMgmt String
|
||||
agentTrigger String
|
||||
agentCancel String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([resourceType, resourceId])
|
||||
@@index([resourceType, resourceId])
|
||||
}
|
||||
|
||||
|
||||
/// Per-role agent trigger grant. Orthogonal to ADR-0004's PermissionGrant:
|
||||
/// ADR-0004 decides "can trigger an agent at all" (triggerAgent capability,
|
||||
/// edit+ role); this table decides "can trigger *which* agent role" (e.g.
|
||||
@@ -363,13 +439,13 @@ model PermissionSettings {
|
||||
/// the compound unique covers "one active grant per (project, principal, role)".
|
||||
/// Revocation via `revokedAt`, same pattern as PermissionGrant.
|
||||
model RoleTriggerGrant {
|
||||
id String @id @default(cuid())
|
||||
id String @id @default(cuid())
|
||||
projectId String
|
||||
roleId String
|
||||
principalType PrincipalType
|
||||
principalId String
|
||||
createdByUserId String?
|
||||
createdAt DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
revokedAt DateTime?
|
||||
|
||||
project Project @relation("projectRoleGrants", fields: [projectId], references: [id], onDelete: Cascade)
|
||||
@@ -427,16 +503,16 @@ model FeishuEventReceipt {
|
||||
/// rows are the queryable/auditable projection for admin APIs. ADR-0003:
|
||||
/// session messages ≠ Feishu group chat history.
|
||||
model AgentMessage {
|
||||
id String @id @default(cuid())
|
||||
sessionId String
|
||||
runId String?
|
||||
role String
|
||||
content String
|
||||
id String @id @default(cuid())
|
||||
sessionId String
|
||||
runId String?
|
||||
role String
|
||||
content String
|
||||
attachments Json
|
||||
createdAt DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
session AgentSession @relation("sessionMessages", fields: [sessionId], references: [id], onDelete: Cascade)
|
||||
run AgentRun? @relation("runMessages", fields: [runId], references: [id], onDelete: SetNull)
|
||||
run AgentRun? @relation("runMessages", fields: [runId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([sessionId, createdAt])
|
||||
@@index([runId])
|
||||
@@ -447,11 +523,11 @@ model AgentMessage {
|
||||
/// that produced it; the tool's execute closure records via a sink the runner
|
||||
/// injects into ToolContext (A-3 file-change capture).
|
||||
model AgentFileChange {
|
||||
id String @id @default(cuid())
|
||||
runId String
|
||||
projectId String
|
||||
path String
|
||||
operation String
|
||||
id String @id @default(cuid())
|
||||
runId String
|
||||
projectId String
|
||||
path String
|
||||
operation String
|
||||
beforeHash String?
|
||||
afterHash String?
|
||||
diff String?
|
||||
@@ -459,7 +535,7 @@ model AgentFileChange {
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
run AgentRun @relation("runFileChanges", fields: [runId], references: [id], onDelete: Cascade)
|
||||
project Project @relation("projectFileChanges", fields: [projectId], references: [id], onDelete: Cascade)
|
||||
project Project @relation("projectFileChanges", fields: [projectId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([runId])
|
||||
@@index([projectId])
|
||||
|
||||
Reference in New Issue
Block a user