-- 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;