forked from bai/curriculum-project-hub
feat: add organization tenant model
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
-- ADR-0020: organization is the SaaS tenant root.
|
||||
-- Backfill all existing data into one default organization so old databases
|
||||
-- upgrade without a manual bootstrap step.
|
||||
|
||||
CREATE TYPE "OrganizationStatus" AS ENUM ('ACTIVE', 'SUSPENDED', 'ARCHIVED');
|
||||
CREATE TYPE "OrganizationMemberRole" AS ENUM ('OWNER', 'ADMIN', 'MEMBER');
|
||||
|
||||
CREATE TABLE "Organization" (
|
||||
"id" TEXT NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"status" "OrganizationStatus" NOT NULL DEFAULT 'ACTIVE',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Organization_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "Organization_slug_key" ON "Organization"("slug");
|
||||
CREATE INDEX "Organization_status_idx" ON "Organization"("status");
|
||||
|
||||
INSERT INTO "Organization" ("id", "slug", "name", "status")
|
||||
VALUES ('org_default', 'legacy-default', 'Legacy Default Organization', 'ACTIVE');
|
||||
|
||||
CREATE TABLE "OrganizationMembership" (
|
||||
"id" TEXT NOT NULL,
|
||||
"organizationId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"role" "OrganizationMemberRole" NOT NULL DEFAULT 'MEMBER',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"revokedAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "OrganizationMembership_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
INSERT INTO "OrganizationMembership" ("id", "organizationId", "userId", "role")
|
||||
SELECT
|
||||
'orgmem_default_' || md5("User"."id"),
|
||||
'org_default',
|
||||
"User"."id",
|
||||
CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1 FROM "PlatformRoleAssignment"
|
||||
WHERE "PlatformRoleAssignment"."userId" = "User"."id"
|
||||
AND "PlatformRoleAssignment"."role" = 'ADMIN'
|
||||
AND "PlatformRoleAssignment"."revokedAt" IS NULL
|
||||
)
|
||||
THEN 'OWNER'::"OrganizationMemberRole"
|
||||
ELSE 'MEMBER'::"OrganizationMemberRole"
|
||||
END
|
||||
FROM "User";
|
||||
|
||||
CREATE INDEX "OrganizationMembership_organizationId_revokedAt_idx" ON "OrganizationMembership"("organizationId", "revokedAt");
|
||||
CREATE INDEX "OrganizationMembership_userId_revokedAt_idx" ON "OrganizationMembership"("userId", "revokedAt");
|
||||
CREATE UNIQUE INDEX "OrganizationMembership_organizationId_userId_revokedAt_key" ON "OrganizationMembership"("organizationId", "userId", "revokedAt");
|
||||
CREATE UNIQUE INDEX "OrganizationMembership_active_key" ON "OrganizationMembership"("organizationId", "userId") WHERE "revokedAt" IS NULL;
|
||||
|
||||
ALTER TABLE "OrganizationMembership" ADD CONSTRAINT "OrganizationMembership_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "OrganizationMembership" ADD CONSTRAINT "OrganizationMembership_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "Project" ADD COLUMN "organizationId" TEXT;
|
||||
UPDATE "Project" SET "organizationId" = 'org_default';
|
||||
ALTER TABLE "Project" ALTER COLUMN "organizationId" SET NOT NULL;
|
||||
ALTER TABLE "Project" ADD CONSTRAINT "Project_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
CREATE INDEX "Project_organizationId_archivedAt_idx" ON "Project"("organizationId", "archivedAt");
|
||||
|
||||
ALTER TABLE "Team" ADD COLUMN "organizationId" TEXT;
|
||||
UPDATE "Team" SET "organizationId" = 'org_default';
|
||||
ALTER TABLE "Team" ALTER COLUMN "organizationId" SET NOT NULL;
|
||||
ALTER TABLE "Team" ADD CONSTRAINT "Team_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
DROP INDEX IF EXISTS "Team_slug_key";
|
||||
CREATE INDEX "Team_organizationId_archivedAt_idx" ON "Team"("organizationId", "archivedAt");
|
||||
CREATE INDEX "Team_organizationId_slug_archivedAt_idx" ON "Team"("organizationId", "slug", "archivedAt");
|
||||
CREATE UNIQUE INDEX "Team_active_slug_key" ON "Team"("organizationId", "slug") WHERE "archivedAt" IS NULL;
|
||||
|
||||
CREATE TABLE "ExternalDirectoryConnection" (
|
||||
"id" TEXT NOT NULL,
|
||||
"organizationId" TEXT NOT NULL,
|
||||
"provider" TEXT NOT NULL,
|
||||
"providerTenantId" TEXT,
|
||||
"source" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'ACTIVE',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"revokedAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "ExternalDirectoryConnection_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
INSERT INTO "ExternalDirectoryConnection" ("id", "organizationId", "provider", "source")
|
||||
SELECT DISTINCT
|
||||
'extconn_default_' || md5("source"),
|
||||
'org_default',
|
||||
'FEISHU',
|
||||
"source"
|
||||
FROM "ExternalPrincipalMembership";
|
||||
|
||||
CREATE UNIQUE INDEX "ExternalDirectoryConnection_organizationId_provider_source_key" ON "ExternalDirectoryConnection"("organizationId", "provider", "source");
|
||||
CREATE INDEX "ExternalDirectoryConnection_organizationId_revokedAt_idx" ON "ExternalDirectoryConnection"("organizationId", "revokedAt");
|
||||
CREATE INDEX "ExternalDirectoryConnection_provider_providerTenantId_idx" ON "ExternalDirectoryConnection"("provider", "providerTenantId");
|
||||
ALTER TABLE "ExternalDirectoryConnection" ADD CONSTRAINT "ExternalDirectoryConnection_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
DROP INDEX IF EXISTS "ExternalPrincipalMembership_userId_principalType_principalId_source_revokedAt_key";
|
||||
DROP INDEX IF EXISTS "ExternalPrincipalMembership_active_key";
|
||||
DROP INDEX IF EXISTS "ExternalPrincipalMembership_source_syncedAt_idx";
|
||||
|
||||
ALTER TABLE "ExternalPrincipalMembership" ADD COLUMN "connectionId" TEXT;
|
||||
UPDATE "ExternalPrincipalMembership"
|
||||
SET "connectionId" = 'extconn_default_' || md5("source");
|
||||
ALTER TABLE "ExternalPrincipalMembership" ALTER COLUMN "connectionId" SET NOT NULL;
|
||||
ALTER TABLE "ExternalPrincipalMembership" DROP COLUMN "source";
|
||||
ALTER TABLE "ExternalPrincipalMembership" ADD CONSTRAINT "ExternalPrincipalMembership_connectionId_fkey" FOREIGN KEY ("connectionId") REFERENCES "ExternalDirectoryConnection"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
CREATE INDEX "ExternalPrincipalMembership_connectionId_revokedAt_idx" ON "ExternalPrincipalMembership"("connectionId", "revokedAt");
|
||||
CREATE UNIQUE INDEX "ExternalPrincipalMembership_userId_principalType_principalId_connectionId_revokedAt_key" ON "ExternalPrincipalMembership"("userId", "principalType", "principalId", "connectionId", "revokedAt");
|
||||
CREATE UNIQUE INDEX "ExternalPrincipalMembership_active_key" ON "ExternalPrincipalMembership"("userId", "principalType", "principalId", "connectionId") WHERE "revokedAt" IS NULL;
|
||||
|
||||
-- A principal should have one active role on a resource. Keep the strongest
|
||||
-- grant and revoke lower active grants to preserve history without ambiguity.
|
||||
DROP INDEX IF EXISTS "PermissionGrant_active_key";
|
||||
|
||||
WITH ranked_permission_grants AS (
|
||||
SELECT
|
||||
"id",
|
||||
row_number() OVER (
|
||||
PARTITION BY "resourceType", "resourceId", "principalType", "principalId"
|
||||
ORDER BY
|
||||
CASE "role"
|
||||
WHEN 'MANAGE' THEN 3
|
||||
WHEN 'EDIT' THEN 2
|
||||
WHEN 'READ' THEN 1
|
||||
END DESC,
|
||||
"createdAt" ASC,
|
||||
"id" ASC
|
||||
) AS rn
|
||||
FROM "PermissionGrant"
|
||||
WHERE "revokedAt" IS NULL
|
||||
)
|
||||
UPDATE "PermissionGrant"
|
||||
SET "revokedAt" = CURRENT_TIMESTAMP
|
||||
WHERE "id" IN (
|
||||
SELECT "id" FROM ranked_permission_grants WHERE rn > 1
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "PermissionGrant_active_principal_key" ON "PermissionGrant"("resourceType", "resourceId", "principalType", "principalId") WHERE "revokedAt" IS NULL;
|
||||
+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