forked from EduCraft/curriculum-project-hub
feat: redesign Feishu project console
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
-- ADR-0017: roles are selected through the project-group control plane, not
|
||||
-- through slash-command names. Existing alpha Organizations keep draft as
|
||||
-- their configured default during migration; runtime code no longer hard-codes it.
|
||||
ALTER TABLE "OrganizationAgentRole"
|
||||
ADD COLUMN "isDefault" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
-- An Organization without any role was valid in the previous schema (the
|
||||
-- runtime failed closed later). Preserve the old alpha baseline so every
|
||||
-- existing binding can acquire a selected role during this migration.
|
||||
INSERT INTO "OrganizationAgentRole" (
|
||||
"id", "organizationId", "roleId", "label", "sortOrder", "isDefault", "updatedAt"
|
||||
)
|
||||
SELECT organization."id" || ':agent-role:draft', organization."id", 'draft', '草稿', 10, true, CURRENT_TIMESTAMP
|
||||
FROM "Organization" organization
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM "OrganizationAgentRole" role
|
||||
WHERE role."organizationId" = organization."id" AND role."disabledAt" IS NULL
|
||||
);
|
||||
|
||||
WITH ranked AS (
|
||||
SELECT "id", row_number() OVER (
|
||||
PARTITION BY "organizationId"
|
||||
ORDER BY CASE WHEN "roleId" = 'draft' THEN 0 ELSE 1 END, "sortOrder", "roleId", "id"
|
||||
) AS position
|
||||
FROM "OrganizationAgentRole"
|
||||
WHERE "disabledAt" IS NULL
|
||||
)
|
||||
UPDATE "OrganizationAgentRole" role
|
||||
SET "isDefault" = (ranked.position = 1)
|
||||
FROM ranked
|
||||
WHERE role."id" = ranked."id";
|
||||
|
||||
CREATE UNIQUE INDEX "OrganizationAgentRole_one_active_default_per_org"
|
||||
ON "OrganizationAgentRole"("organizationId")
|
||||
WHERE "isDefault" = true AND "disabledAt" IS NULL;
|
||||
|
||||
ALTER TABLE "ProjectGroupBinding"
|
||||
ADD COLUMN "selectedAgentRoleId" TEXT;
|
||||
|
||||
UPDATE "ProjectGroupBinding" binding
|
||||
SET "selectedAgentRoleId" = role."id"
|
||||
FROM "Project" project
|
||||
JOIN "OrganizationAgentRole" role
|
||||
ON role."organizationId" = project."organizationId"
|
||||
AND role."isDefault" = true
|
||||
AND role."disabledAt" IS NULL
|
||||
WHERE binding."projectId" = project."id";
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM "ProjectGroupBinding" WHERE "selectedAgentRoleId" IS NULL) THEN
|
||||
RAISE EXCEPTION 'cannot migrate project-group bindings without an active default Agent role';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
ALTER TABLE "ProjectGroupBinding"
|
||||
ALTER COLUMN "selectedAgentRoleId" SET NOT NULL;
|
||||
|
||||
CREATE INDEX "ProjectGroupBinding_selectedAgentRoleId_idx"
|
||||
ON "ProjectGroupBinding"("selectedAgentRoleId");
|
||||
|
||||
ALTER TABLE "ProjectGroupBinding"
|
||||
ADD CONSTRAINT "ProjectGroupBinding_selectedAgentRoleId_fkey"
|
||||
FOREIGN KEY ("selectedAgentRoleId") REFERENCES "OrganizationAgentRole"("id")
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,41 @@
|
||||
-- ADR-0017 / ADR-0020: enforce the selected role's Organization at the
|
||||
-- database boundary. A role primary key alone cannot prove tenant scope.
|
||||
ALTER TABLE "ProjectGroupBinding"
|
||||
ADD COLUMN "organizationId" TEXT;
|
||||
|
||||
UPDATE "ProjectGroupBinding" binding
|
||||
SET "organizationId" = project."organizationId"
|
||||
FROM "Project" project
|
||||
WHERE project."id" = binding."projectId";
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM "ProjectGroupBinding" WHERE "organizationId" IS NULL) THEN
|
||||
RAISE EXCEPTION 'cannot tenant-scope project-group binding without a project Organization';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
ALTER TABLE "ProjectGroupBinding"
|
||||
ALTER COLUMN "organizationId" SET NOT NULL;
|
||||
|
||||
CREATE UNIQUE INDEX "Project_organizationId_id_key"
|
||||
ON "Project"("organizationId", "id");
|
||||
|
||||
ALTER TABLE "ProjectGroupBinding"
|
||||
DROP CONSTRAINT "ProjectGroupBinding_projectId_fkey",
|
||||
DROP CONSTRAINT "ProjectGroupBinding_selectedAgentRoleId_fkey";
|
||||
|
||||
ALTER TABLE "ProjectGroupBinding"
|
||||
ADD CONSTRAINT "ProjectGroupBinding_organizationId_fkey"
|
||||
FOREIGN KEY ("organizationId") REFERENCES "Organization"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
ADD CONSTRAINT "ProjectGroupBinding_organizationId_projectId_fkey"
|
||||
FOREIGN KEY ("organizationId", "projectId") REFERENCES "Project"("organizationId", "id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
ADD CONSTRAINT "ProjectGroupBinding_organizationId_selectedAgentRoleId_fkey"
|
||||
FOREIGN KEY ("organizationId", "selectedAgentRoleId")
|
||||
REFERENCES "OrganizationAgentRole"("organizationId", "id")
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
CREATE INDEX "ProjectGroupBinding_organizationId_idx"
|
||||
ON "ProjectGroupBinding"("organizationId");
|
||||
@@ -0,0 +1,31 @@
|
||||
-- ADR-0017: once an Organization starts configuring roles, every committed
|
||||
-- state must contain exactly one active default. The deferred trigger permits
|
||||
-- an atomic default switch while rejecting zero-default transitions.
|
||||
CREATE FUNCTION cph_enforce_agent_role_default() RETURNS trigger
|
||||
LANGUAGE plpgsql AS $$
|
||||
DECLARE
|
||||
target_organization_id TEXT := COALESCE(NEW."organizationId", OLD."organizationId");
|
||||
active_default_count INTEGER;
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM "Organization" WHERE "id" = target_organization_id) THEN
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
SELECT count(*) INTO active_default_count
|
||||
FROM "OrganizationAgentRole"
|
||||
WHERE "organizationId" = target_organization_id
|
||||
AND "isDefault" = true
|
||||
AND "disabledAt" IS NULL;
|
||||
|
||||
IF active_default_count <> 1 THEN
|
||||
RAISE EXCEPTION 'Organization % must have exactly one active default Agent role; found %',
|
||||
target_organization_id, active_default_count;
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE CONSTRAINT TRIGGER "OrganizationAgentRole_exactly_one_active_default"
|
||||
AFTER INSERT OR UPDATE OR DELETE ON "OrganizationAgentRole"
|
||||
DEFERRABLE INITIALLY DEFERRED
|
||||
FOR EACH ROW EXECUTE FUNCTION cph_enforce_agent_role_default();
|
||||
@@ -0,0 +1,30 @@
|
||||
-- Organization-owned role configuration cannot be re-parented. Besides being
|
||||
-- a tenant boundary, immutability ensures the deferred default-role invariant
|
||||
-- checks the same Organization before and after an update.
|
||||
CREATE OR REPLACE FUNCTION cph_enforce_agent_role_default() RETURNS trigger
|
||||
LANGUAGE plpgsql AS $$
|
||||
DECLARE
|
||||
target_organization_id TEXT := COALESCE(NEW."organizationId", OLD."organizationId");
|
||||
active_default_count INTEGER;
|
||||
BEGIN
|
||||
IF TG_OP = 'UPDATE' AND NEW."organizationId" <> OLD."organizationId" THEN
|
||||
RAISE EXCEPTION 'OrganizationAgentRole.organizationId is immutable';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM "Organization" WHERE "id" = target_organization_id) THEN
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
SELECT count(*) INTO active_default_count
|
||||
FROM "OrganizationAgentRole"
|
||||
WHERE "organizationId" = target_organization_id
|
||||
AND "isDefault" = true
|
||||
AND "disabledAt" IS NULL;
|
||||
|
||||
IF active_default_count <> 1 THEN
|
||||
RAISE EXCEPTION 'Organization % must have exactly one active default Agent role; found %',
|
||||
target_organization_id, active_default_count;
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
@@ -0,0 +1,25 @@
|
||||
-- ADR-0017's default-role function is total over Organizations. Enforce the
|
||||
-- other side of the invariant when an Organization itself is created.
|
||||
CREATE FUNCTION cph_enforce_organization_default_role() RETURNS trigger
|
||||
LANGUAGE plpgsql AS $$
|
||||
DECLARE
|
||||
active_default_count INTEGER;
|
||||
BEGIN
|
||||
SELECT count(*) INTO active_default_count
|
||||
FROM "OrganizationAgentRole"
|
||||
WHERE "organizationId" = NEW."id"
|
||||
AND "isDefault" = true
|
||||
AND "disabledAt" IS NULL;
|
||||
|
||||
IF active_default_count <> 1 THEN
|
||||
RAISE EXCEPTION 'Organization % must have exactly one active default Agent role; found %',
|
||||
NEW."id", active_default_count;
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE CONSTRAINT TRIGGER "Organization_requires_active_default_role"
|
||||
AFTER INSERT ON "Organization"
|
||||
DEFERRABLE INITIALLY DEFERRED
|
||||
FOR EACH ROW EXECUTE FUNCTION cph_enforce_organization_default_role();
|
||||
+71
-61
@@ -45,8 +45,9 @@ model Organization {
|
||||
feishuApplicationConnection OrganizationFeishuApplicationConnection?
|
||||
agentSkills OrganizationAgentSkill[]
|
||||
agentRoles OrganizationAgentRole[]
|
||||
auditEntries AuditEntry[] @relation("organizationAudit")
|
||||
projectSearchDocuments ProjectSearchDocument[]
|
||||
projectGroupBindings ProjectGroupBinding[]
|
||||
auditEntries AuditEntry[] @relation("organizationAudit")
|
||||
projectSearchDocuments ProjectSearchDocument[]
|
||||
|
||||
@@index([status])
|
||||
}
|
||||
@@ -116,12 +117,14 @@ model OrganizationAgentRole {
|
||||
systemPrompt String?
|
||||
tools Json?
|
||||
sortOrder Int @default(0)
|
||||
isDefault Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
disabledAt DateTime?
|
||||
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
skillBindings OrganizationAgentRoleSkill[]
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
skillBindings OrganizationAgentRoleSkill[]
|
||||
selectedByBindings ProjectGroupBinding[] @relation("selectedAgentRole")
|
||||
|
||||
@@unique([organizationId, roleId])
|
||||
@@unique([organizationId, id])
|
||||
@@ -132,10 +135,10 @@ model OrganizationAgentRole {
|
||||
/// gives stable skill listing and prompt discovery order for a role bundle.
|
||||
model OrganizationAgentRoleSkill {
|
||||
organizationId String
|
||||
agentRoleId String
|
||||
agentSkillId String
|
||||
sortOrder Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
agentRoleId String
|
||||
agentSkillId String
|
||||
sortOrder Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
role OrganizationAgentRole @relation(fields: [organizationId, agentRoleId], references: [organizationId, id], onDelete: Cascade)
|
||||
skill OrganizationAgentSkill @relation(fields: [organizationId, agentSkillId], references: [organizationId, id], onDelete: Cascade)
|
||||
@@ -169,16 +172,16 @@ model User {
|
||||
|
||||
platformRoles PlatformRoleAssignment[]
|
||||
organizationMemberships OrganizationMembership[]
|
||||
createdProjects Project[] @relation("projectCreator")
|
||||
requestedRuns AgentRun[] @relation("runRequester")
|
||||
heldLocks ProjectAgentLock[] @relation("lockHolder")
|
||||
feishuBindings ProjectGroupBinding[] @relation("bindingCreator")
|
||||
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")
|
||||
providerCredentialVersions ProviderCredentialVersion[] @relation("providerCredentialVersionCreator")
|
||||
permissionGrants PermissionGrant[] @relation("grantCreator")
|
||||
roleTriggerGrants RoleTriggerGrant[] @relation("roleGrantCreator")
|
||||
auditEntries AuditEntry[] @relation("auditActor")
|
||||
providerCredentialVersions ProviderCredentialVersion[] @relation("providerCredentialVersionCreator")
|
||||
feishuCredentialVersions FeishuApplicationCredentialVersion[] @relation("feishuCredentialVersionCreator")
|
||||
feishuIdentities FeishuUserIdentity[]
|
||||
}
|
||||
@@ -197,7 +200,7 @@ model OrganizationFeishuApplicationConnection {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
secretVersions FeishuApplicationCredentialVersion[] @relation("feishuCredentialVersions")
|
||||
activeSecretVersion FeishuApplicationCredentialVersion? @relation("activeFeishuCredentialVersion", fields: [activeSecretVersionId], references: [id], onDelete: Restrict)
|
||||
userIdentities FeishuUserIdentity[]
|
||||
@@ -229,19 +232,19 @@ model FeishuUserIdentity {
|
||||
/// ADR-0024: all Feishu app material, including provider-local app/bot ids, is
|
||||
/// inside one immutable authenticated envelope version.
|
||||
model FeishuApplicationCredentialVersion {
|
||||
id String @id @default(cuid())
|
||||
id String @id @default(cuid())
|
||||
connectionId String
|
||||
version Int
|
||||
envelopeVersion Int @default(1)
|
||||
envelopeVersion Int @default(1)
|
||||
keyId String
|
||||
envelope Json
|
||||
createdByUserId String?
|
||||
createdAt DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
retiredAt DateTime?
|
||||
|
||||
connection OrganizationFeishuApplicationConnection @relation("feishuCredentialVersions", fields: [connectionId], references: [id], onDelete: Cascade)
|
||||
activeFor OrganizationFeishuApplicationConnection? @relation("activeFeishuCredentialVersion")
|
||||
createdBy User? @relation("feishuCredentialVersionCreator", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||
createdBy User? @relation("feishuCredentialVersionCreator", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@unique([connectionId, version])
|
||||
@@index([connectionId, retiredAt])
|
||||
@@ -288,14 +291,14 @@ enum OrganizationConnectionStatus {
|
||||
/// per immutable version. keyId/envelopeVersion are redacted rotation metadata;
|
||||
/// all provider URL/token/API-key fields remain inside envelope ciphertext.
|
||||
model ProviderCredentialVersion {
|
||||
id String @id @default(cuid())
|
||||
id String @id @default(cuid())
|
||||
connectionId String
|
||||
version Int
|
||||
envelopeVersion Int @default(1)
|
||||
envelopeVersion Int @default(1)
|
||||
keyId String
|
||||
envelope Json
|
||||
createdByUserId String?
|
||||
createdAt DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
retiredAt DateTime?
|
||||
|
||||
connection OrganizationProviderConnection @relation("providerCredentialVersions", fields: [connectionId], references: [id], onDelete: Cascade)
|
||||
@@ -444,14 +447,14 @@ enum FolderKind {
|
||||
}
|
||||
|
||||
model Folder {
|
||||
id String @id @default(cuid())
|
||||
id String @id @default(cuid())
|
||||
organizationId String
|
||||
parentId String?
|
||||
name String
|
||||
kind FolderKind @default(REGULAR)
|
||||
sortKey String @default("")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
sortKey String @default("")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
archivedAt DateTime?
|
||||
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
@@ -475,18 +478,19 @@ model Project {
|
||||
updatedAt DateTime @updatedAt
|
||||
archivedAt DateTime?
|
||||
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
|
||||
createdBy User? @relation("projectCreator", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
|
||||
createdBy User? @relation("projectCreator", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||
groupBindings ProjectGroupBinding[]
|
||||
agentSessions AgentSession[]
|
||||
agentRuns AgentRun[]
|
||||
agentLock ProjectAgentLock?
|
||||
roleTriggerGrants RoleTriggerGrant[] @relation("projectRoleGrants")
|
||||
auditEntries AuditEntry[] @relation("projectAudit")
|
||||
fileChanges AgentFileChange[] @relation("projectFileChanges")
|
||||
roleTriggerGrants RoleTriggerGrant[] @relation("projectRoleGrants")
|
||||
auditEntries AuditEntry[] @relation("projectAudit")
|
||||
fileChanges AgentFileChange[] @relation("projectFileChanges")
|
||||
searchDocument ProjectSearchDocument?
|
||||
|
||||
@@unique([organizationId, id])
|
||||
@@index([organizationId, archivedAt])
|
||||
@@index([folderId, archivedAt])
|
||||
@@index([archivedAt])
|
||||
@@ -496,16 +500,16 @@ model Project {
|
||||
/// triggers keep it synchronized with Project and Folder mutations; Project
|
||||
/// remains the source of truth and authorization remains outside this table.
|
||||
model ProjectSearchDocument {
|
||||
projectId String @id
|
||||
organizationId String
|
||||
name String
|
||||
code String?
|
||||
normalizedCode String
|
||||
normalizedName String
|
||||
breadcrumb String
|
||||
projectId String @id
|
||||
organizationId String
|
||||
name String
|
||||
code String?
|
||||
normalizedCode String
|
||||
normalizedName String
|
||||
breadcrumb String
|
||||
normalizedBreadcrumb String
|
||||
normalizedSearchText String
|
||||
updatedAt DateTime @updatedAt
|
||||
normalizedSearchText String
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
@@ -517,19 +521,25 @@ model ProjectSearchDocument {
|
||||
/// (1:1). Historical archived bindings are retained for audit; partial unique
|
||||
/// indexes in migrations enforce one active binding per project and per chat.
|
||||
model ProjectGroupBinding {
|
||||
id String @id @default(cuid())
|
||||
projectId String
|
||||
chatId String
|
||||
createdByUserId String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
archivedAt DateTime?
|
||||
id String @id @default(cuid())
|
||||
organizationId String
|
||||
projectId String
|
||||
chatId String
|
||||
createdByUserId String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
archivedAt DateTime?
|
||||
selectedAgentRoleId String
|
||||
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
createdBy User? @relation("bindingCreator", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
project Project @relation(fields: [organizationId, projectId], references: [organizationId, id], onDelete: Cascade)
|
||||
createdBy User? @relation("bindingCreator", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||
selectedRole OrganizationAgentRole @relation("selectedAgentRole", fields: [organizationId, selectedAgentRoleId], references: [organizationId, id], onDelete: Restrict)
|
||||
|
||||
@@index([organizationId])
|
||||
@@index([projectId, archivedAt])
|
||||
@@index([chatId, archivedAt])
|
||||
@@index([selectedAgentRoleId])
|
||||
}
|
||||
|
||||
// --- AgentRun, session, lock (ADR-0002, 0017) -----------------------------
|
||||
@@ -727,17 +737,17 @@ model RoleTriggerGrant {
|
||||
/// spec AuditEntry: minimal skeleton — one entry relates to a run. Event type,
|
||||
/// actor, timestamp, details are OPEN. This table mirrors that: `runId` is the
|
||||
model AuditEntry {
|
||||
id String @id @default(cuid())
|
||||
runId String?
|
||||
projectId String?
|
||||
id String @id @default(cuid())
|
||||
runId String?
|
||||
projectId String?
|
||||
organizationId String?
|
||||
actorUserId String?
|
||||
action String
|
||||
metadata Json
|
||||
createdAt DateTime @default(now())
|
||||
actorUserId String?
|
||||
action String
|
||||
metadata Json
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
actor User? @relation("auditActor", fields: [actorUserId], references: [id], onDelete: SetNull)
|
||||
project Project? @relation("projectAudit", fields: [projectId], references: [id], onDelete: SetNull)
|
||||
actor User? @relation("auditActor", fields: [actorUserId], references: [id], onDelete: SetNull)
|
||||
project Project? @relation("projectAudit", fields: [projectId], references: [id], onDelete: SetNull)
|
||||
organization Organization? @relation("organizationAudit", fields: [organizationId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([runId])
|
||||
|
||||
Reference in New Issue
Block a user