Files
simstudioai--sim/apps/sim/ee/workspace-forking/lib/lineage/unlink.ts
T
wehub-resource-sync d25d482dc2
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

79 lines
2.6 KiB
TypeScript

import { db } from '@sim/db'
import {
workspace,
workspaceForkBlockMap,
workspaceForkDependentValue,
workspaceForkPromoteRun,
workspaceForkResourceMap,
} from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import {
acquireForkEdgeLock,
type ForkEdge,
setForkLockTimeout,
} from '@/ee/workspace-forking/lib/lineage/lineage'
const logger = createLogger('ForkUnlink')
export interface UnlinkForkResult {
/** False when the edge was already dissolved by a concurrent unlink (idempotent no-op). */
unlinked: boolean
}
/**
* Permanently dissolve a fork edge: null the child's `forkedFromWorkspaceId` (the
* edge's single source of truth) and purge the edge's fork state — resource map,
* block map, dependent values, and promote-run undo points. Both workspaces are
* left untouched; only the association and its metadata are removed.
*
* Runs in one transaction under the edge advisory lock, which every promote and
* rollback on the edge also holds, so an in-flight sync either finishes before the
* unlink or re-resolves the edge afterwards and fails with "not a direct fork edge".
* The edge is re-verified inside the lock; a concurrently-dissolved edge is an
* idempotent success rather than an error.
*/
export async function unlinkForkEdge(
edge: ForkEdge,
requestId?: string
): Promise<UnlinkForkResult> {
const { childWorkspaceId, parentWorkspaceId } = edge
const unlinked = await db.transaction(async (tx) => {
await setForkLockTimeout(tx)
await acquireForkEdgeLock(tx, childWorkspaceId)
const updated = await tx
.update(workspace)
.set({ forkedFromWorkspaceId: null, updatedAt: new Date() })
.where(
and(
eq(workspace.id, childWorkspaceId),
eq(workspace.forkedFromWorkspaceId, parentWorkspaceId)
)
)
.returning({ id: workspace.id })
if (updated.length === 0) return false
await tx
.delete(workspaceForkResourceMap)
.where(eq(workspaceForkResourceMap.childWorkspaceId, childWorkspaceId))
await tx
.delete(workspaceForkBlockMap)
.where(eq(workspaceForkBlockMap.childWorkspaceId, childWorkspaceId))
await tx
.delete(workspaceForkDependentValue)
.where(eq(workspaceForkDependentValue.childWorkspaceId, childWorkspaceId))
await tx
.delete(workspaceForkPromoteRun)
.where(eq(workspaceForkPromoteRun.childWorkspaceId, childWorkspaceId))
return true
})
logger.info(`[${requestId ?? 'unlink'}] Fork edge ${unlinked ? 'dissolved' : 'already gone'}`, {
childWorkspaceId,
parentWorkspaceId,
})
return { unlinked }
}