forked from EduCraft/curriculum-project-hub
feat: add org admin project onboarding foundation
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
-- ADR-0021: org admin project onboarding.
|
||||
-- Folder is a transparent project explorer node; project remains the permission boundary.
|
||||
|
||||
CREATE TABLE "OrganizationProjectSettings" (
|
||||
"organizationId" TEXT NOT NULL,
|
||||
"membersCanCreateProjects" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "OrganizationProjectSettings_pkey" PRIMARY KEY ("organizationId")
|
||||
);
|
||||
|
||||
ALTER TABLE "OrganizationProjectSettings"
|
||||
ADD CONSTRAINT "OrganizationProjectSettings_organizationId_fkey"
|
||||
FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
INSERT INTO "OrganizationProjectSettings" ("organizationId", "membersCanCreateProjects", "createdAt", "updatedAt")
|
||||
SELECT "id", true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
||||
FROM "Organization"
|
||||
ON CONFLICT ("organizationId") DO NOTHING;
|
||||
|
||||
CREATE TABLE "Folder" (
|
||||
"id" TEXT NOT NULL,
|
||||
"organizationId" TEXT NOT NULL,
|
||||
"parentId" TEXT,
|
||||
"name" TEXT NOT NULL,
|
||||
"sortKey" TEXT NOT NULL DEFAULT '',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"archivedAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "Folder_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
ALTER TABLE "Folder"
|
||||
ADD CONSTRAINT "Folder_organizationId_fkey"
|
||||
FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "Folder"
|
||||
ADD CONSTRAINT "Folder_parentId_fkey"
|
||||
FOREIGN KEY ("parentId") REFERENCES "Folder"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
CREATE INDEX "Folder_organizationId_parentId_archivedAt_idx"
|
||||
ON "Folder"("organizationId", "parentId", "archivedAt");
|
||||
|
||||
CREATE INDEX "Folder_organizationId_parentId_sortKey_idx"
|
||||
ON "Folder"("organizationId", "parentId", "sortKey");
|
||||
|
||||
CREATE UNIQUE INDEX "Folder_active_sibling_name_key"
|
||||
ON "Folder"("organizationId", COALESCE("parentId", ''), lower("name"))
|
||||
WHERE "archivedAt" IS NULL;
|
||||
|
||||
INSERT INTO "Folder" ("id", "organizationId", "parentId", "name", "sortKey", "createdAt", "updatedAt")
|
||||
SELECT 'folder_inbox_' || md5("id"), "id", NULL, 'Inbox', '000000', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
||||
FROM "Organization";
|
||||
|
||||
ALTER TABLE "Project" ADD COLUMN "folderId" TEXT;
|
||||
|
||||
UPDATE "Project" AS p
|
||||
SET "folderId" = f."id"
|
||||
FROM "Folder" AS f
|
||||
WHERE f."organizationId" = p."organizationId"
|
||||
AND f."parentId" IS NULL
|
||||
AND f."name" = 'Inbox'
|
||||
AND p."folderId" IS NULL;
|
||||
|
||||
ALTER TABLE "Project"
|
||||
ADD CONSTRAINT "Project_folderId_fkey"
|
||||
FOREIGN KEY ("folderId") REFERENCES "Folder"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
CREATE INDEX "Project_folderId_archivedAt_idx"
|
||||
ON "Project"("folderId", "archivedAt");
|
||||
|
||||
ALTER TABLE "ProjectGroupBinding" ADD COLUMN "archivedAt" TIMESTAMP(3);
|
||||
|
||||
DROP INDEX IF EXISTS "ProjectGroupBinding_projectId_key";
|
||||
DROP INDEX IF EXISTS "ProjectGroupBinding_chatId_key";
|
||||
DROP INDEX IF EXISTS "ProjectGroupBinding_chatId_idx";
|
||||
|
||||
CREATE INDEX "ProjectGroupBinding_projectId_archivedAt_idx"
|
||||
ON "ProjectGroupBinding"("projectId", "archivedAt");
|
||||
|
||||
CREATE INDEX "ProjectGroupBinding_chatId_archivedAt_idx"
|
||||
ON "ProjectGroupBinding"("chatId", "archivedAt");
|
||||
|
||||
CREATE UNIQUE INDEX "ProjectGroupBinding_active_project_key"
|
||||
ON "ProjectGroupBinding"("projectId")
|
||||
WHERE "archivedAt" IS NULL;
|
||||
|
||||
CREATE UNIQUE INDEX "ProjectGroupBinding_active_chat_key"
|
||||
ON "ProjectGroupBinding"("chatId")
|
||||
WHERE "archivedAt" IS NULL;
|
||||
+55
-17
@@ -36,6 +36,8 @@ model Organization {
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
memberships OrganizationMembership[]
|
||||
projectSettings OrganizationProjectSettings?
|
||||
folders Folder[]
|
||||
projects Project[]
|
||||
teams Team[]
|
||||
externalDirectoryConnections ExternalDirectoryConnection[]
|
||||
@@ -73,6 +75,17 @@ enum OrganizationMemberRole {
|
||||
MEMBER
|
||||
}
|
||||
|
||||
/// ADR-0021: org-level project onboarding policy. Ordinary Feishu users can
|
||||
/// create projects from unbound chats only when membersCanCreateProjects=true.
|
||||
model OrganizationProjectSettings {
|
||||
organizationId String @id
|
||||
membersCanCreateProjects Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
/// A Feishu user known to the Hub. principal sub-typology is OPEN (ADR-0004).
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
@@ -222,9 +235,32 @@ model ExternalPrincipalMembership {
|
||||
|
||||
// --- Project & Feishu binding (ADR-0001) ---------------------------------
|
||||
|
||||
/// ADR-0021: transparent project explorer folder. Folders are org-scoped
|
||||
/// navigation/aggregation nodes, not permission resources; project grants stay
|
||||
/// attached to PROJECT resources.
|
||||
model Folder {
|
||||
id String @id @default(cuid())
|
||||
organizationId String
|
||||
parentId String?
|
||||
name String
|
||||
sortKey String @default("")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
archivedAt DateTime?
|
||||
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
parent Folder? @relation("folderTree", fields: [parentId], references: [id], onDelete: Restrict)
|
||||
children Folder[] @relation("folderTree")
|
||||
projects Project[]
|
||||
|
||||
@@index([organizationId, parentId, archivedAt])
|
||||
@@index([organizationId, parentId, sortKey])
|
||||
}
|
||||
|
||||
model Project {
|
||||
id String @id @default(cuid())
|
||||
organizationId String
|
||||
folderId String?
|
||||
name String
|
||||
workspaceDir String
|
||||
createdByUserId String?
|
||||
@@ -232,37 +268,39 @@ model Project {
|
||||
updatedAt DateTime @updatedAt
|
||||
archivedAt DateTime?
|
||||
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
createdBy User? @relation("projectCreator", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||
groupBinding ProjectGroupBinding?
|
||||
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")
|
||||
|
||||
@@index([organizationId, archivedAt])
|
||||
@@index([folderId, archivedAt])
|
||||
@@index([archivedAt])
|
||||
}
|
||||
|
||||
/// ADR-0001: one project ↔ one Feishu chat (1:1). `chatId` is unique ⇒
|
||||
/// GroupBinding.WellFormed's injectivity half (no two projects bind one chat).
|
||||
/// The "at most one binding per project" half is enforced by the 1:1 relation.
|
||||
/// Group dissolution lifecycle is OPEN (ADR-0001 Consequences) — not modeled
|
||||
/// here; archival is a future policy, not a current column.
|
||||
/// ADR-0001 + ADR-0021: active bindings are one project ↔ one Feishu chat
|
||||
/// (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 @unique
|
||||
chatId String @unique
|
||||
id String @id @default(cuid())
|
||||
projectId String
|
||||
chatId String
|
||||
createdByUserId String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
archivedAt DateTime?
|
||||
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
createdBy User? @relation("bindingCreator", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([chatId])
|
||||
@@index([projectId, archivedAt])
|
||||
@@index([chatId, archivedAt])
|
||||
}
|
||||
|
||||
// --- AgentRun, session, lock (ADR-0002, 0017) -----------------------------
|
||||
|
||||
Reference in New Issue
Block a user