diff --git a/apps/sim/app/api/workspaces/[id]/fork/diff/route.ts b/apps/sim/app/api/workspaces/[id]/fork/diff/route.ts new file mode 100644 index 00000000000..db3d60d1381 --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/fork/diff/route.ts @@ -0,0 +1,86 @@ +import { db } from '@sim/db' +import { type NextRequest, NextResponse } from 'next/server' +import { getForkDiffContract } from '@/lib/api/contracts/workspace-fork' +import { parseRequest } from '@/lib/api/server' +import { getSession } from '@/lib/auth' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { loadSourceDeployedStates } from '@/lib/workspaces/fork/copy/deploy-bridge' +import { assertCanPromote } from '@/lib/workspaces/fork/lineage/authz' +import { computeForkPromotePlan } from '@/lib/workspaces/fork/promote/promote-plan' + +export const GET = withRouteHandler( + async (req: NextRequest, context: { params: Promise<{ id: string }> }) => { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseRequest(getForkDiffContract, req, context) + if (!parsed.success) return parsed.response + const { id } = parsed.data.params + const { otherWorkspaceId, direction } = parsed.data.query + + const auth = await assertCanPromote(id, otherWorkspaceId, direction, session.user.id) + + const { deployedWorkflows, sourceStates } = await loadSourceDeployedStates( + auth.sourceWorkspaceId + ) + const plan = await computeForkPromotePlan({ + executor: db, + edge: auth.edge, + sourceWorkspaceId: auth.sourceWorkspaceId, + targetWorkspaceId: auth.targetWorkspaceId, + direction, + deployedSourceWorkflows: deployedWorkflows, + sourceStates, + }) + + const toRef = (reference: (typeof plan.unmappedRequired)[number]) => ({ + kind: reference.kind, + sourceId: reference.sourceId, + required: reference.required, + blockName: reference.blockName, + }) + + // Orient the mapping around the workspace the modal is open in (`id`): show the + // caller's workflow name first, the sync partner's second, so renames are legible. + const currentIsSource = auth.sourceWorkspaceId === id + const workflows = [ + ...plan.items.map((item) => { + if (item.mode === 'create') { + // The target inherits the source's name, so both sides read the same. + return { + action: 'create' as const, + currentName: item.sourceMeta.name, + otherName: item.sourceMeta.name, + } + } + const targetName = item.targetName ?? item.sourceMeta.name + return { + action: 'update' as const, + currentName: currentIsSource ? item.sourceMeta.name : targetName, + otherName: currentIsSource ? targetName : item.sourceMeta.name, + } + }), + ...plan.archivedTargets.map((target) => ({ + action: 'archive' as const, + currentName: target.name, + otherName: target.name, + })), + ] + + return NextResponse.json({ + sourceWorkspaceId: auth.sourceWorkspaceId, + targetWorkspaceId: auth.targetWorkspaceId, + willUpdate: plan.willUpdate, + willCreate: plan.willCreate, + willArchive: plan.willArchive, + workflows, + unmappedRequired: plan.unmappedRequired.map(toRef), + unmappedOptional: plan.unmappedOptional.map(toRef), + mcpReauthServerIds: plan.mcpReauthServerIds, + inlineSecretSources: plan.inlineSecretSources, + drift: plan.drift, + }) + } +) diff --git a/apps/sim/app/api/workspaces/[id]/fork/lineage/route.ts b/apps/sim/app/api/workspaces/[id]/fork/lineage/route.ts new file mode 100644 index 00000000000..3abd1282754 --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/fork/lineage/route.ts @@ -0,0 +1,58 @@ +import { db } from '@sim/db' +import { workspace } from '@sim/db/schema' +import { eq } from 'drizzle-orm' +import type { NextRequest } from 'next/server' +import { NextResponse } from 'next/server' +import { getForkLineageContract } from '@/lib/api/contracts/workspace-fork' +import { parseRequest } from '@/lib/api/server' +import { getSession } from '@/lib/auth' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { assertWorkspaceAdminAccess } from '@/lib/workspaces/fork/lineage/authz' +import { getForkLineage } from '@/lib/workspaces/fork/lineage/lineage' +import { getUndoableRunForTarget } from '@/lib/workspaces/fork/promote/promote-run-store' + +export const GET = withRouteHandler( + async (req: NextRequest, context: { params: Promise<{ id: string }> }) => { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseRequest(getForkLineageContract, req, context) + if (!parsed.success) return parsed.response + const { id: workspaceId } = parsed.data.params + + await assertWorkspaceAdminAccess(workspaceId, session.user.id) + + const [{ parent, children }, run] = await Promise.all([ + getForkLineage(workspaceId), + getUndoableRunForTarget(db, workspaceId), + ]) + + let undoableRun: { + otherWorkspaceId: string + otherName: string + direction: 'push' | 'pull' + } | null = null + if (run) { + const [other] = await db + .select({ name: workspace.name }) + .from(workspace) + .where(eq(workspace.id, run.sourceWorkspaceId)) + .limit(1) + undoableRun = { + otherWorkspaceId: run.sourceWorkspaceId, + otherName: other?.name ?? 'workspace', + direction: run.direction, + } + } + + return NextResponse.json({ + workspaceId, + parent, + children, + hasUndoableRun: Boolean(run), + undoableRun, + }) + } +) diff --git a/apps/sim/app/api/workspaces/[id]/fork/mapping/route.ts b/apps/sim/app/api/workspaces/[id]/fork/mapping/route.ts new file mode 100644 index 00000000000..a6955f3ee1d --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/fork/mapping/route.ts @@ -0,0 +1,69 @@ +import { db } from '@sim/db' +import { type NextRequest, NextResponse } from 'next/server' +import { + getForkMappingContract, + updateForkMappingContract, +} from '@/lib/api/contracts/workspace-fork' +import { parseRequest } from '@/lib/api/server' +import { getSession } from '@/lib/auth' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { assertCanPromote } from '@/lib/workspaces/fork/lineage/authz' +import { + applyForkMappingEntries, + getForkMappingView, + validateForkMappingTargets, +} from '@/lib/workspaces/fork/mapping/mapping-service' + +export const GET = withRouteHandler( + async (req: NextRequest, context: { params: Promise<{ id: string }> }) => { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseRequest(getForkMappingContract, req, context) + if (!parsed.success) return parsed.response + const { id } = parsed.data.params + const { otherWorkspaceId, direction } = parsed.data.query + + const auth = await assertCanPromote(id, otherWorkspaceId, direction, session.user.id) + + const { entries } = await getForkMappingView({ + edge: auth.edge, + sourceWorkspaceId: auth.sourceWorkspaceId, + targetWorkspaceId: auth.targetWorkspaceId, + }) + + return NextResponse.json({ + childWorkspaceId: auth.edge.childWorkspaceId, + parentWorkspaceId: auth.edge.parentWorkspaceId, + sourceWorkspaceId: auth.sourceWorkspaceId, + targetWorkspaceId: auth.targetWorkspaceId, + entries, + }) + } +) + +export const PUT = withRouteHandler( + async (req: NextRequest, context: { params: Promise<{ id: string }> }) => { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseRequest(updateForkMappingContract, req, context) + if (!parsed.success) return parsed.response + const { id } = parsed.data.params + const { otherWorkspaceId, direction, entries } = parsed.data.body + + const auth = await assertCanPromote(id, otherWorkspaceId, direction, session.user.id) + + await validateForkMappingTargets(auth.targetWorkspaceId, entries) + + const updated = await db.transaction((tx) => + applyForkMappingEntries(tx, auth.edge, session.user.id, direction, entries) + ) + + return NextResponse.json({ success: true as const, updated }) + } +) diff --git a/apps/sim/app/api/workspaces/[id]/fork/promote/route.ts b/apps/sim/app/api/workspaces/[id]/fork/promote/route.ts new file mode 100644 index 00000000000..b8562faa80c --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/fork/promote/route.ts @@ -0,0 +1,80 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { promoteForkContract } from '@/lib/api/contracts/workspace-fork' +import { parseRequest } from '@/lib/api/server' +import { getSession } from '@/lib/auth' +import { generateRequestId } from '@/lib/core/utils/request' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { assertCanPromote } from '@/lib/workspaces/fork/lineage/authz' +import { promoteFork } from '@/lib/workspaces/fork/promote/promote' + +const logger = createLogger('WorkspaceForkPromoteAPI') + +export const POST = withRouteHandler( + async (req: NextRequest, context: { params: Promise<{ id: string }> }) => { + const requestId = generateRequestId() + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseRequest(promoteForkContract, req, context) + if (!parsed.success) return parsed.response + const { id } = parsed.data.params + const { otherWorkspaceId, direction, force } = parsed.data.body + + const auth = await assertCanPromote(id, otherWorkspaceId, direction, session.user.id) + + const result = await promoteFork({ + edge: auth.edge, + sourceWorkspaceId: auth.sourceWorkspaceId, + targetWorkspaceId: auth.targetWorkspaceId, + direction, + force, + userId: session.user.id, + requestId, + }) + + const body = { + promoteRunId: result.promoteRunId, + updated: result.updated, + created: result.created, + archived: result.archived, + redeployed: result.redeployed, + unmappedRequired: result.unmappedRequired, + drift: result.drift, + } + + if (result.blocked) { + logger.info(`[${requestId}] Promote blocked (${result.blocked})`, { + sourceWorkspaceId: auth.sourceWorkspaceId, + targetWorkspaceId: auth.targetWorkspaceId, + }) + return NextResponse.json(body) + } + + recordAudit({ + workspaceId: auth.targetWorkspaceId, + actorId: session.user.id, + action: AuditAction.WORKSPACE_FORK_PROMOTED, + resourceType: AuditResourceType.WORKSPACE, + resourceId: auth.targetWorkspaceId, + actorName: session.user.name ?? undefined, + actorEmail: session.user.email ?? undefined, + resourceName: auth.target.name, + description: `Promoted workflows from "${auth.source.name}" to "${auth.target.name}"`, + metadata: { + direction, + sourceWorkspaceId: auth.sourceWorkspaceId, + updated: result.updated, + created: result.created, + archived: result.archived, + redeployed: result.redeployed, + }, + request: req, + }) + + return NextResponse.json(body) + } +) diff --git a/apps/sim/app/api/workspaces/[id]/fork/resources/route.ts b/apps/sim/app/api/workspaces/[id]/fork/resources/route.ts new file mode 100644 index 00000000000..ef489fc6a18 --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/fork/resources/route.ts @@ -0,0 +1,26 @@ +import { db } from '@sim/db' +import { type NextRequest, NextResponse } from 'next/server' +import { getForkResourcesContract } from '@/lib/api/contracts/workspace-fork' +import { parseRequest } from '@/lib/api/server' +import { getSession } from '@/lib/auth' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { assertWorkspaceAdminAccess } from '@/lib/workspaces/fork/lineage/authz' +import { listForkCopyableResources } from '@/lib/workspaces/fork/mapping/resources' + +export const GET = withRouteHandler( + async (req: NextRequest, context: { params: Promise<{ id: string }> }) => { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseRequest(getForkResourcesContract, req, context) + if (!parsed.success) return parsed.response + const { id } = parsed.data.params + + await assertWorkspaceAdminAccess(id, session.user.id) + + const resources = await listForkCopyableResources(db, id) + return NextResponse.json(resources) + } +) diff --git a/apps/sim/app/api/workspaces/[id]/fork/rollback/route.ts b/apps/sim/app/api/workspaces/[id]/fork/rollback/route.ts new file mode 100644 index 00000000000..0c4acb94f86 --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/fork/rollback/route.ts @@ -0,0 +1,49 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' +import { type NextRequest, NextResponse } from 'next/server' +import { rollbackForkContract } from '@/lib/api/contracts/workspace-fork' +import { parseRequest } from '@/lib/api/server' +import { getSession } from '@/lib/auth' +import { generateRequestId } from '@/lib/core/utils/request' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { assertCanRollback } from '@/lib/workspaces/fork/lineage/authz' +import { rollbackFork } from '@/lib/workspaces/fork/promote/rollback' + +export const POST = withRouteHandler( + async (req: NextRequest, context: { params: Promise<{ id: string }> }) => { + const requestId = generateRequestId() + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseRequest(rollbackForkContract, req, context) + if (!parsed.success) return parsed.response + const { id } = parsed.data.params + const { otherWorkspaceId } = parsed.data.body + + const target = await assertCanRollback(id, session.user.id) + + const result = await rollbackFork({ + targetWorkspaceId: id, + otherWorkspaceId, + userId: session.user.id, + requestId, + }) + + recordAudit({ + workspaceId: id, + actorId: session.user.id, + action: AuditAction.WORKSPACE_FORK_ROLLED_BACK, + resourceType: AuditResourceType.WORKSPACE, + resourceId: id, + actorName: session.user.name ?? undefined, + actorEmail: session.user.email ?? undefined, + resourceName: target.name, + description: `Rolled back the last promote into "${target.name}"`, + metadata: { otherWorkspaceId, ...result }, + request: req, + }) + + return NextResponse.json(result) + } +) diff --git a/apps/sim/app/api/workspaces/[id]/fork/route.ts b/apps/sim/app/api/workspaces/[id]/fork/route.ts new file mode 100644 index 00000000000..cb37a72eb33 --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/fork/route.ts @@ -0,0 +1,66 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { forkWorkspaceContract } from '@/lib/api/contracts/workspace-fork' +import { parseRequest } from '@/lib/api/server' +import { getSession } from '@/lib/auth' +import { generateRequestId } from '@/lib/core/utils/request' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { createFork } from '@/lib/workspaces/fork/create-fork' +import { assertCanFork } from '@/lib/workspaces/fork/lineage/authz' + +const logger = createLogger('WorkspaceForkAPI') + +export const POST = withRouteHandler( + async (req: NextRequest, context: { params: Promise<{ id: string }> }) => { + const { id: sourceWorkspaceId } = await context.params + const requestId = generateRequestId() + + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { source, policy } = await assertCanFork(sourceWorkspaceId, session.user.id) + + const parsed = await parseRequest(forkWorkspaceContract, req, context) + if (!parsed.success) return parsed.response + + const copy = parsed.data.body.copy + const result = await createFork({ + source, + policy, + userId: session.user.id, + name: parsed.data.body.name, + selection: { + files: copy?.files ?? [], + tables: copy?.tables ?? [], + knowledgeBases: copy?.knowledgeBases ?? [], + customTools: copy?.customTools ?? [], + skills: copy?.skills ?? [], + mcpServers: copy?.mcpServers ?? [], + }, + requestId, + }) + + recordAudit({ + workspaceId: result.workspace.id, + actorId: session.user.id, + action: AuditAction.WORKSPACE_FORKED, + resourceType: AuditResourceType.WORKSPACE, + resourceId: result.workspace.id, + actorName: session.user.name ?? undefined, + actorEmail: session.user.email ?? undefined, + resourceName: result.workspace.name, + description: `Forked workspace from "${source.name}"`, + metadata: { + parentWorkspaceId: source.id, + workflowsCopied: result.workflowsCopied, + }, + request: req, + }) + + logger.info(`[${requestId}] Forked workspace ${sourceWorkspaceId} -> ${result.workspace.id}`) + return NextResponse.json(result, { status: 201 }) + } +) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/fork-workspace-modal/fork-workspace-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/fork-workspace-modal/fork-workspace-modal.tsx new file mode 100644 index 00000000000..72e7e8997a5 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/fork-workspace-modal/fork-workspace-modal.tsx @@ -0,0 +1,292 @@ +'use client' + +import { useEffect, useMemo, useState } from 'react' +import { ChevronRight, Search } from 'lucide-react' +import { useRouter } from 'next/navigation' +import { + Checkbox, + ChipInput, + ChipModal, + ChipModalBody, + ChipModalError, + ChipModalField, + ChipModalFooter, + ChipModalHeader, + toast, +} from '@/components/emcn' +import type { + ForkCopyableResource, + GetForkResourcesResponse, +} from '@/lib/api/contracts/workspace-fork' +import { cn } from '@/lib/core/utils/cn' +import { useForkResources, useForkWorkspace } from '@/hooks/queries/workspace-fork' + +interface ForkWorkspaceModalProps { + open: boolean + onOpenChange: (open: boolean) => void + sourceWorkspaceId: string + sourceWorkspaceName: string +} + +type ResourceKey = Exclude +type ResourceSelection = Record> + +const RESOURCE_KINDS: ReadonlyArray<{ key: ResourceKey; label: string }> = [ + { key: 'files', label: 'Files' }, + { key: 'tables', label: 'Tables' }, + { key: 'knowledgeBases', label: 'Knowledge bases' }, + { key: 'customTools', label: 'Custom tools' }, + { key: 'skills', label: 'Skills' }, + { key: 'mcpServers', label: 'MCP servers' }, +] + +/** Show the inline search once a kind has more entries than fit comfortably. */ +const SEARCH_THRESHOLD = 8 + +const emptySelection = (): ResourceSelection => ({ + files: new Set(), + tables: new Set(), + knowledgeBases: new Set(), + customTools: new Set(), + skills: new Set(), + mcpServers: new Set(), +}) + +/** + * One expandable resource kind in the fork picker: a tri-state "select all" header + * (count of selected / total) plus, when expanded, a searchable scrollable list of + * individual resources so the user can copy a specific subset. + */ +function ResourceKindRow({ + label, + items, + selected, + onToggleAll, + onToggleItem, + disabled, +}: { + label: string + items: ForkCopyableResource[] + selected: Set + onToggleAll: (selectAll: boolean) => void + onToggleItem: (id: string, checked: boolean) => void + disabled: boolean +}) { + const [expanded, setExpanded] = useState(false) + const [query, setQuery] = useState('') + + const total = items.length + const selectedCount = selected.size + const headerState = selectedCount === 0 ? false : selectedCount === total ? true : 'indeterminate' + + const filtered = useMemo(() => { + const trimmed = query.trim().toLowerCase() + if (!trimmed) return items + return items.filter((item) => item.label.toLowerCase().includes(trimmed)) + }, [items, query]) + + return ( +
+
+ onToggleAll(headerState !== true)} + disabled={disabled} + /> + +
+ + {expanded ? ( +
+ {total > SEARCH_THRESHOLD ? ( + setQuery(event.target.value)} + placeholder={`Search ${label.toLowerCase()}`} + disabled={disabled} + /> + ) : null} +
+ {filtered.map((item) => { + const isChecked = selected.has(item.id) + return ( + + ) + })} + {filtered.length === 0 ? ( +

No matches

+ ) : null} +
+
+ ) : null} +
+ ) +} + +/** + * Names and creates a fork of the current workspace, lets the user pick which + * resources to copy (whole kinds or a specific subset), then navigates into the new + * fork. Unselected resources leave the corresponding workflow subblocks empty. + */ +export function ForkWorkspaceModal({ + open, + onOpenChange, + sourceWorkspaceId, + sourceWorkspaceName, +}: ForkWorkspaceModalProps) { + const router = useRouter() + const forkWorkspace = useForkWorkspace() + const resources = useForkResources(sourceWorkspaceId, open) + const [name, setName] = useState('') + const [selected, setSelected] = useState(emptySelection) + const [error, setError] = useState(null) + + useEffect(() => { + if (open) { + setName(`${sourceWorkspaceName} (fork)`) + setSelected(emptySelection()) + setError(null) + } + }, [open, sourceWorkspaceName]) + + const isForking = forkWorkspace.isPending + + const availableKinds = useMemo( + () => RESOURCE_KINDS.filter((kind) => (resources.data?.[kind.key].length ?? 0) > 0), + [resources.data] + ) + + // A fork always produces a usable workspace: deployed workflows are copied, and + // when the source has none, create-fork seeds a blank starter workflow (plus any + // selected resources). So forking is never blocked - we just set expectations when + // there are no deployed workflows to carry over. + const noDeployedWorkflows = + Boolean(resources.data) && (resources.data?.deployedWorkflowCount ?? 0) === 0 + + const handleSubmit = () => { + const trimmed = name.trim() + if (!trimmed || isForking) return + setError(null) + const copy = resources.data + ? Object.fromEntries(RESOURCE_KINDS.map((kind) => [kind.key, Array.from(selected[kind.key])])) + : undefined + const copyingResources = RESOURCE_KINDS.some((kind) => selected[kind.key].size > 0) + forkWorkspace.mutate( + { workspaceId: sourceWorkspaceId, body: { name: trimmed, copy } }, + { + onSuccess: (result) => { + toast.success( + copyingResources + ? `Forked to "${result.workspace.name}" — copying selected resources in the background` + : `Forked to "${result.workspace.name}"` + ) + onOpenChange(false) + router.push(`/workspace/${result.workspace.id}/w`) + }, + onError: (err) => setError(err.message || 'Failed to fork workspace'), + } + ) + } + + return ( + + onOpenChange(false)}>Fork workspace + + + + {availableKinds.length > 0 ? ( + +
+ {availableKinds.map((kind) => ( + + setSelected((prev) => ({ + ...prev, + [kind.key]: selectAll + ? new Set((resources.data?.[kind.key] ?? []).map((item) => item.id)) + : new Set(), + })) + } + onToggleItem={(id, checked) => + setSelected((prev) => { + const next = new Set(prev[kind.key]) + if (checked) next.add(id) + else next.delete(id) + return { ...prev, [kind.key]: next } + }) + } + disabled={isForking} + /> + ))} +

+ Unselected resources leave their workflow fields empty in the fork. +

+
+
+ ) : null} + {noDeployedWorkflows ? ( +

+ No deployed workflows to copy — your fork will start with a blank workflow. +

+ ) : null} + {error ?? undefined} +
+ onOpenChange(false)} + cancelDisabled={isForking} + primaryAction={{ + label: isForking ? 'Forking...' : 'Fork', + onClick: handleSubmit, + disabled: !name.trim() || isForking, + }} + /> +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/promote-workspace-modal/promote-workspace-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/promote-workspace-modal/promote-workspace-modal.tsx new file mode 100644 index 00000000000..c0a3d8fea0a --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/promote-workspace-modal/promote-workspace-modal.tsx @@ -0,0 +1,502 @@ +'use client' + +import { useEffect, useMemo, useState } from 'react' +import { getErrorMessage } from '@sim/utils/errors' +import { ArrowRight } from 'lucide-react' +import { + Chip, + ChipCombobox, + ChipConfirmModal, + ChipModal, + ChipModalBody, + ChipModalField, + ChipModalFooter, + ChipModalHeader, + Tooltip, + toast, +} from '@/components/emcn' +import type { + ForkLineageNodeApi, + ForkMappingEntry, + ForkWorkflowChange, +} from '@/lib/api/contracts/workspace-fork' +import { + type ForkDirection, + useForkDiff, + useForkMapping, + usePromoteFork, + useRollbackFork, + useUpdateForkMapping, +} from '@/hooks/queries/workspace-fork' + +interface PromoteWorkspaceModalProps { + open: boolean + onOpenChange: (open: boolean) => void + workspaceId: string + /** 'sync' pushes/pulls along the parent edge; 'manage' lists this workspace's forks. */ + mode: 'sync' | 'manage' + parent: ForkLineageNodeApi | null + childWorkspaces: ForkLineageNodeApi[] + undoableRun: { otherWorkspaceId: string; otherName: string; direction: ForkDirection } | null +} + +const entryKey = (entry: ForkMappingEntry) => `${entry.kind}:${entry.sourceId}` + +/** Join "N label" segments with " · ", dropping any zero counts so toasts never read "0 foo". */ +function summarizeCounts(parts: Array<[number, string]>): string { + return parts + .filter(([count]) => count > 0) + .map(([count, label]) => `${count} ${label}`) + .join(' · ') +} + +/** Section label + display order per mapping kind (grouped, Secrets-page style). */ +const MAPPING_SECTION: Record = { + credential: { label: 'Credentials', order: 0 }, + 'env-var': { label: 'Environment variables', order: 1 }, + table: { label: 'Tables', order: 2 }, + 'knowledge-base': { label: 'Knowledge bases', order: 3 }, + 'knowledge-document': { label: 'Knowledge documents', order: 4 }, + file: { label: 'Files', order: 5 }, + 'mcp-server': { label: 'MCP servers', order: 6 }, + 'custom-tool': { label: 'Custom tools', order: 7 }, + skill: { label: 'Skills', order: 8 }, +} + +interface EdgeOption { + value: string + label: string + otherWorkspaceId: string + direction: ForkDirection +} + +/** + * One mapping as a `display: contents` row so its cells snap into the parent grid + * (Secrets-page grammar): source name on the left, target selector inline on the + * right, columns aligned across every row. A required source is marked with `*`. + */ +function MappingRow({ + entry, + value, + onChange, +}: { + entry: ForkMappingEntry + value: string + onChange: (value: string) => void +}) { + return ( +
+
+ {entry.sourceLabel} + {entry.required ? ( + + * + + ) : null} +
+
+ ({ + label: candidate.label, + value: candidate.id, + }))} + value={value || undefined} + onChange={onChange} + placeholder='Select target' + /> +
+ ) +} + +/** + * Fork sync surface. From a fork (has a parent) it runs a force push/pull along the + * parent edge: pick a direction, map required secrets and optional resources, + * preview the per-workflow change set, and sync - with a force-confirm on drift. + * From a fork root (no parent) it lists the forks for management. Either way, the + * last sync into this workspace can be rolled back to its prior deployed versions. + */ +export function PromoteWorkspaceModal({ + open, + onOpenChange, + workspaceId, + mode, + parent, + childWorkspaces, + undoableRun, +}: PromoteWorkspaceModalProps) { + // Sync is only ever performed along the parent edge (from a fork toward its + // parent). Child edges are intentionally not exposed here - a parent manages its + // forks (read-only list) rather than pushing/pulling into them. + const edgeOptions = useMemo(() => { + if (!parent) return [] + return [ + { + value: `push:${parent.id}`, + label: `Push to ${parent.name}`, + otherWorkspaceId: parent.id, + direction: 'push', + }, + { + value: `pull:${parent.id}`, + label: `Pull from ${parent.name}`, + otherWorkspaceId: parent.id, + direction: 'pull', + }, + ] + }, [parent]) + + const [selectedKey, setSelectedKey] = useState('') + const [targets, setTargets] = useState>({}) + const [confirmDriftOpen, setConfirmDriftOpen] = useState(false) + const [confirmRollbackOpen, setConfirmRollbackOpen] = useState(false) + const [submitting, setSubmitting] = useState(false) + const rollback = useRollbackFork() + + useEffect(() => { + if (open) { + setSelectedKey(edgeOptions[0]?.value ?? '') + setTargets({}) + } + }, [open, edgeOptions]) + + const selected = edgeOptions.find((option) => option.value === selectedKey) + const otherWorkspaceId = selected?.otherWorkspaceId + const direction = selected?.direction ?? 'push' + // 'manage' lists this workspace's forks; 'sync' pushes/pulls along the parent edge. + // A mid-chain workspace (a fork that itself has forks) supports both, chosen by + // which menu entry opened the modal. + const isManage = mode === 'manage' + + const mapping = useForkMapping({ workspaceId, otherWorkspaceId, direction, enabled: open }) + const diff = useForkDiff({ workspaceId, otherWorkspaceId, direction, enabled: open }) + const updateMapping = useUpdateForkMapping() + const promote = usePromoteFork() + + const entries = useMemo(() => mapping.data?.entries ?? [], [mapping.data]) + + // Seed defaults for newly-seen entries and prune entries that no longer exist, + // but preserve targets the user has already chosen - a background refetch of the + // same edge must not clobber in-progress mapping edits. + useEffect(() => { + setTargets((prev) => { + const next: Record = {} + for (const entry of entries) { + const key = entryKey(entry) + next[key] = key in prev ? prev[key] : (entry.targetId ?? '') + } + return next + }) + }, [entries]) + + const requiredComplete = entries.every( + (entry) => !entry.required || (targets[entryKey(entry)] ?? '') !== '' + ) + + // Group mappings by resource type into Secrets-page-style sections: the section + // header conveys the type (no per-row icons), required types sort to the top. + const groupedEntries = useMemo(() => { + const groups = new Map() + for (const entry of entries) { + const list = groups.get(entry.kind) + if (list) list.push(entry) + else groups.set(entry.kind, [entry]) + } + return Array.from(groups, ([kind, items]) => ({ + kind, + label: MAPPING_SECTION[kind].label, + items: items.slice().sort((a, b) => a.sourceLabel.localeCompare(b.sourceLabel)), + })).sort((a, b) => MAPPING_SECTION[a.kind].order - MAPPING_SECTION[b.kind].order) + }, [entries]) + + const runPromote = async (force: boolean) => { + if (!otherWorkspaceId) return + setSubmitting(true) + try { + await updateMapping.mutateAsync({ + workspaceId, + body: { + otherWorkspaceId, + direction, + entries: entries.map((entry) => ({ + resourceType: entry.resourceType, + sourceId: entry.sourceId, + targetId: targets[entryKey(entry)] || null, + })), + }, + }) + + const result = await promote.mutateAsync({ + workspaceId, + body: { otherWorkspaceId, direction, force }, + }) + + if (!result.promoteRunId) { + if (result.unmappedRequired.length > 0) { + toast.error('Map all required credentials and secrets first') + return + } + if (result.drift) { + setConfirmDriftOpen(true) + return + } + toast.error('Sync did not complete') + return + } + + toast.success( + summarizeCounts([ + [result.updated, 'updated'], + [result.created, 'created'], + [result.archived, 'archived'], + [result.redeployed, 'redeployed'], + ]) || 'Sync complete' + ) + onOpenChange(false) + } catch (error) { + toast.error(getErrorMessage(error, 'Sync failed')) + } finally { + setSubmitting(false) + } + } + + const runRollback = async () => { + if (!undoableRun) return + setSubmitting(true) + try { + const result = await rollback.mutateAsync({ + workspaceId, + body: { otherWorkspaceId: undoableRun.otherWorkspaceId }, + }) + const summary = summarizeCounts([ + [result.restored, 'restored'], + [result.archived, 'removed'], + [result.unarchived, 'unarchived'], + [result.skipped, 'skipped'], + ]) + toast.success(summary ? `Undone · ${summary}` : 'Undone') + setConfirmRollbackOpen(false) + onOpenChange(false) + } catch (error) { + toast.error(getErrorMessage(error, 'Undo failed')) + } finally { + setSubmitting(false) + } + } + + const workflowChanges = useMemo(() => { + const order: Record = { update: 0, create: 1, archive: 2 } + return [...(diff.data?.workflows ?? [])].sort( + (a, b) => order[a.action] - order[b.action] || a.currentName.localeCompare(b.currentName) + ) + }, [diff.data?.workflows]) + + // Rollback lives in the footer (left-docked, like a destructive "Delete"). It's + // a custom slot so the explanatory text shows as a tooltip in BOTH states - the + // footer's declarative `disabledTooltip` only covers the greyed state. + const rollbackDisabled = submitting || !undoableRun + const rollbackTooltip = undoableRun + ? `The last sync into this workspace (from ${undoableRun.otherName}) can be undone — it restores each workflow's prior deployed version.` + : 'No sync to roll back yet.' + const showRollback = Boolean(undoableRun) || isManage + + return ( + <> + + onOpenChange(false)}> + {isManage ? 'Manage Forks' : 'Sync workspace'} + + + {isManage ? ( + + {childWorkspaces.length === 0 ? ( +
No forks yet.
+ ) : ( +
+ {childWorkspaces.map((fork) => ( +
+ {fork.name} +
+ ))} +
+ )} +
+ ) : ( + <> + + + {diff.data?.drift ? ( +
+ Target changed since the last sync — syncing will overwrite those changes. +
+ ) : null} + + {workflowChanges.length > 0 ? ( + +
+ {workflowChanges.map((change, index) => { + const renamed = change.currentName !== change.otherName + return ( +
+ + {change.currentName} + + {renamed ? ( + <> + + + {change.otherName} + + + ) : null} +
+ ) + })} +
+
+ ) : null} + + {(diff.data?.mcpReauthServerIds.length ?? 0) > 0 || + (diff.data?.inlineSecretSources.length ?? 0) > 0 ? ( + + {(diff.data?.mcpReauthServerIds.length ?? 0) > 0 ? ( +
+ {diff.data?.mcpReauthServerIds.length} MCP server(s) use OAuth and must be + re-authorized in the target workspace. +
+ ) : null} + {(diff.data?.inlineSecretSources.length ?? 0) > 0 ? ( +
+ {diff.data?.inlineSecretSources.length} inline secret(s) can't be auto-mapped + — set them in the target workspace. +
+ ) : null} +
+ ) : null} + + {groupedEntries.map((group) => ( +
+ {group.label} +
+
+ {group.items.map((entry) => ( + + setTargets((prev) => ({ ...prev, [entryKey(entry)]: value })) + } + /> + ))} +
+
+ ))} + + )} + + onOpenChange(false)} + cancelDisabled={submitting} + secondaryActions={ + showRollback + ? [ + { + custom: ( + + + + setConfirmRollbackOpen(true)} + disabled={rollbackDisabled} + className={rollbackDisabled ? 'pointer-events-none' : undefined} + > + Rollback + + + + {rollbackTooltip} + + ), + }, + ] + : undefined + } + primaryAction={ + isManage + ? { label: 'Done', onClick: () => onOpenChange(false) } + : { + label: submitting ? 'Working...' : 'Sync', + onClick: () => void runPromote(false), + disabled: + submitting || !otherWorkspaceId || !requiredComplete || mapping.isLoading, + disabledTooltip: requiredComplete + ? undefined + : 'Map all required credentials and secrets first', + } + } + /> + + + { + setConfirmDriftOpen(false) + void runPromote(true) + }, + pending: submitting, + pendingLabel: 'Syncing...', + }} + /> + + void runRollback(), + pending: submitting, + pendingLabel: 'Rolling back...', + }} + /> + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/use-forking-available.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/use-forking-available.ts new file mode 100644 index 00000000000..8231e7b6735 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/use-forking-available.ts @@ -0,0 +1,28 @@ +import { getSubscriptionAccessState } from '@/lib/billing/client' +import { getEnv, isTruthy } from '@/lib/core/config/env' +import { useWorkspaceOwnerBilling } from '@/hooks/queries/workspace' + +const isBillingEnabledClient = isTruthy(getEnv('NEXT_PUBLIC_BILLING_ENABLED')) +const isForkingEnabledClient = isTruthy(getEnv('NEXT_PUBLIC_FORKING_ENABLED')) + +/** + * Client mirror of the server fork EE gate (`assertForkingEnabled`): on Sim Cloud + * the active workspace's billed account (its owner's rolled-up plan) must be + * Enterprise; on self-hosted it's the `NEXT_PUBLIC_FORKING_ENABLED` override. Used + * to hide the fork UI (and skip the lineage query) for workspaces that cannot fork. + * + * Gating on the WORKSPACE's plan (not the viewer's) is what matches the server, + * which checks the workspace org's plan: a viewer who belongs to a different + * Enterprise org no longer sees fork UI on a non-Enterprise workspace, and a + * member of an Enterprise workspace isn't denied it just because their own + * highest plan is lower. The server gate remains the security boundary. + * + * Self-hosted relies on `NEXT_PUBLIC_FORKING_ENABLED` / `NEXT_PUBLIC_BILLING_ENABLED` + * mirroring the server's `FORKING_ENABLED` / `BILLING_ENABLED`; set each pair + * together or the UI and API will disagree. + */ +export function useForkingAvailable(workspaceId?: string): boolean { + const { data } = useWorkspaceOwnerBilling(isBillingEnabledClient ? workspaceId : undefined) + if (!isBillingEnabledClient) return isForkingEnabledClient + return getSubscriptionAccessState(data).hasUsableEnterpriseAccess +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx index 499f79b91b2..c286708d027 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx @@ -15,15 +15,21 @@ import { Plus, Send, Skeleton, + Tooltip, } from '@/components/emcn' -import { ManageWorkspace, PanelLeft } from '@/components/emcn/icons' +import { ManageWorkspace, PanelLeft, Rocket, Shuffle } from '@/components/emcn/icons' import { cn } from '@/lib/core/utils/cn' +import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { isBillingEnabled } from '@/app/workspace/[workspaceId]/settings/navigation' import { ContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu' import { DeleteModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/delete-modal/delete-modal' import { CreateWorkspaceModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/create-workspace-modal/create-workspace-modal' +import { ForkWorkspaceModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/fork-workspace-modal/fork-workspace-modal' import { InviteModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal' +import { PromoteWorkspaceModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/promote-workspace-modal/promote-workspace-modal' +import { useForkingAvailable } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/use-forking-available' import type { Workspace, WorkspaceCreationPolicy } from '@/hooks/queries/workspace' +import { useForkLineage } from '@/hooks/queries/workspace-fork' import { usePermissionConfig } from '@/hooks/use-permission-config' import { useSettingsNavigation } from '@/hooks/use-settings-navigation' @@ -96,6 +102,9 @@ function WorkspaceHeaderImpl({ }: WorkspaceHeaderProps) { const [isCreateModalOpen, setIsCreateModalOpen] = useState(false) const [isInviteModalOpen, setIsInviteModalOpen] = useState(false) + const [isForkModalOpen, setIsForkModalOpen] = useState(false) + const [isPromoteModalOpen, setIsPromoteModalOpen] = useState(false) + const [forkModalMode, setForkModalMode] = useState<'sync' | 'manage'>('sync') const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false) const [deleteTarget, setDeleteTarget] = useState(null) const [isLeaveModalOpen, setIsLeaveModalOpen] = useState(false) @@ -120,6 +129,14 @@ function WorkspaceHeaderImpl({ }, []) const { navigateToSettings } = useSettingsNavigation() + const forkingAvailable = useForkingAvailable(workspaceId) + const { canAdmin } = useUserPermissionsContext() + // Forking and sync rewrite workflow state and deployments en masse, so they are + // workspace-admin only (org owners/admins derive workspace admin server-side via + // the resolved viewer permission). Every fork route re-checks this; gating the + // entry points here just keeps the UI honest. The server remains the boundary. + const canUseForking = forkingAvailable && canAdmin + const { data: forkLineage } = useForkLineage(workspaceId, canUseForking) const activeWorkspaceFull = workspaces.find((w) => w.id === workspaceId) || null const isWorkspaceReady = !isWorkspacesLoading && activeWorkspaceFull !== null @@ -383,6 +400,10 @@ function WorkspaceHeaderImpl({ const initial = (stripped[0] || workspace.name[0] || 'W').toUpperCase() const isActive = workspace.id === workspaceId const isMenuOpen = menuOpenWorkspaceId === workspace.id + const forkedFromName = workspace.forkedFromWorkspaceId + ? (workspaces.find((w) => w.id === workspace.forkedFromWorkspaceId)?.name ?? + 'another workspace') + : null return (
@@ -501,6 +522,16 @@ function WorkspaceHeaderImpl({ {workspace.name} + {forkedFromName ? ( + + + + + + + Fork of {forkedFromName} + + ) : null}
@@ -673,6 +756,21 @@ function WorkspaceHeaderImpl({ inviteDisabledReason={inviteDisabledReason} organizationId={activeWorkspaceFull?.organizationId ?? null} /> + + setIsDeleteModalOpen(false)} diff --git a/apps/sim/background/fork-content-copy.ts b/apps/sim/background/fork-content-copy.ts new file mode 100644 index 00000000000..836b387d48e --- /dev/null +++ b/apps/sim/background/fork-content-copy.ts @@ -0,0 +1,25 @@ +import { task } from '@trigger.dev/sdk' +import { + type ForkContentCopyPayload, + runForkContentCopy, +} from '@/lib/workspaces/fork/copy/content-copy-runner' + +/** + * Trigger.dev wrapper for the post-fork heavy-content copy (table rows, KB + * documents + embeddings, file blobs). Backgrounding keeps the fork request fast + * and lets the copy survive app deploys. `maxAttempts: 1` — the copy is + * non-transactional best-effort (per-row inserts with fresh ids), so a blind + * re-run would duplicate rows; a partial failure simply leaves the fork's content + * incomplete (the workflows themselves committed synchronously). + */ +export const forkContentCopyTask = task({ + id: 'fork-content-copy', + retry: { maxAttempts: 1 }, + queue: { + name: 'fork-content-copy', + concurrencyLimit: 10, + }, + run: async (payload: ForkContentCopyPayload) => { + await runForkContentCopy(payload) + }, +}) diff --git a/apps/sim/hooks/queries/workspace-fork.ts b/apps/sim/hooks/queries/workspace-fork.ts new file mode 100644 index 00000000000..4f33c3f4203 --- /dev/null +++ b/apps/sim/hooks/queries/workspace-fork.ts @@ -0,0 +1,157 @@ +import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { requestJson } from '@/lib/api/client/request' +import { + type ForkWorkspaceBody, + forkWorkspaceContract, + getForkDiffContract, + getForkLineageContract, + getForkMappingContract, + getForkResourcesContract, + type PromoteForkBody, + promoteForkContract, + type RollbackForkBody, + rollbackForkContract, + type UpdateForkMappingBody, + updateForkMappingContract, +} from '@/lib/api/contracts/workspace-fork' +import type { WorkspacesResponse } from '@/lib/api/contracts/workspaces' +import { workspaceKeys } from '@/hooks/queries/workspace' + +export type ForkDirection = 'push' | 'pull' + +export const forkKeys = { + all: ['workspace-fork'] as const, + lineages: () => [...forkKeys.all, 'lineage'] as const, + lineage: (workspaceId?: string) => [...forkKeys.lineages(), workspaceId ?? ''] as const, + mappings: () => [...forkKeys.all, 'mapping'] as const, + mapping: (workspaceId?: string, otherWorkspaceId?: string, direction?: ForkDirection) => + [...forkKeys.mappings(), workspaceId ?? '', otherWorkspaceId ?? '', direction ?? ''] as const, + diffs: () => [...forkKeys.all, 'diff'] as const, + diff: (workspaceId?: string, otherWorkspaceId?: string, direction?: ForkDirection) => + [...forkKeys.diffs(), workspaceId ?? '', otherWorkspaceId ?? '', direction ?? ''] as const, + resourcesAll: () => [...forkKeys.all, 'resources'] as const, + resources: (workspaceId?: string) => [...forkKeys.resourcesAll(), workspaceId ?? ''] as const, +} + +export function useForkResources(workspaceId?: string, enabled = true) { + return useQuery({ + queryKey: forkKeys.resources(workspaceId), + queryFn: ({ signal }) => + requestJson(getForkResourcesContract, { params: { id: workspaceId as string }, signal }), + enabled: Boolean(workspaceId) && enabled, + staleTime: 30 * 1000, + }) +} + +export function useForkLineage(workspaceId?: string, enabled = true) { + return useQuery({ + queryKey: forkKeys.lineage(workspaceId), + queryFn: ({ signal }) => + requestJson(getForkLineageContract, { params: { id: workspaceId as string }, signal }), + enabled: Boolean(workspaceId) && enabled, + staleTime: 30 * 1000, + placeholderData: keepPreviousData, + }) +} + +export function useForkWorkspace() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: (vars: { workspaceId: string; body: ForkWorkspaceBody }) => + requestJson(forkWorkspaceContract, { params: { id: vars.workspaceId }, body: vars.body }), + onSuccess: (data) => { + // Merge the new fork into the active list cache before invalidation so the + // immediate navigation into it can't race a stale list and trip the + // not-in-workspaces redirect (mirrors useCreateWorkspace). + const newWorkspace = data.workspace + queryClient.setQueryData(workspaceKeys.list('active'), (previous) => { + if (!previous) { + return { workspaces: [newWorkspace], lastActiveWorkspaceId: null, creationPolicy: null } + } + if (previous.workspaces.some((w) => w.id === newWorkspace.id)) { + return previous + } + return { ...previous, workspaces: [newWorkspace, ...previous.workspaces] } + }) + queryClient.invalidateQueries({ queryKey: workspaceKeys.lists() }) + queryClient.invalidateQueries({ queryKey: workspaceKeys.adminLists() }) + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: forkKeys.lineages() }) + }, + }) +} + +export function useForkMapping(args: { + workspaceId?: string + otherWorkspaceId?: string + direction: ForkDirection + enabled?: boolean +}) { + return useQuery({ + queryKey: forkKeys.mapping(args.workspaceId, args.otherWorkspaceId, args.direction), + queryFn: ({ signal }) => + requestJson(getForkMappingContract, { + params: { id: args.workspaceId as string }, + query: { otherWorkspaceId: args.otherWorkspaceId as string, direction: args.direction }, + signal, + }), + enabled: Boolean(args.workspaceId && args.otherWorkspaceId) && (args.enabled ?? true), + staleTime: 15 * 1000, + placeholderData: keepPreviousData, + }) +} + +export function useUpdateForkMapping() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: (vars: { workspaceId: string; body: UpdateForkMappingBody }) => + requestJson(updateForkMappingContract, { params: { id: vars.workspaceId }, body: vars.body }), + onSettled: () => { + queryClient.invalidateQueries({ queryKey: forkKeys.mappings() }) + queryClient.invalidateQueries({ queryKey: forkKeys.diffs() }) + }, + }) +} + +export function useForkDiff(args: { + workspaceId?: string + otherWorkspaceId?: string + direction: ForkDirection + enabled?: boolean +}) { + return useQuery({ + queryKey: forkKeys.diff(args.workspaceId, args.otherWorkspaceId, args.direction), + queryFn: ({ signal }) => + requestJson(getForkDiffContract, { + params: { id: args.workspaceId as string }, + query: { otherWorkspaceId: args.otherWorkspaceId as string, direction: args.direction }, + signal, + }), + enabled: Boolean(args.workspaceId && args.otherWorkspaceId) && (args.enabled ?? true), + staleTime: 10 * 1000, + placeholderData: keepPreviousData, + }) +} + +export function usePromoteFork() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: (vars: { workspaceId: string; body: PromoteForkBody }) => + requestJson(promoteForkContract, { params: { id: vars.workspaceId }, body: vars.body }), + onSettled: () => { + queryClient.invalidateQueries({ queryKey: forkKeys.all }) + }, + }) +} + +export function useRollbackFork() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: (vars: { workspaceId: string; body: RollbackForkBody }) => + requestJson(rollbackForkContract, { params: { id: vars.workspaceId }, body: vars.body }), + onSettled: () => { + queryClient.invalidateQueries({ queryKey: forkKeys.all }) + }, + }) +} diff --git a/apps/sim/lib/api/contracts/workspace-fork.test.ts b/apps/sim/lib/api/contracts/workspace-fork.test.ts new file mode 100644 index 00000000000..f1851ec9686 --- /dev/null +++ b/apps/sim/lib/api/contracts/workspace-fork.test.ts @@ -0,0 +1,62 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { + forkMappableResourceTypeSchema, + updateForkMappingBodySchema, +} from '@/lib/api/contracts/workspace-fork' + +describe('forkMappableResourceTypeSchema', () => { + it('rejects the system-managed workflow type', () => { + expect(forkMappableResourceTypeSchema.safeParse('workflow').success).toBe(false) + }) + + it('accepts user-mappable resource types', () => { + for (const type of [ + 'oauth_credential', + 'service_account_credential', + 'env_var', + 'table', + 'knowledge_base', + 'knowledge_document', + 'file', + 'mcp_server', + 'custom_tool', + 'skill', + ]) { + expect(forkMappableResourceTypeSchema.safeParse(type).success).toBe(true) + } + }) +}) + +describe('updateForkMappingBodySchema', () => { + const base = { otherWorkspaceId: 'ws-1', direction: 'push' as const } + + it('rejects a body that maps a workflow-type entry', () => { + const result = updateForkMappingBodySchema.safeParse({ + ...base, + entries: [{ resourceType: 'workflow', sourceId: 'wf-src', targetId: 'wf-tgt' }], + }) + expect(result.success).toBe(false) + }) + + it('accepts mappable entries, including a cleared (null target) mapping', () => { + const result = updateForkMappingBodySchema.safeParse({ + ...base, + entries: [ + { resourceType: 'env_var', sourceId: 'API_KEY', targetId: 'API_KEY' }, + { resourceType: 'oauth_credential', sourceId: 'cred-1', targetId: null }, + ], + }) + expect(result.success).toBe(true) + }) + + it('rejects an entry with an empty sourceId', () => { + const result = updateForkMappingBodySchema.safeParse({ + ...base, + entries: [{ resourceType: 'env_var', sourceId: '', targetId: 'API_KEY' }], + }) + expect(result.success).toBe(false) + }) +}) diff --git a/apps/sim/lib/api/contracts/workspace-fork.ts b/apps/sim/lib/api/contracts/workspace-fork.ts new file mode 100644 index 00000000000..9ffeb24a309 --- /dev/null +++ b/apps/sim/lib/api/contracts/workspace-fork.ts @@ -0,0 +1,287 @@ +import { z } from 'zod' +import { nonEmptyIdSchema, workspaceIdSchema } from '@/lib/api/contracts/primitives' +import { defineRouteContract } from '@/lib/api/contracts/types' +import { workspaceSchema } from '@/lib/api/contracts/workspaces' + +const workspaceIdParamsSchema = z.object({ id: nonEmptyIdSchema }) + +export const forkRemapKindSchema = z.enum([ + 'credential', + 'env-var', + 'knowledge-base', + 'knowledge-document', + 'table', + 'file', + 'mcp-server', + 'custom-tool', + 'skill', +]) + +export const forkResourceTypeSchema = z.enum([ + 'workflow', + 'oauth_credential', + 'service_account_credential', + 'env_var', + 'table', + 'knowledge_base', + 'knowledge_document', + 'file', + 'mcp_server', + 'custom_tool', + 'skill', +]) + +/** + * Resource types a user may map via the mapping editor. Excludes `workflow`: + * workflow identity is system-managed (seeded at fork, maintained by promote, + * dissolved by rollback) and must never be written through the mapping editor, or + * a crafted entry could repoint a promote at the wrong target workflow. + */ +export const forkMappableResourceTypeSchema = forkResourceTypeSchema.exclude(['workflow']) +export type ForkMappableResourceType = z.infer + +export const forkDirectionSchema = z.enum(['push', 'pull']) + +export const forkLineageNodeSchema = z.object({ + id: z.string(), + name: z.string(), + organizationId: z.string().nullable(), +}) + +export const getForkLineageContract = defineRouteContract({ + method: 'GET', + path: '/api/workspaces/[id]/fork/lineage', + params: workspaceIdParamsSchema, + response: { + mode: 'json', + schema: z.object({ + workspaceId: z.string(), + parent: forkLineageNodeSchema.nullable(), + children: z.array(forkLineageNodeSchema), + hasUndoableRun: z.boolean(), + /** The most recent undoable promote into this workspace, for the rollback UI. */ + undoableRun: z + .object({ + otherWorkspaceId: z.string(), + otherName: z.string(), + direction: forkDirectionSchema, + }) + .nullable(), + }), + }, +}) +export type ForkLineageNodeApi = z.output +export type GetForkLineageResponse = z.output + +const forkResourceIdList = z.array(nonEmptyIdSchema).max(2000).optional() + +export const forkResourceSelectionSchema = z.object({ + files: forkResourceIdList, + tables: forkResourceIdList, + knowledgeBases: forkResourceIdList, + customTools: forkResourceIdList, + skills: forkResourceIdList, + mcpServers: forkResourceIdList, +}) + +export const forkWorkspaceBodySchema = z.object({ + name: z.string().min(1, 'Name is required').max(100, 'Name is too long').optional(), + copy: forkResourceSelectionSchema.optional(), +}) +export const forkWorkspaceContract = defineRouteContract({ + method: 'POST', + path: '/api/workspaces/[id]/fork', + params: workspaceIdParamsSchema, + body: forkWorkspaceBodySchema, + response: { + mode: 'json', + schema: z.object({ + // Full workspace row so the client can merge it into the workspace-list cache + // (parity with create), not just the lineage node. + workspace: workspaceSchema, + workflowsCopied: z.number().int(), + }), + }, +}) +export type ForkWorkspaceBody = z.input +export type ForkWorkspaceResponse = z.output + +export const forkCopyableResourceSchema = z.object({ id: z.string(), label: z.string() }) +export type ForkCopyableResource = z.output +export const getForkResourcesContract = defineRouteContract({ + method: 'GET', + path: '/api/workspaces/[id]/fork/resources', + params: workspaceIdParamsSchema, + response: { + mode: 'json', + schema: z.object({ + files: z.array(forkCopyableResourceSchema), + tables: z.array(forkCopyableResourceSchema), + knowledgeBases: z.array(forkCopyableResourceSchema), + customTools: z.array(forkCopyableResourceSchema), + skills: z.array(forkCopyableResourceSchema), + mcpServers: z.array(forkCopyableResourceSchema), + deployedWorkflowCount: z.number().int(), + }), + }, +}) +export type GetForkResourcesResponse = z.output + +export const forkMappingCandidateSchema = z.object({ + id: z.string(), + label: z.string(), + providerId: z.string().optional(), +}) + +export const forkMappingEntrySchema = z.object({ + kind: forkRemapKindSchema, + resourceType: forkMappableResourceTypeSchema, + sourceId: z.string(), + sourceLabel: z.string(), + targetId: z.string().nullable(), + required: z.boolean(), + candidates: z.array(forkMappingCandidateSchema), +}) +export type ForkMappingEntry = z.output + +export const getForkMappingQuerySchema = z.object({ + otherWorkspaceId: workspaceIdSchema, + direction: forkDirectionSchema.default('push'), +}) +export const getForkMappingContract = defineRouteContract({ + method: 'GET', + path: '/api/workspaces/[id]/fork/mapping', + params: workspaceIdParamsSchema, + query: getForkMappingQuerySchema, + response: { + mode: 'json', + schema: z.object({ + childWorkspaceId: z.string(), + parentWorkspaceId: z.string(), + sourceWorkspaceId: z.string(), + targetWorkspaceId: z.string(), + entries: z.array(forkMappingEntrySchema), + }), + }, +}) +export type GetForkMappingResponse = z.output + +export const updateForkMappingBodySchema = z.object({ + otherWorkspaceId: workspaceIdSchema, + direction: forkDirectionSchema, + entries: z + .array( + z.object({ + resourceType: forkMappableResourceTypeSchema, + sourceId: z.string().min(1), + targetId: z.string().min(1).nullable(), + }) + ) + .max(5000), +}) +export const updateForkMappingContract = defineRouteContract({ + method: 'PUT', + path: '/api/workspaces/[id]/fork/mapping', + params: workspaceIdParamsSchema, + body: updateForkMappingBodySchema, + response: { + mode: 'json', + schema: z.object({ success: z.literal(true), updated: z.number().int() }), + }, +}) +export type UpdateForkMappingBody = z.input + +export const forkUnmappedReferenceSchema = z.object({ + kind: forkRemapKindSchema, + sourceId: z.string(), + required: z.boolean(), + blockName: z.string().optional(), +}) + +export const forkWorkflowChangeSchema = z.object({ + action: z.enum(['update', 'create', 'archive']), + /** Workflow name in the workspace the modal is open in. */ + currentName: z.string(), + /** Workflow name in the sync-partner workspace (differs from `currentName` after a rename). */ + otherName: z.string(), +}) + +export const getForkDiffQuerySchema = z.object({ + otherWorkspaceId: workspaceIdSchema, + direction: forkDirectionSchema, +}) +export const getForkDiffContract = defineRouteContract({ + method: 'GET', + path: '/api/workspaces/[id]/fork/diff', + params: workspaceIdParamsSchema, + query: getForkDiffQuerySchema, + response: { + mode: 'json', + schema: z.object({ + sourceWorkspaceId: z.string(), + targetWorkspaceId: z.string(), + willUpdate: z.number().int(), + willCreate: z.number().int(), + willArchive: z.number().int(), + /** Per-workflow change list for the sync preview. */ + workflows: z.array(forkWorkflowChangeSchema), + unmappedRequired: z.array(forkUnmappedReferenceSchema), + unmappedOptional: z.array(forkUnmappedReferenceSchema), + /** Source MCP server ids that use OAuth and need re-authorization in the target. */ + mcpReauthServerIds: z.array(z.string()), + /** Review-only descriptions of inline secrets that cannot be id-mapped. */ + inlineSecretSources: z.array(z.string()), + drift: z.boolean(), + }), + }, +}) +export type GetForkDiffResponse = z.output +export type ForkWorkflowChange = z.output + +export const promoteForkBodySchema = z.object({ + otherWorkspaceId: workspaceIdSchema, + direction: forkDirectionSchema, + force: z.boolean().optional().default(false), +}) +export const promoteForkContract = defineRouteContract({ + method: 'POST', + path: '/api/workspaces/[id]/fork/promote', + params: workspaceIdParamsSchema, + body: promoteForkBodySchema, + response: { + mode: 'json', + schema: z.object({ + promoteRunId: z.string(), + updated: z.number().int(), + created: z.number().int(), + archived: z.number().int(), + redeployed: z.number().int(), + unmappedRequired: z.array(forkUnmappedReferenceSchema), + drift: z.boolean(), + }), + }, +}) +export type PromoteForkBody = z.input +export type PromoteForkResponse = z.output + +export const rollbackForkBodySchema = z.object({ + otherWorkspaceId: workspaceIdSchema, +}) +export const rollbackForkContract = defineRouteContract({ + method: 'POST', + path: '/api/workspaces/[id]/fork/rollback', + params: workspaceIdParamsSchema, + body: rollbackForkBodySchema, + response: { + mode: 'json', + schema: z.object({ + restored: z.number().int(), + archived: z.number().int(), + unarchived: z.number().int(), + /** Snapshot workflows that no longer exist and couldn't be reactivated. */ + skipped: z.number().int(), + }), + }, +}) +export type RollbackForkBody = z.input +export type RollbackForkResponse = z.output diff --git a/apps/sim/lib/api/contracts/workspaces.ts b/apps/sim/lib/api/contracts/workspaces.ts index ebee4b66972..661b16d7c05 100644 --- a/apps/sim/lib/api/contracts/workspaces.ts +++ b/apps/sim/lib/api/contracts/workspaces.ts @@ -23,6 +23,9 @@ export const workspaceSchema = z.object({ inviteMembersEnabled: z.boolean().optional(), inviteDisabledReason: z.string().nullable().optional(), inviteUpgradeRequired: z.boolean().optional(), + // Source workspace id when this was created as a fork (null otherwise). Optional + // because not every workspace response builder includes the column. + forkedFromWorkspaceId: z.string().nullable().optional(), }) export type Workspace = z.output diff --git a/apps/sim/lib/core/config/env-flags.ts b/apps/sim/lib/core/config/env-flags.ts index e980a452429..2623831ffec 100644 --- a/apps/sim/lib/core/config/env-flags.ts +++ b/apps/sim/lib/core/config/env-flags.ts @@ -160,6 +160,12 @@ export const isDataRetentionEnabled = isTruthy(env.DATA_RETENTION_ENABLED) */ export const isDataDrainsEnabled = isTruthy(env.DATA_DRAINS_ENABLED) +/** + * Is workspace forking enabled via env var override + * This bypasses hosted (Enterprise) requirements for self-hosted deployments + */ +export const isForkingEnabled = isTruthy(env.FORKING_ENABLED) + /** * Is E2B enabled for remote code execution */ diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 89924eb5685..76ba39b4692 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -405,6 +405,7 @@ export const env = createEnv({ AUDIT_LOGS_ENABLED: z.boolean().optional(), // Enable audit logs on self-hosted (bypasses hosted requirements) DATA_RETENTION_ENABLED: z.boolean().optional(), // Enable data retention settings on self-hosted (bypasses hosted requirements) DATA_DRAINS_ENABLED: z.boolean().optional(), // Enable data drains on self-hosted (bypasses hosted requirements) + FORKING_ENABLED: z.boolean().optional(), // Enable workspace forking on self-hosted (bypasses hosted requirements) // Organizations - for self-hosted deployments ORGANIZATIONS_ENABLED: z.boolean().optional(), // Enable organizations on self-hosted (bypasses plan requirements) @@ -504,6 +505,7 @@ export const env = createEnv({ NEXT_PUBLIC_AUDIT_LOGS_ENABLED: z.boolean().optional(), // Enable audit logs on self-hosted (bypasses hosted requirements) NEXT_PUBLIC_DATA_RETENTION_ENABLED: z.boolean().optional(), // Enable data retention settings on self-hosted (bypasses hosted requirements) NEXT_PUBLIC_DATA_DRAINS_ENABLED: z.boolean().optional(), // Enable data drains on self-hosted (bypasses hosted requirements) + NEXT_PUBLIC_FORKING_ENABLED: z.boolean().optional(), // Enable workspace forking on self-hosted (bypasses hosted requirements) NEXT_PUBLIC_WORKFLOW_COLUMNS_ENABLED: z.boolean().optional(), // Show the "Workflow" column type in user tables (defaults to false) NEXT_PUBLIC_ORGANIZATIONS_ENABLED: z.boolean().optional(), // Enable organizations on self-hosted (bypasses plan requirements) NEXT_PUBLIC_DISABLE_INVITATIONS: z.boolean().optional(), // Disable workspace invitations globally (for self-hosted deployments) @@ -543,6 +545,7 @@ export const env = createEnv({ NEXT_PUBLIC_AUDIT_LOGS_ENABLED: process.env.NEXT_PUBLIC_AUDIT_LOGS_ENABLED, NEXT_PUBLIC_DATA_RETENTION_ENABLED: process.env.NEXT_PUBLIC_DATA_RETENTION_ENABLED, NEXT_PUBLIC_DATA_DRAINS_ENABLED: process.env.NEXT_PUBLIC_DATA_DRAINS_ENABLED, + NEXT_PUBLIC_FORKING_ENABLED: process.env.NEXT_PUBLIC_FORKING_ENABLED, NEXT_PUBLIC_WORKFLOW_COLUMNS_ENABLED: process.env.NEXT_PUBLIC_WORKFLOW_COLUMNS_ENABLED, NEXT_PUBLIC_ORGANIZATIONS_ENABLED: process.env.NEXT_PUBLIC_ORGANIZATIONS_ENABLED, NEXT_PUBLIC_DISABLE_INVITATIONS: process.env.NEXT_PUBLIC_DISABLE_INVITATIONS, diff --git a/apps/sim/lib/workflows/persistence/duplicate.ts b/apps/sim/lib/workflows/persistence/duplicate.ts index 75a3ad2d340..ab075a8be5f 100644 --- a/apps/sim/lib/workflows/persistence/duplicate.ts +++ b/apps/sim/lib/workflows/persistence/duplicate.ts @@ -14,11 +14,17 @@ import { import { generateId } from '@sim/utils/id' import { and, eq, isNull, min } from 'drizzle-orm' import type { DbOrTx } from '@/lib/db/types' -import { remapConditionBlockIds, remapConditionEdgeHandle } from '@/lib/workflows/condition-ids' +import { remapConditionEdgeHandle } from '@/lib/workflows/condition-ids' +import { + remapConditionIdsInSubBlocks, + remapVariableIdsInSubBlocks, + remapWorkflowReferencesInSubBlocks, + type SubBlockRecord, + sanitizeSubBlocksForDuplicate, +} from '@/lib/workflows/persistence/remap-internal-ids' import { deduplicateWorkflowName } from '@/lib/workflows/utils' import type { Variable } from '@/stores/variables/types' import type { LoopConfig, ParallelConfig } from '@/stores/workflows/workflow/types' -import { SYSTEM_SUBBLOCK_IDS, TRIGGER_RUNTIME_SUBBLOCK_IDS } from '@/triggers/constants' const logger = createLogger('WorkflowDuplicateHelper') @@ -54,43 +60,6 @@ interface DuplicateWorkflowResult { edgesCount: number subflowsCount: number } -/** - * Untrusted shape of a persisted block subBlocks JSON column. We narrow `type`/`value` - * with runtime checks before mutating; the index signature exists because callers pass - * the raw record back to drizzle without knowing which subBlock keys it contains. - */ -type SubBlockRecord = Record - -/** - * Untrusted shape of a single entry inside a `variables-input` value array. The - * `variableId` slot is widened to `unknown` so we are forced to type-narrow before - * trusting it as a remap key — persisted JSON may legitimately predate the field. - */ -type VariableAssignment = Record & { variableId?: unknown } -const DUPLICATE_STRIPPED_SYSTEM_SUBBLOCK_IDS = new Set( - SYSTEM_SUBBLOCK_IDS.filter((id) => id !== 'triggerCredentials') -) - -function isRecord(value: unknown): value is Record { - return Boolean(value && typeof value === 'object' && !Array.isArray(value)) -} - -function isSystemSubBlockKey(key: string, ids: Set | string[]): boolean { - const idList = Array.isArray(ids) ? ids : Array.from(ids) - return idList.some((id) => key === id || key.startsWith(`${id}_`)) -} - -function sanitizeSubBlocksForDuplicate(subBlocks: SubBlockRecord): SubBlockRecord { - const sanitized: SubBlockRecord = {} - - for (const [key, subBlock] of Object.entries(subBlocks)) { - if (isSystemSubBlockKey(key, TRIGGER_RUNTIME_SUBBLOCK_IDS)) continue - if (isSystemSubBlockKey(key, DUPLICATE_STRIPPED_SYSTEM_SUBBLOCK_IDS)) continue - sanitized[key] = subBlock - } - - return sanitized -} async function assertTargetFolderMutable( tx: DbOrTx, @@ -124,149 +93,6 @@ async function assertTargetFolderMutable( } } -function remapVariableAssignment(value: unknown, varIdMap: Map): unknown { - if (Array.isArray(value)) { - return value.map((item) => remapVariableAssignment(item, varIdMap)) - } - - if (!isRecord(value)) { - return value - } - - const assignment = value as VariableAssignment - const next: Record = {} - for (const [key, nestedValue] of Object.entries(assignment)) { - next[key] = remapVariableAssignment(nestedValue, varIdMap) - } - - if (typeof assignment.variableId === 'string') { - const newVarId = varIdMap.get(assignment.variableId) - if (newVarId) { - next.variableId = newVarId - } else { - logger.warn('Skipping unknown variable reference during duplication', { - variableId: assignment.variableId, - }) - } - } - - return next -} - -function remapVariableInputValue(value: unknown, varIdMap: Map): unknown { - if (value == null) { - return value - } - - if (Array.isArray(value)) { - return remapVariableAssignment(value, varIdMap) - } - - if (typeof value === 'string') { - const trimmed = value.trim() - if (!trimmed) return value - - let parsed: unknown - try { - parsed = JSON.parse(trimmed) - } catch { - throw new Error('Variables input assignments could not be parsed for duplication') - } - if (Array.isArray(parsed)) { - return remapVariableAssignment(parsed, varIdMap) - } - throw new Error('Variables input assignments must be an array') - } - - throw new Error('Variables input assignments must be an array') -} - -/** - * Remaps old variable IDs to new variable IDs inside block subBlocks. - * Specifically targets `variables-input` subblocks whose value is an array - * of variable assignments containing a `variableId` field. - */ -function remapVariableIdsInSubBlocks( - subBlocks: SubBlockRecord, - varIdMap: Map -): SubBlockRecord { - const updated: SubBlockRecord = {} - - for (const [key, subBlock] of Object.entries(subBlocks)) { - if (subBlock && typeof subBlock === 'object' && subBlock.type === 'variables-input') { - updated[key] = { - ...subBlock, - value: remapVariableInputValue(subBlock.value, varIdMap), - } - } else { - updated[key] = subBlock - } - } - - return updated -} - -function remapWorkflowReferencesInSubBlocks( - subBlocks: SubBlockRecord, - workflowIdMap: Map | undefined -): SubBlockRecord { - if (!workflowIdMap?.size) return subBlocks - - const updated: SubBlockRecord = {} - for (const [key, subBlock] of Object.entries(subBlocks)) { - if ( - subBlock && - typeof subBlock === 'object' && - subBlock.type === 'workflow-selector' && - typeof subBlock.value === 'string' - ) { - updated[key] = { - ...subBlock, - value: workflowIdMap.get(subBlock.value) ?? subBlock.value, - } - continue - } - - updated[key] = subBlock - } - - return updated -} - -/** - * Remaps condition/router block IDs within subBlocks when a block is duplicated. - * Returns a new object without mutating the input. - */ -function remapConditionIdsInSubBlocks( - subBlocks: Record, - oldBlockId: string, - newBlockId: string -): Record { - const updated: Record = {} - - for (const [key, subBlock] of Object.entries(subBlocks)) { - if ( - subBlock && - typeof subBlock === 'object' && - (subBlock.type === 'condition-input' || subBlock.type === 'router-input') && - typeof subBlock.value === 'string' - ) { - try { - const parsed = JSON.parse(subBlock.value) - if (Array.isArray(parsed) && remapConditionBlockIds(parsed, oldBlockId, newBlockId)) { - updated[key] = { ...subBlock, value: JSON.stringify(parsed) } - continue - } - } catch { - // Not valid JSON, skip - } - } - updated[key] = subBlock - } - - return updated -} - /** * Duplicate a workflow with all its blocks, edges, and subflows * This is a shared helper used by both the workflow duplicate API and folder duplicate API diff --git a/apps/sim/lib/workflows/persistence/remap-internal-ids.test.ts b/apps/sim/lib/workflows/persistence/remap-internal-ids.test.ts new file mode 100644 index 00000000000..4ee04c75ae8 --- /dev/null +++ b/apps/sim/lib/workflows/persistence/remap-internal-ids.test.ts @@ -0,0 +1,122 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { + coerceObjectArray, + remapWorkflowReferencesInSubBlocks, + type SubBlockRecord, +} from '@/lib/workflows/persistence/remap-internal-ids' + +describe('remapWorkflowReferencesInSubBlocks', () => { + const map = new Map([ + ['wf-src', 'wf-dst'], + ['sub-src', 'sub-dst'], + ]) + + it('remaps a top-level workflow-selector value', () => { + const subBlocks: SubBlockRecord = { + target: { id: 'target', type: 'workflow-selector', value: 'wf-src' }, + } + const result = remapWorkflowReferencesInSubBlocks(subBlocks, map) + expect(result.target.value).toBe('wf-dst') + }) + + it('remaps a nested workflow_input tool workflowId in a tool-input array', () => { + const subBlocks: SubBlockRecord = { + tools: { + id: 'tools', + type: 'tool-input', + value: [ + { type: 'workflow_input', params: { workflowId: 'sub-src', inputMapping: '{}' } }, + { type: 'custom-tool', customToolId: 'ct-1' }, + ], + }, + } + const result = remapWorkflowReferencesInSubBlocks(subBlocks, map) + const tools = result.tools.value as Array<{ type: string; params?: { workflowId?: string } }> + expect(tools[0].params?.workflowId).toBe('sub-dst') + expect(tools[1]).toEqual({ type: 'custom-tool', customToolId: 'ct-1' }) + }) + + it('handles a JSON-stringified tool-input value', () => { + const subBlocks: SubBlockRecord = { + tools: { + id: 'tools', + type: 'tool-input', + value: JSON.stringify([{ type: 'workflow_input', params: { workflowId: 'sub-src' } }]), + }, + } + const result = remapWorkflowReferencesInSubBlocks(subBlocks, map) + expect(result.tools.value).toBe( + JSON.stringify([{ type: 'workflow_input', params: { workflowId: 'sub-dst' } }]) + ) + }) + + it('leaves unknown workflow ids and non-workflow tools untouched', () => { + const subBlocks: SubBlockRecord = { + sel: { id: 'sel', type: 'workflow-selector', value: 'wf-unknown' }, + tools: { + id: 'tools', + type: 'tool-input', + value: [{ type: 'workflow_input', params: { workflowId: 'wf-unknown' } }], + }, + } + const result = remapWorkflowReferencesInSubBlocks(subBlocks, map) + expect(result.sel.value).toBe('wf-unknown') + expect(result.tools).toBe(subBlocks.tools) + }) + + it('returns the input unchanged when the id map is empty', () => { + const subBlocks: SubBlockRecord = { + target: { id: 'target', type: 'workflow-selector', value: 'wf-src' }, + } + expect(remapWorkflowReferencesInSubBlocks(subBlocks, new Map())).toBe(subBlocks) + }) + + it('clears an unmapped workflow-selector when clearUnmapped is set (cross-workspace)', () => { + const subBlocks: SubBlockRecord = { + sel: { id: 'sel', type: 'workflow-selector', value: 'wf-unknown' }, + } + const result = remapWorkflowReferencesInSubBlocks(subBlocks, map, { clearUnmapped: true }) + expect(result.sel.value).toBe('') + }) + + it('drops an unmapped workflow_input tool when clearUnmapped is set', () => { + const subBlocks: SubBlockRecord = { + tools: { + id: 'tools', + type: 'tool-input', + value: [ + { type: 'workflow_input', params: { workflowId: 'wf-unknown' } }, + { type: 'workflow_input', params: { workflowId: 'sub-src' } }, + ], + }, + } + const result = remapWorkflowReferencesInSubBlocks(subBlocks, map, { clearUnmapped: true }) + const tools = result.tools.value as Array<{ params?: { workflowId?: string } }> + expect(tools).toHaveLength(1) + expect(tools[0].params?.workflowId).toBe('sub-dst') + }) + + it('remaps the advanced-mode manualWorkflowId override', () => { + const subBlocks: SubBlockRecord = { + manualWorkflowId: { id: 'manualWorkflowId', type: 'short-input', value: 'wf-src' }, + } + const result = remapWorkflowReferencesInSubBlocks(subBlocks, map) + expect(result.manualWorkflowId.value).toBe('wf-dst') + }) +}) + +describe('coerceObjectArray', () => { + it('returns arrays directly', () => { + expect(coerceObjectArray([{ a: 1 }])).toEqual({ array: [{ a: 1 }], wasString: false }) + }) + it('parses JSON-string arrays', () => { + expect(coerceObjectArray('[{"a":1}]')).toEqual({ array: [{ a: 1 }], wasString: true }) + }) + it('returns null for non-array values', () => { + expect(coerceObjectArray('hi')).toEqual({ array: null, wasString: false }) + expect(coerceObjectArray(42)).toEqual({ array: null, wasString: false }) + }) +}) diff --git a/apps/sim/lib/workflows/persistence/remap-internal-ids.ts b/apps/sim/lib/workflows/persistence/remap-internal-ids.ts new file mode 100644 index 00000000000..14a9af5b993 --- /dev/null +++ b/apps/sim/lib/workflows/persistence/remap-internal-ids.ts @@ -0,0 +1,237 @@ +import { createLogger } from '@sim/logger' +import { remapConditionBlockIds } from '@/lib/workflows/condition-ids' +import { SYSTEM_SUBBLOCK_IDS, TRIGGER_RUNTIME_SUBBLOCK_IDS } from '@/triggers/constants' + +const logger = createLogger('WorkflowRemapInternalIds') + +/** + * Untrusted shape of a persisted block subBlocks JSON column. Callers narrow + * `type`/`value` with runtime checks before mutating; the index signature exists + * because the raw record is handed back to drizzle without knowing which subBlock + * keys it contains. + */ +export type SubBlockRecord = Record< + string, + { type?: unknown; value?: unknown; [key: string]: unknown } +> + +type VariableAssignment = Record & { variableId?: unknown } + +const DUPLICATE_STRIPPED_SYSTEM_SUBBLOCK_IDS = new Set( + SYSTEM_SUBBLOCK_IDS.filter((id) => id !== 'triggerCredentials') +) + +export function isRecord(value: unknown): value is Record { + return Boolean(value && typeof value === 'object' && !Array.isArray(value)) +} + +/** Coerce a subblock value that holds a JSON array (stored as an array or a JSON string). */ +export function coerceObjectArray(value: unknown): { array: unknown[] | null; wasString: boolean } { + if (Array.isArray(value)) return { array: value, wasString: false } + if (typeof value === 'string' && value.trim()) { + try { + const parsed = JSON.parse(value) + if (Array.isArray(parsed)) return { array: parsed, wasString: true } + } catch {} + } + return { array: null, wasString: false } +} + +export function isSystemSubBlockKey(key: string, ids: Set | string[]): boolean { + const idList = Array.isArray(ids) ? ids : Array.from(ids) + return idList.some((id) => key === id || key.startsWith(`${id}_`)) +} + +/** Strip trigger-runtime and non-credential system subblocks for a fresh copy. */ +export function sanitizeSubBlocksForDuplicate(subBlocks: SubBlockRecord): SubBlockRecord { + const sanitized: SubBlockRecord = {} + for (const [key, subBlock] of Object.entries(subBlocks)) { + if (isSystemSubBlockKey(key, TRIGGER_RUNTIME_SUBBLOCK_IDS)) continue + if (isSystemSubBlockKey(key, DUPLICATE_STRIPPED_SYSTEM_SUBBLOCK_IDS)) continue + sanitized[key] = subBlock + } + return sanitized +} + +function remapVariableAssignment(value: unknown, varIdMap: Map): unknown { + if (Array.isArray(value)) { + return value.map((item) => remapVariableAssignment(item, varIdMap)) + } + if (!isRecord(value)) { + return value + } + const assignment = value as VariableAssignment + const next: Record = {} + for (const [key, nestedValue] of Object.entries(assignment)) { + next[key] = remapVariableAssignment(nestedValue, varIdMap) + } + if (typeof assignment.variableId === 'string') { + const newVarId = varIdMap.get(assignment.variableId) + if (newVarId) { + next.variableId = newVarId + } else { + logger.warn('Skipping unknown variable reference during copy', { + variableId: assignment.variableId, + }) + } + } + return next +} + +function remapVariableInputValue(value: unknown, varIdMap: Map): unknown { + if (value == null) { + return value + } + if (Array.isArray(value)) { + return remapVariableAssignment(value, varIdMap) + } + if (typeof value === 'string') { + const trimmed = value.trim() + if (!trimmed) return value + let parsed: unknown + try { + parsed = JSON.parse(trimmed) + } catch { + throw new Error('Variables input assignments could not be parsed for copy') + } + if (Array.isArray(parsed)) { + return remapVariableAssignment(parsed, varIdMap) + } + throw new Error('Variables input assignments must be an array') + } + throw new Error('Variables input assignments must be an array') +} + +/** + * Remap old variable IDs to new variable IDs inside block subBlocks, targeting + * `variables-input` subblocks whose value is an array of variable assignments. + */ +export function remapVariableIdsInSubBlocks( + subBlocks: SubBlockRecord, + varIdMap: Map +): SubBlockRecord { + const updated: SubBlockRecord = {} + for (const [key, subBlock] of Object.entries(subBlocks)) { + if (subBlock && typeof subBlock === 'object' && subBlock.type === 'variables-input') { + updated[key] = { + ...subBlock, + value: remapVariableInputValue(subBlock.value, varIdMap), + } + } else { + updated[key] = subBlock + } + } + return updated +} + +/** + * Rewrite cross-workflow references through a workflow id map: top-level + * `workflow-selector` values, the advanced-mode `manualWorkflowId` override, and + * `workflow_input` sub-workflow tools nested in a `tool-input` array (an agent + * calling another workflow as a tool). + * + * `clearUnmapped` controls the cross-workspace case: fork/promote pass `true` so a + * reference to a workflow that wasn't copied is cleared/dropped rather than left + * pointing at the source workspace (a silent cross-workspace execution). Same- + * workspace duplication leaves it `false` to preserve references to untouched + * sibling workflows. + */ +export function remapWorkflowReferencesInSubBlocks( + subBlocks: SubBlockRecord, + workflowIdMap: Map | undefined, + options?: { clearUnmapped?: boolean } +): SubBlockRecord { + if (!workflowIdMap?.size) return subBlocks + const clearUnmapped = options?.clearUnmapped ?? false + const remapScalar = (value: string): string => { + const mapped = workflowIdMap.get(value) + if (mapped) return mapped + return clearUnmapped ? '' : value + } + const updated: SubBlockRecord = {} + for (const [key, subBlock] of Object.entries(subBlocks)) { + if (subBlock && typeof subBlock === 'object') { + const baseKey = key.replace(/_\d+$/, '') + if ( + (subBlock.type === 'workflow-selector' || baseKey === 'manualWorkflowId') && + typeof subBlock.value === 'string' && + subBlock.value + ) { + updated[key] = { ...subBlock, value: remapScalar(subBlock.value) } + continue + } + if (subBlock.type === 'tool-input') { + const remapped = remapWorkflowInputTools(subBlock.value, workflowIdMap, clearUnmapped) + if (remapped !== subBlock.value) { + updated[key] = { ...subBlock, value: remapped } + continue + } + } + } + updated[key] = subBlock + } + return updated +} + +/** + * Rewrite `workflow_input` tools' `params.workflowId` through a workflow id map. + * When `clearUnmapped` is set, a tool pointing at a workflow that wasn't copied is + * dropped (it can't be referenced cross-workspace). + */ +function remapWorkflowInputTools( + value: unknown, + workflowIdMap: Map, + clearUnmapped: boolean +): unknown { + const { array, wasString } = coerceObjectArray(value) + if (!array) return value + let changed = false + const next = array.flatMap((tool) => { + if (!isRecord(tool) || tool.type !== 'workflow_input' || !isRecord(tool.params)) return [tool] + const workflowId = tool.params.workflowId + if (typeof workflowId !== 'string') return [tool] + const mapped = workflowIdMap.get(workflowId) + if (mapped) { + if (mapped === workflowId) return [tool] + changed = true + return [{ ...tool, params: { ...tool.params, workflowId: mapped } }] + } + if (clearUnmapped) { + changed = true + return [] + } + return [tool] + }) + if (!changed) return value + return wasString ? JSON.stringify(next) : next +} + +/** + * Remap condition/router block IDs within subBlocks when a block is copied with + * a new ID. Returns a new object without mutating the input. + */ +export function remapConditionIdsInSubBlocks( + subBlocks: SubBlockRecord, + oldBlockId: string, + newBlockId: string +): SubBlockRecord { + const updated: SubBlockRecord = {} + for (const [key, subBlock] of Object.entries(subBlocks)) { + if ( + subBlock && + typeof subBlock === 'object' && + (subBlock.type === 'condition-input' || subBlock.type === 'router-input') && + typeof subBlock.value === 'string' + ) { + try { + const parsed = JSON.parse(subBlock.value) + if (Array.isArray(parsed) && remapConditionBlockIds(parsed, oldBlockId, newBlockId)) { + updated[key] = { ...subBlock, value: JSON.stringify(parsed) } + continue + } + } catch {} + } + updated[key] = subBlock + } + return updated +} diff --git a/apps/sim/lib/workflows/search-replace/indexer.ts b/apps/sim/lib/workflows/search-replace/indexer.ts index c6941bd60a6..bfd1662eaa4 100644 --- a/apps/sim/lib/workflows/search-replace/indexer.ts +++ b/apps/sim/lib/workflows/search-replace/indexer.ts @@ -682,7 +682,13 @@ function isVisibleToolParameter(param: ToolParameterConfig, values: Record { + const { contentPlan, blobTasks, requestId } = payload + await copyForkResourceContent({ contentPlan, requestId }) + await executeForkFileBlobCopies(blobTasks, requestId) +} diff --git a/apps/sim/lib/workspaces/fork/copy/copy-files.ts b/apps/sim/lib/workspaces/fork/copy/copy-files.ts new file mode 100644 index 00000000000..5a5d101fac0 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/copy/copy-files.ts @@ -0,0 +1,138 @@ +import { workspaceFiles } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { generateId } from '@sim/utils/id' +import { and, eq, inArray, isNull } from 'drizzle-orm' +import type { DbOrTx } from '@/lib/db/types' +import { generateWorkspaceFileKey } from '@/lib/uploads/contexts/workspace/workspace-file-manager' +import { downloadFile, uploadFile } from '@/lib/uploads/core/storage-service' +import type { StorageContext } from '@/lib/uploads/shared/types' +import { MAX_FILE_SIZE } from '@/lib/uploads/utils/validation' + +const logger = createLogger('WorkspaceForkCopyFiles') + +export interface BlobCopyTask { + sourceKey: string + targetKey: string + context: StorageContext + fileName: string + contentType: string + userId: string + workspaceId: string +} + +export interface PlanForkFileCopiesResult { + /** + * source storage key -> child storage key. `file-upload` subblocks reference + * files by storage key (not `workspace_files.id`), so the fork remap keys on the + * storage key. File identity is not persisted in the fork resource map - files + * are a fork-copy-only resource (not remapped on promote). + */ + keyMap: Map + /** Blob duplications to run after the fork transaction commits. */ + blobTasks: BlobCopyTask[] +} + +/** + * Insert child `workspace_files` metadata rows for the selected files (new id + + * new storage key) and return the source→child storage-key map plus the blob + * copies to run post-commit. The metadata row must exist before the blob upload + * (its idempotent metadata insert reuses the row), and both must run after the + * child workspace row exists (FK). Runs in the fork transaction; blob I/O is + * deferred to {@link executeForkFileBlobCopies}. + */ +export async function planForkFileCopies(params: { + tx: DbOrTx + sourceWorkspaceId: string + childWorkspaceId: string + userId: string + fileIds: string[] + now: Date +}): Promise { + const { tx, sourceWorkspaceId, childWorkspaceId, userId, fileIds, now } = params + const keyMap = new Map() + const blobTasks: BlobCopyTask[] = [] + if (fileIds.length === 0) return { keyMap, blobTasks } + + // Batch the metadata read (one query for all selected files) instead of a + // per-file lookup. Matches getFileMetadataById's filters: non-deleted + scoped + // to the source workspace. + const metas = await tx + .select() + .from(workspaceFiles) + .where( + and( + inArray(workspaceFiles.id, fileIds), + eq(workspaceFiles.workspaceId, sourceWorkspaceId), + isNull(workspaceFiles.deletedAt) + ) + ) + + for (const meta of metas) { + const childFileId = generateId() + // Use the canonical workspace-file key (`workspace/{id}/...`) so the file-serve + // API can infer the storage context; a bare `{id}/...` key has no context prefix. + const targetKey = generateWorkspaceFileKey(childWorkspaceId, meta.originalName) + await tx.insert(workspaceFiles).values({ + ...meta, + id: childFileId, + key: targetKey, + workspaceId: childWorkspaceId, + userId, + folderId: null, + deletedAt: null, + uploadedAt: now, + }) + keyMap.set(meta.key, targetKey) + blobTasks.push({ + sourceKey: meta.key, + targetKey, + context: meta.context as StorageContext, + fileName: meta.originalName, + contentType: meta.contentType, + userId, + workspaceId: childWorkspaceId, + }) + } + + return { keyMap, blobTasks } +} + +/** + * Duplicate each planned file blob to its new key. `uploadFile`'s metadata insert + * is idempotent on the key (the row was already created in the transaction), so + * this only copies bytes. Best-effort: a failed blob leaves the metadata row + * pointing at a missing object, which the user can re-upload. + */ +export async function executeForkFileBlobCopies( + blobTasks: BlobCopyTask[], + requestId = 'unknown' +): Promise { + for (const task of blobTasks) { + try { + const buffer = await downloadFile({ + key: task.sourceKey, + context: task.context, + maxBytes: MAX_FILE_SIZE, + }) + await uploadFile({ + file: buffer, + fileName: task.fileName, + contentType: task.contentType, + context: task.context, + customKey: task.targetKey, + preserveKey: true, + metadata: { + userId: task.userId, + workspaceId: task.workspaceId, + originalName: task.fileName, + }, + }) + } catch (error) { + logger.warn(`[${requestId}] Failed to copy file blob during fork`, { + targetKey: task.targetKey, + error: getErrorMessage(error), + }) + } + } +} diff --git a/apps/sim/lib/workspaces/fork/copy/copy-resources.ts b/apps/sim/lib/workspaces/fork/copy/copy-resources.ts new file mode 100644 index 00000000000..410e4cbf4a9 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/copy/copy-resources.ts @@ -0,0 +1,393 @@ +import { db } from '@sim/db' +import { + customTools, + document, + embedding, + knowledgeBase, + mcpServers, + skill, + userTableDefinitions, + userTableRows, +} from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { generateId } from '@sim/utils/id' +import { and, asc, eq, gt, inArray, isNull, type SQL } from 'drizzle-orm' +import type { DbOrTx } from '@/lib/db/types' +import { generateMcpServerId } from '@/lib/mcp/utils' +import type { TableSchema } from '@/lib/table/types' +import type { + ForkMappingUpsert, + ForkResourceType, +} from '@/lib/workspaces/fork/mapping/mapping-store' +import { remapForkTableWorkflowGroups } from '@/lib/workspaces/fork/remap/remap-table-groups' + +const logger = createLogger('WorkspaceForkCopyResources') + +/** Page size for the post-transaction bulk content copy (keyset-paginated). */ +const CONTENT_PAGE = 500 + +export interface CopyResourcesParams { + tx: DbOrTx + sourceWorkspaceId: string + childWorkspaceId: string + userId: string + now: Date + /** Source resource ids selected for copy, by kind. */ + selection: { + customTools: string[] + skills: string[] + mcpServers: string[] + tables: string[] + knowledgeBases: string[] + } + /** source workflow id -> child workflow id, for table workflow-group remap. */ + workflowIdMap: Map +} + +export interface ForkContentPlanEntry { + sourceId: string + childId: string +} + +/** Bulk content to copy AFTER the fork transaction commits (best-effort, batched). */ +export interface ForkContentPlan { + sourceWorkspaceId: string + childWorkspaceId: string + tables: ForkContentPlanEntry[] + knowledgeBases: ForkContentPlanEntry[] +} + +export interface CopyResourcesResult { + /** source resource id -> child resource id, keyed by fork resource type. */ + idMap: Map> + /** Identity mapping rows to persist for every copied resource. */ + mappingEntries: ForkMappingUpsert[] + /** Heavy row/document/embedding content to copy post-commit. */ + contentPlan: ForkContentPlan +} + +function setId(idMap: Map>, type: ForkResourceType) { + let map = idMap.get(type) + if (!map) { + map = new Map() + idMap.set(type, map) + } + return map +} + +/** + * Copy the selected resources' **container rows** into the child workspace inside + * the fork transaction: custom tools, skills, and MCP server configs (each a + * single row), plus table definitions and knowledge-base rows (without their bulk + * rows / documents / embeddings). This keeps the fork transaction bounded to + * O(selected resources) single-row writes. The heavy content (table rows, KB + * documents + embeddings) is returned as a {@link ForkContentPlan} for + * {@link copyForkResourceContent} to copy best-effort after commit. Secrets are + * never copied: MCP OAuth tokens are omitted (re-auth required) and KB connectors + * are not copied (the child is a content snapshot without live sync). + */ +export async function copyForkResourceContainers( + params: CopyResourcesParams +): Promise { + const { tx, sourceWorkspaceId, childWorkspaceId, userId, now, selection, workflowIdMap } = params + const idMap = new Map>() + const mappingEntries: ForkMappingUpsert[] = [] + const contentPlan: ForkContentPlan = { + sourceWorkspaceId, + childWorkspaceId, + tables: [], + knowledgeBases: [], + } + + const record = (type: ForkResourceType, sourceId: string, childId: string) => { + setId(idMap, type).set(sourceId, childId) + mappingEntries.push({ + resourceType: type, + parentResourceId: sourceId, + childResourceId: childId, + }) + } + + if (selection.customTools.length > 0) { + const rows = await tx + .select() + .from(customTools) + .where( + and( + inArray(customTools.id, selection.customTools), + eq(customTools.workspaceId, sourceWorkspaceId) + ) + ) + for (const row of rows) { + const childId = generateId() + await tx.insert(customTools).values({ + ...row, + id: childId, + workspaceId: childWorkspaceId, + userId, + createdAt: now, + updatedAt: now, + }) + record('custom_tool', row.id, childId) + } + } + + if (selection.skills.length > 0) { + const rows = await tx + .select() + .from(skill) + .where(and(inArray(skill.id, selection.skills), eq(skill.workspaceId, sourceWorkspaceId))) + for (const row of rows) { + const childId = generateId() + await tx.insert(skill).values({ + ...row, + id: childId, + workspaceId: childWorkspaceId, + userId, + createdAt: now, + updatedAt: now, + }) + record('skill', row.id, childId) + } + } + + if (selection.mcpServers.length > 0) { + const rows = await tx + .select() + .from(mcpServers) + .where( + and( + inArray(mcpServers.id, selection.mcpServers), + eq(mcpServers.workspaceId, sourceWorkspaceId), + isNull(mcpServers.deletedAt) + ) + ) + // `generateMcpServerId` is deterministic on (workspace, url), so two selected + // servers with the same normalized URL derive the same child id. Insert once + // and map both source ids to the surviving child rather than aborting the fork. + const insertedMcpIds = new Set() + for (const row of rows) { + const childId = row.url ? generateMcpServerId(childWorkspaceId, row.url) : generateId() + record('mcp_server', row.id, childId) + if (insertedMcpIds.has(childId)) continue + insertedMcpIds.add(childId) + await tx + .insert(mcpServers) + .values({ + ...row, + id: childId, + workspaceId: childWorkspaceId, + createdBy: userId, + // Secrets are never copied across workspaces: drop the registered OAuth + // client + any auth headers so the child re-authenticates from scratch. + oauthClientId: null, + oauthClientSecret: null, + headers: {}, + connectionStatus: 'disconnected', + lastConnected: null, + lastError: null, + deletedAt: null, + createdAt: now, + updatedAt: now, + }) + .onConflictDoNothing() + } + } + + if (selection.tables.length > 0) { + const definitions = await tx + .select() + .from(userTableDefinitions) + .where( + and( + inArray(userTableDefinitions.id, selection.tables), + eq(userTableDefinitions.workspaceId, sourceWorkspaceId), + isNull(userTableDefinitions.archivedAt) + ) + ) + for (const definition of definitions) { + const childTableId = generateId() + const remappedSchema = remapForkTableWorkflowGroups( + definition.schema as TableSchema, + workflowIdMap + ) + await tx.insert(userTableDefinitions).values({ + ...definition, + id: childTableId, + workspaceId: childWorkspaceId, + schema: remappedSchema, + createdBy: userId, + rowsVersion: 0, + // Start at 0 - the post-commit content copy raises it to the rows actually + // copied, so a failed/partial copy never advertises the source's count. + rowCount: 0, + archivedAt: null, + createdAt: now, + updatedAt: now, + }) + record('table', definition.id, childTableId) + contentPlan.tables.push({ sourceId: definition.id, childId: childTableId }) + } + } + + if (selection.knowledgeBases.length > 0) { + const bases = await tx + .select() + .from(knowledgeBase) + .where( + and( + inArray(knowledgeBase.id, selection.knowledgeBases), + eq(knowledgeBase.workspaceId, sourceWorkspaceId), + isNull(knowledgeBase.deletedAt) + ) + ) + for (const base of bases) { + const childKbId = generateId() + await tx.insert(knowledgeBase).values({ + ...base, + id: childKbId, + workspaceId: childWorkspaceId, + userId, + deletedAt: null, + createdAt: now, + updatedAt: now, + }) + record('knowledge_base', base.id, childKbId) + contentPlan.knowledgeBases.push({ sourceId: base.id, childId: childKbId }) + } + } + + return { idMap, mappingEntries, contentPlan } +} + +/** + * Copy the heavy resource content described by a {@link ForkContentPlan} AFTER the + * fork transaction has committed: table rows, and KB documents + embeddings. Reads + * and writes are keyset-paginated so peak memory is bounded to one page, and each + * resource is copied in its own short statements (never one long transaction). + * Best-effort: a failure on one resource is logged and the others continue - the + * fork itself (workflows + container rows) already succeeded. + */ +export async function copyForkResourceContent(params: { + contentPlan: ForkContentPlan + requestId?: string +}): Promise { + const { contentPlan, requestId = 'unknown' } = params + const { childWorkspaceId } = contentPlan + + for (const table of contentPlan.tables) { + try { + let copied = 0 + let afterId: string | null = null + for (;;) { + const where: SQL | undefined = + afterId === null + ? eq(userTableRows.tableId, table.sourceId) + : and(eq(userTableRows.tableId, table.sourceId), gt(userTableRows.id, afterId)) + const rows = await db + .select() + .from(userTableRows) + .where(where) + .orderBy(asc(userTableRows.id)) + .limit(CONTENT_PAGE) + if (rows.length === 0) break + await db.insert(userTableRows).values( + rows.map((row) => ({ + ...row, + id: generateId(), + tableId: table.childId, + workspaceId: childWorkspaceId, + })) + ) + copied += rows.length + afterId = rows[rows.length - 1].id + if (rows.length < CONTENT_PAGE) break + } + await db + .update(userTableDefinitions) + .set({ rowCount: copied }) + .where(eq(userTableDefinitions.id, table.childId)) + } catch (error) { + logger.warn(`[${requestId}] Failed to copy table rows during fork`, { + sourceTableId: table.sourceId, + error: getErrorMessage(error), + }) + } + } + + for (const kb of contentPlan.knowledgeBases) { + try { + let afterDocId: string | null = null + for (;;) { + // Only copy LIVE documents - exclude soft-deleted and archived rows, matching + // how the rest of the KB system treats them as gone (chunks/tags/search filter + // both). A fork must not resurrect documents removed from the source base. + const liveDocs = and( + eq(document.knowledgeBaseId, kb.sourceId), + isNull(document.deletedAt), + isNull(document.archivedAt) + ) + const where: SQL | undefined = + afterDocId === null ? liveDocs : and(liveDocs, gt(document.id, afterDocId)) + const docs = await db + .select() + .from(document) + .where(where) + .orderBy(asc(document.id)) + .limit(CONTENT_PAGE) + if (docs.length === 0) break + for (const doc of docs) { + const childDocId = generateId() + await db.insert(document).values({ + ...doc, + id: childDocId, + knowledgeBaseId: kb.childId, + connectorId: null, + deletedAt: null, + archivedAt: null, + }) + await copyDocumentEmbeddings(doc.id, childDocId, kb.childId) + } + afterDocId = docs[docs.length - 1].id + if (docs.length < CONTENT_PAGE) break + } + } catch (error) { + logger.warn(`[${requestId}] Failed to copy knowledge base content during fork`, { + sourceKnowledgeBaseId: kb.sourceId, + error: getErrorMessage(error), + }) + } + } +} + +async function copyDocumentEmbeddings( + sourceDocumentId: string, + childDocumentId: string, + childKnowledgeBaseId: string +): Promise { + let afterId: string | null = null + for (;;) { + const where: SQL | undefined = + afterId === null + ? eq(embedding.documentId, sourceDocumentId) + : and(eq(embedding.documentId, sourceDocumentId), gt(embedding.id, afterId)) + const rows = await db + .select() + .from(embedding) + .where(where) + .orderBy(asc(embedding.id)) + .limit(CONTENT_PAGE) + if (rows.length === 0) break + await db.insert(embedding).values( + rows.map((row) => ({ + ...row, + id: generateId(), + documentId: childDocumentId, + knowledgeBaseId: childKnowledgeBaseId, + })) + ) + afterId = rows[rows.length - 1].id + if (rows.length < CONTENT_PAGE) break + } +} diff --git a/apps/sim/lib/workspaces/fork/copy/copy-workflows.ts b/apps/sim/lib/workspaces/fork/copy/copy-workflows.ts new file mode 100644 index 00000000000..3f60de40cea --- /dev/null +++ b/apps/sim/lib/workspaces/fork/copy/copy-workflows.ts @@ -0,0 +1,377 @@ +import { workflow, workflowFolder } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { generateId } from '@sim/utils/id' +import { and, eq, isNull, ne } from 'drizzle-orm' +import type { DbOrTx } from '@/lib/db/types' +import { remapConditionEdgeHandle } from '@/lib/workflows/condition-ids' +import { + remapConditionIdsInSubBlocks, + remapVariableIdsInSubBlocks, + remapWorkflowReferencesInSubBlocks, + type SubBlockRecord, + sanitizeSubBlocksForDuplicate, +} from '@/lib/workflows/persistence/remap-internal-ids' +import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils' +import { deriveForkBlockId } from '@/lib/workspaces/fork/remap/block-identity' +import type { + BlockData, + BlockState, + Loop, + Parallel, + SubBlockState, + WorkflowState, + Variable as WorkflowStateVariable, +} from '@/stores/workflows/workflow/types' + +const logger = createLogger('WorkspaceForkCopyWorkflows') + +type SubBlockTransform = (subBlocks: SubBlockRecord) => SubBlockRecord + +interface ResolveForkFolderMappingParams { + tx: DbOrTx + sourceWorkspaceId: string + targetWorkspaceId: string + userId: string + now: Date +} + +/** + * Mirror the source workspace's folder tree into the target workspace, creating + * folders as needed and reusing target folders that already match by name within + * the same (mapped) parent. Returns a map from source folder id to target folder + * id so copied workflows can be placed in the corresponding folder. + */ +export async function resolveForkFolderMapping({ + tx, + sourceWorkspaceId, + targetWorkspaceId, + userId, + now, +}: ResolveForkFolderMappingParams): Promise> { + const map = new Map() + + const sourceFolders = await tx + .select() + .from(workflowFolder) + .where( + and(eq(workflowFolder.workspaceId, sourceWorkspaceId), isNull(workflowFolder.archivedAt)) + ) + + if (sourceFolders.length === 0) return map + + const targetFolders = await tx + .select() + .from(workflowFolder) + .where( + and(eq(workflowFolder.workspaceId, targetWorkspaceId), isNull(workflowFolder.archivedAt)) + ) + + const targetByKey = new Map() + for (const folder of targetFolders) { + targetByKey.set(`${folder.parentId ?? ''}::${folder.name}`, folder.id) + } + + const byId = new Map(sourceFolders.map((folder) => [folder.id, folder])) + const ordered: typeof sourceFolders = [] + const seen = new Set() + const visit = (folder: (typeof sourceFolders)[number]) => { + if (seen.has(folder.id)) return + const parent = folder.parentId ? byId.get(folder.parentId) : undefined + if (parent) visit(parent) + seen.add(folder.id) + ordered.push(folder) + } + for (const folder of sourceFolders) visit(folder) + + const newFolders: (typeof sourceFolders)[number][] = [] + for (const folder of ordered) { + const mappedParentId = folder.parentId ? (map.get(folder.parentId) ?? null) : null + const key = `${mappedParentId ?? ''}::${folder.name}` + const existing = targetByKey.get(key) + if (existing) { + map.set(folder.id, existing) + continue + } + const newFolderId = generateId() + map.set(folder.id, newFolderId) + targetByKey.set(key, newFolderId) + newFolders.push({ + ...folder, + id: newFolderId, + userId, + workspaceId: targetWorkspaceId, + parentId: mappedParentId, + locked: false, + createdAt: now, + updatedAt: now, + }) + } + + if (newFolders.length > 0) { + await tx.insert(workflowFolder).values(newFolders) + } + + return map +} + +async function resolveTargetWorkflowName( + tx: DbOrTx, + workspaceId: string, + folderId: string | null, + name: string, + excludeWorkflowId: string | null +): Promise { + const folderCondition = folderId ? eq(workflow.folderId, folderId) : isNull(workflow.folderId) + + const nameTaken = async (candidate: string): Promise => { + const conditions = [ + eq(workflow.workspaceId, workspaceId), + folderCondition, + eq(workflow.name, candidate), + isNull(workflow.archivedAt), + ] + if (excludeWorkflowId) conditions.push(ne(workflow.id, excludeWorkflowId)) + const [row] = await tx + .select({ id: workflow.id }) + .from(workflow) + .where(and(...conditions)) + .limit(1) + return Boolean(row) + } + + if (!(await nameTaken(name))) return name + for (let i = 2; i < 100; i++) { + const candidate = `${name} (${i})` + if (!(await nameTaken(candidate))) return candidate + } + return `${name} (${generateId().slice(0, 6)})` +} + +export interface CopyWorkflowResult { + targetWorkflowId: string + mode: 'create' | 'replace' + name: string + blocksCount: number + edgesCount: number + subflowsCount: number +} + +export interface CopyWorkflowStateParams { + tx: DbOrTx + targetWorkflowId: string + targetWorkspaceId: string + userId: string + mode: 'create' | 'replace' + now: Date + /** Source workflow's deployed state (the only thing fork/promote copies). */ + sourceState: WorkflowState + /** Source workflow metadata for naming, folder placement, and sort order. */ + sourceMeta: { + name: string + description: string | null + folderId: string | null + sortOrder: number + } + /** source workflow id -> target workflow id, for `workflow-selector` references */ + workflowIdMap: Map + /** source folder id -> target folder id */ + folderIdMap: Map + /** Optional resource-reference remap applied to every block's subBlocks. */ + transformSubBlocks?: SubBlockTransform + requestId?: string +} + +/** + * Copy a source workflow's deployed `WorkflowState` into a target workflow, + * assigning deterministic block ids (so trigger webhook URLs and external block + * references stay stable across promotes) and applying the resource-reference + * transform. Writes the remapped state to the target's draft via + * `saveWorkflowToNormalizedTables`. In `create` mode a new workflow row is + * inserted (undeployed); in `replace` mode the existing target row is kept and + * its draft is overwritten. Deploying the target (and capturing the rollback + * point) is the caller's responsibility. + */ +export async function copyWorkflowStateIntoTarget( + params: CopyWorkflowStateParams +): Promise { + const { + tx, + targetWorkflowId, + targetWorkspaceId, + userId, + mode, + now, + sourceState, + sourceMeta, + workflowIdMap, + folderIdMap, + transformSubBlocks, + requestId = 'unknown', + } = params + + const targetFolderId = sourceMeta.folderId ? (folderIdMap.get(sourceMeta.folderId) ?? null) : null + + const varIdMapping = new Map() + const remappedVariables: Record = {} + for (const [oldVarId, variable] of Object.entries(sourceState.variables ?? {})) { + const newVarId = generateId() + varIdMapping.set(oldVarId, newVarId) + remappedVariables[newVarId] = { ...variable, id: newVarId } + } + + const blockIdMapping = new Map() + for (const oldBlockId of Object.keys(sourceState.blocks)) { + blockIdMapping.set(oldBlockId, deriveForkBlockId(targetWorkflowId, oldBlockId)) + } + + const newBlocks: Record = {} + for (const [oldBlockId, block] of Object.entries(sourceState.blocks)) { + const newBlockId = blockIdMapping.get(oldBlockId)! + + let updatedData = block.data + if (block.data && typeof block.data === 'object' && !Array.isArray(block.data)) { + const dataObj = block.data as Record + if (typeof dataObj.parentId === 'string' && blockIdMapping.has(dataObj.parentId)) { + updatedData = { + ...dataObj, + parentId: blockIdMapping.get(dataObj.parentId)!, + extent: 'parent', + } as BlockData + } + } + + // double-cast-allowed: SubBlockState is structurally a SubBlockRecord entry but lacks the open index signature SubBlockRecord declares + const sourceSubBlocks = (block.subBlocks ?? {}) as unknown as SubBlockRecord + let subBlocks: SubBlockRecord = sanitizeSubBlocksForDuplicate(sourceSubBlocks) + if (transformSubBlocks) { + subBlocks = transformSubBlocks(subBlocks) + } + if (varIdMapping.size > 0) { + subBlocks = remapVariableIdsInSubBlocks(subBlocks, varIdMapping) + } + // Cross-workspace copy: clear references to workflows that weren't copied + // rather than leave them pointing at the source workspace. + subBlocks = remapWorkflowReferencesInSubBlocks(subBlocks, workflowIdMap, { + clearUnmapped: true, + }) + subBlocks = remapConditionIdsInSubBlocks(subBlocks, oldBlockId, newBlockId) as SubBlockRecord + + newBlocks[newBlockId] = { + ...block, + id: newBlockId, + // double-cast-allowed: remap helpers return SubBlockRecord; the entries retain the SubBlockState shape this block requires + subBlocks: subBlocks as unknown as Record, + data: updatedData, + } + } + + const newEdges = sourceState.edges.flatMap((edge) => { + const newSource = blockIdMapping.get(edge.source) + const newTarget = blockIdMapping.get(edge.target) + if (!newSource || !newTarget) { + logger.warn(`[${requestId}] Skipping edge with unmapped block reference during fork copy`, { + edgeId: edge.id, + }) + return [] + } + const newSourceHandle = edge.sourceHandle + ? remapConditionEdgeHandle(edge.sourceHandle, edge.source, newSource) + : edge.sourceHandle + return [ + { + ...edge, + id: generateId(), + source: newSource, + target: newTarget, + sourceHandle: newSourceHandle, + targetHandle: edge.targetHandle, + }, + ] + }) + + const newLoops: Record = {} + for (const [oldId, loop] of Object.entries(sourceState.loops ?? {})) { + const newId = blockIdMapping.get(oldId) ?? oldId + newLoops[newId] = { + ...loop, + id: newId, + nodes: loop.nodes.flatMap((nodeId) => { + const mapped = blockIdMapping.get(nodeId) + return mapped ? [mapped] : [] + }), + } + } + + const newParallels: Record = {} + for (const [oldId, parallel] of Object.entries(sourceState.parallels ?? {})) { + const newId = blockIdMapping.get(oldId) ?? oldId + newParallels[newId] = { + ...parallel, + id: newId, + nodes: parallel.nodes.flatMap((nodeId) => { + const mapped = blockIdMapping.get(nodeId) + return mapped ? [mapped] : [] + }), + } + } + + const resolvedName = await resolveTargetWorkflowName( + tx, + targetWorkspaceId, + targetFolderId, + sourceMeta.name, + mode === 'replace' ? targetWorkflowId : null + ) + + if (mode === 'create') { + await tx.insert(workflow).values({ + id: targetWorkflowId, + userId, + workspaceId: targetWorkspaceId, + folderId: targetFolderId, + sortOrder: sourceMeta.sortOrder, + name: resolvedName, + description: sourceMeta.description, + lastSynced: now, + createdAt: now, + updatedAt: now, + isDeployed: false, + runCount: 0, + locked: false, + variables: remappedVariables, + }) + } else { + await tx + .update(workflow) + .set({ + name: resolvedName, + description: sourceMeta.description, + folderId: targetFolderId, + variables: remappedVariables, + lastSynced: now, + updatedAt: now, + }) + .where(eq(workflow.id, targetWorkflowId)) + } + + const remappedState: WorkflowState = { + blocks: newBlocks, + edges: newEdges, + loops: newLoops, + parallels: newParallels, + variables: remappedVariables, + } + const saved = await saveWorkflowToNormalizedTables(targetWorkflowId, remappedState, tx) + if (!saved.success) { + throw new Error(`Failed to write forked workflow ${targetWorkflowId}: ${saved.error}`) + } + + return { + targetWorkflowId, + mode, + name: resolvedName, + blocksCount: Object.keys(newBlocks).length, + edgesCount: newEdges.length, + subflowsCount: Object.keys(newLoops).length + Object.keys(newParallels).length, + } +} diff --git a/apps/sim/lib/workspaces/fork/copy/deploy-bridge.ts b/apps/sim/lib/workspaces/fork/copy/deploy-bridge.ts new file mode 100644 index 00000000000..47fdf2e15d6 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/copy/deploy-bridge.ts @@ -0,0 +1,134 @@ +import { db, runOutsideTransactionContext } from '@sim/db' +import { workflow, workflowDeploymentVersion } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, eq, isNull } from 'drizzle-orm' +import type { DbOrTx } from '@/lib/db/types' +import { loadDeployedWorkflowState } from '@/lib/workflows/persistence/utils' +import { ForkError } from '@/lib/workspaces/fork/lineage/authz' +import type { Variable, WorkflowState } from '@/stores/workflows/workflow/types' + +const logger = createLogger('WorkspaceForkDeployBridge') + +/** + * Hard ceiling on how many deployed workflows one fork/promote loads into memory at + * once (each as a full `WorkflowState`). There is no per-workspace workflow cap in + * the product, so this is the safety valve: real workspaces hold tens to low + * hundreds, making this ~5-10x headroom that never blocks legitimate use, it sits + * below the fork feature's other item caps (resource selection 2000, mapping + * entries 5000 - both lighter-weight than full states), and it bounds a pathological + * workspace to a few hundred MB of transient state instead of an unbounded load. + */ +export const MAX_FORK_DEPLOYED_WORKFLOWS = 1000 + +export interface DeployedWorkflowSummary { + id: string + name: string + description: string | null + folderId: string | null + sortOrder: number +} + +/** Workflows in a workspace that are deployed and not archived - the only ones that fork/promote. */ +export async function listDeployedWorkflows( + executor: DbOrTx, + workspaceId: string +): Promise { + return executor + .select({ + id: workflow.id, + name: workflow.name, + description: workflow.description, + folderId: workflow.folderId, + sortOrder: workflow.sortOrder, + }) + .from(workflow) + .where( + and( + eq(workflow.workspaceId, workspaceId), + eq(workflow.isDeployed, true), + isNull(workflow.archivedAt) + ) + ) +} + +/** The active deployment version number for a workflow, or null when it has none. */ +export async function getActiveDeploymentVersionNumber( + executor: DbOrTx, + workflowId: string +): Promise { + const [row] = await executor + .select({ version: workflowDeploymentVersion.version }) + .from(workflowDeploymentVersion) + .where( + and( + eq(workflowDeploymentVersion.workflowId, workflowId), + eq(workflowDeploymentVersion.isActive, true) + ) + ) + .limit(1) + return row?.version ?? null +} + +/** + * Read a source workspace's deployed workflows and each one's active deployed state + * on the global pool. Fork/promote callers MUST run this BEFORE opening their + * transaction: doing these heavy per-workflow reads inside the tx checks out a + * SECOND pooled connection while the tx holds the first, which can deadlock the + * pool at saturation (primary pool max is 15). The source is read-only for the + * operation, so a pre-transaction snapshot is the value that gets force-pushed. + * + * Holds every source state in memory at once (bounded by the workspace's deployed + * workflow count) - the apply step needs each state to write its target inside the + * single atomic transaction, so it cannot stream them one at a time. + */ +export async function loadSourceDeployedStates(sourceWorkspaceId: string): Promise<{ + deployedWorkflows: DeployedWorkflowSummary[] + sourceStates: Map +}> { + const deployedWorkflows = await listDeployedWorkflows(db, sourceWorkspaceId) + // Fail fast on the cheap count before loading any heavy state into memory. + if (deployedWorkflows.length > MAX_FORK_DEPLOYED_WORKFLOWS) { + throw new ForkError( + `This workspace has ${deployedWorkflows.length} deployed workflows, which exceeds the fork/sync limit of ${MAX_FORK_DEPLOYED_WORKFLOWS}.`, + 400 + ) + } + const sourceStates = new Map() + for (const wf of deployedWorkflows) { + const state = await readDeployedState(wf.id, sourceWorkspaceId) + if (state) sourceStates.set(wf.id, state) + } + return { deployedWorkflows, sourceStates } +} + +/** + * Read a workflow's active deployed state as a `WorkflowState`. Returns null ONLY + * when the workflow genuinely has no active deployment (a legitimate skip); real + * DB/migration errors propagate so the caller fails loudly instead of silently + * dropping the workflow from the fork/promote. Block migrations (credential remap + * to current ids) are applied so copied references reflect current resources. + */ +export async function readDeployedState( + workflowId: string, + workspaceId: string +): Promise { + // This reads the (unchanged) SOURCE workspace on the global pool. Callers like + // promote run it inside their transaction, so escape the tx context: the read + // must not join the promote's transaction (and the tripwire forbids global-pool + // queries inside a tx). Outside a transaction this is a no-op. + return runOutsideTransactionContext(async () => { + const version = await getActiveDeploymentVersionNumber(db, workflowId) + if (version == null) { + logger.warn('No active deployment for workflow during fork/promote', { workflowId }) + return null + } + const data = await loadDeployedWorkflowState(workflowId, workspaceId) + return { + blocks: data.blocks, + edges: data.edges, + loops: data.loops, + parallels: data.parallels, + variables: (data.variables ?? {}) as Record, + } + }) +} diff --git a/apps/sim/lib/workspaces/fork/create-fork.ts b/apps/sim/lib/workspaces/fork/create-fork.ts new file mode 100644 index 00000000000..20dd49630a6 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/create-fork.ts @@ -0,0 +1,297 @@ +import { db } from '@sim/db' +import { permissions, workflow, workspace } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import type { PermissionType } from '@sim/platform-authz/workspace' +import { generateId } from '@sim/utils/id' +import { and, eq } from 'drizzle-orm' +import type { Workspace } from '@/lib/api/contracts/workspaces' +import { isTriggerDevEnabled } from '@/lib/core/config/env-flags' +import { runDetached } from '@/lib/core/utils/background' +import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults' +import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils' +import { + type ForkContentCopyPayload, + runForkContentCopy, +} from '@/lib/workspaces/fork/copy/content-copy-runner' +import { planForkFileCopies } from '@/lib/workspaces/fork/copy/copy-files' +import { copyForkResourceContainers } from '@/lib/workspaces/fork/copy/copy-resources' +import { + copyWorkflowStateIntoTarget, + resolveForkFolderMapping, +} from '@/lib/workspaces/fork/copy/copy-workflows' +import { loadSourceDeployedStates } from '@/lib/workspaces/fork/copy/deploy-bridge' +import { + type ForkMappingUpsert, + type ForkResourceType, + seedEdgeMappings, +} from '@/lib/workspaces/fork/mapping/mapping-store' +import { createForkBootstrapTransform } from '@/lib/workspaces/fork/remap/fork-bootstrap' +import type { ForkRemapKind } from '@/lib/workspaces/fork/remap/remap-references' +import type { WorkspaceWithOwner } from '@/lib/workspaces/permissions/utils' +import type { WorkspaceCreationPolicy } from '@/lib/workspaces/policy' +import { WORKSPACE_MODE } from '@/lib/workspaces/policy' + +const logger = createLogger('WorkspaceForkCreate') + +/** Source resource ids the user selected to copy into the child, by kind. */ +export interface ForkResourceSelection { + files: string[] + tables: string[] + knowledgeBases: string[] + customTools: string[] + skills: string[] + mcpServers: string[] +} + +const EMPTY_SELECTION: ForkResourceSelection = { + files: [], + tables: [], + knowledgeBases: [], + customTools: [], + skills: [], + mcpServers: [], +} + +export interface CreateForkParams { + source: WorkspaceWithOwner + policy: WorkspaceCreationPolicy + userId: string + name?: string + selection?: ForkResourceSelection + requestId?: string +} + +export interface CreateForkResult { + /** Full child workspace row so callers can merge it into the workspace-list cache. */ + workspace: Workspace + workflowsCopied: number +} + +const FORK_KIND_TO_RESOURCE_TYPE: Partial> = { + 'custom-tool': 'custom_tool', + skill: 'skill', + 'mcp-server': 'mcp_server', + table: 'table', + 'knowledge-base': 'knowledge_base', +} + +/** + * Create a fork of `source`: a new child workspace that copies the parent's + * **deployed** workflows (left undeployed in the child), snapshots the parent's + * member list, copies the user-selected resources (files, tables, knowledge bases, + * custom tools, skills, MCP server configs) with fresh ids, and records the + * source→child identity for each. Workflow references to copied resources are + * rewritten to the child ids; references to resources that were not copied (and + * all credential references) are cleared; env-var references are preserved. + */ +export async function createFork(params: CreateForkParams): Promise { + const { source, policy, userId, requestId = 'unknown' } = params + const selection = params.selection ?? EMPTY_SELECTION + const childName = params.name?.trim() || `${source.name} (fork)` + + // Read the source's deployed workflows + states BEFORE the transaction so these + // global-pool reads don't check out a second pooled connection from inside the + // fork tx (which can deadlock the pool at saturation). + const { deployedWorkflows, sourceStates } = await loadSourceDeployedStates(source.id) + + const { result, blobTasks, contentPlan } = await db.transaction(async (tx) => { + const now = new Date() + const childWorkspaceId = generateId() + + await tx.insert(workspace).values({ + id: childWorkspaceId, + name: childName, + ownerId: userId, + organizationId: policy.organizationId, + workspaceMode: policy.workspaceMode, + billedAccountUserId: policy.billedAccountUserId, + allowPersonalApiKeys: true, + forkedFromWorkspaceId: source.id, + createdAt: now, + updatedAt: now, + }) + + const sourcePermissions = await tx + .select({ userId: permissions.userId, permissionType: permissions.permissionType }) + .from(permissions) + .where(and(eq(permissions.entityType, 'workspace'), eq(permissions.entityId, source.id))) + + const permissionByUser = new Map() + for (const row of sourcePermissions) { + permissionByUser.set(row.userId, row.permissionType) + } + permissionByUser.set(userId, 'admin') + if ( + policy.workspaceMode === WORKSPACE_MODE.ORGANIZATION && + policy.billedAccountUserId && + policy.billedAccountUserId !== userId + ) { + permissionByUser.set(policy.billedAccountUserId, 'admin') + } + + await tx.insert(permissions).values( + Array.from(permissionByUser.entries()).map(([memberUserId, permissionType]) => ({ + id: generateId(), + entityType: 'workspace' as const, + entityId: childWorkspaceId, + userId: memberUserId, + permissionType, + createdAt: now, + updatedAt: now, + })) + ) + + const workflowIdMap = new Map() + for (const wf of deployedWorkflows) workflowIdMap.set(wf.id, generateId()) + + const fileResult = await planForkFileCopies({ + tx, + sourceWorkspaceId: source.id, + childWorkspaceId, + userId, + fileIds: selection.files, + now, + }) + + const resourceResult = await copyForkResourceContainers({ + tx, + sourceWorkspaceId: source.id, + childWorkspaceId, + userId, + now, + selection: { + customTools: selection.customTools, + skills: selection.skills, + mcpServers: selection.mcpServers, + tables: selection.tables, + knowledgeBases: selection.knowledgeBases, + }, + workflowIdMap, + }) + + const resolveCopied = (kind: ForkRemapKind, sourceId: string): string | null => { + if (kind === 'file') return fileResult.keyMap.get(sourceId) ?? null + const resourceType = FORK_KIND_TO_RESOURCE_TYPE[kind] + if (!resourceType) return null + return resourceResult.idMap.get(resourceType)?.get(sourceId) ?? null + } + const transform = createForkBootstrapTransform(resolveCopied) + + const folderIdMap = await resolveForkFolderMapping({ + tx, + sourceWorkspaceId: source.id, + targetWorkspaceId: childWorkspaceId, + userId, + now, + }) + + let workflowsCopied = 0 + for (const wf of deployedWorkflows) { + const sourceState = sourceStates.get(wf.id) + if (!sourceState) continue + await copyWorkflowStateIntoTarget({ + tx, + targetWorkflowId: workflowIdMap.get(wf.id)!, + targetWorkspaceId: childWorkspaceId, + userId, + mode: 'create', + now, + sourceState, + sourceMeta: { + name: wf.name, + description: wf.description, + folderId: wf.folderId, + sortOrder: wf.sortOrder, + }, + workflowIdMap, + folderIdMap, + transformSubBlocks: transform, + requestId, + }) + workflowsCopied += 1 + } + + // A fork carries only DEPLOYED workflows. When the source has none (e.g. it was + // itself just forked and never redeployed), seed a default workflow so the child + // is a usable workspace rather than a blank one with no workflow at all - the same + // starter "New workspace" creates. Any copied resources still land alongside it. + if (workflowsCopied === 0) { + const defaultWorkflowId = generateId() + await tx.insert(workflow).values({ + id: defaultWorkflowId, + userId, + workspaceId: childWorkspaceId, + folderId: null, + name: 'default-agent', + description: 'Your first workflow - start building here!', + lastSynced: now, + createdAt: now, + updatedAt: now, + isDeployed: false, + runCount: 0, + variables: {}, + }) + const { workflowState } = buildDefaultWorkflowArtifacts() + await saveWorkflowToNormalizedTables(defaultWorkflowId, workflowState, tx) + } + + const seedEntries: ForkMappingUpsert[] = [] + for (const [sourceWorkflowId, childWorkflowId] of workflowIdMap.entries()) { + seedEntries.push({ + resourceType: 'workflow', + parentResourceId: sourceWorkflowId, + childResourceId: childWorkflowId, + }) + } + seedEntries.push(...resourceResult.mappingEntries) + await seedEdgeMappings(tx, childWorkspaceId, userId, seedEntries) + + logger.info(`[${requestId}] Created fork ${childWorkspaceId} from ${source.id}`, { + workflowsCopied, + mappingsSeeded: seedEntries.length, + }) + + return { + result: { + workspace: { + id: childWorkspaceId, + name: childName, + ownerId: userId, + organizationId: policy.organizationId, + workspaceMode: policy.workspaceMode, + billedAccountUserId: policy.billedAccountUserId, + allowPersonalApiKeys: true, + forkedFromWorkspaceId: source.id, + }, + workflowsCopied, + }, + blobTasks: fileResult.blobTasks, + contentPlan: resourceResult.contentPlan, + } + }) + + // Bulk content (table rows, KB documents + embeddings) and file blobs are copied + // AFTER the fork commits, in the background, so the fork request returns as soon + // as the workflows exist and is never blocked on (or timed out by) heavy I/O. + // Trigger.dev runs it out-of-process (surviving deploys); without it, runDetached + // runs it inline best-effort. Both are batched/bounded internally. + const hasContent = + contentPlan.tables.length > 0 || contentPlan.knowledgeBases.length > 0 || blobTasks.length > 0 + if (hasContent) { + const payload: ForkContentCopyPayload = { contentPlan, blobTasks, requestId } + if (isTriggerDevEnabled) { + const [{ forkContentCopyTask }, { tasks }, { resolveTriggerRegion }] = await Promise.all([ + import('@/background/fork-content-copy'), + import('@trigger.dev/sdk'), + import('@/lib/core/async-jobs/region'), + ]) + await tasks.trigger('fork-content-copy', payload, { + region: await resolveTriggerRegion(), + }) + } else { + runDetached('fork-content-copy', () => runForkContentCopy(payload)) + } + } + + return result +} diff --git a/apps/sim/lib/workspaces/fork/lineage/authz.ts b/apps/sim/lib/workspaces/fork/lineage/authz.ts new file mode 100644 index 00000000000..c1abf513f26 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/lineage/authz.ts @@ -0,0 +1,158 @@ +import { isOrganizationOnEnterprisePlan } from '@/lib/billing/core/subscription' +import { isBillingEnabled, isForkingEnabled } from '@/lib/core/config/env-flags' +import { HttpError } from '@/lib/core/utils/http-error' +import { type ForkEdge, resolveForkEdge } from '@/lib/workspaces/fork/lineage/lineage' +import { checkWorkspaceAccess, type WorkspaceWithOwner } from '@/lib/workspaces/permissions/utils' +import { getWorkspaceCreationPolicy, type WorkspaceCreationPolicy } from '@/lib/workspaces/policy' + +/** Direction of a promote, relative to the workspace the caller is acting from. */ +export type PromoteDirection = 'push' | 'pull' + +/** + * Enterprise-only gate shared by every fork/promote route. On Sim Cloud the gate + * is the Enterprise plan; on self-hosted it's `FORKING_ENABLED`, which 404s when + * unset so a newer image doesn't silently expose forking. Mirrors the data-drains + * gate - this repo gates EE features by plan + env flag, not by directory. + */ +async function assertForkingEnabled(organizationId: string | null): Promise { + if (!isBillingEnabled && !isForkingEnabled) { + throw new ForkError('Workspace forking is not enabled on this deployment', 404) + } + if (isBillingEnabled) { + const hasEnterprise = organizationId + ? await isOrganizationOnEnterprisePlan(organizationId) + : false + if (!hasEnterprise) { + throw new ForkError('Workspace forking is available on Enterprise plans only', 403) + } + } +} + +/** + * Domain error for fork/promote operations. Carries a concrete `statusCode` so + * `withRouteHandler` maps it to the right HTTP status and forwards the + * client-safe `message`. + */ +export class ForkError extends HttpError { + readonly statusCode: number + + constructor(message: string, statusCode = 400) { + super(message) + this.name = 'ForkError' + this.statusCode = statusCode + } +} + +async function requireWorkspace( + workspaceId: string, + userId: string +): Promise<{ + workspace: WorkspaceWithOwner + hasAccess: boolean + canWrite: boolean + canAdmin: boolean +}> { + const access = await checkWorkspaceAccess(workspaceId, userId) + if (!access.exists || !access.workspace) { + throw new ForkError('Workspace not found', 404) + } + await assertForkingEnabled(access.workspace.organizationId) + return { + workspace: access.workspace, + hasAccess: access.hasAccess, + canWrite: access.canWrite, + canAdmin: access.canAdmin, + } +} + +/** Require at least read access; returns the (active) workspace. */ +export async function assertWorkspaceReadAccess( + workspaceId: string, + userId: string +): Promise { + const { workspace, hasAccess } = await requireWorkspace(workspaceId, userId) + if (!hasAccess) { + throw new ForkError('You do not have access to this workspace', 403) + } + return workspace +} + +/** Require admin access; returns the (active) workspace. */ +export async function assertWorkspaceAdminAccess( + workspaceId: string, + userId: string +): Promise { + const { workspace, canAdmin } = await requireWorkspace(workspaceId, userId) + if (!canAdmin) { + throw new ForkError('Admin access is required for this workspace', 403) + } + return workspace +} + +export interface ForkAuthorization { + source: WorkspaceWithOwner + policy: WorkspaceCreationPolicy +} + +/** + * Authorize forking `sourceWorkspaceId`: the caller needs admin access to the + * source (a fork copies its deployed workflows and resources en masse) and must + * pass the workspace-creation policy for the parent's org (the child inherits the + * parent's org/mode; plan caps apply). Org owners/admins derive workspace admin. + */ +export async function assertCanFork( + sourceWorkspaceId: string, + userId: string +): Promise { + const source = await assertWorkspaceAdminAccess(sourceWorkspaceId, userId) + const policy = await getWorkspaceCreationPolicy({ + userId, + activeOrganizationId: source.organizationId, + }) + if (!policy.canCreate) { + throw new ForkError( + policy.reason ?? 'You cannot create another workspace on your current plan', + policy.status >= 400 ? policy.status : 403 + ) + } + return { source, policy } +} + +export interface PromoteAuthorization { + edge: ForkEdge + source: WorkspaceWithOwner + target: WorkspaceWithOwner + sourceWorkspaceId: string + targetWorkspaceId: string +} + +/** + * Authorize a promote along the strict edge between `currentWorkspaceId` and + * `otherWorkspaceId`. Requires read on the source and admin on the target (a + * force replace is destructive). `push` sends current -> other; `pull` brings + * other -> current. + */ +export async function assertCanPromote( + currentWorkspaceId: string, + otherWorkspaceId: string, + direction: PromoteDirection, + userId: string +): Promise { + const edge = await resolveForkEdge(currentWorkspaceId, otherWorkspaceId) + if (!edge) { + throw new ForkError('These workspaces are not a direct fork edge', 400) + } + const sourceWorkspaceId = direction === 'push' ? currentWorkspaceId : otherWorkspaceId + const targetWorkspaceId = direction === 'push' ? otherWorkspaceId : currentWorkspaceId + const source = await assertWorkspaceReadAccess(sourceWorkspaceId, userId) + const target = await assertWorkspaceAdminAccess(targetWorkspaceId, userId) + return { edge, source, target, sourceWorkspaceId, targetWorkspaceId } +} + +/** Authorize rolling back the last promote into `targetWorkspaceId` (admin only). */ +export async function assertCanRollback( + targetWorkspaceId: string, + userId: string +): Promise { + return assertWorkspaceAdminAccess(targetWorkspaceId, userId) +} diff --git a/apps/sim/lib/workspaces/fork/lineage/lineage.ts b/apps/sim/lib/workspaces/fork/lineage/lineage.ts new file mode 100644 index 00000000000..f3fb7fb750f --- /dev/null +++ b/apps/sim/lib/workspaces/fork/lineage/lineage.ts @@ -0,0 +1,112 @@ +import { db } from '@sim/db' +import { workspace } from '@sim/db/schema' +import { and, eq, isNull, sql } from 'drizzle-orm' +import type { DbOrTx } from '@/lib/db/types' + +export interface ForkLineageNode { + id: string + name: string + organizationId: string | null +} + +export interface ForkEdge { + childWorkspaceId: string + parentWorkspaceId: string +} + +/** + * The parent workspace id a fork was created from, or null when the workspace + * is not a fork (or has been archived). + */ +export async function getForkParentId(workspaceId: string): Promise { + const [row] = await db + .select({ parentId: workspace.forkedFromWorkspaceId }) + .from(workspace) + .where(and(eq(workspace.id, workspaceId), isNull(workspace.archivedAt))) + .limit(1) + return row?.parentId ?? null +} + +/** The parent lineage node for a fork, or null when it has no live parent. */ +export async function getForkParent(workspaceId: string): Promise { + const parentId = await getForkParentId(workspaceId) + if (!parentId) return null + const [row] = await db + .select({ + id: workspace.id, + name: workspace.name, + organizationId: workspace.organizationId, + }) + .from(workspace) + .where(and(eq(workspace.id, parentId), isNull(workspace.archivedAt))) + .limit(1) + return row ?? null +} + +/** The live direct children forked from this workspace. */ +export async function getForkChildren(workspaceId: string): Promise { + return db + .select({ + id: workspace.id, + name: workspace.name, + organizationId: workspace.organizationId, + }) + .from(workspace) + .where(and(eq(workspace.forkedFromWorkspaceId, workspaceId), isNull(workspace.archivedAt))) +} + +/** The parent plus direct children of a workspace, for lineage display. */ +export async function getForkLineage( + workspaceId: string +): Promise<{ parent: ForkLineageNode | null; children: ForkLineageNode[] }> { + const [parent, children] = await Promise.all([ + getForkParent(workspaceId), + getForkChildren(workspaceId), + ]) + return { parent, children } +} + +/** + * Resolve the strict fork edge between two workspaces, identifying which is the + * child (the one whose `forkedFromWorkspaceId` points at the other). Returns + * null when the two workspaces are not a direct parent/child pair. + */ +export async function resolveForkEdge( + workspaceAId: string, + workspaceBId: string +): Promise { + if (workspaceAId === workspaceBId) return null + if ((await getForkParentId(workspaceAId)) === workspaceBId) { + return { childWorkspaceId: workspaceAId, parentWorkspaceId: workspaceBId } + } + if ((await getForkParentId(workspaceBId)) === workspaceAId) { + return { childWorkspaceId: workspaceBId, parentWorkspaceId: workspaceAId } + } + return null +} + +/** + * Serialize concurrent promote/rollback on a fork edge with a transaction-scoped + * advisory lock keyed by the edge (the child workspace id). `hashtextextended` + * (64-bit, matching every other advisory lock in the repo) makes a collision + * between distinct keys astronomically unlikely; a collision would only cause + * unnecessary serialization, never a correctness issue. + */ +export async function acquireForkEdgeLock(tx: DbOrTx, childWorkspaceId: string): Promise { + await tx.execute( + sql`select pg_advisory_xact_lock(hashtextextended(${`fork-edge:${childWorkspaceId}`}, 0))` + ) +} + +/** + * Serialize every promote/rollback whose TARGET is this workspace. Sibling forks + * promote into the same parent on different edge locks, so the edge lock alone does + * not serialize them; this lock does, keeping concurrent syncs into one target from + * interleaving and keeping rollback's "newest sync" check race-free. Always acquire + * this BEFORE {@link acquireForkEdgeLock} so the two are taken in a consistent order. + */ +export async function acquireForkTargetLock(tx: DbOrTx, targetWorkspaceId: string): Promise { + await tx.execute( + sql`select pg_advisory_xact_lock(hashtextextended(${`fork-target:${targetWorkspaceId}`}, 0))` + ) +} diff --git a/apps/sim/lib/workspaces/fork/mapping/cascade.test.ts b/apps/sim/lib/workspaces/fork/mapping/cascade.test.ts new file mode 100644 index 00000000000..f8f97f09c0d --- /dev/null +++ b/apps/sim/lib/workspaces/fork/mapping/cascade.test.ts @@ -0,0 +1,148 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import type { DbOrTx } from '@/lib/db/types' +import { detectForkCascadeReferences } from '@/lib/workspaces/fork/mapping/cascade' +import type { + ForkReference, + ForkReferenceResolver, +} from '@/lib/workspaces/fork/remap/remap-references' + +/** Executor that returns the queued result arrays in the order queries are issued. */ +function queuedExecutor(results: unknown[][]): DbOrTx { + let index = 0 + const builder = { + from: () => builder, + where: () => Promise.resolve(results[index++] ?? []), + } + return { select: () => builder } as unknown as DbOrTx +} + +function ref(kind: ForkReference['kind'], sourceId: string): ForkReference { + return { kind, sourceId, subBlockKey: 'tools', required: false } +} + +const resolveNone: ForkReferenceResolver = () => null +const resolveAll: ForkReferenceResolver = (_kind, sourceId) => sourceId + +describe('detectForkCascadeReferences', () => { + it('returns empty when there are no content references', async () => { + const result = await detectForkCascadeReferences({ + executor: queuedExecutor([]), + sourceWorkspaceId: 'ws', + references: [ref('credential', 'cred-1'), ref('table', 'tbl-1')], + resolve: resolveNone, + }) + expect(result.references).toEqual([]) + expect(result.unmapped).toEqual([]) + expect(result.mcpReauthServerIds).toEqual([]) + }) + + it('surfaces env keys from custom tool code as required unmapped env-var refs', async () => { + const result = await detectForkCascadeReferences({ + executor: queuedExecutor([[{ id: 't1', title: 'Weather', code: 'fetch(`{{API_KEY}}`)' }]]), + sourceWorkspaceId: 'ws', + references: [ref('custom-tool', 't1')], + resolve: resolveNone, + }) + expect(result.references).toHaveLength(1) + expect(result.references[0]).toMatchObject({ + kind: 'env-var', + sourceId: 'API_KEY', + required: true, + }) + expect(result.unmapped).toHaveLength(1) + }) + + it('marks env-var cascade refs mapped when the resolver finds them in the target', async () => { + const result = await detectForkCascadeReferences({ + executor: queuedExecutor([[{ id: 't1', title: 'Weather', code: '{{API_KEY}}' }]]), + sourceWorkspaceId: 'ws', + references: [ref('custom-tool', 't1')], + resolve: resolveAll, + }) + expect(result.references).toHaveLength(1) + expect(result.unmapped).toHaveLength(0) + }) + + it('extracts env keys from MCP url/headers and flags oauth servers for re-auth', async () => { + const result = await detectForkCascadeReferences({ + executor: queuedExecutor([ + [ + { + id: 'mcp-1', + name: 'Server', + url: 'https://x/{{HOST_KEY}}', + headers: { Authorization: '{{TOKEN}}' }, + authType: 'headers', + }, + { id: 'mcp-2', name: 'OAuth Server', url: 'https://y', headers: {}, authType: 'oauth' }, + ], + ]), + sourceWorkspaceId: 'ws', + references: [ref('mcp-server', 'mcp-1'), ref('mcp-server', 'mcp-2')], + resolve: resolveNone, + }) + const envIds = result.references + .filter((r) => r.kind === 'env-var') + .map((r) => r.sourceId) + .sort() + expect(envIds).toEqual(['HOST_KEY', 'TOKEN']) + expect(result.mcpReauthServerIds).toEqual(['mcp-2']) + }) + + it('flags literal MCP header values as inline secrets (not env)', async () => { + const result = await detectForkCascadeReferences({ + executor: queuedExecutor([ + [ + { + id: 'mcp-1', + name: 'Server', + url: 'https://x', + headers: { Authorization: 'sk-literal' }, + authType: 'headers', + }, + ], + ]), + sourceWorkspaceId: 'ws', + references: [ref('mcp-server', 'mcp-1')], + resolve: resolveNone, + }) + expect(result.references).toHaveLength(0) + expect(result.inlineSecretSources).toHaveLength(1) + }) + + it('surfaces KB connector credentials as required credential refs', async () => { + const result = await detectForkCascadeReferences({ + executor: queuedExecutor([ + [{ id: 'kc-1', knowledgeBaseId: 'kb-1', credentialId: 'cred-9', encryptedApiKey: null }], + ]), + sourceWorkspaceId: 'ws', + references: [ref('knowledge-base', 'kb-1')], + resolve: resolveNone, + }) + expect(result.references).toHaveLength(1) + expect(result.references[0]).toMatchObject({ + kind: 'credential', + sourceId: 'cred-9', + required: true, + }) + }) + + it('dedupes a shared env key referenced by two custom tools', async () => { + const result = await detectForkCascadeReferences({ + executor: queuedExecutor([ + [ + { id: 't1', title: 'A', code: '{{SHARED}}' }, + { id: 't2', title: 'B', code: '{{SHARED}}' }, + ], + ]), + sourceWorkspaceId: 'ws', + references: [ref('custom-tool', 't1'), ref('custom-tool', 't2')], + resolve: resolveNone, + }) + expect(result.references).toHaveLength(1) + expect(result.references[0].sourceId).toBe('SHARED') + }) +}) diff --git a/apps/sim/lib/workspaces/fork/mapping/cascade.ts b/apps/sim/lib/workspaces/fork/mapping/cascade.ts new file mode 100644 index 00000000000..27d15f9ac48 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/mapping/cascade.ts @@ -0,0 +1,180 @@ +import { customTools, knowledgeConnector, mcpServers } from '@sim/db/schema' +import { and, eq, inArray, isNull } from 'drizzle-orm' +import type { DbOrTx } from '@/lib/db/types' +import { + ENV_REF_PATTERN, + type ForkReference, + type ForkReferenceResolver, +} from '@/lib/workspaces/fork/remap/remap-references' + +function extractEnvKeys(text: string): string[] { + const keys = new Set() + for (const match of text.matchAll(ENV_REF_PATTERN)) { + if (match[1]) keys.add(match[1]) + } + return Array.from(keys) +} + +export interface ForkCascadeResult { + /** Transitive env-var / credential references discovered inside referenced resources. */ + references: ForkReference[] + unmapped: ForkReference[] + /** Source MCP server ids that use OAuth and need re-authorization in the target. */ + mcpReauthServerIds: string[] + /** Human-readable descriptions of inline secrets that cannot be mapped (review-only). */ + inlineSecretSources: string[] +} + +const EMPTY: ForkCascadeResult = { + references: [], + unmapped: [], + mcpReauthServerIds: [], + inlineSecretSources: [], +} + +/** + * Walk the bodies of resources a workflow references (custom tools, MCP servers, + * knowledge bases) and surface the secrets they carry transitively: `{{ENV}}` + * keys inside custom tool code and MCP url/headers, and credential ids on KB + * connectors. These become additional required env-var / credential references + * (validated for existence in the target via `resolve`). OAuth MCP servers and + * inline connector keys are surfaced separately for review since they cannot be + * id-mapped. Reads only the source workspace's resources. + */ +export async function detectForkCascadeReferences(params: { + executor: DbOrTx + sourceWorkspaceId: string + references: ForkReference[] + resolve: ForkReferenceResolver +}): Promise { + const { executor, sourceWorkspaceId, references, resolve } = params + + const customToolIds = new Set() + const mcpServerIds = new Set() + const knowledgeBaseIds = new Set() + for (const reference of references) { + if (reference.kind === 'custom-tool') customToolIds.add(reference.sourceId) + else if (reference.kind === 'mcp-server') mcpServerIds.add(reference.sourceId) + else if (reference.kind === 'knowledge-base') knowledgeBaseIds.add(reference.sourceId) + } + + if (customToolIds.size === 0 && mcpServerIds.size === 0 && knowledgeBaseIds.size === 0) { + return EMPTY + } + + const refs = new Map() + const unmapped = new Map() + const mcpReauthServerIds = new Set() + const inlineSecretSources: string[] = [] + + const recordEnv = (key: string, sourceLabel: string) => { + const dedupeKey = `env-var:${key}` + if (refs.has(dedupeKey)) return + const reference: ForkReference = { + kind: 'env-var', + sourceId: key, + subBlockKey: '(cascade)', + blockName: sourceLabel, + required: true, + } + refs.set(dedupeKey, reference) + if (resolve('env-var', key) == null) unmapped.set(dedupeKey, reference) + } + + const recordCredential = (credentialId: string, sourceLabel: string) => { + const dedupeKey = `credential:${credentialId}` + if (refs.has(dedupeKey)) return + const reference: ForkReference = { + kind: 'credential', + sourceId: credentialId, + subBlockKey: '(cascade)', + blockName: sourceLabel, + required: true, + } + refs.set(dedupeKey, reference) + if (resolve('credential', credentialId) == null) unmapped.set(dedupeKey, reference) + } + + if (customToolIds.size > 0) { + const tools = await executor + .select({ id: customTools.id, title: customTools.title, code: customTools.code }) + .from(customTools) + .where( + and( + inArray(customTools.id, Array.from(customToolIds)), + eq(customTools.workspaceId, sourceWorkspaceId) + ) + ) + for (const tool of tools) { + for (const key of extractEnvKeys(tool.code ?? '')) { + recordEnv(key, `Custom tool: ${tool.title}`) + } + } + } + + if (mcpServerIds.size > 0) { + const servers = await executor + .select({ + id: mcpServers.id, + name: mcpServers.name, + url: mcpServers.url, + headers: mcpServers.headers, + authType: mcpServers.authType, + }) + .from(mcpServers) + .where( + and( + inArray(mcpServers.id, Array.from(mcpServerIds)), + eq(mcpServers.workspaceId, sourceWorkspaceId) + ) + ) + for (const server of servers) { + const label = `MCP server: ${server.name}` + if (server.url) { + for (const key of extractEnvKeys(server.url)) recordEnv(key, label) + } + const headers = (server.headers ?? {}) as Record + for (const [headerName, headerValue] of Object.entries(headers)) { + if (typeof headerValue !== 'string') continue + const keys = extractEnvKeys(headerValue) + if (keys.length > 0) { + for (const key of keys) recordEnv(key, label) + } else if (server.authType === 'headers' && headerValue) { + inlineSecretSources.push(`${label} (header ${headerName})`) + } + } + if (server.authType === 'oauth') mcpReauthServerIds.add(server.id) + } + } + + if (knowledgeBaseIds.size > 0) { + const connectors = await executor + .select({ + id: knowledgeConnector.id, + knowledgeBaseId: knowledgeConnector.knowledgeBaseId, + credentialId: knowledgeConnector.credentialId, + encryptedApiKey: knowledgeConnector.encryptedApiKey, + }) + .from(knowledgeConnector) + .where( + and( + inArray(knowledgeConnector.knowledgeBaseId, Array.from(knowledgeBaseIds)), + isNull(knowledgeConnector.deletedAt) + ) + ) + for (const connector of connectors) { + if (connector.credentialId) { + recordCredential(connector.credentialId, `Knowledge base connector`) + } else if (connector.encryptedApiKey) { + inlineSecretSources.push(`Knowledge base connector ${connector.id} (API key)`) + } + } + } + + return { + references: Array.from(refs.values()), + unmapped: Array.from(unmapped.values()), + mcpReauthServerIds: Array.from(mcpReauthServerIds), + inlineSecretSources, + } +} diff --git a/apps/sim/lib/workspaces/fork/mapping/mapping-service.test.ts b/apps/sim/lib/workspaces/fork/mapping/mapping-service.test.ts new file mode 100644 index 00000000000..a89a238698e --- /dev/null +++ b/apps/sim/lib/workspaces/fork/mapping/mapping-service.test.ts @@ -0,0 +1,74 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { ForkRemapKind } from '@/lib/workspaces/fork/remap/remap-references' + +const { mockListForkResourceCandidates } = vi.hoisted(() => ({ + mockListForkResourceCandidates: vi.fn(), +})) + +vi.mock('@/lib/workspaces/fork/mapping/resources', () => ({ + listForkResourceCandidates: mockListForkResourceCandidates, + classifyCredentialResourceType: vi.fn(), + getWorkspaceEnvKeys: vi.fn(), +})) + +import { ForkError } from '@/lib/workspaces/fork/lineage/authz' +import { validateForkMappingTargets } from '@/lib/workspaces/fork/mapping/mapping-service' + +const emptyCandidates: Record> = { + credential: [], + 'env-var': [], + table: [], + 'knowledge-base': [], + 'knowledge-document': [], + file: [], + 'mcp-server': [], + 'custom-tool': [], + skill: [], +} + +describe('validateForkMappingTargets', () => { + beforeEach(() => { + vi.clearAllMocks() + mockListForkResourceCandidates.mockResolvedValue(emptyCandidates) + }) + + it('rejects a workflow-type entry with a target (identity is system-managed)', async () => { + await expect( + validateForkMappingTargets('ws-target', [ + { resourceType: 'workflow', sourceId: 'wf-src', targetId: 'wf-tgt' }, + ]) + ).rejects.toBeInstanceOf(ForkError) + }) + + it('short-circuits without querying candidates when no entry has a target', async () => { + await expect( + validateForkMappingTargets('ws-target', [ + { resourceType: 'env_var', sourceId: 'API_KEY', targetId: null }, + ]) + ).resolves.toBeUndefined() + expect(mockListForkResourceCandidates).not.toHaveBeenCalled() + }) + + it('accepts a mappable entry whose target is a valid candidate', async () => { + mockListForkResourceCandidates.mockResolvedValue({ + ...emptyCandidates, + 'env-var': [{ id: 'API_KEY', label: 'API_KEY' }], + }) + await expect( + validateForkMappingTargets('ws-target', [ + { resourceType: 'env_var', sourceId: 'API_KEY', targetId: 'API_KEY' }, + ]) + ).resolves.toBeUndefined() + }) + + it('rejects a mappable entry whose target is not a candidate in the target workspace', async () => { + await expect( + validateForkMappingTargets('ws-target', [ + { resourceType: 'env_var', sourceId: 'API_KEY', targetId: 'not-in-target' }, + ]) + ).rejects.toBeInstanceOf(ForkError) + }) +}) diff --git a/apps/sim/lib/workspaces/fork/mapping/mapping-service.ts b/apps/sim/lib/workspaces/fork/mapping/mapping-service.ts new file mode 100644 index 00000000000..0b33ae43d05 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/mapping/mapping-service.ts @@ -0,0 +1,246 @@ +import { db } from '@sim/db' +import type { ForkMappingEntry } from '@/lib/api/contracts/workspace-fork' +import type { DbOrTx } from '@/lib/db/types' +import { listDeployedWorkflows, readDeployedState } from '@/lib/workspaces/fork/copy/deploy-bridge' +import { ForkError } from '@/lib/workspaces/fork/lineage/authz' +import type { ForkEdge } from '@/lib/workspaces/fork/lineage/lineage' +import { detectForkCascadeReferences } from '@/lib/workspaces/fork/mapping/cascade' +import { + buildForkResolver, + deleteEdgeMappingsByChildResources, + type ForkMappingRow, + type ForkResourceType, + getEdgeMappingRows, + nonCredentialForkKindToResourceType, + resourceTypeToForkKind, + upsertEdgeMappings, +} from '@/lib/workspaces/fork/mapping/mapping-store' +import { + classifyCredentialResourceType, + type ForkResourceCandidate, + getWorkspaceEnvKeys, + listForkResourceCandidates, +} from '@/lib/workspaces/fork/mapping/resources' +import { + type ForkReference, + type ForkRemapKind, + scanWorkflowReferences, +} from '@/lib/workspaces/fork/remap/remap-references' + +interface ForkMappingViewParams { + edge: ForkEdge + sourceWorkspaceId: string + targetWorkspaceId: string +} + +function suggestTarget( + kind: ForkRemapKind, + sourceLabel: string, + sourceProviderId: string | undefined, + candidates: ForkResourceCandidate[] +): string | null { + const normalized = sourceLabel.trim().toLowerCase() + const byLabel = candidates.filter((c) => c.label.trim().toLowerCase() === normalized) + if (kind === 'credential' && sourceProviderId) { + const match = byLabel.find((c) => c.providerId === sourceProviderId) + if (match) return match.id + } + if (byLabel.length === 1) return byLabel[0].id + return null +} + +/** + * Build the direction-oriented mapping view: every detected source reference with + * its current target (persisted or env identity), an auto-suggested target by + * name/provider, and the list of target candidates the UI can choose from. + */ +export async function getForkMappingView( + params: ForkMappingViewParams +): Promise<{ entries: ForkMappingEntry[] }> { + const { edge, sourceWorkspaceId, targetWorkspaceId } = params + const sourceIsParent = sourceWorkspaceId === edge.parentWorkspaceId + + const [mappingRows, targetEnvKeys, sourceEnvKeys, sourceCandidates, targetCandidates] = + await Promise.all([ + getEdgeMappingRows(db, edge.childWorkspaceId), + getWorkspaceEnvKeys(db, targetWorkspaceId), + getWorkspaceEnvKeys(db, sourceWorkspaceId), + listForkResourceCandidates(db, sourceWorkspaceId), + listForkResourceCandidates(db, targetWorkspaceId), + ]) + + const resolver = buildForkResolver(mappingRows, { sourceIsParent, targetEnvKeys, sourceEnvKeys }) + + const resourceTypeBySourceId = new Map>() + for (const row of mappingRows) { + // Workflow identity rows are system-managed, not user-mappable; skip them so a + // scanned reference can never be labeled `workflow` and the view stays within + // the mappable-type contract. + if (row.resourceType === 'workflow') continue + const key = sourceIsParent ? row.parentResourceId : row.childResourceId + if (key) resourceTypeBySourceId.set(key, row.resourceType) + } + + // Scan one deployed workflow state at a time and merge deduped references, so + // peak memory stays at a single workflow state rather than all of them at once. + const deployedWorkflows = await listDeployedWorkflows(db, sourceWorkspaceId) + const referenceByKey = new Map() + for (const wf of deployedWorkflows) { + const state = await readDeployedState(wf.id, sourceWorkspaceId) + if (!state) continue + const blocks = Object.values(state.blocks).map((block) => ({ + id: block.id, + name: block.name, + subBlocks: block.subBlocks as unknown, + })) + for (const reference of scanWorkflowReferences(blocks, () => null).references) { + referenceByKey.set(`${reference.kind}:${reference.sourceId}`, reference) + } + } + + const cascade = await detectForkCascadeReferences({ + executor: db, + sourceWorkspaceId, + references: Array.from(referenceByKey.values()), + resolve: () => null, + }) + for (const reference of cascade.references) { + referenceByKey.set(`${reference.kind}:${reference.sourceId}`, reference) + } + const references: ForkReference[] = Array.from(referenceByKey.values()) + + const entries: ForkMappingEntry[] = [] + for (const reference of references) { + // Only SOURCE workspace secrets are mappable; a `{{KEY}}` that isn't a source + // workspace env var is a personal (user-scoped) secret - leave it as-is. + if (reference.kind === 'env-var' && !sourceEnvKeys.has(reference.sourceId)) continue + let resourceType = resourceTypeBySourceId.get(reference.sourceId) + if (!resourceType) { + resourceType = + reference.kind === 'credential' + ? await classifyCredentialResourceType(db, reference.sourceId, sourceWorkspaceId) + : nonCredentialForkKindToResourceType(reference.kind) + } + + const sourceCandidate = sourceCandidates[reference.kind].find( + (c) => c.id === reference.sourceId + ) + const sourceLabel = sourceCandidate?.label ?? reference.sourceId + const candidates = targetCandidates[reference.kind] + const currentTargetId = resolver(reference.kind, reference.sourceId) + const targetId = + currentTargetId ?? + suggestTarget(reference.kind, sourceLabel, sourceCandidate?.providerId, candidates) + + entries.push({ + kind: reference.kind, + resourceType, + sourceId: reference.sourceId, + sourceLabel, + targetId, + required: reference.required, + candidates, + }) + } + + return { entries } +} + +export interface ApplyForkMappingEntry { + resourceType: ForkResourceType + sourceId: string + targetId: string | null +} + +/** + * Persist mapping edits for a direction. Pull maps a parent source to a child + * target; push maps a child source to a parent target (clearing a push mapping + * deletes the row). + */ +export async function applyForkMappingEntries( + tx: DbOrTx, + edge: ForkEdge, + userId: string, + direction: 'push' | 'pull', + entries: ApplyForkMappingEntry[] +): Promise { + if (entries.length === 0) return 0 + if (direction === 'pull') { + // Pull maps a parent source to a child target - one batched upsert. + await upsertEdgeMappings( + tx, + edge.childWorkspaceId, + userId, + entries.map((entry) => ({ + resourceType: entry.resourceType, + parentResourceId: entry.sourceId, + childResourceId: entry.targetId, + })) + ) + return entries.length + } + // Push rows are keyed by the child (source) side, but the table's unique key is on + // the parent side - so clear any existing row for each source first (one grouped + // delete), otherwise changing a push target leaves the old (parent, source) row + // behind and resolution becomes ambiguous. Then upsert the new (target, source) + // rows in one batch; a null target is a cleared mapping (delete only, no reinsert). + await deleteEdgeMappingsByChildResources( + tx, + edge.childWorkspaceId, + entries.map((entry) => ({ resourceType: entry.resourceType, childResourceId: entry.sourceId })) + ) + await upsertEdgeMappings( + tx, + edge.childWorkspaceId, + userId, + entries + .filter((entry) => entry.targetId != null) + .map((entry) => ({ + resourceType: entry.resourceType, + parentResourceId: entry.targetId as string, + childResourceId: entry.sourceId, + })) + ) + return entries.length +} + +/** + * Reject mapping entries whose chosen target does not belong to the target + * workspace, so a caller cannot point a remapped reference (or credential-access + * propagation) at a resource in a workspace they do not administer. Entries whose + * resource type is not user-mappable (only `workflow`, whose identity is + * system-managed) are rejected outright. + */ +export async function validateForkMappingTargets( + targetWorkspaceId: string, + entries: ApplyForkMappingEntry[] +): Promise { + const hasTargets = entries.some((entry) => entry.targetId != null) + if (!hasTargets) return + const candidates = await listForkResourceCandidates(db, targetWorkspaceId) + for (const entry of entries) { + if (entry.targetId == null) continue + const kind = resourceTypeToForkKind(entry.resourceType) + if (!kind) { + // `workflow` is the only null-kind type, and its identity is system-managed by + // fork/promote/rollback. A non-null target for it here is an invalid (or + // crafted) entry the editor must never persist - reject instead of skipping. + throw new ForkError( + `Resource type "${entry.resourceType}" cannot be mapped via the mapping editor`, + 400 + ) + } + const list = candidates[kind] + // An empty candidate list means the target workspace has no admissible + // resource of this kind, so ANY non-null target is invalid - reject rather + // than wave it through (the previous early-continue was the security hole). + if (!list.some((candidate) => candidate.id === entry.targetId)) { + throw new ForkError( + `Mapping target "${entry.targetId}" is not a valid ${kind} in the target workspace`, + 400 + ) + } + } +} + +export type { ForkMappingRow } diff --git a/apps/sim/lib/workspaces/fork/mapping/mapping-store.ts b/apps/sim/lib/workspaces/fork/mapping/mapping-store.ts new file mode 100644 index 00000000000..85c9af0ff48 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/mapping/mapping-store.ts @@ -0,0 +1,295 @@ +import { workspaceForkResourceMap } from '@sim/db/schema' +import { generateId } from '@sim/utils/id' +import { and, asc, eq, inArray, or, sql } from 'drizzle-orm' +import type { z } from 'zod' +import type { forkResourceTypeSchema } from '@/lib/api/contracts/workspace-fork' +import type { DbOrTx } from '@/lib/db/types' +import type { + ForkReferenceResolver, + ForkRemapKind, +} from '@/lib/workspaces/fork/remap/remap-references' + +/** Mapping rows per insert; each row binds ~8 params, keeping well under PG's limit. */ +const MAPPING_INSERT_CHUNK = 1000 + +/** Derived from the wire contract so the DB enum, Zod schema, and TS type stay in lockstep. */ +export type ForkResourceType = z.infer + +export interface ForkMappingRow { + id: string + childWorkspaceId: string + resourceType: ForkResourceType + parentResourceId: string + childResourceId: string | null +} + +export interface ForkMappingUpsert { + resourceType: ForkResourceType + parentResourceId: string + childResourceId: string | null +} + +const RESOURCE_TYPE_TO_FORK_KIND: Record = { + workflow: null, + oauth_credential: 'credential', + service_account_credential: 'credential', + env_var: 'env-var', + table: 'table', + knowledge_base: 'knowledge-base', + knowledge_document: 'knowledge-document', + file: 'file', + mcp_server: 'mcp-server', + custom_tool: 'custom-tool', + skill: 'skill', +} + +/** The remapper kind a stored resource type participates in, or null when it does not remap. */ +export function resourceTypeToForkKind(resourceType: ForkResourceType): ForkRemapKind | null { + return RESOURCE_TYPE_TO_FORK_KIND[resourceType] +} + +const NON_CREDENTIAL_FORK_KIND_TO_RESOURCE_TYPE: Record< + Exclude, + Exclude +> = { + 'env-var': 'env_var', + table: 'table', + 'knowledge-base': 'knowledge_base', + 'knowledge-document': 'knowledge_document', + file: 'file', + 'mcp-server': 'mcp_server', + 'custom-tool': 'custom_tool', + skill: 'skill', +} + +/** + * Stored resource type for a non-credential remap kind. Credentials are resolved + * separately via `classifyCredentialResourceType` since the type (oauth vs + * service account) depends on the credential row. + */ +export function nonCredentialForkKindToResourceType( + kind: Exclude +): Exclude { + return NON_CREDENTIAL_FORK_KIND_TO_RESOURCE_TYPE[kind] +} + +export async function getEdgeMappingRows( + executor: DbOrTx, + childWorkspaceId: string +): Promise { + const rows = await executor + .select({ + id: workspaceForkResourceMap.id, + childWorkspaceId: workspaceForkResourceMap.childWorkspaceId, + resourceType: workspaceForkResourceMap.resourceType, + parentResourceId: workspaceForkResourceMap.parentResourceId, + childResourceId: workspaceForkResourceMap.childResourceId, + }) + .from(workspaceForkResourceMap) + .where(eq(workspaceForkResourceMap.childWorkspaceId, childWorkspaceId)) + // Deterministic order so resolver/identity construction is stable if duplicates + // ever exist (the push edit + rollback cleanup prevent them, this is defense). + .orderBy(asc(workspaceForkResourceMap.createdAt), asc(workspaceForkResourceMap.id)) + return rows as ForkMappingRow[] +} + +/** + * Delete workflow-identity mapping rows by the ids on one side (parent or child). + * Used by rollback to dissolve the identity rows a promote created, so a later + * re-promote of the same source converges instead of leaking a second row. + */ +export async function deleteWorkflowIdentityByIds( + tx: DbOrTx, + childWorkspaceId: string, + side: 'parent' | 'child', + ids: string[] +): Promise { + if (ids.length === 0) return + const sideColumn = + side === 'parent' + ? workspaceForkResourceMap.parentResourceId + : workspaceForkResourceMap.childResourceId + await tx + .delete(workspaceForkResourceMap) + .where( + and( + eq(workspaceForkResourceMap.childWorkspaceId, childWorkspaceId), + eq(workspaceForkResourceMap.resourceType, 'workflow'), + inArray(sideColumn, ids) + ) + ) +} + +/** + * Insert mapping rows that don't already exist (used at fork time to seed every + * detected reference as unmapped). Existing rows are left untouched. + */ +export async function seedEdgeMappings( + tx: DbOrTx, + childWorkspaceId: string, + userId: string, + entries: ForkMappingUpsert[] +): Promise { + if (entries.length === 0) return + const now = new Date() + // Chunked so a fork copying many resources stays well under the Postgres bind + // parameter limit (each row binds ~8 params). + for (let i = 0; i < entries.length; i += MAPPING_INSERT_CHUNK) { + const batch = entries.slice(i, i + MAPPING_INSERT_CHUNK) + await tx + .insert(workspaceForkResourceMap) + .values( + batch.map((entry) => ({ + id: generateId(), + childWorkspaceId, + resourceType: entry.resourceType, + parentResourceId: entry.parentResourceId, + childResourceId: entry.childResourceId, + createdBy: userId, + createdAt: now, + updatedAt: now, + })) + ) + .onConflictDoNothing({ + target: [ + workspaceForkResourceMap.childWorkspaceId, + workspaceForkResourceMap.resourceType, + workspaceForkResourceMap.parentResourceId, + ], + }) + } +} + +/** + * Insert or update mapping rows in batched, chunked multi-row upserts, setting + * `childResourceId` (the chosen target) from the incoming row. Used when a user + * saves a mapping and to persist promote identity rows - one query per chunk + * instead of one per row, so a large save stays a short transaction. + * + * Entries are deduped by the conflict key (resourceType, parentResourceId), keeping + * the last (matching the prior per-row last-write-wins) so a batch can never trip + * Postgres's "ON CONFLICT DO UPDATE cannot affect row a second time". + */ +export async function upsertEdgeMappings( + tx: DbOrTx, + childWorkspaceId: string, + userId: string, + entries: ForkMappingUpsert[] +): Promise { + if (entries.length === 0) return + const now = new Date() + const byConflictKey = new Map() + for (const entry of entries) { + byConflictKey.set(`${entry.resourceType}:${entry.parentResourceId}`, entry) + } + const deduped = Array.from(byConflictKey.values()) + for (let i = 0; i < deduped.length; i += MAPPING_INSERT_CHUNK) { + const batch = deduped.slice(i, i + MAPPING_INSERT_CHUNK) + await tx + .insert(workspaceForkResourceMap) + .values( + batch.map((entry) => ({ + id: generateId(), + childWorkspaceId, + resourceType: entry.resourceType, + parentResourceId: entry.parentResourceId, + childResourceId: entry.childResourceId, + createdBy: userId, + createdAt: now, + updatedAt: now, + })) + ) + .onConflictDoUpdate({ + target: [ + workspaceForkResourceMap.childWorkspaceId, + workspaceForkResourceMap.resourceType, + workspaceForkResourceMap.parentResourceId, + ], + set: { childResourceId: sql`excluded.child_resource_id`, updatedAt: now }, + }) + } +} + +/** + * Remove mapping rows matched by their child-side (source) resource id, grouped by + * resource type into a single OR-of-INs - one query for the whole push save (the + * unique key is on the parent side, so a changed push target must drop the old + * (parent, source) row before the new one is inserted). + */ +export async function deleteEdgeMappingsByChildResources( + tx: DbOrTx, + childWorkspaceId: string, + pairs: Array<{ resourceType: ForkResourceType; childResourceId: string }> +): Promise { + if (pairs.length === 0) return + const idsByType = new Map() + for (const { resourceType, childResourceId } of pairs) { + const list = idsByType.get(resourceType) + if (list) list.push(childResourceId) + else idsByType.set(resourceType, [childResourceId]) + } + const conditions = Array.from(idsByType, ([resourceType, ids]) => + and( + eq(workspaceForkResourceMap.resourceType, resourceType), + inArray(workspaceForkResourceMap.childResourceId, ids) + ) + ) + await tx + .delete(workspaceForkResourceMap) + .where(and(eq(workspaceForkResourceMap.childWorkspaceId, childWorkspaceId), or(...conditions))) +} + +export interface BuildForkResolverOptions { + /** When the source side of the promote is the parent workspace (a pull). */ + sourceIsParent: boolean + /** + * Env keys present in the target workspace. A workspace-secret env reference with + * no explicit mapping resolves to itself when the same key exists in the target. + */ + targetEnvKeys?: Set + /** + * Env keys defined at the SOURCE workspace level. Only these are workspace secrets + * that can be mapped; any other `{{KEY}}` is a personal (user-scoped) secret that + * resolves identically in any workspace and is left as-is (never mapped/required). + */ + sourceEnvKeys?: Set +} + +/** + * Build a reference resolver from persisted mapping rows for the chosen + * direction. Translates a source-space resource id to its mapped target id; + * rows whose `childResourceId` is null (unmapped) are skipped. Env keys fall + * back to an identity mapping when the target workspace already has the key. + */ +export function buildForkResolver( + rows: ForkMappingRow[], + options: BuildForkResolverOptions +): ForkReferenceResolver { + const index = new Map>() + for (const row of rows) { + const kind = resourceTypeToForkKind(row.resourceType) + if (!kind) continue + if (row.childResourceId == null) continue + const sourceId = options.sourceIsParent ? row.parentResourceId : row.childResourceId + const targetId = options.sourceIsParent ? row.childResourceId : row.parentResourceId + let kindIndex = index.get(kind) + if (!kindIndex) { + kindIndex = new Map() + index.set(kind, kindIndex) + } + kindIndex.set(sourceId, targetId) + } + + return (kind, sourceId) => { + const mapped = index.get(kind)?.get(sourceId) + if (mapped != null) return mapped + if (kind === 'env-var') { + // Personal/global env vars (not a source workspace secret) are user-scoped and + // resolve identically in any workspace - leave them as-is, never map them. + if (options.sourceEnvKeys && !options.sourceEnvKeys.has(sourceId)) return sourceId + // Workspace secret already present in the target by the same name → identity. + if (options.targetEnvKeys?.has(sourceId)) return sourceId + } + return null + } +} diff --git a/apps/sim/lib/workspaces/fork/mapping/resources.ts b/apps/sim/lib/workspaces/fork/mapping/resources.ts new file mode 100644 index 00000000000..97b90be59f3 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/mapping/resources.ts @@ -0,0 +1,218 @@ +import { + credential, + customTools, + knowledgeBase, + mcpServers, + skill, + userTableDefinitions, + workflow, + workspaceEnvironment, + workspaceFiles, +} from '@sim/db/schema' +import { and, count, eq, isNull, sql } from 'drizzle-orm' +import type { DbOrTx } from '@/lib/db/types' +import type { ForkResourceType } from '@/lib/workspaces/fork/mapping/mapping-store' +import type { ForkRemapKind } from '@/lib/workspaces/fork/remap/remap-references' + +export interface ForkResourceCandidate { + id: string + label: string + providerId?: string +} + +const CANDIDATE_LIMIT = 1000 + +/** The set of env-var keys defined in a workspace (for resolver identity + gating). */ +export async function getWorkspaceEnvKeys( + executor: DbOrTx, + workspaceId: string +): Promise> { + const [row] = await executor + .select({ variables: workspaceEnvironment.variables }) + .from(workspaceEnvironment) + .where(eq(workspaceEnvironment.workspaceId, workspaceId)) + .limit(1) + const variables = row?.variables + if (!variables || typeof variables !== 'object') return new Set() + return new Set(Object.keys(variables as Record)) +} + +/** + * List the resources in a workspace that can serve as mapping targets, grouped by + * remap kind. Used to populate the mapping UI's target pickers and to label the + * source resources being mapped. `knowledge-document` and `file` are intentionally + * left empty for v1 (optional kinds resolved manually). + */ +export async function listForkResourceCandidates( + executor: DbOrTx, + workspaceId: string +): Promise> { + const [creds, wsEnvRows, tables, kbs, servers, tools, skills] = await Promise.all([ + executor + .select({ + id: credential.id, + displayName: credential.displayName, + providerId: credential.providerId, + }) + .from(credential) + .where(eq(credential.workspaceId, workspaceId)) + .limit(CANDIDATE_LIMIT), + executor + .select({ variables: workspaceEnvironment.variables }) + .from(workspaceEnvironment) + .where(eq(workspaceEnvironment.workspaceId, workspaceId)) + .limit(1), + executor + .select({ id: userTableDefinitions.id, name: userTableDefinitions.name }) + .from(userTableDefinitions) + .where( + and( + eq(userTableDefinitions.workspaceId, workspaceId), + isNull(userTableDefinitions.archivedAt) + ) + ) + .limit(CANDIDATE_LIMIT), + executor + .select({ id: knowledgeBase.id, name: knowledgeBase.name }) + .from(knowledgeBase) + .where(and(eq(knowledgeBase.workspaceId, workspaceId), isNull(knowledgeBase.deletedAt))) + .limit(CANDIDATE_LIMIT), + executor + .select({ id: mcpServers.id, name: mcpServers.name }) + .from(mcpServers) + .where(and(eq(mcpServers.workspaceId, workspaceId), isNull(mcpServers.deletedAt))) + .limit(CANDIDATE_LIMIT), + executor + .select({ id: customTools.id, title: customTools.title }) + .from(customTools) + .where(eq(customTools.workspaceId, workspaceId)) + .limit(CANDIDATE_LIMIT), + executor + .select({ id: skill.id, name: skill.name }) + .from(skill) + .where(eq(skill.workspaceId, workspaceId)) + .limit(CANDIDATE_LIMIT), + ]) + + const envVariables = wsEnvRows[0]?.variables + const envKeys = + envVariables && typeof envVariables === 'object' + ? Object.keys(envVariables as Record) + : [] + + return { + credential: creds.map((c) => ({ + id: c.id, + label: c.displayName, + providerId: c.providerId ?? undefined, + })), + 'env-var': envKeys.map((key) => ({ id: key, label: key })), + table: tables.map((t) => ({ id: t.id, label: t.name })), + 'knowledge-base': kbs.map((kb) => ({ id: kb.id, label: kb.name })), + 'mcp-server': servers.map((server) => ({ id: server.id, label: server.name })), + 'custom-tool': tools.map((tool) => ({ id: tool.id, label: tool.title })), + skill: skills.map((s) => ({ id: s.id, label: s.name })), + 'knowledge-document': [], + file: [], + } +} + +export interface ForkCopyableResources { + files: ForkResourceCandidate[] + tables: ForkResourceCandidate[] + knowledgeBases: ForkResourceCandidate[] + customTools: ForkResourceCandidate[] + skills: ForkResourceCandidate[] + mcpServers: ForkResourceCandidate[] + /** + * Count of deployed workflows that the fork would copy. The fork modal disables + * the action (with a reason) when this is 0 and there are no copyable resources, + * since the fork would otherwise produce an empty workspace. + */ + deployedWorkflowCount: number +} + +/** + * List the resources in a workspace that can be selected for copy at fork time + * (the content kinds — never credentials or env vars). Powers the fork modal's + * resource picker. + */ +export async function listForkCopyableResources( + executor: DbOrTx, + workspaceId: string +): Promise { + const [files, tables, kbs, tools, skills, servers, deployed] = await Promise.all([ + executor + .select({ + id: workspaceFiles.id, + // displayName is nullable; fall back to the (non-null) original name. + label: sql`coalesce(${workspaceFiles.displayName}, ${workspaceFiles.originalName})`, + }) + .from(workspaceFiles) + .where(and(eq(workspaceFiles.workspaceId, workspaceId), isNull(workspaceFiles.deletedAt))) + .limit(CANDIDATE_LIMIT), + executor + .select({ id: userTableDefinitions.id, label: userTableDefinitions.name }) + .from(userTableDefinitions) + .where( + and( + eq(userTableDefinitions.workspaceId, workspaceId), + isNull(userTableDefinitions.archivedAt) + ) + ) + .limit(CANDIDATE_LIMIT), + executor + .select({ id: knowledgeBase.id, label: knowledgeBase.name }) + .from(knowledgeBase) + .where(and(eq(knowledgeBase.workspaceId, workspaceId), isNull(knowledgeBase.deletedAt))) + .limit(CANDIDATE_LIMIT), + executor + .select({ id: customTools.id, label: customTools.title }) + .from(customTools) + .where(eq(customTools.workspaceId, workspaceId)) + .limit(CANDIDATE_LIMIT), + executor + .select({ id: skill.id, label: skill.name }) + .from(skill) + .where(eq(skill.workspaceId, workspaceId)) + .limit(CANDIDATE_LIMIT), + executor + .select({ id: mcpServers.id, label: mcpServers.name }) + .from(mcpServers) + .where(and(eq(mcpServers.workspaceId, workspaceId), isNull(mcpServers.deletedAt))) + .limit(CANDIDATE_LIMIT), + executor + .select({ value: count() }) + .from(workflow) + .where( + and( + eq(workflow.workspaceId, workspaceId), + eq(workflow.isDeployed, true), + isNull(workflow.archivedAt) + ) + ), + ]) + return { + files, + tables, + knowledgeBases: kbs, + customTools: tools, + skills, + mcpServers: servers, + deployedWorkflowCount: deployed[0]?.value ?? 0, + } +} + +/** Resolve a credential id to its stored mapping resource type. */ +export async function classifyCredentialResourceType( + executor: DbOrTx, + credentialId: string, + workspaceId: string +): Promise> { + const [row] = await executor + .select({ type: credential.type }) + .from(credential) + .where(and(eq(credential.id, credentialId), eq(credential.workspaceId, workspaceId))) + .limit(1) + return row?.type === 'service_account' ? 'service_account_credential' : 'oauth_credential' +} diff --git a/apps/sim/lib/workspaces/fork/promote/promote-plan.test.ts b/apps/sim/lib/workspaces/fork/promote/promote-plan.test.ts new file mode 100644 index 00000000000..d6e49f2c23d --- /dev/null +++ b/apps/sim/lib/workspaces/fork/promote/promote-plan.test.ts @@ -0,0 +1,82 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { buildPromoteWorkflowIdMap } from '@/lib/workspaces/fork/promote/promote-plan' + +/** + * `buildPromoteWorkflowIdMap` decides which cross-workflow references survive a + * promote: the resulting map is handed to `remapWorkflowReferencesInSubBlocks`, + * where a hit repoints the reference and a miss (with `clearUnmapped`) blanks it. + * These cases lock in the seed/overlay matrix so the "mapped sibling not in this + * push" repoint and the "deleted / archived / never-mapped" clears can't drift. + */ +describe('buildPromoteWorkflowIdMap', () => { + it("overlays this push's items (replace + create)", () => { + const map = buildPromoteWorkflowIdMap({ + identityMap: new Map(), + existingSourceIds: new Set(), + targetActiveIds: new Set(), + items: [ + { sourceWorkflowId: 'a-src', targetWorkflowId: 'a-tgt' }, + { sourceWorkflowId: 'b-src', targetWorkflowId: 'b-new' }, + ], + }) + expect(map.get('a-src')).toBe('a-tgt') + expect(map.get('b-src')).toBe('b-new') + expect(map.size).toBe(2) + }) + + it('repoints a mapped sibling that is not in this push when source exists and target is active', () => { + // B is mapped + still deployed in the target but undeployed in the source, so it + // is not an item this push. A references B and must keep pointing at target-B. + const map = buildPromoteWorkflowIdMap({ + identityMap: new Map([['b-src', 'b-tgt']]), + existingSourceIds: new Set(['b-src']), + targetActiveIds: new Set(['b-tgt']), + items: [{ sourceWorkflowId: 'a-src', targetWorkflowId: 'a-tgt' }], + }) + expect(map.get('b-src')).toBe('b-tgt') + expect(map.get('a-src')).toBe('a-tgt') + }) + + it('does not seed a mapped pair whose source was deleted (reference clears)', () => { + const map = buildPromoteWorkflowIdMap({ + identityMap: new Map([['b-src', 'b-tgt']]), + existingSourceIds: new Set(), // b-src deleted in the source + targetActiveIds: new Set(['b-tgt']), + items: [], + }) + expect(map.has('b-src')).toBe(false) + }) + + it('does not seed a mapped pair whose target was archived (reference clears)', () => { + const map = buildPromoteWorkflowIdMap({ + identityMap: new Map([['b-src', 'b-tgt']]), + existingSourceIds: new Set(['b-src']), + targetActiveIds: new Set(), // b-tgt archived by a prior push + items: [], + }) + expect(map.has('b-src')).toBe(false) + }) + + it('does not map a workflow that was never mapped (reference clears)', () => { + const map = buildPromoteWorkflowIdMap({ + identityMap: new Map([['b-src', 'b-tgt']]), + existingSourceIds: new Set(['b-src', 'c-src']), + targetActiveIds: new Set(['b-tgt']), + items: [], + }) + expect(map.has('c-src')).toBe(false) + }) + + it('lets this push override a stale identity mapping (re-created target wins)', () => { + const map = buildPromoteWorkflowIdMap({ + identityMap: new Map([['s', 't-old']]), + existingSourceIds: new Set(['s']), + targetActiveIds: new Set(['t-old']), + items: [{ sourceWorkflowId: 's', targetWorkflowId: 't-new' }], + }) + expect(map.get('s')).toBe('t-new') + }) +}) diff --git a/apps/sim/lib/workspaces/fork/promote/promote-plan.ts b/apps/sim/lib/workspaces/fork/promote/promote-plan.ts new file mode 100644 index 00000000000..719d716035e --- /dev/null +++ b/apps/sim/lib/workspaces/fork/promote/promote-plan.ts @@ -0,0 +1,257 @@ +import { workflow } from '@sim/db/schema' +import { generateId } from '@sim/utils/id' +import { and, eq, isNull } from 'drizzle-orm' +import type { DbOrTx } from '@/lib/db/types' +import type { DeployedWorkflowSummary } from '@/lib/workspaces/fork/copy/deploy-bridge' +import type { ForkEdge } from '@/lib/workspaces/fork/lineage/lineage' +import { detectForkCascadeReferences } from '@/lib/workspaces/fork/mapping/cascade' +import { buildForkResolver, getEdgeMappingRows } from '@/lib/workspaces/fork/mapping/mapping-store' +import { getWorkspaceEnvKeys } from '@/lib/workspaces/fork/mapping/resources' +import { getPromoteRunForEdge } from '@/lib/workspaces/fork/promote/promote-run-store' +import { + type ForkReference, + type ForkReferenceResolver, + scanWorkflowReferences, +} from '@/lib/workspaces/fork/remap/remap-references' +import type { WorkflowState } from '@/stores/workflows/workflow/types' + +export interface ForkPromotePlanItem { + sourceWorkflowId: string + targetWorkflowId: string + /** The matched target workflow's current name (for rename-aware mapping), null when creating. */ + targetName: string | null + mode: 'create' | 'replace' + sourceMeta: { + name: string + description: string | null + folderId: string | null + sortOrder: number + } +} + +export interface ForkPromotePlan { + childWorkspaceId: string + sourceWorkspaceId: string + targetWorkspaceId: string + direction: 'push' | 'pull' + resolver: ForkReferenceResolver + items: ForkPromotePlanItem[] + workflowIdMap: Map + /** Previously-mapped target workflows whose source no longer exists (to remove). */ + archivedTargetIds: string[] + /** Same as `archivedTargetIds`, with the target workflow name for the preview. */ + archivedTargets: Array<{ id: string; name: string }> + + references: ForkReference[] + unmappedRequired: ForkReference[] + unmappedOptional: ForkReference[] + /** Source MCP server ids that use OAuth and need re-authorization in the target. */ + mcpReauthServerIds: string[] + /** Review-only descriptions of inline secrets that cannot be id-mapped. */ + inlineSecretSources: string[] + willUpdate: number + willCreate: number + willArchive: number + drift: boolean +} + +/** + * Build the cross-workflow reference map used to rewrite `workflow-selector`, + * `manualWorkflowId`, and `workflow_input` references inside promoted workflows. + * + * Seeded from the persistent identity mappings - not just the workflows in THIS + * push - so a reference to a mapped sibling that isn't part of the current push + * (e.g. a workflow undeployed in the source but still existing and already + * deployed in the target) repoints at the existing target instead of clearing. + * Only pairs whose source still EXISTS and whose target is still ACTIVE are + * seeded: a deleted source (whose target is archived this push) stays unmapped so + * its inbound references clear, and a target archived by a prior push is never + * re-pointed at. The push's own items are overlaid last, so a created workflow + * contributes its fresh target id and a replaced one re-sets the same id. + */ +export function buildPromoteWorkflowIdMap(params: { + identityMap: Map + existingSourceIds: Set + targetActiveIds: Set + items: Array<{ sourceWorkflowId: string; targetWorkflowId: string }> +}): Map { + const { identityMap, existingSourceIds, targetActiveIds, items } = params + const workflowIdMap = new Map() + for (const [sourceId, targetId] of identityMap) { + if (existingSourceIds.has(sourceId) && targetActiveIds.has(targetId)) { + workflowIdMap.set(sourceId, targetId) + } + } + for (const item of items) workflowIdMap.set(item.sourceWorkflowId, item.targetWorkflowId) + return workflowIdMap +} + +/** + * Compute everything a promote needs without mutating. Only the source's + * **deployed** workflows participate; each plan item carries the source's active + * deployed state. Targets matched by the persisted workflow identity map are + * replaced; unmatched deployed sources create new targets. A target is archived + * only when it was previously mapped and its source is no longer deployed - + * target-native workflows are never touched. Shared by the diff preview and the + * promote orchestrator. + */ +export async function computeForkPromotePlan(params: { + executor: DbOrTx + edge: ForkEdge + sourceWorkspaceId: string + targetWorkspaceId: string + direction: 'push' | 'pull' + /** + * Source deployed workflows + their states, read by the caller BEFORE its + * transaction (see `loadSourceDeployedStates`) so the plan never checks out a + * second pooled connection from inside a tx. + */ + deployedSourceWorkflows: DeployedWorkflowSummary[] + sourceStates: Map +}): Promise { + const { + executor, + edge, + sourceWorkspaceId, + targetWorkspaceId, + direction, + deployedSourceWorkflows, + sourceStates, + } = params + + const mappingRows = await getEdgeMappingRows(executor, edge.childWorkspaceId) + const [targetEnvKeys, sourceEnvKeys] = await Promise.all([ + getWorkspaceEnvKeys(executor, targetWorkspaceId), + getWorkspaceEnvKeys(executor, sourceWorkspaceId), + ]) + const sourceIsParent = sourceWorkspaceId === edge.parentWorkspaceId + const resolver = buildForkResolver(mappingRows, { sourceIsParent, targetEnvKeys, sourceEnvKeys }) + + const identityMap = new Map() + for (const row of mappingRows) { + if (row.resourceType !== 'workflow' || row.childResourceId == null) continue + if (sourceIsParent) identityMap.set(row.parentResourceId, row.childResourceId) + else identityMap.set(row.childResourceId, row.parentResourceId) + } + + const [targetWorkflows, sourceWorkflowRows] = await Promise.all([ + executor + .select({ id: workflow.id, name: workflow.name, updatedAt: workflow.updatedAt }) + .from(workflow) + .where(and(eq(workflow.workspaceId, targetWorkspaceId), isNull(workflow.archivedAt))), + executor + .select({ id: workflow.id }) + .from(workflow) + .where(and(eq(workflow.workspaceId, sourceWorkspaceId), isNull(workflow.archivedAt))), + ]) + + const targetActiveIds = new Set(targetWorkflows.map((w) => w.id)) + const targetNameById = new Map(targetWorkflows.map((w) => [w.id, w.name])) + // Every source workflow that still EXISTS (deployed or not). A mapped target is + // archived only when its source was DELETED - not merely undeployed. A fresh fork + // leaves the child's workflows undeployed, so pushing back must not archive the + // parent's originals; undeployed sources are simply skipped (target left as-is). + const existingSourceIds = new Set(sourceWorkflowRows.map((w) => w.id)) + + // Build the items and scan references in one pass from the pre-read source states + // (loaded before the caller's transaction; see loadSourceDeployedStates). + const items: ForkPromotePlanItem[] = [] + const referenceByKey = new Map() + for (const source of deployedSourceWorkflows) { + const sourceState = sourceStates.get(source.id) + if (!sourceState) continue + + const mappedTargetId = identityMap.get(source.id) + const isReplace = Boolean(mappedTargetId && targetActiveIds.has(mappedTargetId)) + const targetWorkflowId = isReplace ? (mappedTargetId as string) : generateId() + items.push({ + sourceWorkflowId: source.id, + targetWorkflowId, + targetName: isReplace ? (targetNameById.get(targetWorkflowId) ?? null) : null, + mode: isReplace ? 'replace' : 'create', + sourceMeta: { + name: source.name, + description: source.description, + folderId: source.folderId, + sortOrder: source.sortOrder, + }, + }) + + const blocks = Object.values(sourceState.blocks).map((block) => ({ + id: block.id, + name: block.name, + subBlocks: block.subBlocks as unknown, + })) + for (const reference of scanWorkflowReferences(blocks, resolver).references) { + referenceByKey.set(`${reference.kind}:${reference.sourceId}`, reference) + } + } + + const workflowIdMap = buildPromoteWorkflowIdMap({ + identityMap, + existingSourceIds, + targetActiveIds, + items, + }) + + const writtenTargetIds = new Set(items.map((item) => item.targetWorkflowId)) + const archivedTargetIds: string[] = [] + for (const row of mappingRows) { + if (row.resourceType !== 'workflow' || row.childResourceId == null) continue + const mappedSourceId = sourceIsParent ? row.parentResourceId : row.childResourceId + const mappedTargetId = sourceIsParent ? row.childResourceId : row.parentResourceId + if (existingSourceIds.has(mappedSourceId)) continue + if (writtenTargetIds.has(mappedTargetId)) continue + if (targetActiveIds.has(mappedTargetId)) archivedTargetIds.push(mappedTargetId) + } + const archivedTargets = archivedTargetIds.map((id) => ({ + id, + name: targetNameById.get(id) ?? id, + })) + + const cascade = await detectForkCascadeReferences({ + executor, + sourceWorkspaceId, + references: Array.from(referenceByKey.values()), + resolve: resolver, + }) + for (const reference of cascade.references) { + referenceByKey.set(`${reference.kind}:${reference.sourceId}`, reference) + } + + const allReferences = Array.from(referenceByKey.values()) + const allUnmapped = allReferences.filter( + (reference) => resolver(reference.kind, reference.sourceId) == null + ) + const unmappedRequired = allUnmapped.filter((reference) => reference.required) + const unmappedOptional = allUnmapped.filter((reference) => !reference.required) + + const previousRun = await getPromoteRunForEdge(executor, edge.childWorkspaceId, targetWorkspaceId) + const drift = Boolean( + previousRun && targetWorkflows.some((w) => w.updatedAt > previousRun.createdAt) + ) + + const willUpdate = items.filter((i) => i.mode === 'replace').length + const willCreate = items.filter((i) => i.mode === 'create').length + + return { + childWorkspaceId: edge.childWorkspaceId, + sourceWorkspaceId, + targetWorkspaceId, + direction, + resolver, + items, + workflowIdMap, + archivedTargetIds, + archivedTargets, + references: allReferences, + unmappedRequired, + unmappedOptional, + mcpReauthServerIds: cascade.mcpReauthServerIds, + inlineSecretSources: cascade.inlineSecretSources, + willUpdate, + willCreate, + willArchive: archivedTargetIds.length, + drift, + } +} diff --git a/apps/sim/lib/workspaces/fork/promote/promote-run-store.ts b/apps/sim/lib/workspaces/fork/promote/promote-run-store.ts new file mode 100644 index 00000000000..3d12d78b544 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/promote/promote-run-store.ts @@ -0,0 +1,156 @@ +import { workspaceForkPromoteRun } from '@sim/db/schema' +import { generateId } from '@sim/utils/id' +import { and, desc, eq } from 'drizzle-orm' +import type { DbOrTx } from '@/lib/db/types' + +/** + * A target workflow's pre-promote deployed-version reference. Rollback reactivates + * `priorVersion` (and loads it into the draft); `null` means the target was not + * deployed before the promote, so rollback undeploys it instead. + */ +export interface PromoteRunWorkflowSnapshot { + workflowId: string + priorVersion: number | null +} + +export interface PromoteRunSnapshot { + /** Replaced targets: reactivate their prior deployed version on rollback. */ + updated: PromoteRunWorkflowSnapshot[] + /** Targets the promote created: undeploy + archive on rollback. */ + created: string[] + /** Orphan targets the promote archived: un-archive + reactivate on rollback. */ + archived: PromoteRunWorkflowSnapshot[] +} + +export interface PromoteRunRow { + id: string + childWorkspaceId: string + sourceWorkspaceId: string + targetWorkspaceId: string + direction: 'push' | 'pull' + snapshot: PromoteRunSnapshot + createdAt: Date +} + +/** The promote undo point for an edge in one direction (keyed by its target), or null. */ +export async function getPromoteRunForEdge( + executor: DbOrTx, + childWorkspaceId: string, + targetWorkspaceId: string +): Promise { + const [row] = await executor + .select({ + id: workspaceForkPromoteRun.id, + childWorkspaceId: workspaceForkPromoteRun.childWorkspaceId, + sourceWorkspaceId: workspaceForkPromoteRun.sourceWorkspaceId, + targetWorkspaceId: workspaceForkPromoteRun.targetWorkspaceId, + direction: workspaceForkPromoteRun.direction, + snapshot: workspaceForkPromoteRun.snapshot, + createdAt: workspaceForkPromoteRun.createdAt, + }) + .from(workspaceForkPromoteRun) + .where( + and( + eq(workspaceForkPromoteRun.childWorkspaceId, childWorkspaceId), + eq(workspaceForkPromoteRun.targetWorkspaceId, targetWorkspaceId) + ) + ) + .limit(1) + if (!row) return null + return { ...row, snapshot: row.snapshot as PromoteRunSnapshot } +} + +/** Replace the edge's undo point with a new run (single-level history). */ +export async function upsertPromoteRun( + tx: DbOrTx, + params: { + childWorkspaceId: string + sourceWorkspaceId: string + targetWorkspaceId: string + direction: 'push' | 'pull' + snapshot: PromoteRunSnapshot + userId: string + } +): Promise { + const now = new Date() + const id = generateId() + await tx + .insert(workspaceForkPromoteRun) + .values({ + id, + childWorkspaceId: params.childWorkspaceId, + sourceWorkspaceId: params.sourceWorkspaceId, + targetWorkspaceId: params.targetWorkspaceId, + direction: params.direction, + snapshot: params.snapshot, + createdBy: params.userId, + createdAt: now, + }) + .onConflictDoUpdate({ + target: [workspaceForkPromoteRun.childWorkspaceId, workspaceForkPromoteRun.targetWorkspaceId], + set: { + id, + sourceWorkspaceId: params.sourceWorkspaceId, + direction: params.direction, + snapshot: params.snapshot, + createdBy: params.userId, + createdAt: now, + }, + }) + return id +} + +/** + * Remove EVERY undo point targeting this workspace. Called after a rollback so the + * undo is single-level: only the latest sync into a target is ever undoable, and + * once it is undone there is no stack of older syncs to walk back into. + */ +export async function deleteAllPromoteRunsForTarget( + tx: DbOrTx, + targetWorkspaceId: string +): Promise { + await tx + .delete(workspaceForkPromoteRun) + .where(eq(workspaceForkPromoteRun.targetWorkspaceId, targetWorkspaceId)) +} + +/** + * The newest undo point targeting this workspace. A workspace can be the target of + * several edges (pushes from its children, a pull from its parent), so order by + * recency: this is the ONLY undoable sync - older ones are stale the moment a newer + * sync lands, and rollback refuses them. + */ +export async function getLatestPromoteRunForTarget( + executor: DbOrTx, + targetWorkspaceId: string +): Promise { + const [row] = await executor + .select({ + id: workspaceForkPromoteRun.id, + childWorkspaceId: workspaceForkPromoteRun.childWorkspaceId, + sourceWorkspaceId: workspaceForkPromoteRun.sourceWorkspaceId, + targetWorkspaceId: workspaceForkPromoteRun.targetWorkspaceId, + direction: workspaceForkPromoteRun.direction, + snapshot: workspaceForkPromoteRun.snapshot, + createdAt: workspaceForkPromoteRun.createdAt, + }) + .from(workspaceForkPromoteRun) + .where(eq(workspaceForkPromoteRun.targetWorkspaceId, targetWorkspaceId)) + .orderBy(desc(workspaceForkPromoteRun.createdAt)) + .limit(1) + if (!row) return null + return { ...row, snapshot: row.snapshot as PromoteRunSnapshot } +} + +/** + * The "other" workspace and direction of the latest sync into this target, for the + * UI's undo affordance. `sourceWorkspaceId` is the workspace the sync came from + * (rollback resolves the edge from target + other). + */ +export async function getUndoableRunForTarget( + executor: DbOrTx, + targetWorkspaceId: string +): Promise<{ sourceWorkspaceId: string; direction: 'push' | 'pull' } | null> { + const run = await getLatestPromoteRunForTarget(executor, targetWorkspaceId) + return run ? { sourceWorkspaceId: run.sourceWorkspaceId, direction: run.direction } : null +} diff --git a/apps/sim/lib/workspaces/fork/promote/promote.ts b/apps/sim/lib/workspaces/fork/promote/promote.ts new file mode 100644 index 00000000000..58ce7e9eb27 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/promote/promote.ts @@ -0,0 +1,366 @@ +import { db } from '@sim/db' +import { credential, credentialMember, workflow } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { generateId } from '@sim/utils/id' +import { and, eq, inArray } from 'drizzle-orm' +import { performFullDeploy } from '@/lib/workflows/orchestration/deploy' +import { undeployWorkflow } from '@/lib/workflows/persistence/utils' +import { + copyWorkflowStateIntoTarget, + resolveForkFolderMapping, +} from '@/lib/workspaces/fork/copy/copy-workflows' +import { + getActiveDeploymentVersionNumber, + loadSourceDeployedStates, +} from '@/lib/workspaces/fork/copy/deploy-bridge' +import { + acquireForkEdgeLock, + acquireForkTargetLock, + type ForkEdge, +} from '@/lib/workspaces/fork/lineage/lineage' +import { + type ForkMappingUpsert, + upsertEdgeMappings, +} from '@/lib/workspaces/fork/mapping/mapping-store' +import { + computeForkPromotePlan, + type ForkPromotePlan, +} from '@/lib/workspaces/fork/promote/promote-plan' +import { + type PromoteRunWorkflowSnapshot, + upsertPromoteRun, +} from '@/lib/workspaces/fork/promote/promote-run-store' +import { + createForkSubBlockTransform, + type ForkReference, +} from '@/lib/workspaces/fork/remap/remap-references' +import { notifyForkWorkflowChanged } from '@/lib/workspaces/fork/socket' +import { getUsersWithPermissions } from '@/lib/workspaces/permissions/utils' + +const logger = createLogger('WorkspaceForkPromote') + +export interface PromoteForkParams { + edge: ForkEdge + sourceWorkspaceId: string + targetWorkspaceId: string + direction: 'push' | 'pull' + force: boolean + userId: string + requestId?: string +} + +export interface PromoteForkResult { + promoteRunId: string + updated: number + created: number + archived: number + redeployed: number + unmappedRequired: Array> + drift: boolean + blocked: 'unmapped' | 'drift' | null +} + +function collectCredentialPairs(plan: ForkPromotePlan): Array<[string, string]> { + const pairs = new Map() + for (const reference of plan.references) { + if (reference.kind !== 'credential') continue + const target = plan.resolver('credential', reference.sourceId) + if (target) pairs.set(reference.sourceId, target) + } + return Array.from(pairs.entries()) +} + +interface PromoteTxBlocked { + blocked: 'unmapped' | 'drift' + unmappedRequired: PromoteForkResult['unmappedRequired'] + drift: boolean +} + +interface PromoteTxApplied { + blocked: null + promoteRunId: string + deployTargetIds: string[] + /** Actual written/archived counts (post-skip), not the pre-copy plan totals. */ + updated: number + created: number + archived: number + drift: boolean +} + +/** + * Execute a force promote along the edge. Only the source's deployed workflows + * participate: each one's active deployed state is remapped into the target + * (replacing mapped targets in place with deterministic block ids, creating new + * ones, archiving previously-mapped orphans whose source is no longer deployed), + * a version-reference rollback snapshot is captured, credential access is + * propagated, and every promoted target is deployed. The plan is computed inside + * the edge lock so concurrent promotes serialize. Blocks (without mutating) when + * required references are unmapped, or the target has drifted and `force` is not + * set. + */ +export async function promoteFork(params: PromoteForkParams): Promise { + const { edge, sourceWorkspaceId, targetWorkspaceId, direction, force, userId } = params + const requestId = params.requestId ?? 'unknown' + + const targetMembers = (await getUsersWithPermissions(targetWorkspaceId)).map((m) => m.userId) + + // Read the source's deployed workflows + states BEFORE the transaction so these + // heavy per-workflow reads never check out a second pooled connection from inside + // the promote tx (which can deadlock the pool at saturation). The source is + // read-only here, so this pre-tx snapshot is exactly what gets force-pushed. + const { deployedWorkflows, sourceStates } = await loadSourceDeployedStates(sourceWorkspaceId) + + const txResult: PromoteTxBlocked | PromoteTxApplied = await db.transaction(async (tx) => { + // Target lock before edge lock (consistent ordering): the target lock serializes + // every sync into this target so sibling forks can't interleave writes, and so + // rollback's "newest sync" check stays race-free against a concurrent promote. + await acquireForkTargetLock(tx, targetWorkspaceId) + await acquireForkEdgeLock(tx, edge.childWorkspaceId) + + const plan = await computeForkPromotePlan({ + executor: tx, + edge, + sourceWorkspaceId, + targetWorkspaceId, + direction, + deployedSourceWorkflows: deployedWorkflows, + sourceStates, + }) + + if (plan.unmappedRequired.length > 0) { + return { + blocked: 'unmapped', + unmappedRequired: plan.unmappedRequired.map((reference) => ({ + kind: reference.kind, + sourceId: reference.sourceId, + required: reference.required, + blockName: reference.blockName, + })), + drift: plan.drift, + } + } + + if (plan.drift && !force) { + return { blocked: 'drift', unmappedRequired: [], drift: true } + } + + const now = new Date() + const transform = createForkSubBlockTransform(plan.resolver) + const folderIdMap = await resolveForkFolderMapping({ + tx, + sourceWorkspaceId, + targetWorkspaceId, + userId, + now, + }) + + const updatedSnapshots: PromoteRunWorkflowSnapshot[] = [] + const createdTargetIds: string[] = [] + const writtenItems: typeof plan.items = [] + for (const item of plan.items) { + // Use the pre-read source state (loaded above, before the tx). An item only + // exists when its state was present at read time, so this lookup hits; the + // guard stays as defense so the written counts below never over-report. + const sourceState = sourceStates.get(item.sourceWorkflowId) + if (!sourceState) continue + if (item.mode === 'replace') { + const priorVersion = await getActiveDeploymentVersionNumber(tx, item.targetWorkflowId) + updatedSnapshots.push({ workflowId: item.targetWorkflowId, priorVersion }) + } else { + createdTargetIds.push(item.targetWorkflowId) + } + await copyWorkflowStateIntoTarget({ + tx, + targetWorkflowId: item.targetWorkflowId, + targetWorkspaceId, + userId, + mode: item.mode, + now, + sourceState, + sourceMeta: item.sourceMeta, + workflowIdMap: plan.workflowIdMap, + folderIdMap, + transformSubBlocks: transform, + requestId, + }) + writtenItems.push(item) + } + + const archivedSnapshots: PromoteRunWorkflowSnapshot[] = [] + for (const targetWorkflowId of plan.archivedTargetIds) { + const priorVersion = await getActiveDeploymentVersionNumber(tx, targetWorkflowId) + archivedSnapshots.push({ workflowId: targetWorkflowId, priorVersion }) + await undeployWorkflow({ workflowId: targetWorkflowId, tx }) + await tx + .update(workflow) + .set({ archivedAt: now, updatedAt: now }) + .where(eq(workflow.id, targetWorkflowId)) + } + + const identityEntries: ForkMappingUpsert[] = writtenItems.map((item) => ({ + resourceType: 'workflow' as const, + parentResourceId: direction === 'pull' ? item.sourceWorkflowId : item.targetWorkflowId, + childResourceId: direction === 'pull' ? item.targetWorkflowId : item.sourceWorkflowId, + })) + await upsertEdgeMappings(tx, edge.childWorkspaceId, userId, identityEntries) + + const credentialPairs = collectCredentialPairs(plan) + const propagationTargetIds = credentialPairs.map(([, targetCredId]) => targetCredId) + const validTargetCredentialIds = new Set() + if (propagationTargetIds.length > 0) { + const validRows = await tx + .select({ id: credential.id }) + .from(credential) + .where( + and( + inArray(credential.id, propagationTargetIds), + eq(credential.workspaceId, targetWorkspaceId) + ) + ) + for (const row of validRows) validTargetCredentialIds.add(row.id) + } + + const validPairs = credentialPairs.filter(([, targetCredId]) => + validTargetCredentialIds.has(targetCredId) + ) + if (validPairs.length > 0) { + // Batch all source credentials' active members in one query (instead of one + // per pair), then build a single insert. `targetMembers` becomes a Set for + // O(1) membership checks. + const targetMemberSet = new Set(targetMembers) + const memberRows = await tx + .select({ + credentialId: credentialMember.credentialId, + userId: credentialMember.userId, + role: credentialMember.role, + }) + .from(credentialMember) + .where( + and( + inArray( + credentialMember.credentialId, + validPairs.map(([sourceCredId]) => sourceCredId) + ), + eq(credentialMember.status, 'active') + ) + ) + const membersBySource = new Map< + string, + Array> + >() + for (const row of memberRows) { + if (!targetMemberSet.has(row.userId)) continue + const list = membersBySource.get(row.credentialId) + if (list) list.push({ userId: row.userId, role: row.role }) + else membersBySource.set(row.credentialId, [{ userId: row.userId, role: row.role }]) + } + const memberInserts = validPairs.flatMap(([sourceCredId, targetCredId]) => + (membersBySource.get(sourceCredId) ?? []).map((member) => ({ + id: generateId(), + credentialId: targetCredId, + userId: member.userId, + role: member.role, + status: 'active' as const, + joinedAt: now, + createdAt: now, + updatedAt: now, + })) + ) + if (memberInserts.length > 0) { + await tx + .insert(credentialMember) + .values(memberInserts) + .onConflictDoNothing({ + target: [credentialMember.credentialId, credentialMember.userId], + }) + } + } + + const promoteRunId = await upsertPromoteRun(tx, { + childWorkspaceId: edge.childWorkspaceId, + sourceWorkspaceId, + targetWorkspaceId, + direction, + userId, + snapshot: { + updated: updatedSnapshots, + created: createdTargetIds, + archived: archivedSnapshots, + }, + }) + + // A source whose active deployment vanished between plan and copy is skipped + // above, so report what was actually written - the plan totals would overstate. + const skipped = plan.items.length - writtenItems.length + if (skipped > 0) { + logger.warn( + `[${requestId}] Promote skipped ${skipped} source workflow(s) whose deployment disappeared between plan and apply`, + { sourceWorkspaceId, targetWorkspaceId, skipped } + ) + } + + return { + blocked: null, + promoteRunId, + deployTargetIds: writtenItems.map((item) => item.targetWorkflowId), + updated: updatedSnapshots.length, + created: createdTargetIds.length, + archived: archivedSnapshots.length, + drift: plan.drift, + } + }) + + if (txResult.blocked !== null) { + return { + promoteRunId: '', + updated: 0, + created: 0, + archived: 0, + redeployed: 0, + unmappedRequired: txResult.blocked === 'unmapped' ? txResult.unmappedRequired : [], + drift: txResult.drift, + blocked: txResult.blocked, + } + } + + let redeployed = 0 + for (const targetWorkflowId of txResult.deployTargetIds) { + try { + const result = await performFullDeploy({ workflowId: targetWorkflowId, userId, requestId }) + if (result.success) { + redeployed += 1 + } else { + logger.warn(`[${requestId}] Deploy after promote failed`, { + workflowId: targetWorkflowId, + error: result.error, + }) + void notifyForkWorkflowChanged(targetWorkflowId) + } + } catch (error) { + logger.error(`[${requestId}] Deploy after promote threw`, { + workflowId: targetWorkflowId, + error: getErrorMessage(error), + }) + void notifyForkWorkflowChanged(targetWorkflowId) + } + } + + logger.info(`[${requestId}] Promoted ${sourceWorkspaceId} -> ${targetWorkspaceId}`, { + updated: txResult.updated, + created: txResult.created, + archived: txResult.archived, + redeployed, + }) + + return { + promoteRunId: txResult.promoteRunId, + updated: txResult.updated, + created: txResult.created, + archived: txResult.archived, + redeployed, + unmappedRequired: [], + drift: txResult.drift, + blocked: null, + } +} diff --git a/apps/sim/lib/workspaces/fork/promote/rollback.ts b/apps/sim/lib/workspaces/fork/promote/rollback.ts new file mode 100644 index 00000000000..fca8980f5c7 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/promote/rollback.ts @@ -0,0 +1,224 @@ +import { db } from '@sim/db' +import { workflow } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { inArray } from 'drizzle-orm' +import { + performActivateVersion, + performRevertToVersion, +} from '@/lib/workflows/orchestration/deploy' +import { undeployWorkflow } from '@/lib/workflows/persistence/utils' +import { ForkError } from '@/lib/workspaces/fork/lineage/authz' +import { + acquireForkEdgeLock, + acquireForkTargetLock, + resolveForkEdge, +} from '@/lib/workspaces/fork/lineage/lineage' +import { deleteWorkflowIdentityByIds } from '@/lib/workspaces/fork/mapping/mapping-store' +import { + deleteAllPromoteRunsForTarget, + getLatestPromoteRunForTarget, +} from '@/lib/workspaces/fork/promote/promote-run-store' +import { notifyForkWorkflowChanged } from '@/lib/workspaces/fork/socket' + +const logger = createLogger('WorkspaceForkRollback') + +export interface RollbackForkParams { + targetWorkspaceId: string + otherWorkspaceId: string + userId: string + requestId?: string +} + +export interface RollbackForkResult { + restored: number + archived: number + unarchived: number + /** Snapshot workflows that no longer exist and so couldn't be reactivated. */ + skipped: number +} + +// A type alias (not interface) so it satisfies the `Record` +// param of performActivateVersion/performRevertToVersion. workspaceId is nullable +// to match the workflow table column. +type WorkflowRecord = { + id: string + workspaceId: string | null + name: string +} + +/** + * Undo the most recent promote into `targetWorkspaceId`. Ordering is durability- + * sensitive: prior deployed versions are reactivated (and loaded back into the + * draft) FIRST; only once every reactivation succeeds do we undeploy/archive the + * workflows the promote created, dissolve the identity rows it created, and delete + * the undo point. A reactivation failure throws with the undo point intact, so the + * rollback is retryable (every step is idempotent). No draft blobs are stored - + * the deployed version is the source of truth. + */ +export async function rollbackFork(params: RollbackForkParams): Promise { + const { targetWorkspaceId, otherWorkspaceId, userId } = params + const requestId = params.requestId ?? 'unknown' + + const edge = await resolveForkEdge(targetWorkspaceId, otherWorkspaceId) + if (!edge) { + throw new ForkError('These workspaces are not a direct fork edge', 400) + } + + // Only the most recent sync into the target is undoable. Undoing an older + // sibling's sync while a newer one stands would partially revert the target and + // strand the newer sync's changes (and its now-stale undo point). + const run = await getLatestPromoteRunForTarget(db, targetWorkspaceId) + if (!run) { + throw new ForkError('There is no promote to undo for this workspace', 404) + } + if (run.childWorkspaceId !== edge.childWorkspaceId) { + throw new ForkError( + 'A newer sync into this workspace exists; reopen and undo the most recent sync.', + 409 + ) + } + + const { updated, created, archived } = run.snapshot + + const reactivate: Array<{ workflowId: string; version: number }> = [] + const undeployIds = new Set() + for (const item of updated) { + if (item.priorVersion != null) + reactivate.push({ workflowId: item.workflowId, version: item.priorVersion }) + else undeployIds.add(item.workflowId) + } + for (const item of archived) { + if (item.priorVersion != null) + reactivate.push({ workflowId: item.workflowId, version: item.priorVersion }) + } + for (const workflowId of created) undeployIds.add(workflowId) + + const reactivateIds = reactivate.map((r) => r.workflowId) + const records = new Map() + if (reactivateIds.length > 0) { + const rows = await db + .select({ id: workflow.id, workspaceId: workflow.workspaceId, name: workflow.name }) + .from(workflow) + .where(inArray(workflow.id, reactivateIds)) + for (const row of rows) records.set(row.id, row) + } + + // Un-archive the orphans the promote archived BEFORE reactivating them. + if (archived.length > 0) { + await db.transaction(async (tx) => { + await acquireForkTargetLock(tx, targetWorkspaceId) + await acquireForkEdgeLock(tx, edge.childWorkspaceId) + await tx + .update(workflow) + .set({ archivedAt: null, updatedAt: new Date() }) + .where( + inArray( + workflow.id, + archived.map((i) => i.workflowId) + ) + ) + }) + } + + // Reactivate prior versions + restore drafts. Any failure aborts with the undo + // point intact so the operation can be retried. + const skipped: string[] = [] + for (const { workflowId, version } of reactivate) { + const record = records.get(workflowId) + if (!record) { + // The target was hard-deleted since the promote, so it can't be reactivated. + // Skipping (vs throwing) is deliberate: a throw here would wedge the undo + // point forever since the workflow never reappears. We record it so the + // partial restore is surfaced instead of silently over-reported. + skipped.push(workflowId) + continue + } + const activated = await performActivateVersion({ + workflowId, + version, + userId, + workflow: record, + requestId, + }) + if (!activated.success) { + throw new ForkError( + `Rollback could not reactivate workflow ${workflowId} (v${version}): ${activated.error ?? 'unknown error'}. The undo point is preserved - retry the rollback.`, + 500 + ) + } + const reverted = await performRevertToVersion({ workflowId, version, userId, workflow: record }) + if (!reverted.success) { + throw new ForkError( + `Rollback reactivated workflow ${workflowId} but could not restore its draft: ${reverted.error ?? 'unknown error'}. Retry the rollback.`, + 500 + ) + } + } + + if (skipped.length > 0) { + logger.warn( + `[${requestId}] Rollback skipped ${skipped.length} workflow(s) no longer in the database`, + { targetWorkspaceId, skipped } + ) + } + + // Reactivation fully succeeded: remove the workflows the promote created, + // dissolve the identity rows it created, and consume the undo point. + await db.transaction(async (tx) => { + await acquireForkTargetLock(tx, targetWorkspaceId) + await acquireForkEdgeLock(tx, edge.childWorkspaceId) + + // Under the target lock, confirm our run is still the newest sync into the + // target. A concurrent promote (same edge or a sibling) would make it stale; + // abort so we never clean up against a newer sync's state. + const current = await getLatestPromoteRunForTarget(tx, targetWorkspaceId) + if (!current || current.id !== run.id) { + throw new ForkError( + 'This undo was superseded by a newer sync into the target; reopen and retry.', + 409 + ) + } + + const now = new Date() + + for (const workflowId of undeployIds) { + await undeployWorkflow({ workflowId, tx }) + } + if (created.length > 0) { + await tx + .update(workflow) + .set({ archivedAt: now, updatedAt: now }) + .where(inArray(workflow.id, created)) + // A created target is the child side on pull and the parent side on push. + await deleteWorkflowIdentityByIds( + tx, + edge.childWorkspaceId, + run.direction === 'pull' ? 'child' : 'parent', + created + ) + } + + // Single-level undo: drop every undo point for this target (not just ours) so + // no older sibling sync becomes undoable once this one is undone. + await deleteAllPromoteRunsForTarget(tx, targetWorkspaceId) + }) + + for (const workflowId of undeployIds) void notifyForkWorkflowChanged(workflowId) + + // Attribute skips to their bucket so the counts reflect what was actually + // restored, not the snapshot size (a workflow is in exactly one of the two). + const skippedSet = new Set(skipped) + const skippedUpdated = updated.filter( + (item) => item.priorVersion != null && skippedSet.has(item.workflowId) + ).length + const result: RollbackForkResult = { + restored: updated.length - skippedUpdated, + archived: created.length, + unarchived: archived.length - (skipped.length - skippedUpdated), + skipped: skipped.length, + } + + logger.info(`[${requestId}] Rolled back promote into ${targetWorkspaceId}`, result) + + return result +} diff --git a/apps/sim/lib/workspaces/fork/remap/block-identity.test.ts b/apps/sim/lib/workspaces/fork/remap/block-identity.test.ts new file mode 100644 index 00000000000..4880f454bb3 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/remap/block-identity.test.ts @@ -0,0 +1,35 @@ +/** + * @vitest-environment node + */ +import { isValidUuid } from '@sim/utils/id' +import { describe, expect, it } from 'vitest' +import { deriveForkBlockId } from '@/lib/workspaces/fork/remap/block-identity' + +describe('deriveForkBlockId', () => { + const targetA = 'wf-target-a' + const targetB = 'wf-target-b' + const block1 = 'block-1' + const block2 = 'block-2' + + it('is deterministic for the same (targetWorkflowId, sourceBlockId)', () => { + expect(deriveForkBlockId(targetA, block1)).toBe(deriveForkBlockId(targetA, block1)) + }) + + it('yields different ids for the same source block in different target workflows', () => { + expect(deriveForkBlockId(targetA, block1)).not.toBe(deriveForkBlockId(targetB, block1)) + }) + + it('yields different ids for different source blocks in the same target workflow', () => { + expect(deriveForkBlockId(targetA, block1)).not.toBe(deriveForkBlockId(targetA, block2)) + }) + + it('produces a valid UUID string (v5)', () => { + const id = deriveForkBlockId(targetA, block1) + expect(isValidUuid(id)).toBe(true) + expect(id[14]).toBe('5') + }) + + it('does not collide the colon separator (a:bc vs ab:c)', () => { + expect(deriveForkBlockId('a', 'bc')).not.toBe(deriveForkBlockId('ab', 'c')) + }) +}) diff --git a/apps/sim/lib/workspaces/fork/remap/block-identity.ts b/apps/sim/lib/workspaces/fork/remap/block-identity.ts new file mode 100644 index 00000000000..a18f96a042e --- /dev/null +++ b/apps/sim/lib/workspaces/fork/remap/block-identity.ts @@ -0,0 +1,42 @@ +import { createHash } from 'node:crypto' + +/** + * Fixed namespace UUID for fork block-identity derivation. Changing this value + * would re-key every forked workflow's block ids, breaking webhook URLs and + * external block references (table workflow groups, chat output configs) across + * promotes - so it must never change. + */ +const FORK_BLOCK_NAMESPACE = '6f1c0e2a-9b3d-5e47-8a1c-2d4f6b8e0c13' + +function uuidToBytes(uuid: string): Buffer { + return Buffer.from(uuid.replace(/-/g, ''), 'hex') +} + +/** + * Deterministic UUIDv5 (SHA-1) of `name` within `namespace`. The same inputs + * always yield the same UUID, which is how fork block identity stays stable. + */ +function uuidV5(name: string, namespace: string): string { + const hash = createHash('sha1') + hash.update(uuidToBytes(namespace)) + hash.update(Buffer.from(name, 'utf8')) + const bytes = hash.digest().subarray(0, 16) + bytes[6] = (bytes[6] & 0x0f) | 0x50 + bytes[8] = (bytes[8] & 0x3f) | 0x80 + const hex = bytes.toString('hex') + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}` +} + +/** + * Derive the target block id for a source block copied into a target workflow. + * + * Identity is deterministic in `(targetWorkflowId, sourceBlockId)`, so a logical + * block keeps the same target id across every promote. This is what keeps trigger + * webhook URLs consistent and keeps external block-id references (table workflow + * groups, chat output configs, sim_trigger_state) valid across promotes. A source + * block that no longer exists simply has no derived target, so the target block + * disappears on the next force-replace. + */ +export function deriveForkBlockId(targetWorkflowId: string, sourceBlockId: string): string { + return uuidV5(`${targetWorkflowId}:${sourceBlockId}`, FORK_BLOCK_NAMESPACE) +} diff --git a/apps/sim/lib/workspaces/fork/remap/fork-bootstrap.ts b/apps/sim/lib/workspaces/fork/remap/fork-bootstrap.ts new file mode 100644 index 00000000000..b272e50d2d2 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/remap/fork-bootstrap.ts @@ -0,0 +1,26 @@ +import type { SubBlockRecord } from '@/lib/workflows/persistence/remap-internal-ids' +import { + type ForkRemapKind, + remapForkSubBlocks, +} from '@/lib/workspaces/fork/remap/remap-references' + +/** + * Resolves a source resource reference to its copied child id, or null when the + * resource was not copied into the fork. Credentials are never copied (always + * null), so credential references are cleared. + */ +export type ForkCopyResolver = (kind: ForkRemapKind, sourceId: string) => string | null + +/** + * A `copyWorkflowStateIntoTarget` transform for the initial fork. Runs the shared + * fork remapper in `create` mode: copyable resources the user selected are + * rewritten to their child ids; references to resources that were not copied (and + * all credential references) are cleared so the child workflow's subblocks start + * empty; env-var `{{KEY}}` references are preserved (name-based, they resolve once + * the child defines the key). + */ +export function createForkBootstrapTransform( + resolveCopied: ForkCopyResolver +): (subBlocks: SubBlockRecord) => SubBlockRecord { + return (subBlocks) => remapForkSubBlocks(subBlocks, resolveCopied, 'create').subBlocks +} diff --git a/apps/sim/lib/workspaces/fork/remap/remap-files.test.ts b/apps/sim/lib/workspaces/fork/remap/remap-files.test.ts new file mode 100644 index 00000000000..e1c5fb909a1 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/remap/remap-files.test.ts @@ -0,0 +1,52 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { remapForkFileUploadValue } from '@/lib/workspaces/fork/remap/remap-files' + +const map = (entries: Record) => (key: string) => entries[key] ?? null + +describe('remapForkFileUploadValue', () => { + it('rewrites a copied single object key, preserving other fields', () => { + const value = { key: 'src/a.pdf', name: 'a.pdf', type: 'application/pdf', size: 10 } + const result = remapForkFileUploadValue(value, map({ 'src/a.pdf': 'child/a.pdf' })) + expect(result).toEqual({ key: 'child/a.pdf', name: 'a.pdf', type: 'application/pdf', size: 10 }) + }) + + it('clears a single object whose file was not copied', () => { + const value = { key: 'src/a.pdf', name: 'a.pdf' } + expect(remapForkFileUploadValue(value, map({}))).toBe('') + }) + + it('remaps copied items and drops uncopied ones in an array', () => { + const value = [ + { key: 'src/a.pdf', name: 'a.pdf' }, + { key: 'src/b.pdf', name: 'b.pdf' }, + ] + const result = remapForkFileUploadValue(value, map({ 'src/a.pdf': 'child/a.pdf' })) + expect(result).toEqual([{ key: 'child/a.pdf', name: 'a.pdf' }]) + }) + + it('handles a JSON-stringified value and re-serializes', () => { + const value = JSON.stringify({ key: 'src/a.pdf', name: 'a.pdf' }) + const result = remapForkFileUploadValue(value, map({ 'src/a.pdf': 'child/a.pdf' })) + expect(result).toBe(JSON.stringify({ key: 'child/a.pdf', name: 'a.pdf' })) + }) + + it('falls back to the path field when there is no key', () => { + const value = { path: 'src/a.pdf', name: 'a.pdf' } + const result = remapForkFileUploadValue(value, map({ 'src/a.pdf': 'child/a.pdf' })) + expect(result).toEqual({ path: 'child/a.pdf', name: 'a.pdf' }) + }) + + it('returns the value unchanged when no items match', () => { + const value = { key: 'src/a.pdf', name: 'a.pdf' } + const sameKey = map({ 'src/a.pdf': 'src/a.pdf' }) + expect(remapForkFileUploadValue(value, sameKey)).toBe(value) + }) + + it('returns empty/unparseable values untouched', () => { + expect(remapForkFileUploadValue('', map({}))).toBe('') + expect(remapForkFileUploadValue(null, map({}))).toBe(null) + }) +}) diff --git a/apps/sim/lib/workspaces/fork/remap/remap-files.ts b/apps/sim/lib/workspaces/fork/remap/remap-files.ts new file mode 100644 index 00000000000..ea83a6d161c --- /dev/null +++ b/apps/sim/lib/workspaces/fork/remap/remap-files.ts @@ -0,0 +1,77 @@ +/** + * `file-upload` subblock remapping for fork/promote. + * + * A `file-upload` value is a workspace-file reference (or array of them) stored as + * objects `{ key, name, ... }` where `key` is the object-storage key (NOT the + * `workspace_files.id`). Forking copies the blob to a new key; this rewrites each + * reference's key to the copied key, preserving the rest of the object. References + * whose file was not copied are dropped (the field is emptied) rather than left + * pointing at another workspace's blob. External `file-selector` references + * (provider file ids, credential-scoped) are NOT handled here - they carry over + * unchanged. + */ + +function parseMaybeJson(value: unknown): { value: unknown; serialized: boolean } { + if (typeof value !== 'string') return { value, serialized: false } + const trimmed = value.trim() + const looksJson = + (trimmed.startsWith('{') && trimmed.endsWith('}')) || + (trimmed.startsWith('[') && trimmed.endsWith(']')) + if (!looksJson) return { value, serialized: false } + try { + return { value: JSON.parse(trimmed), serialized: true } + } catch { + return { value, serialized: false } + } +} + +/** The field a file-upload item uses as its storage key, and that key's value. */ +function fileItemKeyField(item: unknown): { field: 'key' | 'path' | 'name'; key: string } | null { + if (!item || typeof item !== 'object' || Array.isArray(item)) return null + const record = item as Record + for (const field of ['key', 'path', 'name'] as const) { + const value = record[field] + if (typeof value === 'string' && value.trim().length > 0) return { field, key: value } + } + return null +} + +/** + * Remap a `file-upload` subblock value. `resolveFileKey(sourceKey)` returns the + * copied target storage key, or null when the file was not copied (drop the ref). + */ +export function remapForkFileUploadValue( + value: unknown, + resolveFileKey: (sourceKey: string) => string | null +): unknown { + const parsed = parseMaybeJson(value) + const isArray = Array.isArray(parsed.value) + const items = isArray ? (parsed.value as unknown[]) : parsed.value ? [parsed.value] : [] + if (items.length === 0) return value + + const next: unknown[] = [] + let changed = false + for (const item of items) { + const keyInfo = fileItemKeyField(item) + if (!keyInfo) { + next.push(item) + continue + } + const targetKey = resolveFileKey(keyInfo.key) + if (targetKey == null) { + changed = true + continue + } + if (targetKey === keyInfo.key) { + next.push(item) + continue + } + changed = true + next.push({ ...(item as Record), [keyInfo.field]: targetKey }) + } + + if (!changed) return value + if (next.length === 0) return '' + if (isArray) return parsed.serialized ? JSON.stringify(next) : next + return parsed.serialized ? JSON.stringify(next[0]) : next[0] +} diff --git a/apps/sim/lib/workspaces/fork/remap/remap-references.test.ts b/apps/sim/lib/workspaces/fork/remap/remap-references.test.ts new file mode 100644 index 00000000000..a6099956daf --- /dev/null +++ b/apps/sim/lib/workspaces/fork/remap/remap-references.test.ts @@ -0,0 +1,241 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it, vi } from 'vitest' +import type { SubBlockConfig } from '@/blocks/types' + +// The indexer resolves a tool's params via the tool registry; stub it so the +// injected blockConfigs subBlocks drive resolution deterministically in tests. +vi.mock('@/tools/params', () => ({ + getToolIdForOperation: () => undefined, + getToolParametersConfig: () => null, + getSubBlocksForToolInput: ( + _toolId: string, + _type: string, + _values: unknown, + _modes: unknown, + provided?: { subBlocks?: SubBlockConfig[] } + ) => ({ subBlocks: provided?.subBlocks ?? [] }), + formatParameterLabel: (label: string) => label, +})) + +import type { SubBlockRecord } from '@/lib/workflows/persistence/remap-internal-ids' +import { + getOverrideForkKind, + remapForkSubBlocks, + remapToolBlockResources, + rewriteMcpToolSelectorValue, +} from '@/lib/workspaces/fork/remap/remap-references' + +const blockConfigs: Record = { + testblock: { + subBlocks: [ + { id: 'credential', title: 'Credential', type: 'oauth-input', serviceId: 'gmail' }, + { id: 'knowledgeBaseId', title: 'KB', type: 'knowledge-base-selector' }, + { id: 'channel', title: 'Channel', type: 'channel-selector', serviceId: 'slack' }, + ], + }, +} + +describe('remapToolBlockResources', () => { + it('remaps nested credential + knowledge-base ids and leaves external selectors', () => { + const tool = { + type: 'testblock', + toolId: 'testblock_run', + params: { credential: 'cred-src', knowledgeBaseId: 'kb-src', channel: 'C123' }, + } + const map: Record = { + 'credential:cred-src': 'cred-dst', + 'knowledge-base:kb-src': 'kb-dst', + } + const result = remapToolBlockResources(tool, { + resolve: (kind, id) => map[`${kind}:${id}`] ?? null, + resolveFileKey: () => null, + clearUnresolved: false, + blockConfigs, + }) + expect(result.params).toEqual({ + credential: 'cred-dst', + knowledgeBaseId: 'kb-dst', + channel: 'C123', + }) + }) + + it('clears unresolved copyable refs when clearUnresolved is set (fork)', () => { + const tool = { + type: 'testblock', + toolId: 'testblock_run', + params: { credential: 'cred-src', knowledgeBaseId: 'kb-src', channel: 'C123' }, + } + const result = remapToolBlockResources(tool, { + resolve: () => null, + resolveFileKey: () => null, + clearUnresolved: true, + blockConfigs, + }) + expect(result.params).toEqual({ credential: '', knowledgeBaseId: '', channel: 'C123' }) + }) + + it('keeps unresolved refs and records them when not clearing (promote)', () => { + const tool = { + type: 'testblock', + toolId: 'testblock_run', + params: { credential: 'cred-src', channel: 'C123' }, + } + const recorded: Array<{ kind: string; id: string; mapped: boolean }> = [] + const result = remapToolBlockResources(tool, { + resolve: () => null, + resolveFileKey: () => null, + record: (kind, id, mapped) => recorded.push({ kind, id, mapped }), + clearUnresolved: false, + blockConfigs, + }) + expect((result.params as Record).credential).toBe('cred-src') + expect(recorded).toContainEqual({ kind: 'credential', id: 'cred-src', mapped: false }) + }) + + it('returns the tool unchanged when it has no params', () => { + const tool = { type: 'testblock' } + expect( + remapToolBlockResources(tool, { + resolve: () => null, + resolveFileKey: () => null, + clearUnresolved: true, + blockConfigs, + }) + ).toBe(tool) + }) + + it('clears a nested advanced-mode manualCredential id (keyed by id, not type)', () => { + const tool = { + type: 'testblock', + toolId: 'testblock_run', + params: { manualCredential: 'mc-src', knowledgeBaseId: 'kb-src' }, + } + const result = remapToolBlockResources(tool, { + resolve: (kind, id) => (kind === 'knowledge-base' && id === 'kb-src' ? 'kb-dst' : null), + resolveFileKey: () => null, + clearUnresolved: true, + blockConfigs, + }) + expect(result.params).toEqual({ manualCredential: '', knowledgeBaseId: 'kb-dst' }) + }) + + it('drops only the uncopied entry in a mixed multi-value field', () => { + const tool = { + type: 'testblock', + toolId: 'testblock_run', + params: { knowledgeBaseId: 'kb1,kb2' }, + } + const result = remapToolBlockResources(tool, { + resolve: (_kind, id) => (id === 'kb1' ? 'kb1-dst' : null), + resolveFileKey: () => null, + clearUnresolved: true, + blockConfigs, + }) + const value = (result.params as Record).knowledgeBaseId + expect(value.split(',').filter(Boolean)).toEqual(['kb1-dst']) + }) + + it('resolves a credential param by id even when its config is filtered out (reactive)', () => { + // blockConfigs has no `credential` subBlock (simulating a reactive-gated field + // hidden from getToolInputParamConfigs); the raw id-scan must still catch it. + const tool = { + type: 'reactiveblock', + toolId: 'reactiveblock_run', + params: { credential: 'cred-src' }, + } + const result = remapToolBlockResources(tool, { + resolve: (kind, id) => (kind === 'credential' && id === 'cred-src' ? 'cred-dst' : null), + resolveFileKey: () => null, + clearUnresolved: false, + blockConfigs: { reactiveblock: { subBlocks: [] } }, + }) + expect((result.params as Record).credential).toBe('cred-dst') + }) +}) + +describe('remapForkSubBlocks', () => { + const subBlocks = (): SubBlockRecord => ({ + credential: { id: 'credential', type: 'oauth-input', value: 'c-src' }, + knowledgeBaseId: { id: 'knowledgeBaseId', type: 'knowledge-base-selector', value: 'kb-src' }, + manualCredential: { id: 'manualCredential', type: 'short-input', value: 'mc-src' }, + }) + + it('create mode: clears unresolved credentials and remaps copied resources', () => { + const result = remapForkSubBlocks( + subBlocks(), + (kind, id) => (kind === 'knowledge-base' && id === 'kb-src' ? 'kb-dst' : null), + 'create' + ) + expect(result.subBlocks.credential.value).toBe('') + expect(result.subBlocks.knowledgeBaseId.value).toBe('kb-dst') + expect(result.subBlocks.manualCredential.value).toBe('') + expect(result.references).toHaveLength(0) + }) + + it('promote mode: keeps + records unmapped credentials (incl. manual override)', () => { + const result = remapForkSubBlocks( + subBlocks(), + (kind, id) => (kind === 'knowledge-base' && id === 'kb-src' ? 'kb-dst' : null), + 'promote' + ) + // unresolved credentials are kept (not cleared) and surfaced as required. + expect(result.subBlocks.credential.value).toBe('c-src') + expect(result.subBlocks.manualCredential.value).toBe('mc-src') + expect(result.subBlocks.knowledgeBaseId.value).toBe('kb-dst') + const unmappedKinds = result.unmapped.map((r) => `${r.kind}:${r.sourceId}`) + expect(unmappedKinds).toContain('credential:c-src') + expect(unmappedKinds).toContain('credential:mc-src') + expect(result.unmapped.every((r) => r.kind !== 'knowledge-base')).toBe(true) + }) + + it('promote mode: rewrites {{ENV}} nested in an array-form tool param', () => { + const sb: SubBlockRecord = { + tools: { + id: 'tools', + type: 'tool-input', + value: [{ type: 'genericblock', params: { subject: 'Hi {{OLD}}' } }], + }, + } + const result = remapForkSubBlocks( + sb, + (kind, id) => (kind === 'env-var' && id === 'OLD' ? 'NEW' : null), + 'promote' + ) + const tools = result.subBlocks.tools.value as Array<{ params: { subject: string } }> + expect(tools[0].params.subject).toBe('Hi {{NEW}}') + }) +}) + +describe('getOverrideForkKind', () => { + it('maps manual* short-input overrides to their fork kind', () => { + expect(getOverrideForkKind('manualCredential')).toBe('credential') + expect(getOverrideForkKind('manualTableId')).toBe('table') + expect(getOverrideForkKind('manualCredential_2')).toBe('credential') + }) + it('returns null for type-handled / unrelated ids', () => { + expect(getOverrideForkKind('credential')).toBeNull() + expect(getOverrideForkKind('knowledgeBaseId')).toBeNull() + expect(getOverrideForkKind('manualWorkflowId')).toBeNull() + }) +}) + +describe('rewriteMcpToolSelectorValue', () => { + const remaps = new Map([['server-old', 'server-new']]) + + it('rewrites the embedded server id', () => { + expect(rewriteMcpToolSelectorValue('mcp-server-old-search', remaps)).toBe( + 'mcp-server-new-search' + ) + }) + + it('leaves values without the old server id', () => { + expect(rewriteMcpToolSelectorValue('mcp-other-search', remaps)).toBe('mcp-other-search') + }) + + it('is a no-op for empty values or empty remaps', () => { + expect(rewriteMcpToolSelectorValue('', remaps)).toBe('') + expect(rewriteMcpToolSelectorValue('mcp-server-old-x', new Map())).toBe('mcp-server-old-x') + }) +}) diff --git a/apps/sim/lib/workspaces/fork/remap/remap-references.ts b/apps/sim/lib/workspaces/fork/remap/remap-references.ts new file mode 100644 index 00000000000..9df2d43082e --- /dev/null +++ b/apps/sim/lib/workspaces/fork/remap/remap-references.ts @@ -0,0 +1,620 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import type { SubBlockType } from '@sim/workflow-types/blocks' +import type { z } from 'zod' +import type { forkRemapKindSchema } from '@/lib/api/contracts/workspace-fork' +import { createMcpToolId } from '@/lib/mcp/shared' +import { + coerceObjectArray, + isRecord, + type SubBlockRecord, +} from '@/lib/workflows/persistence/remap-internal-ids' +import { CREDENTIAL_SUBBLOCK_IDS } from '@/lib/workflows/persistence/utils' +import { getToolInputParamConfigs } from '@/lib/workflows/search-replace/indexer' +import { + getWorkflowSearchSubBlockResourceDefinition, + parseWorkflowSearchSubBlockResources, + type StructuredWorkflowSearchResourceKind, +} from '@/lib/workflows/search-replace/resources/registry' +import type { ParsedStoredTool } from '@/lib/workflows/tool-input/types' +import { remapForkFileUploadValue } from '@/lib/workspaces/fork/remap/remap-files' + +/** + * Resource kinds the fork remapper rewrites across workspaces, derived from the + * wire contract so the union can't drift from `forkRemapKindSchema`. `workflow`, + * `mcp-tool`, and the service-specific `selector-resource` kinds are deliberately + * excluded: workflow references are remapped via the workflow identity map, and + * MCP tool / selector ids are not workspace-local so they carry over unchanged. + */ +export type ForkRemapKind = z.infer + +const logger = createLogger('WorkspaceForkRemapReferences') + +const REQUIRED_KINDS = new Set(['credential', 'env-var']) + +/** + * Advanced-mode `short-input` overrides whose VALUE is a workspace-scoped id keyed + * by subblock id (not type) - the type-based registry path misses them. `credential` + * / `triggerCredentials` are excluded here because they are `oauth-input` (already + * type-handled); `manualWorkflowId` is handled by the workflow id map in + * `remap-internal-ids`. Returns the fork kind to resolve the raw string value as. + */ +export function getOverrideForkKind(subBlockId: string): ForkRemapKind | null { + const base = subBlockId.replace(/_\d+$/, '') + if (base === 'manualCredential') return 'credential' + if (base === 'manualTableId') return 'table' + return null +} + +/** + * Id-based override kind for a TOOL param. Broader than {@link getOverrideForkKind}: + * it also covers the canonical `credential` / `triggerCredentials` keys (not just + * the `manual*` variants) because a tool param's config can be filtered out by a + * reactive condition (no `credentialTypeById` here), which would otherwise skip its + * credential. Resolving by id makes nested credential handling reactive-robust. + */ +function getToolParamOverrideKind(paramId: string): ForkRemapKind | null { + const base = paramId.replace(/_\d+$/, '') + if (CREDENTIAL_SUBBLOCK_IDS.has(base)) return 'credential' + if (base === 'manualTableId') return 'table' + return null +} + +export const REGISTRY_KIND_TO_FORK_KIND: Partial< + Record +> = { + 'oauth-credential': 'credential', + 'knowledge-base': 'knowledge-base', + table: 'table', + 'mcp-server': 'mcp-server', +} +// `file` and `knowledge-document` are intentionally excluded from the generic +// registry path. `file-upload` (workspace files) is remapped by storage key via +// `remapForkFileUploadValue`; `file-selector` (external provider file ids, +// credential-scoped) carries over unchanged; `document-selector` ids are never +// valid cross-workspace (docs get fresh ids on fork and aren't synced on promote), +// so they're cleared via `remapDocumentSelectorValue` rather than dangled. + +/** Matches `{{ENV_KEY}}` references inside subblock values; shared with cascade detection. */ +export const ENV_REF_PATTERN = /\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g + +/** + * Resolves a source-workspace resource id (or env key, for `env-var`) to its + * mapped target id. Returns the target id (which may equal the source for env + * keys that exist under the same name), or null/undefined when there is no + * mapping — which surfaces the reference as unmapped. + */ +export type ForkReferenceResolver = ( + kind: ForkRemapKind, + sourceId: string +) => string | null | undefined + +export interface ForkReference { + kind: ForkRemapKind + sourceId: string + blockId?: string + blockName?: string + subBlockKey: string + required: boolean +} + +export interface RemapSubBlocksResult { + subBlocks: SubBlockRecord + references: ForkReference[] + unmapped: ForkReference[] +} + +function remapEnvInValue( + value: unknown, + resolve: ForkReferenceResolver, + record: (sourceId: string, mapped: boolean) => void +): unknown { + if (typeof value === 'string') { + return value.replace(ENV_REF_PATTERN, (full, key: string) => { + const target = resolve('env-var', key) + if (target == null) { + record(key, false) + return full + } + record(key, true) + return `{{${target}}}` + }) + } + if (Array.isArray(value)) { + return value.map((item) => remapEnvInValue(item, resolve, record)) + } + // Recurse plain objects so `{{ENV}}` nested in array-form tool params (and other + // object-valued subblocks) is rewritten, not just top-level strings/arrays. + if (isRecord(value)) { + let changed = false + const next: Record = {} + for (const [key, nested] of Object.entries(value)) { + const remapped = remapEnvInValue(nested, resolve, record) + if (remapped !== nested) changed = true + next[key] = remapped + } + return changed ? next : value + } + return value +} + +interface ToolBlockRemapOptions { + resolve: ForkReferenceResolver + /** Resolve a copied file storage key; null when the file was not copied. */ + resolveFileKey: (sourceKey: string) => string | null + /** Record a detected reference so it surfaces in the mapping UI / cascade. */ + record?: (kind: ForkRemapKind, sourceId: string, mapped: boolean) => void + /** Fork-create clears unresolved copyable refs; promote keeps them (surfaced as unmapped). */ + clearUnresolved: boolean + /** Injected block configs (production falls back to the block registry). */ + blockConfigs?: Parameters[0]['blockConfigs'] +} + +/** + * Rewrite the workspace-scoped resource ids nested inside a block tool's `params` + * (credentials, KBs, tables, files, MCP servers). Param→subBlock-config resolution + * reuses `getToolInputParamConfigs` so it matches exactly what the editor/search + * index sees. Custom-tool / MCP / workflow_input tools carry their ids in dedicated + * fields (handled by the callers / the workflow id map), not block params, so they + * pass through here untouched. Returns a new tool object only when something changed. + */ +export function remapToolBlockResources( + tool: Record, + opts: ToolBlockRemapOptions +): Record { + if (typeof tool.type !== 'string') return tool + const params = tool.params + if (!isRecord(params)) return tool + + let nextParams: Record | null = null + const setParam = (paramId: string, value: unknown) => { + nextParams ??= { ...params } + nextParams[paramId] = value + } + + // Id-keyed resource params (credential / triggerCredentials / manual* overrides): + // walked from the raw params so they're caught even when their config is filtered + // out by a reactive condition (the registry loop below would otherwise miss them). + for (const paramId of Object.keys(params)) { + const overrideKind = getToolParamOverrideKind(paramId) + if (!overrideKind) continue + const currentValue = params[paramId] + if (typeof currentValue !== 'string' || !currentValue) continue + const target = opts.resolve(overrideKind, currentValue) + opts.record?.(overrideKind, currentValue, target != null) + if (target != null) { + if (target !== currentValue) setParam(paramId, target) + } else if (opts.clearUnresolved) { + setParam(paramId, '') + } + } + + const toolView: ParsedStoredTool = { + type: tool.type, + operation: typeof tool.operation === 'string' ? tool.operation : undefined, + toolId: typeof tool.toolId === 'string' ? tool.toolId : undefined, + customToolId: typeof tool.customToolId === 'string' ? tool.customToolId : undefined, + params, + } + let configs: ReturnType + try { + configs = getToolInputParamConfigs({ tool: toolView, blockConfigs: opts.blockConfigs }) + } catch (error) { + // Unknown block / resolver failure: don't crash the fork/promote, but log so a + // real bug isn't masked. Nested resource ids in this tool stay as-is. + logger.warn('Could not resolve tool params for fork remap', { + toolType: tool.type, + error: getErrorMessage(error), + }) + return nextParams ? { ...tool, params: nextParams } : tool + } + + for (const { paramId, config } of configs) { + if (getToolParamOverrideKind(paramId)) continue + const definition = getWorkflowSearchSubBlockResourceDefinition(config) + if (!definition) continue + const currentValue = (nextParams ?? params)[paramId] + + if (definition.kind === 'file') { + // file-upload (workspace file) remaps by storage key; file-selector (external + // provider id) carries over unchanged. + if (config.type !== 'file-upload') continue + const remapped = remapForkFileUploadValue(currentValue, opts.resolveFileKey) + if (remapped !== currentValue) setParam(paramId, remapped) + continue + } + + const forkKind = REGISTRY_KIND_TO_FORK_KIND[definition.kind] + if (!forkKind) continue + + const refs = parseWorkflowSearchSubBlockResources(currentValue, config) + if (refs.length === 0) continue + + let value = currentValue + const seen = new Set() + for (const ref of refs) { + if (seen.has(ref.rawValue)) continue + seen.add(ref.rawValue) + const target = opts.resolve(forkKind, ref.rawValue) + const mapped = target != null + opts.record?.(forkKind, ref.rawValue, mapped) + if (mapped) { + if (target !== ref.rawValue) { + const replaced = definition.codec.replace(value, ref.rawValue, target) + if (replaced.success) value = replaced.nextValue + } + } else if (opts.clearUnresolved) { + // Drop only this unresolved entry (blank it - empties are filtered at parse + // time), so a mixed copied/uncopied multi-value field keeps its copied refs. + const replaced = definition.codec.replace(value, ref.rawValue, '') + if (replaced.success) value = replaced.nextValue + } + } + + if (value !== currentValue) setParam(paramId, value) + } + + if (!nextParams) return tool + return { ...tool, params: nextParams } +} + +interface ForkToolInputOptions { + /** Fork-create drops unresolved tools / clears params; promote keeps + records. */ + clearUnresolved: boolean + record?: (kind: ForkRemapKind, sourceId: string, mapped: boolean) => void +} + +/** + * Rewrite resource references inside a `tool-input` subblock (an array of + * StoredTool). Custom-tool and MCP-server ids live in dedicated fields; every + * other workspace-scoped id (credential, KB, table, file, MCP server) is nested in + * a block tool's `params` and rewritten via {@link remapToolBlockResources}. The + * MCP entry's derived `toolId` is rebuilt when the server id changes. On fork an + * unresolved custom-tool/MCP tool is dropped; on promote it's kept and recorded. + */ +function remapForkToolInputValue( + value: unknown, + resolve: ForkReferenceResolver, + opts: ForkToolInputOptions +): unknown { + const { array, wasString } = coerceObjectArray(value) + if (!array) return value + let changed = false + const next = array.flatMap((tool) => { + if (!isRecord(tool) || typeof tool.type !== 'string') return [tool] + if (tool.type === 'custom-tool' && typeof tool.customToolId === 'string') { + const target = resolve('custom-tool', tool.customToolId) + opts.record?.('custom-tool', tool.customToolId, target != null) + if (target != null) { + if (target !== tool.customToolId) { + changed = true + return [{ ...tool, customToolId: target }] + } + return [tool] + } + if (opts.clearUnresolved) { + changed = true + return [] + } + return [tool] + } + if (tool.type === 'mcp' && isRecord(tool.params) && typeof tool.params.serverId === 'string') { + const serverId = tool.params.serverId + const target = resolve('mcp-server', serverId) + opts.record?.('mcp-server', serverId, target != null) + if (target != null) { + if (target !== serverId) { + changed = true + const toolName = + typeof tool.params.toolName === 'string' ? tool.params.toolName : undefined + return [ + { + ...tool, + params: { ...tool.params, serverId: target }, + toolId: toolName ? createMcpToolId(target, toolName) : tool.toolId, + }, + ] + } + return [tool] + } + if (opts.clearUnresolved) { + changed = true + return [] + } + return [tool] + } + const remapped = remapToolBlockResources(tool, { + resolve, + resolveFileKey: (key) => resolve('file', key) ?? null, + record: opts.record, + clearUnresolved: opts.clearUnresolved, + }) + if (remapped !== tool) changed = true + return [remapped] + }) + if (!changed) return value + return wasString ? JSON.stringify(next) : next +} + +/** + * Rewrite skill references inside a `skill-input` subblock (an array of + * StoredSkill). Builtin skills (`builtin-*` ids) are workspace-agnostic and left + * unchanged. On fork an unresolved skill is dropped; on promote it's kept + recorded. + */ +function remapForkSkillInputValue( + value: unknown, + resolve: ForkReferenceResolver, + opts: ForkToolInputOptions +): unknown { + const { array, wasString } = coerceObjectArray(value) + if (!array) return value + let changed = false + const next = array.flatMap((entry) => { + if (!isRecord(entry) || typeof entry.skillId !== 'string') return [entry] + if (entry.skillId.startsWith('builtin-')) return [entry] + const target = resolve('skill', entry.skillId) + opts.record?.('skill', entry.skillId, target != null) + if (target != null) { + if (target !== entry.skillId) { + changed = true + return [{ ...entry, skillId: target }] + } + return [entry] + } + if (opts.clearUnresolved) { + changed = true + return [] + } + return [entry] + }) + if (!changed) return value + return wasString ? JSON.stringify(next) : next +} + +/** + * Rewrite the server id embedded in an `mcp-tool-selector` value + * (`-`) when the sibling MCP server was remapped. + * Server ids are UUIDs, so substring replacement is unambiguous. Without this the + * tool reference keeps the old server id and resolves to a malformed tool id at + * runtime after the server is remapped. + */ +export function rewriteMcpToolSelectorValue( + value: unknown, + serverRemaps: Map +): unknown { + if (typeof value !== 'string' || value.length === 0 || serverRemaps.size === 0) return value + let next = value + for (const [sourceId, targetId] of serverRemaps) { + if (next.includes(sourceId)) next = next.split(sourceId).join(targetId) + } + return next +} + +/** + * Single subblock remapper shared by fork-create and promote (the `mode` selects + * the policy - see below). Structured selectors use the search-replace registry + * codecs; advanced-mode `manual*` overrides and nested tool params are handled by + * id; `{{ENV}}` refs are inline string references. Returns the rewritten subBlocks + * plus, in promote mode, the detected and still-unmapped references. + */ +export function remapForkSubBlocks( + subBlocks: SubBlockRecord, + resolve: ForkReferenceResolver, + mode: 'create' | 'promote', + context?: { blockId?: string; blockName?: string } +): RemapSubBlocksResult { + // create (initial fork): clear/drop refs that weren't copied; leave `{{ENV}}` by + // name. promote: keep unresolved refs + record them (so the mapping UI can surface + // and block on required credentials) and rewrite `{{ENV}}`. + const clearUnresolved = mode === 'create' + const result: SubBlockRecord = {} + const references = new Map() + const unmapped = new Map() + // Source→target ids for any remapped MCP server, applied to sibling + // `mcp-tool-selector` values (which embed the server id) in a post-pass. + const mcpServerRemaps = new Map() + + const recordReference = (key: string, reference: ForkReference, mapped: boolean) => { + if (mode !== 'promote') return + references.set(key, reference) + if (!mapped) unmapped.set(key, reference) + } + + for (const [subBlockKey, subBlock] of Object.entries(subBlocks)) { + if (!subBlock || typeof subBlock !== 'object') { + result[subBlockKey] = subBlock + continue + } + + let value = subBlock.value + const subBlockType = typeof subBlock.type === 'string' ? subBlock.type : undefined + + const definition = getWorkflowSearchSubBlockResourceDefinition( + subBlockType ? { type: subBlockType as SubBlockType } : undefined + ) + const forkKind = definition ? REGISTRY_KIND_TO_FORK_KIND[definition.kind] : undefined + + if (definition && forkKind && subBlockType) { + const parsed = parseWorkflowSearchSubBlockResources(value, { + type: subBlockType as SubBlockType, + }) + const seen = new Set() + for (const ref of parsed) { + if (seen.has(ref.rawValue)) continue + seen.add(ref.rawValue) + const required = REQUIRED_KINDS.has(forkKind) + const reference: ForkReference = { + kind: forkKind, + sourceId: ref.rawValue, + blockId: context?.blockId, + blockName: context?.blockName, + subBlockKey, + required, + } + const target = resolve(forkKind, ref.rawValue) + const mapped = target != null + recordReference(`${forkKind}:${ref.rawValue}`, reference, mapped) + if (mapped) { + if (forkKind === 'mcp-server' && target !== ref.rawValue) { + mcpServerRemaps.set(ref.rawValue, target) + } + if (target !== ref.rawValue) { + const replaceResult = definition.codec.replace(value, ref.rawValue, target) + if (replaceResult.success) value = replaceResult.nextValue + } + } else if (clearUnresolved) { + // Drop only this unresolved entry (blank it - empties are filtered at + // parse time) so a mixed copied/uncopied multi-value field keeps its rest. + const replaceResult = definition.codec.replace(value, ref.rawValue, '') + if (replaceResult.success) value = replaceResult.nextValue + } + } + } + + // Advanced-mode `manual*` overrides hold a workspace-scoped id by subblock id + // (not type), so the registry path above misses them. Resolve the raw value as + // the override kind (credentials become required → block promote until mapped). + const overrideKind = getOverrideForkKind(subBlockKey) + if (overrideKind && typeof value === 'string' && value) { + const reference: ForkReference = { + kind: overrideKind, + sourceId: value, + blockId: context?.blockId, + blockName: context?.blockName, + subBlockKey, + required: REQUIRED_KINDS.has(overrideKind), + } + const target = resolve(overrideKind, value) + const mapped = target != null + recordReference(`${overrideKind}:${value}`, reference, mapped) + if (mapped) { + if (target !== value) value = target + } else if (clearUnresolved) { + value = '' + } + } + + if (subBlockType === 'file-upload') { + // Workspace-file refs don't sync on promote (the target lacks the source's + // blob); clear them rather than carry a cross-workspace key. On fork, the + // resolver returns the copied key. `file-selector` (external) is untouched. + value = remapForkFileUploadValue(value, (sourceKey) => resolve('file', sourceKey) ?? null) + } else if (subBlockType === 'document-selector') { + // Document ids are never valid in the target workspace; clear rather than + // carry a dangling reference (the sibling KB selector is still remapped). + if (value) value = '' + } else if (subBlockType === 'tool-input' || subBlockType === 'skill-input') { + const record = (kind: ForkRemapKind, sourceId: string, mapped: boolean) => + recordReference( + `${kind}:${sourceId}`, + { + kind, + sourceId, + blockId: context?.blockId, + blockName: context?.blockName, + subBlockKey, + required: REQUIRED_KINDS.has(kind), + }, + mapped + ) + value = + subBlockType === 'tool-input' + ? remapForkToolInputValue(value, resolve, { clearUnresolved, record }) + : remapForkSkillInputValue(value, resolve, { clearUnresolved, record }) + } + + // Promote rewrites `{{ENV}}` refs via the resolver; fork preserves them by name. + if (mode === 'promote') { + value = remapEnvInValue(value, resolve, (sourceId, mapped) => { + recordReference( + `env-var:${sourceId}`, + { + kind: 'env-var', + sourceId, + blockId: context?.blockId, + blockName: context?.blockName, + subBlockKey, + required: true, + }, + mapped + ) + }) + } + + result[subBlockKey] = { ...subBlock, value } + } + + if (mcpServerRemaps.size > 0) { + for (const [subBlockKey, subBlock] of Object.entries(result)) { + if (!isRecord(subBlock) || subBlock.type !== 'mcp-tool-selector') continue + const rewritten = rewriteMcpToolSelectorValue(subBlock.value, mcpServerRemaps) + if (rewritten !== subBlock.value) result[subBlockKey] = { ...subBlock, value: rewritten } + } + } + + return { + subBlocks: result, + references: Array.from(references.values()), + unmapped: Array.from(unmapped.values()), + } +} + +/** + * Promote-mode remap (keep + record unmapped references). Thin wrapper over + * {@link remapForkSubBlocks}; also used by the reference scan. + */ +export function remapSubBlocks( + subBlocks: SubBlockRecord, + resolve: ForkReferenceResolver, + context?: { blockId?: string; blockName?: string } +): RemapSubBlocksResult { + return remapForkSubBlocks(subBlocks, resolve, 'promote', context) +} + +/** A `copyWorkflowStateIntoTarget` subBlock transform that rewrites references via the resolver. */ +export function createForkSubBlockTransform( + resolve: ForkReferenceResolver +): (subBlocks: SubBlockRecord) => SubBlockRecord { + return (subBlocks) => remapSubBlocks(subBlocks, resolve).subBlocks +} + +export interface WorkflowReferenceScan { + references: ForkReference[] + unmapped: ForkReference[] +} + +/** + * Scan a set of blocks for all remappable references, aggregating unique + * (kind, sourceId) pairs across the workflow. Used by the mapping/diff/promote + * paths to surface what needs mapping and to block on unmapped required refs. + */ +export function scanWorkflowReferences( + blocks: Array<{ id: string; name: string; subBlocks: unknown }>, + resolve: ForkReferenceResolver +): WorkflowReferenceScan { + const references = new Map() + const unmapped = new Map() + + for (const block of blocks) { + if (!block.subBlocks || typeof block.subBlocks !== 'object' || Array.isArray(block.subBlocks)) { + continue + } + const blockResult = remapSubBlocks(block.subBlocks as SubBlockRecord, resolve, { + blockId: block.id, + blockName: block.name, + }) + for (const reference of blockResult.references) { + const key = `${reference.kind}:${reference.sourceId}` + if (!references.has(key)) references.set(key, reference) + } + for (const reference of blockResult.unmapped) { + const key = `${reference.kind}:${reference.sourceId}` + if (!unmapped.has(key)) unmapped.set(key, reference) + } + } + + return { + references: Array.from(references.values()), + unmapped: Array.from(unmapped.values()), + } +} diff --git a/apps/sim/lib/workspaces/fork/remap/remap-table-groups.test.ts b/apps/sim/lib/workspaces/fork/remap/remap-table-groups.test.ts new file mode 100644 index 00000000000..e05b8b00971 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/remap/remap-table-groups.test.ts @@ -0,0 +1,68 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import type { TableSchema } from '@/lib/table/types' +import { deriveForkBlockId } from '@/lib/workspaces/fork/remap/block-identity' +import { remapForkTableWorkflowGroups } from '@/lib/workspaces/fork/remap/remap-table-groups' + +describe('remapForkTableWorkflowGroups', () => { + it('remaps a manual group workflowId and outputs[].blockId to child ids', () => { + const map = new Map([['src-wf', 'child-wf']]) + const schema: TableSchema = { + columns: [{ id: 'col_1', name: 'Out', type: 'string', workflowGroupId: 'g1' }], + workflowGroups: [ + { + id: 'g1', + workflowId: 'src-wf', + outputs: [{ blockId: 'src-block', path: 'out', columnName: 'col_1' }], + }, + ], + } + const result = remapForkTableWorkflowGroups(schema, map) + const group = result.workflowGroups?.[0] + expect(group?.workflowId).toBe('child-wf') + expect(group?.outputs[0].blockId).toBe(deriveForkBlockId('child-wf', 'src-block')) + expect(group?.outputs[0].columnName).toBe('col_1') + expect(result.columns[0].id).toBe('col_1') + expect(result.columns[0].workflowGroupId).toBe('g1') + }) + + it('drops a group whose backing workflow was not copied and clears its column wiring', () => { + const schema: TableSchema = { + columns: [{ id: 'col_1', name: 'Out', type: 'string', workflowGroupId: 'g1' }], + workflowGroups: [ + { + id: 'g1', + workflowId: 'missing-wf', + outputs: [{ blockId: 'b', path: 'p', columnName: 'col_1' }], + }, + ], + } + const result = remapForkTableWorkflowGroups(schema, new Map()) + expect(result.workflowGroups).toHaveLength(0) + expect(result.columns[0].workflowGroupId).toBeUndefined() + expect(result.columns[0].id).toBe('col_1') + }) + + it('leaves enrichment groups (empty workflowId) untouched', () => { + const schema: TableSchema = { + columns: [], + workflowGroups: [ + { + id: 'g1', + workflowId: '', + enrichmentId: 'enr', + outputs: [{ blockId: '', path: '', columnName: 'col_1', outputId: 'o' }], + }, + ], + } + const result = remapForkTableWorkflowGroups(schema, new Map([['src-wf', 'child-wf']])) + expect(result.workflowGroups?.[0]).toEqual(schema.workflowGroups?.[0]) + }) + + it('returns the schema unchanged when there are no groups', () => { + const schema: TableSchema = { columns: [{ id: 'col_1', name: 'A', type: 'string' }] } + expect(remapForkTableWorkflowGroups(schema, new Map())).toBe(schema) + }) +}) diff --git a/apps/sim/lib/workspaces/fork/remap/remap-table-groups.ts b/apps/sim/lib/workspaces/fork/remap/remap-table-groups.ts new file mode 100644 index 00000000000..9eb00d914c3 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/remap/remap-table-groups.ts @@ -0,0 +1,53 @@ +import type { TableSchema } from '@/lib/table/types' +import { deriveForkBlockId } from '@/lib/workspaces/fork/remap/block-identity' + +/** + * Remap the workflow/block references embedded in a copied table's schema so its + * workflow groups keep working in the child workspace. `workflowGroups[].workflowId` + * is rewritten through the source→child workflow identity map, and each + * `outputs[].blockId` is rewritten to the deterministic forked block id (matching + * `copyWorkflowStateIntoTarget`). Manual groups whose backing workflow was not + * copied are dropped, and any columns wired to a dropped group have their + * `workflowGroupId` cleared. Enrichment groups (empty `workflowId`) and column + * ids are left untouched. + */ +export function remapForkTableWorkflowGroups( + schema: TableSchema, + workflowIdMap: Map +): TableSchema { + const groups = schema.workflowGroups ?? [] + if (groups.length === 0) return schema + + const droppedGroupIds = new Set() + const remappedGroups = groups.flatMap((group) => { + if (!group.workflowId) return [group] + const childWorkflowId = workflowIdMap.get(group.workflowId) + if (!childWorkflowId) { + droppedGroupIds.add(group.id) + return [] + } + return [ + { + ...group, + workflowId: childWorkflowId, + outputs: group.outputs.map((output) => ({ + ...output, + blockId: output.blockId + ? deriveForkBlockId(childWorkflowId, output.blockId) + : output.blockId, + })), + }, + ] + }) + + const columns = + droppedGroupIds.size === 0 + ? schema.columns + : schema.columns.map((column) => + column.workflowGroupId && droppedGroupIds.has(column.workflowGroupId) + ? { ...column, workflowGroupId: undefined } + : column + ) + + return { ...schema, columns, workflowGroups: remappedGroups } +} diff --git a/apps/sim/lib/workspaces/fork/socket.ts b/apps/sim/lib/workspaces/fork/socket.ts new file mode 100644 index 00000000000..0dfdbf10732 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/socket.ts @@ -0,0 +1,22 @@ +import { createLogger } from '@sim/logger' +import { env } from '@/lib/core/config/env' +import { getSocketServerUrl } from '@/lib/core/utils/urls' + +const logger = createLogger('WorkspaceForkSocket') + +/** + * Best-effort notify connected canvas clients that a workflow was force-replaced + * by a fork promote/rollback so they refresh instead of clobbering the new state. + * Non-fatal: a failed notification never blocks the operation. + */ +export async function notifyForkWorkflowChanged(workflowId: string): Promise { + try { + await fetch(`${getSocketServerUrl()}/api/workflow-deployed`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'x-api-key': env.INTERNAL_API_SECRET }, + body: JSON.stringify({ workflowId }), + }) + } catch (error) { + logger.warn('Fork sync socket notification failed', { workflowId, error }) + } +} diff --git a/packages/audit/src/types.ts b/packages/audit/src/types.ts index 611c2fd509b..0d5bbe02388 100644 --- a/packages/audit/src/types.ts +++ b/packages/audit/src/types.ts @@ -182,6 +182,9 @@ export const AuditAction = { WORKSPACE_UPDATED: 'workspace.updated', WORKSPACE_DELETED: 'workspace.deleted', WORKSPACE_DUPLICATED: 'workspace.duplicated', + WORKSPACE_FORKED: 'workspace.forked', + WORKSPACE_FORK_PROMOTED: 'workspace.fork_promoted', + WORKSPACE_FORK_ROLLED_BACK: 'workspace.fork_rolled_back', } as const export type AuditActionType = (typeof AuditAction)[keyof typeof AuditAction] diff --git a/packages/db/migrations/0250_workspace_forking.sql b/packages/db/migrations/0250_workspace_forking.sql new file mode 100644 index 00000000000..3c690ec725e --- /dev/null +++ b/packages/db/migrations/0250_workspace_forking.sql @@ -0,0 +1,64 @@ +-- Replay-safety: this file ends in a CONCURRENTLY index op below an embedded COMMIT, +-- so a failure there replays the whole file from the top — every statement here is idempotent. +DO $$ BEGIN + CREATE TYPE "public"."workspace_fork_promote_direction" AS ENUM('push', 'pull'); +EXCEPTION WHEN duplicate_object THEN null; +END $$;--> statement-breakpoint +DO $$ BEGIN + CREATE TYPE "public"."workspace_fork_resource_type" AS ENUM('workflow', 'oauth_credential', 'service_account_credential', 'env_var', 'table', 'knowledge_base', 'knowledge_document', 'file', 'mcp_server', 'custom_tool', 'skill'); +EXCEPTION WHEN duplicate_object THEN null; +END $$;--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "workspace_fork_promote_run" ( + "id" text PRIMARY KEY NOT NULL, + "child_workspace_id" text NOT NULL, + "source_workspace_id" text NOT NULL, + "target_workspace_id" text NOT NULL, + "direction" "workspace_fork_promote_direction" NOT NULL, + "snapshot" jsonb NOT NULL, + "created_by" text, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "workspace_fork_resource_map" ( + "id" text PRIMARY KEY NOT NULL, + "child_workspace_id" text NOT NULL, + "resource_type" "workspace_fork_resource_type" NOT NULL, + "parent_resource_id" text NOT NULL, + "child_resource_id" text, + "created_by" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "workspace" ADD COLUMN IF NOT EXISTS "forked_from_workspace_id" text;--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "workspace_fork_promote_run" ADD CONSTRAINT "workspace_fork_promote_run_child_workspace_id_workspace_id_fk" FOREIGN KEY ("child_workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION WHEN duplicate_object THEN null; +END $$;--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "workspace_fork_promote_run" ADD CONSTRAINT "workspace_fork_promote_run_created_by_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action; +EXCEPTION WHEN duplicate_object THEN null; +END $$;--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "workspace_fork_resource_map" ADD CONSTRAINT "workspace_fork_resource_map_child_workspace_id_workspace_id_fk" FOREIGN KEY ("child_workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION WHEN duplicate_object THEN null; +END $$;--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "workspace_fork_resource_map" ADD CONSTRAINT "workspace_fork_resource_map_created_by_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action; +EXCEPTION WHEN duplicate_object THEN null; +END $$;--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "workspace" ADD CONSTRAINT "workspace_forked_from_workspace_id_workspace_id_fk" FOREIGN KEY ("forked_from_workspace_id") REFERENCES "public"."workspace"("id") ON DELETE set null ON UPDATE no action NOT VALID; +EXCEPTION WHEN duplicate_object THEN null; +END $$;--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "workspace_fork_promote_run_child_ws_target_unique" ON "workspace_fork_promote_run" USING btree ("child_workspace_id","target_workspace_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "workspace_fork_promote_run_target_ws_idx" ON "workspace_fork_promote_run" USING btree ("target_workspace_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "workspace_fork_resource_map_child_ws_idx" ON "workspace_fork_resource_map" USING btree ("child_workspace_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "workspace_fork_resource_map_child_ws_type_idx" ON "workspace_fork_resource_map" USING btree ("child_workspace_id","resource_type");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "workspace_fork_resource_map_child_type_parent_unique" ON "workspace_fork_resource_map" USING btree ("child_workspace_id","resource_type","parent_resource_id");--> statement-breakpoint +-- workspace is an existing table: build its new index CONCURRENTLY so the build never +-- write-locks the relation (runner convention — plain CREATE INDEX takes ACCESS EXCLUSIVE). +COMMIT;--> statement-breakpoint +SET lock_timeout = 0;--> statement-breakpoint +CREATE INDEX CONCURRENTLY IF NOT EXISTS "workspace_forked_from_workspace_id_idx" ON "workspace" USING btree ("forked_from_workspace_id");--> statement-breakpoint +SET lock_timeout = '5s'; diff --git a/packages/db/migrations/meta/0250_snapshot.json b/packages/db/migrations/meta/0250_snapshot.json new file mode 100644 index 00000000000..9648374c52a --- /dev/null +++ b/packages/db/migrations/meta/0250_snapshot.json @@ -0,0 +1,17087 @@ +{ + "id": "5e9026af-bce0-4abc-b4f6-82ca3b396ed1", + "prevId": "b482015e-7334-472b-a6bc-1ed05bcacfe7", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.a2a_agent": { + "name": "a2a_agent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "authentication": { + "name": "authentication", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "signatures": { + "name": "signatures", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_agent_workflow_id_idx": { + "name": "a2a_agent_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_created_by_idx": { + "name": "a2a_agent_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_workflow_unique": { + "name": "a2a_agent_workspace_workflow_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"a2a_agent\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_archived_at_idx": { + "name": "a2a_agent_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_archived_partial_idx": { + "name": "a2a_agent_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"a2a_agent\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_agent_workspace_id_workspace_id_fk": { + "name": "a2a_agent_workspace_id_workspace_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_workflow_id_workflow_id_fk": { + "name": "a2a_agent_workflow_id_workflow_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_created_by_user_id_fk": { + "name": "a2a_agent_created_by_user_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_push_notification_config": { + "name": "a2a_push_notification_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_schemes": { + "name": "auth_schemes", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "auth_credentials": { + "name": "auth_credentials", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_push_notification_config_task_unique": { + "name": "a2a_push_notification_config_task_unique", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_push_notification_config_task_id_a2a_task_id_fk": { + "name": "a2a_push_notification_config_task_id_a2a_task_id_fk", + "tableFrom": "a2a_push_notification_config", + "tableTo": "a2a_task", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_task": { + "name": "a2a_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "a2a_task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'submitted'" + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "artifacts": { + "name": "artifacts", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "a2a_task_agent_id_idx": { + "name": "a2a_task_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_session_id_idx": { + "name": "a2a_task_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_status_idx": { + "name": "a2a_task_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_execution_id_idx": { + "name": "a2a_task_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_created_at_idx": { + "name": "a2a_task_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_task_agent_id_a2a_agent_id_fk": { + "name": "a2a_task_agent_id_a2a_agent_id_fk", + "tableFrom": "a2a_task", + "tableTo": "a2a_agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.academy_certificate": { + "name": "academy_certificate", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "academy_cert_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "certificate_number": { + "name": "certificate_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "academy_certificate_user_id_idx": { + "name": "academy_certificate_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_course_id_idx": { + "name": "academy_certificate_course_id_idx", + "columns": [ + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_user_course_unique": { + "name": "academy_certificate_user_course_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_number_idx": { + "name": "academy_certificate_number_idx", + "columns": [ + { + "expression": "certificate_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_status_idx": { + "name": "academy_certificate_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "academy_certificate_user_id_user_id_fk": { + "name": "academy_certificate_user_id_user_id_fk", + "tableFrom": "academy_certificate", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "academy_certificate_certificate_number_unique": { + "name": "academy_certificate_certificate_number_unique", + "nullsNotDistinct": false, + "columns": ["certificate_number"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_key_hash_idx": { + "name": "api_key_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_pending_run_at_idx": { + "name": "async_jobs_schedule_pending_run_at_idx", + "columns": [ + { + "expression": "run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_processing_started_at_idx": { + "name": "async_jobs_schedule_processing_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'processing'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_workspace_created_idx": { + "name": "audit_log_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_workspace_created_at_id_idx": { + "name": "audit_log_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_actor_created_idx": { + "name": "audit_log_actor_created_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_resource_idx": { + "name": "audit_log_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_action_idx": { + "name": "audit_log_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_workspace_id_workspace_id_fk": { + "name": "audit_log_workspace_id_workspace_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_actor_id_user_id_fk": { + "name": "audit_log_actor_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": ["actor_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_archived_at_partial_idx": { + "name": "chat_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_on_workflow_id_archived_at": { + "name": "idx_chat_on_workflow_id_archived_at", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_async_tool_calls": { + "name": "copilot_async_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "copilot_async_tool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_async_tool_calls_run_id_idx": { + "name": "copilot_async_tool_calls_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_checkpoint_id_idx": { + "name": "copilot_async_tool_calls_checkpoint_id_idx", + "columns": [ + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_idx": { + "name": "copilot_async_tool_calls_tool_call_id_idx", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_status_idx": { + "name": "copilot_async_tool_calls_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_run_status_idx": { + "name": "copilot_async_tool_calls_run_status_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_unique": { + "name": "copilot_async_tool_calls_tool_call_id_unique", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_async_tool_calls_run_id_copilot_runs_id_fk": { + "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": { + "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_run_checkpoints", + "columnsFrom": ["checkpoint_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "chat_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'copilot'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_idx": { + "name": "copilot_chats_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workspace_created_at_id_idx": { + "name": "copilot_chats_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workspace_id_workspace_id_fk": { + "name": "copilot_chats_workspace_id_workspace_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_messages": { + "name": "copilot_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_message_id": { + "name": "parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens_in": { + "name": "tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_out": { + "name": "tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_messages_chat_message_unique": { + "name": "copilot_messages_chat_message_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_created_at_idx": { + "name": "copilot_messages_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_seq_idx": { + "name": "copilot_messages_chat_seq_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_stream_idx": { + "name": "copilot_messages_chat_stream_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"stream_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_messages_chat_id_copilot_chats_id_fk": { + "name": "copilot_messages_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_messages", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_run_checkpoints": { + "name": "copilot_run_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pending_tool_call_id": { + "name": "pending_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_snapshot": { + "name": "conversation_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "provider_request": { + "name": "provider_request", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_run_checkpoints_run_id_idx": { + "name": "copilot_run_checkpoints_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_pending_tool_call_id_idx": { + "name": "copilot_run_checkpoints_pending_tool_call_id_idx", + "columns": [ + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_run_pending_tool_unique": { + "name": "copilot_run_checkpoints_run_pending_tool_unique", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_run_checkpoints_run_id_copilot_runs_id_fk": { + "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_run_checkpoints", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_runs": { + "name": "copilot_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "copilot_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_runs_execution_id_idx": { + "name": "copilot_runs_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_parent_run_id_idx": { + "name": "copilot_runs_parent_run_id_idx", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_id_idx": { + "name": "copilot_runs_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_user_id_idx": { + "name": "copilot_runs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workflow_id_idx": { + "name": "copilot_runs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_id_idx": { + "name": "copilot_runs_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_status_idx": { + "name": "copilot_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_execution_idx": { + "name": "copilot_runs_chat_execution_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_execution_started_at_idx": { + "name": "copilot_runs_execution_started_at_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_completed_at_id_idx": { + "name": "copilot_runs_workspace_completed_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"completed_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_stream_id_unique": { + "name": "copilot_runs_stream_id_unique", + "columns": [ + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_runs_chat_id_copilot_chats_id_fk": { + "name": "copilot_runs_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_user_id_user_id_fk": { + "name": "copilot_runs_user_id_user_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workflow_id_workflow_id_fk": { + "name": "copilot_runs_workflow_id_workflow_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workspace_id_workspace_id_fk": { + "name": "copilot_runs_workspace_id_workspace_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_workflow_read_hashes": { + "name": "copilot_workflow_read_hashes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_workflow_read_hashes_chat_id_idx": { + "name": "copilot_workflow_read_hashes_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_workflow_id_idx": { + "name": "copilot_workflow_read_hashes_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_chat_workflow_unique": { + "name": "copilot_workflow_read_hashes_chat_workflow_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk": { + "name": "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_workflow_read_hashes_workflow_id_workflow_id_fk": { + "name": "copilot_workflow_read_hashes_workflow_id_workflow_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "credential_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_owner_user_id": { + "name": "env_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_service_account_key": { + "name": "encrypted_service_account_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_workspace_id_idx": { + "name": "credential_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_type_idx": { + "name": "credential_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_provider_id_idx": { + "name": "credential_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_account_id_idx": { + "name": "credential_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_env_owner_user_id_idx": { + "name": "credential_env_owner_user_id_idx", + "columns": [ + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_account_unique": { + "name": "credential_workspace_account_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "account_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_env_unique": { + "name": "credential_workspace_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_workspace'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_personal_env_unique": { + "name": "credential_workspace_personal_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_personal'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_workspace_id_workspace_id_fk": { + "name": "credential_workspace_id_workspace_id_fk", + "tableFrom": "credential", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_account_id_account_id_fk": { + "name": "credential_account_id_account_id_fk", + "tableFrom": "credential", + "tableTo": "account", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_env_owner_user_id_user_id_fk": { + "name": "credential_env_owner_user_id_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["env_owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_created_by_user_id_fk": { + "name": "credential_created_by_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_oauth_source_check": { + "name": "credential_oauth_source_check", + "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)" + }, + "credential_workspace_env_source_check": { + "name": "credential_workspace_env_source_check", + "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)" + }, + "credential_personal_env_source_check": { + "name": "credential_personal_env_source_check", + "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.credential_member": { + "name": "credential_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "credential_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "credential_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_member_user_id_idx": { + "name": "credential_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_role_idx": { + "name": "credential_member_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_status_idx": { + "name": "credential_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_unique": { + "name": "credential_member_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_member_credential_id_credential_id_fk": { + "name": "credential_member_credential_id_credential_id_fk", + "tableFrom": "credential_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_user_id_user_id_fk": { + "name": "credential_member_user_id_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_invited_by_user_id_fk": { + "name": "credential_member_invited_by_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set": { + "name": "credential_set", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_created_by_idx": { + "name": "credential_set_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_org_name_unique": { + "name": "credential_set_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_provider_id_idx": { + "name": "credential_set_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_organization_id_organization_id_fk": { + "name": "credential_set_organization_id_organization_id_fk", + "tableFrom": "credential_set", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_created_by_user_id_fk": { + "name": "credential_set_created_by_user_id_fk", + "tableFrom": "credential_set", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_invitation": { + "name": "credential_set_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "accepted_by_user_id": { + "name": "accepted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_invitation_set_id_idx": { + "name": "credential_set_invitation_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_token_idx": { + "name": "credential_set_invitation_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_status_idx": { + "name": "credential_set_invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_expires_at_idx": { + "name": "credential_set_invitation_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_invitation_credential_set_id_credential_set_id_fk": { + "name": "credential_set_invitation_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_invited_by_user_id_fk": { + "name": "credential_set_invitation_invited_by_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_accepted_by_user_id_user_id_fk": { + "name": "credential_set_invitation_accepted_by_user_id_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["accepted_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credential_set_invitation_token_unique": { + "name": "credential_set_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_member": { + "name": "credential_set_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_member_user_id_idx": { + "name": "credential_set_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_unique": { + "name": "credential_set_member_unique", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_status_idx": { + "name": "credential_set_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_member_credential_set_id_credential_set_id_fk": { + "name": "credential_set_member_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_user_id_user_id_fk": { + "name": "credential_set_member_user_id_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_invited_by_user_id_fk": { + "name": "credential_set_member_invited_by_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drain_runs": { + "name": "data_drain_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_drain_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "data_drain_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rows_exported": { + "name": "rows_exported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bytes_written": { + "name": "bytes_written", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cursor_before": { + "name": "cursor_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_after": { + "name": "cursor_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locators": { + "name": "locators", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "data_drain_runs_drain_started_idx": { + "name": "data_drain_runs_drain_started_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drain_runs_drain_id_data_drains_id_fk": { + "name": "data_drain_runs_drain_id_data_drains_id_fk", + "tableFrom": "data_drain_runs", + "tableTo": "data_drains", + "columnsFrom": ["drain_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drains": { + "name": "data_drains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "data_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_type": { + "name": "destination_type", + "type": "data_drain_destination", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_config": { + "name": "destination_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_credentials": { + "name": "destination_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_cadence": { + "name": "schedule_cadence", + "type": "data_drain_cadence", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "data_drains_org_idx": { + "name": "data_drains_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_due_idx": { + "name": "data_drains_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_org_name_unique": { + "name": "data_drains_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drains_organization_id_organization_id_fk": { + "name": "data_drains_organization_id_organization_id_fk", + "tableFrom": "data_drains", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "data_drains_created_by_user_id_fk": { + "name": "data_drains_created_by_user_id_fk", + "tableFrom": "data_drains", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_excluded": { + "name": "user_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_external_id_idx": { + "name": "doc_connector_external_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_id_idx": { + "name": "doc_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_storage_key_idx": { + "name": "doc_storage_key_idx", + "columns": [ + { + "expression": "storage_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"storage_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_archived_at_partial_idx": { + "name": "doc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_deleted_at_partial_idx": { + "name": "doc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_connector_id_knowledge_connector_id_fk": { + "name": "document_connector_id_knowledge_connector_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_uploaded_by_user_id_fk": { + "name": "document_uploaded_by_user_id_fk", + "tableFrom": "document", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_dependencies": { + "name": "execution_large_value_dependencies", + "schema": "", + "columns": { + "parent_key": { + "name": "parent_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_key": { + "name": "child_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_dependencies_workspace_parent_key_idx": { + "name": "execution_large_value_dependencies_workspace_parent_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_dependencies_workspace_child_key_idx": { + "name": "execution_large_value_dependencies_workspace_child_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_dependencies_workspace_id_workspace_id_fk": { + "name": "execution_large_value_dependencies_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_dependencies", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_dependencies_parent_key_child_key_pk": { + "name": "execution_large_value_dependencies_parent_key_child_key_pk", + "columns": ["parent_key", "child_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_references": { + "name": "execution_large_value_references", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "execution_large_value_reference_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_references_workspace_execution_source_idx": { + "name": "execution_large_value_references_workspace_execution_source_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_references_workspace_id_workspace_id_fk": { + "name": "execution_large_value_references_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_value_references_workflow_id_workflow_id_fk": { + "name": "execution_large_value_references_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_references_key_execution_id_source_pk": { + "name": "execution_large_value_references_key_execution_id_source_pk", + "columns": ["key", "execution_id", "source"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_values": { + "name": "execution_large_values", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_execution_id": { + "name": "owner_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "execution_large_values_owner_execution_id_idx": { + "name": "execution_large_values_owner_execution_id_idx", + "columns": [ + { + "expression": "owner_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_cleanup_idx": { + "name": "execution_large_values_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_tombstone_cleanup_idx": { + "name": "execution_large_values_tombstone_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_values_workspace_id_workspace_id_fk": { + "name": "execution_large_values_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_values_workflow_id_workflow_id_fk": { + "name": "execution_large_values_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "invitation_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "membership_intent": { + "name": "membership_intent", + "type": "invitation_membership_intent", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_status_idx": { + "name": "invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_pending_email_org_unique": { + "name": "invitation_pending_email_org_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"invitation\".\"status\" = 'pending' AND \"invitation\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invitation_token_unique": { + "name": "invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation_workspace_grant": { + "name": "invitation_workspace_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invitation_id": { + "name": "invitation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_workspace_grant_unique": { + "name": "invitation_workspace_grant_unique", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_workspace_grant_workspace_id_idx": { + "name": "invitation_workspace_grant_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_workspace_grant_invitation_id_invitation_id_fk": { + "name": "invitation_workspace_grant_invitation_id_invitation_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "invitation", + "columnsFrom": ["invitation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_workspace_grant_workspace_id_workspace_id_fk": { + "name": "invitation_workspace_grant_workspace_id_workspace_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_execution_logs": { + "name": "job_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "job_execution_logs_schedule_id_idx": { + "name": "job_execution_logs_schedule_id_idx", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_started_at_idx": { + "name": "job_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_ended_at_id_idx": { + "name": "job_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_execution_id_unique": { + "name": "job_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_trigger_idx": { + "name": "job_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_execution_logs_schedule_id_workflow_schedule_id_fk": { + "name": "job_execution_logs_schedule_id_workflow_schedule_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workflow_schedule", + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "job_execution_logs_workspace_id_workspace_id_fk": { + "name": "job_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_deleted_partial_idx": { + "name": "kb_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_base\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_name_active_unique": { + "name": "kb_workspace_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector": { + "name": "knowledge_connector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "sync_mode": { + "name": "sync_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "sync_interval_minutes": { + "name": "sync_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1440 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_doc_count": { + "name": "last_sync_doc_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_archived_at_partial_idx": { + "name": "kc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_partial_idx": { + "name": "kc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector_sync_log": { + "name": "knowledge_connector_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_deleted": { + "name": "docs_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_id_idx": { + "name": "kcsl_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server_oauth": { + "name": "mcp_server_oauth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_information": { + "name": "client_information", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens": { + "name": "tokens", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_created_at": { + "name": "state_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_server_oauth_server_unique": { + "name": "mcp_server_oauth_server_unique", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_server_oauth_state_idx": { + "name": "mcp_server_oauth_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk": { + "name": "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "mcp_servers", + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_oauth_user_id_user_id_fk": { + "name": "mcp_server_oauth_user_id_user_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_server_oauth_workspace_id_workspace_id_fk": { + "name": "mcp_server_oauth_workspace_id_workspace_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'headers'" + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_client_secret": { + "name": "oauth_client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_partial_idx": { + "name": "mcp_servers_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_servers\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_deleted_partial_idx": { + "name": "memory_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"memory\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_allowed_sender": { + "name": "mothership_inbox_allowed_sender", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_sender_ws_email_idx": { + "name": "inbox_sender_ws_email_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_allowed_sender_added_by_user_id_fk": { + "name": "mothership_inbox_allowed_sender_added_by_user_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "user", + "columnsFrom": ["added_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_task": { + "name": "mothership_inbox_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_preview": { + "name": "body_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_message_id": { + "name": "email_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agentmail_message_id": { + "name": "agentmail_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger_job_id": { + "name": "trigger_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cc_recipients": { + "name": "cc_recipients", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inbox_task_ws_created_at_idx": { + "name": "inbox_task_ws_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_ws_status_idx": { + "name": "inbox_task_ws_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_response_msg_id_idx": { + "name": "inbox_task_response_msg_id_idx", + "columns": [ + { + "expression": "response_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_email_msg_id_idx": { + "name": "inbox_task_email_msg_id_idx", + "columns": [ + { + "expression": "email_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_task_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_task_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_task_chat_id_copilot_chats_id_fk": { + "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_webhook": { + "name": "mothership_inbox_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "webhook_id": { + "name": "webhook_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_inbox_webhook_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_webhook", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mothership_inbox_webhook_workspace_id_unique": { + "name": "mothership_inbox_webhook_workspace_id_unique", + "nullsNotDistinct": false, + "columns": ["workspace_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_settings": { + "name": "mothership_settings", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_tool_refs": { + "name": "mcp_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "custom_tool_refs": { + "name": "custom_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "skill_refs": { + "name": "skill_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mothership_settings_workspace_id_idx": { + "name": "mothership_settings_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_settings_workspace_id_workspace_id_fk": { + "name": "mothership_settings_workspace_id_workspace_id_fk", + "tableFrom": "mothership_settings", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "whitelabel_settings": { + "name": "whitelabel_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data_retention_settings": { + "name": "data_retention_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "departed_member_usage": { + "name": "departed_member_usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_member_usage_limit": { + "name": "organization_member_usage_limit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_limit": { + "name": "usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "set_by": { + "name": "set_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "org_member_usage_limit_org_user_unique": { + "name": "org_member_usage_limit_org_user_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_member_usage_limit_organization_id_idx": { + "name": "org_member_usage_limit_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_member_usage_limit_organization_id_organization_id_fk": { + "name": "organization_member_usage_limit_organization_id_organization_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_user_id_user_id_fk": { + "name": "organization_member_usage_limit_user_id_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_set_by_user_id_fk": { + "name": "organization_member_usage_limit_set_by_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["set_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_event": { + "name": "outbox_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "available_at": { + "name": "available_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "outbox_event_status_available_idx": { + "name": "outbox_event_status_available_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_locked_at_idx": { + "name": "outbox_event_locked_at_idx", + "columns": [ + { + "expression": "locked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_resume_at": { + "name": "next_resume_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_next_resume_at_idx": { + "name": "paused_executions_next_resume_at_idx", + "columns": [ + { + "expression": "next_resume_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'paused' AND next_resume_at IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_credential_draft": { + "name": "pending_credential_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pending_draft_user_provider_ws": { + "name": "pending_draft_user_provider_ws", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_credential_draft_user_id_user_id_fk": { + "name": "pending_credential_draft_user_id_user_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_workspace_id_workspace_id_fk": { + "name": "pending_credential_draft_workspace_id_workspace_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_credential_id_credential_id_fk": { + "name": "pending_credential_draft_credential_id_credential_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_name_unique": { + "name": "permission_group_organization_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_default_unique": { + "name": "permission_group_organization_default_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_organization_id_organization_id_fk": { + "name": "permission_group_organization_id_organization_id_fk", + "tableFrom": "permission_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_id_idx": { + "name": "permission_group_member_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_group_user_unique": { + "name": "permission_group_member_group_user_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_organization_user_idx": { + "name": "permission_group_member_organization_user_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_organization_id_organization_id_fk": { + "name": "permission_group_member_organization_id_organization_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_workspace": { + "name": "permission_group_workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_workspace_workspace_id_idx": { + "name": "permission_group_workspace_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_group_workspace_unique": { + "name": "permission_group_workspace_group_workspace_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_workspace_permission_group_id_permission_group_id_fk": { + "name": "permission_group_workspace_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_workspace_id_workspace_id_fk": { + "name": "permission_group_workspace_workspace_id_workspace_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_organization_id_organization_id_fk": { + "name": "permission_group_workspace_organization_id_organization_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_idx": { + "name": "permissions_user_entity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.public_share": { + "name": "public_share", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "public_share_token_unique": { + "name": "public_share_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_unique": { + "name": "public_share_resource_unique", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_id_idx": { + "name": "public_share_resource_id_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_workspace_id_idx": { + "name": "public_share_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "public_share_workspace_id_workspace_id_fk": { + "name": "public_share_workspace_id_workspace_id_fk", + "tableFrom": "public_share", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "public_share_created_by_user_id_fk": { + "name": "public_share_created_by_user_id_fk", + "tableFrom": "public_share", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "mothership_environment": { + "name": "mothership_environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_active_workspace_id": { + "name": "last_active_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sim_trigger_state": { + "name": "sim_trigger_state", + "schema": "", + "columns": { + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sim_trigger_state_workflow_id_workflow_id_fk": { + "name": "sim_trigger_state_workflow_id_workflow_id_fk", + "tableFrom": "sim_trigger_state", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sim_trigger_state_workflow_id_block_id_scope_key_pk": { + "name": "sim_trigger_state_workflow_id_block_id_scope_key_pk", + "columns": ["workflow_id", "block_id", "scope_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill": { + "name": "skill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_workspace_name_unique": { + "name": "skill_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_workspace_id_workspace_id_fk": { + "name": "skill_workspace_id_workspace_id_fk", + "tableFrom": "skill", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_user_id_user_id_fk": { + "name": "skill_user_id_user_id_fk", + "tableFrom": "skill", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sso_provider_provider_id_idx": { + "name": "sso_provider_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancel_at": { + "name": "cancel_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.table_jobs": { + "name": "table_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rows_processed": { + "name": "rows_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_jobs_one_active_per_table": { + "name": "table_jobs_one_active_per_table", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"table_jobs\".\"status\" = 'running' AND \"table_jobs\".\"type\" <> 'export'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_watchdog_idx": { + "name": "table_jobs_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_table_started_idx": { + "name": "table_jobs_table_started_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_jobs_table_id_user_table_definitions_id_fk": { + "name": "table_jobs_table_id_user_table_definitions_id_fk", + "tableFrom": "table_jobs", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_jobs_workspace_id_workspace_id_fk": { + "name": "table_jobs_workspace_id_workspace_id_fk", + "tableFrom": "table_jobs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_row_executions": { + "name": "table_row_executions", + "schema": "", + "columns": { + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "running_block_ids": { + "name": "running_block_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "block_errors": { + "name": "block_errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enrichment_details": { + "name": "enrichment_details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_row_executions_table_status_idx": { + "name": "table_row_executions_table_status_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"status\" IN ('queued', 'running', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_execution_id_idx": { + "name": "table_row_executions_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"execution_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_table_group_idx": { + "name": "table_row_executions_table_group_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_row_executions_table_id_user_table_definitions_id_fk": { + "name": "table_row_executions_table_id_user_table_definitions_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_row_id_user_table_rows_id_fk": { + "name": "table_row_executions_row_id_user_table_rows_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "table_row_executions_row_id_group_id_pk": { + "name": "table_row_executions_row_id_group_id_pk", + "columns": ["row_id", "group_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_run_dispatches": { + "name": "table_run_dispatches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit": { + "name": "limit", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processed_count": { + "name": "processed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_manual_run": { + "name": "is_manual_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "triggered_by_user_id": { + "name": "triggered_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_run_dispatches_active_idx": { + "name": "table_run_dispatches_active_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_watchdog_idx": { + "name": "table_run_dispatches_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_run_dispatches_table_id_user_table_definitions_id_fk": { + "name": "table_run_dispatches_table_id_user_table_definitions_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_workspace_id_workspace_id_fk": { + "name": "table_run_dispatches_workspace_id_workspace_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_triggered_by_user_id_user_id_fk": { + "name": "table_run_dispatches_triggered_by_user_id_user_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user", + "columnsFrom": ["triggered_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_entity_type": { + "name": "billing_entity_type", + "type": "billing_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "billing_entity_id": { + "name": "billing_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_id_idx": { + "name": "usage_log_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_event_key_unique": { + "name": "usage_log_event_key_unique", + "columns": [ + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_period_idx": { + "name": "usage_log_billing_entity_period_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_created_at_idx": { + "name": "usage_log_workspace_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_execution_id_idx": { + "name": "usage_log_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "usage_log_billing_scope_all_or_none": { + "name": "usage_log_billing_scope_all_or_none", + "value": "(\n (\"usage_log\".\"billing_entity_type\" IS NULL AND \"usage_log\".\"billing_entity_id\" IS NULL AND \"usage_log\".\"billing_period_start\" IS NULL AND \"usage_log\".\"billing_period_end\" IS NULL)\n OR\n (\"usage_log\".\"billing_entity_type\" IS NOT NULL AND \"usage_log\".\"billing_entity_id\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" IS NOT NULL AND \"usage_log\".\"billing_period_end\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" < \"usage_log\".\"billing_period_end\")\n )" + } + }, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_normalized_email_unique": { + "name": "user_normalized_email_unique", + "nullsNotDistinct": false, + "columns": ["normalized_email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_executions": { + "name": "total_mcp_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_a2a_executions": { + "name": "total_a2a_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'5'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "pro_period_cost_snapshot": { + "name": "pro_period_cost_snapshot", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "pro_period_cost_snapshot_at": { + "name": "pro_period_cost_snapshot_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_copilot_cost": { + "name": "current_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_calls": { + "name": "total_mcp_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_cost": { + "name": "total_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_mcp_copilot_cost": { + "name": "current_period_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_definitions": { + "name": "user_table_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "max_rows": { + "name": "max_rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "row_count": { + "name": "row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "rows_version": { + "name": "rows_version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_table_def_workspace_id_idx": { + "name": "user_table_def_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_name_unique": { + "name": "user_table_def_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_table_definitions\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_archived_at_idx": { + "name": "user_table_def_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_archived_partial_idx": { + "name": "user_table_def_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_table_definitions\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_definitions_workspace_id_workspace_id_fk": { + "name": "user_table_definitions_workspace_id_workspace_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_definitions_created_by_user_id_fk": { + "name": "user_table_definitions_created_by_user_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_rows": { + "name": "user_table_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_table_rows_tenant_data_gin_idx": { + "name": "user_table_rows_tenant_data_gin_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"data\" jsonb_path_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "user_table_rows_workspace_table_idx": { + "name": "user_table_rows_workspace_table_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_position_idx": { + "name": "user_table_rows_table_position_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_order_key_idx": { + "name": "user_table_rows_table_order_key_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_id_id_idx": { + "name": "user_table_rows_table_id_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_rows_table_id_user_table_definitions_id_fk": { + "name": "user_table_rows_table_id_user_table_definitions_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_workspace_id_workspace_id_fk": { + "name": "user_table_rows_workspace_id_workspace_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_created_by_user_id_fk": { + "name": "user_table_rows_created_by_user_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_deployment_unique": { + "name": "path_deployment_unique", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_credential_set_id_idx": { + "name": "webhook_credential_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_archived_at_partial_idx": { + "name": "webhook_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468": { + "name": "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id_updated_at_desc": { + "name": "idx_webhook_on_workflow_id_block_id_updated_at_desc", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_credential_set_id_credential_set_id_fk": { + "name": "webhook_credential_set_id_credential_set_id_fk", + "tableFrom": "webhook", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_public_api": { + "name": "is_public_api", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_folder_name_active_unique": { + "name": "workflow_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_archived_at_idx": { + "name": "workflow_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_archived_partial_idx": { + "name": "workflow_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_workflow_folder_id_fk": { + "name": "workflow_folder_id_workflow_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "workflow_folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost_total": { + "name": "cost_total", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "models_used": { + "name": "models_used", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_id_desc_idx": { + "name": "workflow_execution_logs_workspace_started_at_id_desc_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_cost_total_idx": { + "name": "workflow_execution_logs_workspace_cost_total_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_total", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_models_used_idx": { + "name": "workflow_execution_logs_models_used_idx", + "columns": [ + { + "expression": "models_used", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "workflow_execution_logs_workspace_ended_at_id_idx": { + "name": "workflow_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_started_at_idx": { + "name": "workflow_execution_logs_running_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_archived_at_idx": { + "name": "workflow_folder_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_archived_partial_idx": { + "name": "workflow_folder_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_folder\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_deleted_at_idx": { + "name": "workflow_mcp_server_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_workspace_deleted_partial_idx": { + "name": "workflow_mcp_server_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_server\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "parameter_description_overrides": { + "name": "parameter_description_overrides", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_archived_at_partial_idx": { + "name": "workflow_mcp_tool_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "infra_retry_count": { + "name": "infra_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workflow'" + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'persistent'" + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_runs": { + "name": "max_runs", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source_chat_id": { + "name": "source_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_task_name": { + "name": "source_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_user_id": { + "name": "source_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_history": { + "name": "job_history", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "contexts": { + "name": "contexts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "excluded_dates": { + "name": "excluded_dates", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_deployment_unique": { + "name": "workflow_schedule_workflow_block_deployment_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_workflow_deployment_idx": { + "name": "workflow_schedule_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_archived_at_partial_idx": { + "name": "workflow_schedule_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6": { + "name": "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6", + "columns": [ + { + "expression": "source_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_workflow_idx": { + "name": "workflow_schedule_due_workflow_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND (\"workflow_schedule\".\"source_type\" = 'workflow' OR \"workflow_schedule\".\"source_type\" IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_job_idx": { + "name": "workflow_schedule_due_job_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND \"workflow_schedule\".\"source_type\" = 'job'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_user_id_user_id_fk": { + "name": "workflow_schedule_source_user_id_user_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "user", + "columnsFrom": ["source_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_workspace_id_workspace_id_fk": { + "name": "workflow_schedule_source_workspace_id_workspace_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workspace", + "columnsFrom": ["source_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#33C482'" + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_mode": { + "name": "workspace_mode", + "type": "workspace_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'grandfathered_shared'" + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inbox_enabled": { + "name": "inbox_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "inbox_address": { + "name": "inbox_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_provider_id": { + "name": "inbox_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "forked_from_workspace_id": { + "name": "forked_from_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_owner_id_idx": { + "name": "workspace_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_organization_id_idx": { + "name": "workspace_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_mode_idx": { + "name": "workspace_mode_idx", + "columns": [ + { + "expression": "workspace_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_forked_from_workspace_id_idx": { + "name": "workspace_forked_from_workspace_id_idx", + "columns": [ + { + "expression": "forked_from_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_organization_id_organization_id_fk": { + "name": "workspace_organization_id_organization_id_fk", + "tableFrom": "workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_forked_from_workspace_id_workspace_id_fk": { + "name": "workspace_forked_from_workspace_id_workspace_id_fk", + "tableFrom": "workspace", + "tableTo": "workspace", + "columnsFrom": ["forked_from_workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_byok_workspace_provider_idx": { + "name": "workspace_byok_workspace_provider_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_key_idx": { + "name": "workspace_file_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_deleted_at_idx": { + "name": "workspace_file_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_workspace_deleted_partial_idx": { + "name": "workspace_file_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_folders": { + "name": "workspace_file_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_folders_workspace_parent_idx": { + "name": "workspace_file_folders_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_parent_sort_idx": { + "name": "workspace_file_folders_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_deleted_at_idx": { + "name": "workspace_file_folders_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_deleted_partial_idx": { + "name": "workspace_file_folders_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_parent_name_active_unique": { + "name": "workspace_file_folders_workspace_parent_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_folders_user_id_user_id_fk": { + "name": "workspace_file_folders_user_id_user_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_workspace_id_workspace_id_fk": { + "name": "workspace_file_folders_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_parent_id_workspace_file_folders_id_fk": { + "name": "workspace_file_folders_parent_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace_file_folders", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_files_key_active_unique": { + "name": "workspace_files_key_active_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_folder_name_active_unique": { + "name": "workspace_files_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "original_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_display_name_unique": { + "name": "workspace_files_chat_display_name_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"context\" = 'mothership' AND \"workspace_files\".\"chat_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_folder_id_idx": { + "name": "workspace_files_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_id_idx": { + "name": "workspace_files_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_deleted_at_idx": { + "name": "workspace_files_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_deleted_partial_idx": { + "name": "workspace_files_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_folder_id_workspace_file_folders_id_fk": { + "name": "workspace_files_folder_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace_file_folders", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_files_chat_id_copilot_chats_id_fk": { + "name": "workspace_files_chat_id_copilot_chats_id_fk", + "tableFrom": "workspace_files", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_promote_run": { + "name": "workspace_fork_promote_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workspace_id": { + "name": "target_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "workspace_fork_promote_direction", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_promote_run_child_ws_target_unique": { + "name": "workspace_fork_promote_run_child_ws_target_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_promote_run_target_ws_idx": { + "name": "workspace_fork_promote_run_target_ws_idx", + "columns": [ + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_promote_run_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_promote_run_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_promote_run_created_by_user_id_fk": { + "name": "workspace_fork_promote_run_created_by_user_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_resource_map": { + "name": "workspace_fork_resource_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "workspace_fork_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "parent_resource_id": { + "name": "parent_resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_resource_id": { + "name": "child_resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_resource_map_child_ws_idx": { + "name": "workspace_fork_resource_map_child_ws_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_ws_type_idx": { + "name": "workspace_fork_resource_map_child_ws_type_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_type_parent_unique": { + "name": "workspace_fork_resource_map_child_type_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_resource_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_resource_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_resource_map_created_by_user_id_fk": { + "name": "workspace_fork_resource_map_created_by_user_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.a2a_task_status": { + "name": "a2a_task_status", + "schema": "public", + "values": [ + "submitted", + "working", + "input-required", + "completed", + "failed", + "canceled", + "rejected", + "auth-required", + "unknown" + ] + }, + "public.academy_cert_status": { + "name": "academy_cert_status", + "schema": "public", + "values": ["active", "revoked", "expired"] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.billing_entity_type": { + "name": "billing_entity_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.chat_type": { + "name": "chat_type", + "schema": "public", + "values": ["mothership", "copilot"] + }, + "public.copilot_async_tool_status": { + "name": "copilot_async_tool_status", + "schema": "public", + "values": ["pending", "running", "completed", "failed", "cancelled", "delivered"] + }, + "public.copilot_run_status": { + "name": "copilot_run_status", + "schema": "public", + "values": ["active", "paused_waiting_for_tool", "resuming", "complete", "error", "cancelled"] + }, + "public.credential_member_role": { + "name": "credential_member_role", + "schema": "public", + "values": ["admin", "member"] + }, + "public.credential_member_status": { + "name": "credential_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_set_invitation_status": { + "name": "credential_set_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "expired", "cancelled"] + }, + "public.credential_set_member_status": { + "name": "credential_set_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": ["oauth", "env_workspace", "env_personal", "service_account"] + }, + "public.data_drain_cadence": { + "name": "data_drain_cadence", + "schema": "public", + "values": ["hourly", "daily"] + }, + "public.data_drain_destination": { + "name": "data_drain_destination", + "schema": "public", + "values": ["s3", "gcs", "azure_blob", "datadog", "bigquery", "snowflake", "webhook"] + }, + "public.data_drain_run_status": { + "name": "data_drain_run_status", + "schema": "public", + "values": ["running", "success", "failed"] + }, + "public.data_drain_run_trigger": { + "name": "data_drain_run_trigger", + "schema": "public", + "values": ["cron", "manual"] + }, + "public.data_drain_source": { + "name": "data_drain_source", + "schema": "public", + "values": ["workflow_logs", "job_logs", "audit_logs", "copilot_chats", "copilot_runs"] + }, + "public.execution_large_value_reference_source": { + "name": "execution_large_value_reference_source", + "schema": "public", + "values": ["execution_log", "paused_snapshot"] + }, + "public.invitation_kind": { + "name": "invitation_kind", + "schema": "public", + "values": ["organization", "workspace"] + }, + "public.invitation_membership_intent": { + "name": "invitation_membership_intent", + "schema": "public", + "values": ["internal", "external"] + }, + "public.invitation_status": { + "name": "invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled", "expired"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed", "tool"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": [ + "workflow", + "wand", + "copilot", + "workspace-chat", + "mcp_copilot", + "mothership_block", + "knowledge-base", + "voice-input", + "enrichment" + ] + }, + "public.workspace_fork_promote_direction": { + "name": "workspace_fork_promote_direction", + "schema": "public", + "values": ["push", "pull"] + }, + "public.workspace_fork_resource_type": { + "name": "workspace_fork_resource_type", + "schema": "public", + "values": [ + "workflow", + "oauth_credential", + "service_account_credential", + "env_var", + "table", + "knowledge_base", + "knowledge_document", + "file", + "mcp_server", + "custom_tool", + "skill" + ] + }, + "public.workspace_mode": { + "name": "workspace_mode", + "schema": "public", + "values": ["personal", "organization", "grandfathered_shared"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 12ca35683eb..2ed45b2c508 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1744,6 +1744,13 @@ "when": 1782260194924, "tag": "0249_drop_permission_group_applies_to_all_workspaces", "breakpoints": true + }, + { + "idx": 250, + "version": "7", + "when": 1782365854239, + "tag": "0250_workspace_forking", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index fb0cd842e1a..a604e6ee367 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -1325,6 +1325,10 @@ export const workspace = pgTable( inboxAddress: text('inbox_address'), inboxProviderId: text('inbox_provider_id'), archivedAt: timestamp('archived_at'), + forkedFromWorkspaceId: text('forked_from_workspace_id').references( + (): AnyPgColumn => workspace.id, + { onDelete: 'set null' } + ), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, @@ -1332,6 +1336,87 @@ export const workspace = pgTable( ownerIdIdx: index('workspace_owner_id_idx').on(table.ownerId), organizationIdIdx: index('workspace_organization_id_idx').on(table.organizationId), workspaceModeIdx: index('workspace_mode_idx').on(table.workspaceMode), + forkedFromWorkspaceIdx: index('workspace_forked_from_workspace_id_idx').on( + table.forkedFromWorkspaceId + ), + }) +) + +export const workspaceForkResourceTypeEnum = pgEnum('workspace_fork_resource_type', [ + 'workflow', + 'oauth_credential', + 'service_account_credential', + 'env_var', + 'table', + 'knowledge_base', + 'knowledge_document', + 'file', + 'mcp_server', + 'custom_tool', + 'skill', +]) + +export const workspaceForkResourceMap = pgTable( + 'workspace_fork_resource_map', + { + id: text('id').primaryKey(), + childWorkspaceId: text('child_workspace_id') + .notNull() + .references(() => workspace.id, { onDelete: 'cascade' }), + resourceType: workspaceForkResourceTypeEnum('resource_type').notNull(), + parentResourceId: text('parent_resource_id').notNull(), + childResourceId: text('child_resource_id'), + // SET NULL (not CASCADE): deleting the creating user must not delete the fork's + // identity mappings, which the edge depends on for every future promote. + createdBy: text('created_by').references(() => user.id, { onDelete: 'set null' }), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (table) => ({ + childWorkspaceIdx: index('workspace_fork_resource_map_child_ws_idx').on(table.childWorkspaceId), + childWorkspaceTypeIdx: index('workspace_fork_resource_map_child_ws_type_idx').on( + table.childWorkspaceId, + table.resourceType + ), + childTypeParentUnique: uniqueIndex('workspace_fork_resource_map_child_type_parent_unique').on( + table.childWorkspaceId, + table.resourceType, + table.parentResourceId + ), + }) +) + +export const workspaceForkPromoteDirectionEnum = pgEnum('workspace_fork_promote_direction', [ + 'push', + 'pull', +]) + +export const workspaceForkPromoteRun = pgTable( + 'workspace_fork_promote_run', + { + id: text('id').primaryKey(), + childWorkspaceId: text('child_workspace_id') + .notNull() + .references(() => workspace.id, { onDelete: 'cascade' }), + sourceWorkspaceId: text('source_workspace_id').notNull(), + targetWorkspaceId: text('target_workspace_id').notNull(), + direction: workspaceForkPromoteDirectionEnum('direction').notNull(), + snapshot: jsonb('snapshot').notNull(), + // SET NULL (not CASCADE): deleting the creating user must not delete a pending + // undo point for a target workspace. + createdBy: text('created_by').references(() => user.id, { onDelete: 'set null' }), + createdAt: timestamp('created_at').notNull().defaultNow(), + }, + (table) => ({ + // One undo point per (edge, target) so a push (target=parent) and a pull + // (target=child) on the same edge keep independent undo points. + childWorkspaceTargetUnique: uniqueIndex('workspace_fork_promote_run_child_ws_target_unique').on( + table.childWorkspaceId, + table.targetWorkspaceId + ), + targetWorkspaceIdx: index('workspace_fork_promote_run_target_ws_idx').on( + table.targetWorkspaceId + ), }) ) diff --git a/packages/testing/src/mocks/audit.mock.ts b/packages/testing/src/mocks/audit.mock.ts index 3fd8d55b04c..d7a16b82364 100644 --- a/packages/testing/src/mocks/audit.mock.ts +++ b/packages/testing/src/mocks/audit.mock.ts @@ -154,6 +154,9 @@ export const auditMock = { WORKSPACE_UPDATED: 'workspace.updated', WORKSPACE_DELETED: 'workspace.deleted', WORKSPACE_DUPLICATED: 'workspace.duplicated', + WORKSPACE_FORKED: 'workspace.forked', + WORKSPACE_FORK_PROMOTED: 'workspace.fork_promoted', + WORKSPACE_FORK_ROLLED_BACK: 'workspace.fork_rolled_back', }, AuditResourceType: { API_KEY: 'api_key', diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index a45c2d84914..a8f39f34e2f 100644 --- a/scripts/check-api-validation-contracts.ts +++ b/scripts/check-api-validation-contracts.ts @@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries') const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors') const BASELINE = { - totalRoutes: 862, - zodRoutes: 862, + totalRoutes: 869, + zodRoutes: 869, nonZodRoutes: 0, } as const