diff --git a/apps/sim/app/api/webhooks/outbox/process/route.ts b/apps/sim/app/api/webhooks/outbox/process/route.ts index 4ac098c3a2d..251c556ec79 100644 --- a/apps/sim/app/api/webhooks/outbox/process/route.ts +++ b/apps/sim/app/api/webhooks/outbox/process/route.ts @@ -1,3 +1,4 @@ +import { db } from '@sim/db' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { type NextRequest, NextResponse } from 'next/server' @@ -7,6 +8,7 @@ import { processOutboxEvents } from '@/lib/core/outbox/service' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { workflowDeploymentOutboxHandlers } from '@/lib/workflows/deployment-outbox' +import { reapStaleBackgroundWork } from '@/lib/workspaces/fork/background-work/store' const logger = createLogger('OutboxProcessorAPI') @@ -33,12 +35,23 @@ export const GET = withRouteHandler(async (request: NextRequest) => { minRemainingMs: 95_000, }) - logger.info('Outbox processing completed', { requestId, ...result }) + // Reap fork background-work rows stuck `processing` past their TTL (worker crash / + // restart has no in-task hook). Independent of the outbox; a failure here must not + // fail the outbox run, so it's guarded separately. + let reapedBackgroundWork = 0 + try { + reapedBackgroundWork = await reapStaleBackgroundWork(db) + } catch (error) { + logger.error('Background-work reap failed', { requestId, error: toError(error).message }) + } + + logger.info('Outbox processing completed', { requestId, ...result, reapedBackgroundWork }) return NextResponse.json({ success: true, requestId, result, + reapedBackgroundWork, }) } catch (error) { logger.error('Outbox processing failed', { requestId, error: toError(error).message }) diff --git a/apps/sim/app/api/workspaces/[id]/background-work/route.ts b/apps/sim/app/api/workspaces/[id]/background-work/route.ts new file mode 100644 index 00000000000..066ba5c4a8b --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/background-work/route.ts @@ -0,0 +1,45 @@ +import { db } from '@sim/db' +import { type NextRequest, NextResponse } from 'next/server' +import { getWorkspaceBackgroundWorkContract } 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 { listSurfacedBackgroundWork } from '@/lib/workspaces/fork/background-work/store' +import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' + +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(getWorkspaceBackgroundWorkContract, req, context) + if (!parsed.success) return parsed.response + const { id } = parsed.data.params + + const access = await checkWorkspaceAccess(id, session.user.id) + if (!access.exists) { + return NextResponse.json({ error: 'Workspace not found' }, { status: 404 }) + } + if (!access.canAdmin) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + + const rows = await listSurfacedBackgroundWork(db, id) + return NextResponse.json({ + items: rows.map((row) => ({ + id: row.id, + workspaceId: row.workspaceId, + workflowId: row.workflowId, + kind: row.kind, + status: row.status, + message: row.message, + error: row.error, + metadata: row.metadata ?? null, + startedAt: row.startedAt.toISOString(), + completedAt: row.completedAt ? row.completedAt.toISOString() : null, + })), + }) + } +) 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..4631e16fda2 --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/fork/diff/route.ts @@ -0,0 +1,160 @@ +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 { loadTargetDraftSubBlocks } from '@/lib/workspaces/fork/copy/copy-workflows' +import { loadSourceDeployedStates } from '@/lib/workspaces/fork/copy/deploy-bridge' +import { assertCanPromote } from '@/lib/workspaces/fork/lineage/authz' +import { loadForkBlockMap } from '@/lib/workspaces/fork/mapping/block-map-store' +import { + collectForkDependentReconfigs, + collectForkResourceUsages, +} from '@/lib/workspaces/fork/mapping/dependent-reconfigs' +import { + forkDependentValueKey, + loadForkDependentValues, +} from '@/lib/workspaces/fork/mapping/dependent-value-store' +import { computeForkPromotePlan } from '@/lib/workspaces/fork/promote/promote-plan' +import { buildForkBlockIdResolver } from '@/lib/workspaces/fork/remap/block-identity' +import { readTargetDraftDependentValue } from '@/lib/workspaces/fork/remap/remap-references' + +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, + }) + + // Resolve dependent-reconfig target block ids through the SAME persisted block map the + // sync will use, so a re-pick the modal keys by target block id lands on the block the + // promote actually writes (on push that's the parent's original id, not a derived one). + const sourceIsParent = auth.sourceWorkspaceId === auth.edge.parentWorkspaceId + const blockMap = await loadForkBlockMap(db, auth.edge.childWorkspaceId) + const resolveBlockId = buildForkBlockIdResolver(sourceIsParent, blockMap) + + // Stored dependent values are the source of truth for what each selector is set to. Overlay + // them as each field's currentValue so the modal pre-fills what the user actually saved. For + // an edge that predates the store the fallback is the TARGET's own configured value (loaded + // from its draft) - never the source's, which would overwrite the target's selection on the + // first sync. Both the stored read and the draft read are scoped to the plan's replace + // targets, the only workflows with dependents to reconfigure. + const replaceTargetIds = plan.items + .filter((item) => item.mode === 'replace') + .map((item) => item.targetWorkflowId) + const [storedValues, targetDraftByWorkflow] = await Promise.all([ + loadForkDependentValues(db, auth.edge.childWorkspaceId, replaceTargetIds), + loadTargetDraftSubBlocks(db, replaceTargetIds), + ]) + const storedByKey = new Map( + storedValues.map((entry) => [ + forkDependentValueKey(entry.targetWorkflowId, entry.targetBlockId, entry.subBlockKey), + entry.value, + ]) + ) + + // Source block subBlocks keyed by their resolved target identity, so the first-sync draft + // fallback can identity-check a nested tool against the SOURCE dependent tool it came from - + // an index alone may point at a different tool in the target draft, whose value isn't the + // dependent's. Read structurally (only each subblock's `value`), so the in-memory state's + // blocks pass without a cast. + const sourceBlocksByTarget = new Map>>() + for (const item of plan.items) { + if (item.mode !== 'replace') continue + const state = sourceStates.get(item.sourceWorkflowId) + if (!state) continue + const byBlock = new Map>() + for (const [sourceBlockId, block] of Object.entries(state.blocks)) { + byBlock.set(resolveBlockId(item.targetWorkflowId, sourceBlockId), block.subBlocks ?? {}) + } + sourceBlocksByTarget.set(item.targetWorkflowId, byBlock) + } + + const dependentReconfigs = collectForkDependentReconfigs( + plan.items, + sourceStates, + resolveBlockId + ).map((field) => ({ + ...field, + currentValue: + storedByKey.get( + forkDependentValueKey(field.targetWorkflowId, field.targetBlockId, field.subBlockKey) + ) ?? + readTargetDraftDependentValue( + targetDraftByWorkflow.get(field.targetWorkflowId)?.get(field.targetBlockId), + sourceBlocksByTarget.get(field.targetWorkflowId)?.get(field.targetBlockId), + field.subBlockKey + ), + })) + + 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, + dependentReconfigs, + resourceUsages: collectForkResourceUsages(plan.items, sourceStates), + }) + } +) 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..5e409b78acd --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/fork/lineage/route.ts @@ -0,0 +1,56 @@ +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 { getForkParent } 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, run] = await Promise.all([ + getForkParent(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, + 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..0729b0d4ff6 --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/fork/mapping/route.ts @@ -0,0 +1,75 @@ +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 { acquireForkEdgeLock, setForkLockTimeout } from '@/lib/workspaces/fork/lineage/lineage' +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.sourceWorkspaceId, auth.targetWorkspaceId, entries) + + // Serialize concurrent mapping saves on this edge so a push (keyed child-side, deleted + // then re-upserted parent-side) can't leave duplicate rows for the same source. Same + // edge lock promote/rollback use, with a bounded wait. + const updated = await db.transaction(async (tx) => { + await setForkLockTimeout(tx) + await acquireForkEdgeLock(tx, auth.edge.childWorkspaceId) + return 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..eba0e7b1c8f --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/fork/promote/route.ts @@ -0,0 +1,118 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' +import { db } from '@sim/db' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +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 { recordBackgroundWork } from '@/lib/workspaces/fork/background-work/store' +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, dependentValues } = 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, + userId: session.user.id, + dependentValues, + requestId, + }) + + const body = { + promoteRunId: result.promoteRunId, + updated: result.updated, + created: result.created, + archived: result.archived, + redeployed: result.redeployed, + deployFailed: result.deployFailed, + unmappedRequired: result.unmappedRequired, + needsConfiguration: result.needsConfiguration, + clearedOptional: result.clearedOptional, + } + + 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, + }) + + const otherName = + otherWorkspaceId === auth.sourceWorkspaceId ? auth.source.name : auth.target.name + await recordBackgroundWork(db, { + workspaceId: id, + kind: 'fork_sync', + status: + result.deployFailed > 0 || + result.needsConfiguration.length > 0 || + result.clearedOptional.length > 0 + ? 'completed_with_warnings' + : 'completed', + message: direction === 'pull' ? `Pulled from "${otherName}"` : `Pushed to "${otherName}"`, + metadata: { + actorName: session.user.name ?? undefined, + otherWorkspaceName: otherName, + direction, + updated: result.updated, + created: result.created, + archived: result.archived, + redeployed: result.redeployed, + deployFailed: result.deployFailed, + updatedNames: result.updatedNames, + createdNames: result.createdNames, + archivedNames: result.archivedNames, + needsConfiguration: result.needsConfiguration, + clearedOptional: result.clearedOptional, + }, + }).catch((error) => + logger.error(`[${requestId}] Failed to record sync activity`, { + error: getErrorMessage(error), + }) + ) + + 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..dffb841af5e --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/fork/rollback/route.ts @@ -0,0 +1,84 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' +import { db } from '@sim/db' +import { workspace } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { eq } from 'drizzle-orm' +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 { recordBackgroundWork } from '@/lib/workspaces/fork/background-work/store' +import { assertCanRollback } from '@/lib/workspaces/fork/lineage/authz' +import { rollbackFork } from '@/lib/workspaces/fork/promote/rollback' + +const logger = createLogger('WorkspaceForkRollbackAPI') + +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, + }) + + // Durable audit entry scoped to this workspace so the undo shows in its Manage Forks + // → Activity log. Non-critical: a failure must not fail the (committed) rollback. + const [other] = await db + .select({ name: workspace.name }) + .from(workspace) + .where(eq(workspace.id, otherWorkspaceId)) + .limit(1) + const otherName = other?.name ?? 'the source workspace' + await recordBackgroundWork(db, { + workspaceId: id, + kind: 'fork_rollback', + status: result.skipped > 0 ? 'completed_with_warnings' : 'completed', + message: `Undid the last sync from "${otherName}"`, + metadata: { + actorName: session.user.name ?? undefined, + otherWorkspaceName: otherName, + restored: result.restored, + removed: result.archived, + unarchived: result.unarchived, + skipped: result.skipped, + }, + }).catch((error) => + logger.error(`[${requestId}] Failed to record rollback activity`, { + error: getErrorMessage(error), + }) + ) + + 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..27bea2f1b99 --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/fork/route.ts @@ -0,0 +1,67 @@ +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, + actorName: session.user.name ?? undefined, + 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/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx index fd82f07fadd..00798f4a976 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx @@ -6,6 +6,7 @@ import { resolveToolParamSync, } from '@/lib/workflows/tool-input/synthetic-subblocks' import { parseStoredToolInputValue } from '@/lib/workflows/tool-input/types' +import { DependencyBlockTypeProvider } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-dependency-block-type' import { SubBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block' import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' @@ -17,6 +18,8 @@ interface ToolSubBlockRendererProps { toolIndex: number subBlock: BlockSubBlockConfig effectiveParamId: string + /** The tool's block type (e.g. `gmail`), so its params' selectors resolve dependencies. */ + toolType: string toolParams: Record | undefined onParamChange: (toolIndex: number, paramId: string, value: string) => void disabled: boolean @@ -44,6 +47,7 @@ export function ToolSubBlockRenderer({ toolIndex, subBlock, effectiveParamId, + toolType, toolParams, onParamChange, disabled, @@ -118,13 +122,15 @@ export function ToolSubBlockRenderer({ } return ( - + + + ) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index 976119bd960..92bbc361c7e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -2111,6 +2111,7 @@ export const ToolInput = memo(function ToolInput({ toolIndex={toolIndex} subBlock={sbWithTitle} effectiveParamId={effectiveParamId} + toolType={tool.type} toolParams={tool.params} onParamChange={handleParamChange} disabled={disabled} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-dependency-block-type.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-dependency-block-type.ts new file mode 100644 index 00000000000..05dac09c4db --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-dependency-block-type.ts @@ -0,0 +1,19 @@ +'use client' + +import { createContext, useContext } from 'react' + +const DependencyBlockTypeContext = createContext(null) + +/** + * Provider set by tool-input param rendering (value = the tool's block type, e.g. `gmail`). + */ +export const DependencyBlockTypeProvider = DependencyBlockTypeContext.Provider + +/** + * The block type whose config should drive dependency (`dependsOn`) canonical resolution + * for the current subblock. Null for normal blocks (resolve against the host block). Set + * to the tool's type for tool-input params, so a nested tool's selector resolves its + * parents against the TOOL's config (e.g. a Gmail tool's `credential` -> `oauthCredential`, + * which the host Agent block's subblocks don't define) and can fetch its options. + */ +export const useDependencyBlockType = () => useContext(DependencyBlockTypeContext) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate.ts index 338e3b9a11f..54576b7c819 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate.ts @@ -15,6 +15,7 @@ import type { SubBlockConfig } from '@/blocks/types' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' +import { useDependencyBlockType } from './use-dependency-block-type' /** * Centralized dependsOn gating for sub-block components. @@ -33,7 +34,13 @@ export function useDependsOnGate( const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId) const blockState = useWorkflowStore((state) => state.blocks[blockId]) - const blockConfig = blockState?.type ? getBlock(blockState.type) : null + + const dependencyBlockType = useDependencyBlockType() + const blockConfig = dependencyBlockType + ? getBlock(dependencyBlockType) + : blockState?.type + ? getBlock(blockState.type) + : null const canonicalIndex = useMemo( () => buildCanonicalIndex(blockConfig?.subBlocks || []), [blockConfig?.subBlocks] diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx index 094e1d5a43d..9b20bee8cf5 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx @@ -19,6 +19,8 @@ import { Mail, Pencil, Plus, + Rocket, + Shuffle, SquareArrowUpRight, Trash, Unlock, @@ -68,6 +70,10 @@ interface ContextMenuProps { onUploadLogo?: () => void showUploadLogo?: boolean disableUploadLogo?: boolean + onFork?: () => void + onSync?: () => void + showFork?: boolean + showSync?: boolean } /** @@ -118,6 +124,10 @@ export function ContextMenu({ onUploadLogo, showUploadLogo = false, disableUploadLogo = false, + onFork, + onSync, + showFork = false, + showSync = false, }: ContextMenuProps) { const hasNavigationSection = showOpenInNewTab && onOpenInNewTab const hasStatusSection = @@ -131,6 +141,7 @@ export function ContextMenu({ (showLock && onToggleLock) || (showUploadLogo && onUploadLogo) const hasCopySection = (showDuplicate && onDuplicate) || (showExport && onExport) + const hasForkSection = (showFork && onFork) || (showSync && onSync) return ( !open && onClose()} modal={false}> @@ -294,6 +305,35 @@ export function ContextMenu({ )} {(hasNavigationSection || hasStatusSection || hasEditSection || hasCopySection) && + hasForkSection && } + {showFork && onFork && ( + { + onFork() + onClose() + }} + > + + Manage Forks + + )} + {showSync && onSync && ( + { + onSync() + onClose() + }} + > + + Sync workspace + + )} + + {(hasNavigationSection || + hasStatusSection || + hasEditSection || + hasCopySection || + hasForkSection) && (showLeave || showDelete) && } {showLeave && onLeave && ( `${n} ${noun}${n === 1 ? '' : 's'}` + +/** Join "N verb" segments (verbs like "updated" aren't pluralized), dropping zero counts. */ +function countList(pairs: Array<[number | undefined, string]>): string { + return pairs + .filter(([n]) => (n ?? 0) > 0) + .map(([n, verb]) => `${n} ${verb}`) + .join(' · ') +} + +/** A named, collapsible group (one resource kind or change action) of a job's report. */ +interface ReportGroup { + label: string + names: string[] +} + +/** A job's expanded report: collapsible named groups plus plain notes (counts / warnings). */ +interface JobReport { + groups: ReportGroup[] + notes: Array<{ value: string; warning?: boolean }> +} + +/** The audit-row title, derived per kind from the job's metadata. */ +function jobTitle(job: BackgroundWorkItem): string { + const m = job.metadata + switch (job.kind) { + case 'fork_content_copy': + return m?.childWorkspaceName + ? `Forked into "${m.childWorkspaceName}"` + : (job.message ?? 'Fork') + case 'fork_sync': + if (!m?.otherWorkspaceName) return job.message ?? 'Sync' + return m.direction === 'pull' + ? `Pulled from "${m.otherWorkspaceName}"` + : `Pushed to "${m.otherWorkspaceName}"` + case 'fork_rollback': + return m?.otherWorkspaceName + ? `Undid sync from "${m.otherWorkspaceName}"` + : (job.message ?? 'Rollback') + default: + return job.message ?? 'Activity' + } +} + +/** Build a job's report (collapsible named groups + plain notes) from its metadata. */ +function jobReport(job: BackgroundWorkItem): JobReport { + const m = job.metadata + const groups: ReportGroup[] = [] + const notes: JobReport['notes'] = [] + if (!m) return { groups, notes } + + const addGroup = (label: string, names: string[] | undefined) => { + if (names && names.length > 0) groups.push({ label, names }) + } + + if (job.kind === 'fork_sync') { + addGroup('Updated', m.updatedNames) + addGroup('Created', m.createdNames) + addGroup('Archived', m.archivedNames) + // Pre-names entries fall back to the count summary (redeployed mirrors updated). + if (groups.length === 0) { + const counts = countList([ + [m.updated, 'updated'], + [m.created, 'created'], + [m.archived, 'archived'], + ]) + if (counts) notes.push({ value: counts }) + } + if (m.needsConfiguration && m.needsConfiguration.length > 0) { + for (const item of m.needsConfiguration) { + notes.push({ + value: `${item.workflowName} — re-check ${item.blocks.join(', ')}`, + warning: true, + }) + } + } + if (m.clearedOptional && m.clearedOptional.length > 0) { + for (const item of m.clearedOptional) { + notes.push({ + value: `${item.workflowName} — optional cleared in ${item.blocks.join(', ')}`, + }) + } + } + if (m.deployFailed && m.deployFailed > 0) { + notes.push({ value: `${plural(m.deployFailed, 'workflow')} failed to deploy`, warning: true }) + } + return { groups, notes } + } + + if (job.kind === 'fork_rollback') { + const counts = countList([ + [m.restored, 'restored'], + [m.unarchived, 'unarchived'], + [m.removed, 'removed'], + [m.skipped, 'skipped'], + ]) + if (counts) notes.push({ value: counts }) + return { groups, notes } + } + + // fork_content_copy: a named breakdown of everything copied, by kind. + addGroup('Workflows', m.workflowNames) + addGroup('Knowledge bases', m.knowledgeBaseNames) + addGroup('Tables', m.tableNames) + addGroup('Files', m.fileNames) + addGroup('Custom tools', m.customToolNames) + addGroup('Skills', m.skillNames) + addGroup('MCP servers', m.mcpServerNames) + // Pre-names entries fall back to the per-kind counts. + if (groups.length === 0) { + const counts = [ + [m.workflowsCopied, 'workflow'], + [m.knowledgeBases, 'knowledge base'], + [m.tables, 'table'], + [m.files, 'file'], + ] + .filter(([n]) => ((n as number | undefined) ?? 0) > 0) + .map(([n, noun]) => plural(n as number, noun as string)) + .join(' · ') + if (counts) notes.push({ value: counts }) + } + if (m.failed && m.failed > 0) { + notes.push({ value: `${plural(m.failed, 'resource')} failed to copy`, warning: true }) + } + return { groups, notes } +} + +/** Status indicator: the platform loader while active, a colored dot once terminal. */ +function JobStatusIndicator({ status }: { status: BackgroundWorkItem['status'] }) { + if (status === 'pending' || status === 'processing') { + return + } + const color = + status === 'failed' + ? 'bg-[var(--text-error)]' + : status === 'completed_with_warnings' + ? 'bg-[var(--badge-amber-text)]' + : 'bg-[var(--indicator-active)]' + const label = + status === 'failed' + ? 'Failed' + : status === 'completed_with_warnings' + ? 'Completed with warnings' + : 'Done' + return +} + +/** A collapsed report group ("Label N ⌄") that expands to its scrollable name list. */ +function ReportGroupRow({ group }: { group: ReportGroup }) { + const [open, setOpen] = useState(false) + return ( +
+ + {open ? ( +
+ {group.names.map((name) => ( +
+ {name} +
+ ))} +
+ ) : null} +
+ ) +} + +/** One audit-log row: status + title + actor; expands to the timestamp + report. */ +function ForkJobRow({ job }: { job: BackgroundWorkItem }) { + const [expanded, setExpanded] = useState(false) + const report = jobReport(job) + const title = jobTitle(job) + + return ( + +
setExpanded((value) => !value)} + onKeyDown={(event) => { + if (event.target !== event.currentTarget) return + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault() + setExpanded((value) => !value) + } + }} + > +
+ + + {title} + +
+ + {job.metadata?.actorName || '—'} + + +
+ {expanded ? ( +
+ + {formatDateTime(new Date(job.startedAt))} + + {report.groups.map((group) => ( + + ))} + {report.notes.map((note) => ( + + {note.value} + + ))} + {job.error ? ( + {job.error} + ) : null} +
+ ) : null} +
+ ) +} + +/** Audit-log table of fork jobs, mirroring the deployment-versions table chrome. */ +function ForkJobsTable({ jobs }: { jobs: BackgroundWorkItem[] }) { + return ( +
+
+ Activity + By + +
+
+ {jobs.map((job) => ( + + ))} +
+
+ ) +} + +interface ForkActivityPanelProps { + /** The triggering operation is currently running (mutation in flight). */ + pending?: boolean + pendingLabel?: string + /** Poll the durable fork-job audit trail for this workspace. */ + backgroundWorkspaceId?: string +} + +/** + * The "Activity" tab for Manage Forks: a durable audit log of every fork, sync, and + * rollback as its own row (status, title, actor), each expanding to the timestamp and a + * collapsible per-kind breakdown of what changed. A loader shows while the current + * action runs. + */ +export function ForkActivityPanel({ + pending = false, + pendingLabel = 'Working…', + backgroundWorkspaceId, +}: ForkActivityPanelProps) { + const { data: jobs = [] } = useWorkspaceBackgroundWork(backgroundWorkspaceId) + + if (!pending && jobs.length === 0) { + return ( +
+ Nothing here yet. Forks, syncs, and rollbacks will appear here. +
+ ) + } + + return ( +
+ {pending ? ( +
+ + {pendingLabel} +
+ ) : null} + + {jobs.length > 0 ? : null} +
+ ) +} 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..43046bbc1a2 --- /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,443 @@ +'use client' + +import { useEffect, useId, useMemo, useState } from 'react' +import { getErrorMessage } from '@sim/utils/errors' +import { Search } from 'lucide-react' +import { useRouter } from 'next/navigation' +import { + Checkbox, + ChevronDown, + Chip, + ChipConfirmModal, + ChipCopyInput, + ChipInput, + ChipModal, + ChipModalBody, + ChipModalError, + ChipModalFooter, + type ChipModalFooterSlotAction, + ChipModalHeader, + ChipModalTabs, + Tooltip, + toast, +} from '@/components/emcn' +import type { + ForkCopyableResource, + GetForkResourcesResponse, +} from '@/lib/api/contracts/workspace-fork' +import { cn } from '@/lib/core/utils/cn' +import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' +import { ForkActivityPanel } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/fork-activity-panel/fork-activity-panel' +import { + type ForkDirection, + useForkResources, + useForkWorkspace, + useRollbackFork, +} from '@/hooks/queries/workspace-fork' + +interface ForkWorkspaceModalProps { + open: boolean + onOpenChange: (open: boolean) => void + sourceWorkspaceId: string + sourceWorkspaceName: string + /** The last sync into this workspace that can be undone (drives the rollback action). */ + undoableRun: { otherWorkspaceId: string; otherName: string; direction: ForkDirection } | null + /** Whether the user is under their workspace cap; creating a fork is gated on this. */ + canFork: boolean + /** Sends the user to upgrade (billing) when they try to fork at the cap. */ + onUpgrade: () => void +} + +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 fieldId = useId() + + 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) + const itemId = `${fieldId}-${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, + undoableRun, + canFork, + onUpgrade, +}: ForkWorkspaceModalProps) { + const router = useRouter() + const forkWorkspace = useForkWorkspace() + const rollback = useRollbackFork() + const resources = useForkResources(sourceWorkspaceId, open) + const [name, setName] = useState('') + const [selected, setSelected] = useState(emptySelection) + const [error, setError] = useState(null) + + const [activeTab, setActiveTab] = useState<'config' | 'activity'>('config') + const [forkedWorkspace, setForkedWorkspace] = useState<{ id: string; name: string } | null>(null) + const [confirmRollbackOpen, setConfirmRollbackOpen] = useState(false) + + useEffect(() => { + if (open) { + setName(`${sourceWorkspaceName} (fork)`) + setSelected(emptySelection()) + setError(null) + setActiveTab('config') + setForkedWorkspace(null) + setConfirmRollbackOpen(false) + } + }, [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 = () => { + // At a workspace cap, creating a fork is the only gated action - send the user to + // upgrade rather than blocking the whole modal (rollback / Activity stay reachable). + if (!canFork) { + onUpgrade() + return + } + 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 + forkWorkspace.mutate( + { workspaceId: sourceWorkspaceId, body: { name: trimmed, copy } }, + { + onSuccess: (result) => { + toast.success(`Forked into "${result.workspace.name}"`) + setForkedWorkspace({ id: result.workspace.id, name: result.workspace.name }) + setActiveTab('activity') + }, + onError: (err) => setError(err.message || 'Failed to fork workspace'), + } + ) + } + + const openFork = () => { + if (!forkedWorkspace) return + onOpenChange(false) + router.push(`/workspace/${forkedWorkspace.id}/w`) + } + + // Rollback undoes the last sync INTO this workspace, restoring each affected workflow + // to its prior deployed version. Lives in the Activity tab's footer. + const runRollback = async () => { + if (!undoableRun) return + try { + await rollback.mutateAsync({ + workspaceId: sourceWorkspaceId, + body: { otherWorkspaceId: undoableRun.otherWorkspaceId }, + }) + toast.success(`Undid sync from "${undoableRun.otherName}"`) + setConfirmRollbackOpen(false) + setActiveTab('activity') + } catch (err) { + toast.error(getErrorMessage(err, 'Undo failed')) + } + } + + const rollbackDisabled = rollback.isPending || !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 rollbackChip = ( + + + + setConfirmRollbackOpen(true)} + disabled={rollbackDisabled} + className={rollbackDisabled ? 'pointer-events-none' : undefined} + > + Rollback + + + + {rollbackTooltip} + + ) + const rollbackAction: ChipModalFooterSlotAction[] = + activeTab === 'activity' && undoableRun ? [{ custom: rollbackChip }] : [] + + return ( + <> + + onOpenChange(false)}>Manage Forks + + setActiveTab(value as 'config' | 'activity')} + className='mx-2' + /> + {activeTab === 'activity' ? ( + + ) : ( + <> +
+ + + + + + * + + } + > + setName(event.target.value)} + onKeyDown={(event) => { + if (event.key === 'Enter' && !event.nativeEvent.isComposing) { + event.preventDefault() + handleSubmit() + } + }} + placeholder='Workspace name' + maxLength={100} + autoComplete='off' + disabled={isForking} + aria-label='Workspace name' + /> + + + {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} + secondaryActions={rollbackAction.length > 0 ? rollbackAction : undefined} + primaryAction={ + activeTab === 'activity' + ? forkedWorkspace + ? { label: 'Open fork', onClick: openFork } + : { label: 'Done', onClick: () => onOpenChange(false) } + : { + label: isForking ? 'Forking...' : 'Fork', + onClick: handleSubmit, + // At the cap the button stays clickable (no name needed) so it can route to upgrade. + disabled: isForking || (canFork && !name.trim()), + } + } + /> +
+ + void runRollback(), + pending: rollback.isPending, + pendingLabel: 'Rolling back...', + }} + /> + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/promote-workspace-modal/components/dependent-field-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/promote-workspace-modal/components/dependent-field-selector.tsx new file mode 100644 index 00000000000..ddffcf48d1f --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/promote-workspace-modal/components/dependent-field-selector.tsx @@ -0,0 +1,71 @@ +'use client' + +import { useMemo } from 'react' +import { ChipCombobox, type ComboboxOption, Loader } from '@/components/emcn' +import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types' +import { useSelectorOptions } from '@/hooks/selectors/use-selector-query' + +interface DependentFieldSelectorProps { + selectorKey: SelectorKey + /** Full selector context, including the newly-chosen parent value. */ + context: Record + /** False until the parent (credential/KB) target is chosen. */ + enabled: boolean + value: string + onChange: (value: string) => void + title: string +} + +/** + * A controlled, standalone selector for the sync modal's pre-sync reconfigure: fetches + * options via the shared selector data layer (the same `useSelectorOptions` registry the + * canvas selectors use) without the canvas store/blockId coupling. Mirrors + * {@link ConnectorSelectorField}. + */ +export function DependentFieldSelector({ + selectorKey, + context, + enabled, + value, + onChange, + title, +}: DependentFieldSelectorProps) { + const selectorContext = useMemo(() => { + const ctx: SelectorContext = {} + Object.assign(ctx, context) + return ctx + }, [context]) + + const { data: options = [], isLoading } = useSelectorOptions(selectorKey, { + context: selectorContext, + enabled, + }) + + const comboboxOptions = useMemo( + () => options.map((option) => ({ label: option.label, value: option.id })), + [options] + ) + + if (isLoading && enabled) { + return ( +
+ + Loading… +
+ ) + } + + return ( + onChange(next)} + searchable + searchPlaceholder={`Search ${title.toLowerCase()}...`} + placeholder={`Select ${title.toLowerCase()}`} + disabled={!enabled} + emptyMessage={`No ${title.toLowerCase()} found`} + /> + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/promote-workspace-modal/components/resource-reconfigure.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/promote-workspace-modal/components/resource-reconfigure.tsx new file mode 100644 index 00000000000..390656e7808 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/promote-workspace-modal/components/resource-reconfigure.tsx @@ -0,0 +1,268 @@ +'use client' + +import { type Dispatch, type SetStateAction, useMemo, useState } from 'react' +import { ChevronDown } from '@/components/emcn' +import type { ForkDependentReconfig, ForkResourceUsage } from '@/lib/api/contracts/workspace-fork' +import { cn } from '@/lib/core/utils/cn' +import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' +import { DependentFieldSelector } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/promote-workspace-modal/components/dependent-field-selector' +import { + dependentKey, + effectiveDependentValue, +} from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/promote-workspace-modal/dependent-value' +import type { SelectorKey } from '@/hooks/selectors/types' + +/** Stable empty array so a workflow with no dependents reuses one reference (no per-map alloc). */ +const EMPTY_DEPENDENTS: ForkDependentReconfig[] = [] + +interface ReconfigBlock { + targetBlockId: string + blockName: string + fields: ForkDependentReconfig[] +} + +/** Group a workflow's dependent fields by their block, sorted by block name. */ +function groupByBlock(fields: ForkDependentReconfig[]): ReconfigBlock[] { + const byBlock = new Map() + for (const field of fields) { + let block = byBlock.get(field.targetBlockId) + if (!block) { + block = { targetBlockId: field.targetBlockId, blockName: field.blockName, fields: [] } + byBlock.set(field.targetBlockId, block) + } + block.fields.push(field) + } + return Array.from(byBlock.values()).sort((a, b) => a.blockName.localeCompare(b.blockName)) +} + +interface ResourceReconfigureProps { + /** Every workflow this resource is used in (from the diff's `resourceUsages`). */ + workflows: ForkResourceUsage['workflows'] + /** This resource's dependent fields across all its workflows (from `dependentReconfigs`). */ + dependents: ForkDependentReconfig[] + /** The chosen target id (credential/KB/table) the selectors query against. */ + parentTargetValue: string + /** True when the target was changed in-session: start blank (the old value won't resolve). */ + parentChanged: boolean + /** + * The target workspace the dependent selectors query against (direction-aware: the parent on + * push, the child on pull). Workspace-scoped selectors like `table.columns` and sim workflow + * pickers gate on it - the canvas supplies it from the active workspace, so the modal must too. + */ + workspaceId: string + reconfig: Record + setReconfig: Dispatch>> +} + +/** + * Always-on per-resource reconfigure listing: every workflow the resource is used in, each a + * chevron row that expands to its blocks + dependent selectors so the user can (re)configure + * them at any time - not only right after a target swap. A workflow with nothing configurable + * (a secret/file, or a credential with no dependent selector here) renders greyed and + * non-expandable with a tooltip, so the usage is still visible. + */ +export function ResourceReconfigure({ + workflows, + dependents, + parentTargetValue, + parentChanged, + workspaceId, + reconfig, + setReconfig, +}: ResourceReconfigureProps) { + // Group each workflow's dependents into blocks once per (workflows, dependents) change, so + // the grouping doesn't re-run on every parent re-render (setTargets / setReconfig fire often + // during the editing step). Bucket dependents by target workflow in a single pass first, so + // the per-workflow lookup is O(1) instead of a fresh `.filter` per workflow (O(W x D)). + const workflowBlocks = useMemo(() => { + const dependentsByWorkflow = new Map() + for (const dependent of dependents) { + const list = dependentsByWorkflow.get(dependent.targetWorkflowId) + if (list) list.push(dependent) + else dependentsByWorkflow.set(dependent.targetWorkflowId, [dependent]) + } + return workflows.map((workflow) => ({ + workflowId: workflow.workflowId, + workflowName: workflow.workflowName, + blocks: groupByBlock(dependentsByWorkflow.get(workflow.workflowId) ?? EMPTY_DEPENDENTS), + })) + }, [workflows, dependents]) + + if (workflows.length === 0) return null + return ( +
+ +
+ {workflowBlocks.map((workflow) => ( + + ))} +
+
+
+ ) +} + +interface ReconfigWorkflowRowProps { + workflowName: string + blocks: ReconfigBlock[] + parentTargetValue: string + parentChanged: boolean + workspaceId: string + reconfig: Record + setReconfig: Dispatch>> +} + +/** One workflow row: a chevron header (greyed + non-expandable when nothing to configure). */ +function ReconfigWorkflowRow({ + workflowName, + blocks, + parentTargetValue, + parentChanged, + workspaceId, + reconfig, + setReconfig, +}: ReconfigWorkflowRowProps) { + // Auto-open a row that has a required dependent so it's visible without hunting through + // chevrons (a required field is what gates Sync). Deterministic from the block config at mount + // (lazy initializer, no effect/flicker); the user can still collapse it, and it won't reopen on + // re-render - only if the row remounts (its workflow changes). + const [open, setOpen] = useState(() => + blocks.some((block) => block.fields.some((field) => field.required)) + ) + const configurable = blocks.length > 0 + + return ( +
+ {/* Chevron styling mirrors the Activity panel's collapsible rows exactly. A greyed, + non-expandable row uses a native title tooltip to explain why. */} + + {configurable && open + ? blocks.map((block) => ( + + )) + : null} +
+ ) +} + +interface BlockReconfigProps { + block: ReconfigBlock + parentTargetValue: string + parentChanged: boolean + workspaceId: string + reconfig: Record + setReconfig: Dispatch>> +} + +/** One block card: its dependent selectors, chained so a parent feeds its in-block children. */ +function BlockReconfig({ + block, + parentTargetValue, + parentChanged, + workspaceId, + reconfig, + setReconfig, +}: BlockReconfigProps) { + // A field's effective value: the user's re-pick, else the stored value (stable parent) - but + // blank after a parent change, since the old value no longer resolves. Shared with the modal. + const effectiveValue = (field: ForkDependentReconfig) => + effectiveDependentValue(field, reconfig, parentChanged) + + // Chain re-picks: a field that provides a SelectorContext key feeds its effective value to + // its in-block descendants (a spreadsheet drives the sheet selector). Track only WHICH keys + // an in-block field provides (a Set) - the readiness check below tests membership, never a value. + const providedValues: Record = {} + const providedContextKeys = new Set() + for (const field of block.fields) { + if (field.providesContextKey) { + providedContextKeys.add(field.providesContextKey) + const value = effectiveValue(field) + if (value) providedValues[field.providesContextKey] = value + } + } + + return ( +
+ {block.blockName} + {block.fields.map((field) => { + // Disabled until the parent target is set AND every in-block parent it depends on has + // a value, so a child never queries a stale upstream value. + const ready = field.consumesContextKeys.every( + (key) => !providedContextKeys.has(key) || providedValues[key] !== undefined + ) + return ( +
+ + {field.title} + {field.required ? * : null} + + + setReconfig((prev) => { + const nextState = { ...prev, [dependentKey(field)]: value } + // A changed parent invalidates its children's stale re-picks. + const providedKey = field.providesContextKey + if (providedKey) { + for (const sibling of block.fields) { + if (sibling.consumesContextKeys.includes(providedKey)) { + delete nextState[dependentKey(sibling)] + } + } + } + return nextState + }) + } + title={field.title} + /> +
+ ) + })} +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/promote-workspace-modal/dependent-value.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/promote-workspace-modal/dependent-value.ts new file mode 100644 index 00000000000..653e9d1fdcd --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/promote-workspace-modal/dependent-value.ts @@ -0,0 +1,22 @@ +import type { ForkDependentReconfig } from '@/lib/api/contracts/workspace-fork' + +/** Stable key for a per-target dependent re-pick (target workflow + block + subblock). */ +export function dependentKey(dependent: ForkDependentReconfig): string { + return `${dependent.targetWorkflowId}:${dependent.targetBlockId}:${dependent.subBlockKey}` +} + +/** + * The value sent + displayed for a dependent: the user's in-session re-pick if present, else the + * stored value (`currentValue`). Blank when the parent target changed in-session, since the old + * stored value was for the previous parent and won't resolve against the new one. Shared by the + * modal (gate + payload) and the per-block selector so the rule can't drift between them. + */ +export function effectiveDependentValue( + field: ForkDependentReconfig, + reconfig: Record, + parentChanged: boolean +): string { + const repicked = reconfig[dependentKey(field)] + if (repicked !== undefined) return repicked + return parentChanged ? '' : field.currentValue +} 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..9c123ca2c9e --- /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,652 @@ +'use client' + +import { useEffect, useMemo, useState } from 'react' +import { getErrorMessage } from '@sim/utils/errors' +import { ArrowRight } from 'lucide-react' +import { + Badge, + ChipCombobox, + ChipConfirmModal, + ChipDropdown, + ChipModal, + ChipModalBody, + ChipModalFooter, + type ChipModalFooterSlotAction, + ChipModalHeader, + toast, +} from '@/components/emcn' +import type { + ForkDependentReconfig, + ForkLineageNodeApi, + ForkMappingEntry, + ForkResourceUsage, + ForkWorkflowChange, +} from '@/lib/api/contracts/workspace-fork' +import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' +import { ResourceReconfigure } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/promote-workspace-modal/components/resource-reconfigure' +import { + dependentKey, + effectiveDependentValue, +} from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/promote-workspace-modal/dependent-value' +import { + type ForkDirection, + useForkDiff, + useForkMapping, + usePromoteFork, + useUpdateForkMapping, +} from '@/hooks/queries/workspace-fork' + +interface PromoteWorkspaceModalProps { + open: boolean + onOpenChange: (open: boolean) => void + workspaceId: string + parent: ForkLineageNodeApi | null +} + +const entryKey = (entry: ForkMappingEntry) => `${entry.kind}:${entry.sourceId}` + +/** + * Whether a mapping entry needs an in-place reconfigure: its effective target was changed + * in-session, or it's an unconfirmed suggestion (accepting it as-is still remaps + clears + * the dependents). Pure over (entry, in-session targets) so both the inline render and the + * Sync gate / override collection share one predicate instead of drifting copies. + */ +function shouldReconfigureEntry(entry: ForkMappingEntry, targets: Record): boolean { + const next = targets[entryKey(entry)] ?? entry.targetId ?? '' + if (next === '') return false + return entry.suggested || next !== (entry.targetId ?? '') +} + +/** Shared empty owners map for the pull direction so the options mapper never re-allocates. */ +const EMPTY_TARGET_OWNERS: ReadonlyMap = new Map() + +/** + * Stable empty arrays so an entry with no usages/dependents keeps a constant prop reference, + * letting ResourceReconfigure's grouping memo skip recompute across the editing step's frequent + * re-renders. + */ +const EMPTY_USAGES: ForkResourceUsage['workflows'] = [] +const EMPTY_DEPENDENTS: ForkDependentReconfig[] = [] + +/** + * Targets already taken by OTHER sources in the same kind, each mapped to the owning + * source's label (for a hint). Used to disable those targets on PUSH: a push row is unique + * on the parent (target) side, so a parent target can back only one source - a second source + * picking it would be silently dropped on save. Pull is the inverse (many parent sources may + * share one fork target, which resolves correctly), so pull passes the empty map and never + * disables. Excludes `exclude` so a source never disables its own current selection. + */ +function takenTargetOwners( + items: ForkMappingEntry[], + targets: Record, + exclude: ForkMappingEntry +): Map { + const owners = new Map() + for (const item of items) { + if (entryKey(item) === entryKey(exclude)) continue + const target = targets[entryKey(item)] ?? item.targetId ?? '' + if (target !== '') owners.set(target, item.sourceLabel) + } + return owners +} + +/** Section label + display order per mapping kind (one mapping step per kind). */ +const MAPPING_SECTION: Record = { + credential: { label: 'Credentials', order: 0 }, + 'env-var': { label: 'Secrets', 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 +} + +/** + * Fork sync surface. Along the parent edge it force pushes/pulls: the overview + * picks a direction and lists each resource kind's mapping status, then Sync. + * "Edit mappings" steps through every kind (Back/Next, each source a + * settings-style section + full-width target) to set or review targets before + * landing back on Sync - which always confirms the overwrite first. The durable record of + * every sync is the Activity log in Manage Forks, so this modal just closes on + * success. + */ +export function PromoteWorkspaceModal({ + open, + onOpenChange, + workspaceId, + parent, +}: 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('') + // User's IN-SESSION mapping overrides only - NOT the source of truth. The + // displayed/persisted target falls back to each entry's stored `targetId` + // (see `targetFor`), so a reopened edge shows its remembered mappings even + // though React Query's structural sharing keeps `entries` referentially stable + // (a target-seeding effect gated on `entries` would never re-run there). + const [targets, setTargets] = useState>({}) + // In-session re-picks for dependent fields whose parent the user swapped, keyed by + // `dependentKey`. Folded into the full effective set sent on sync, which promote persists as + // the stored mapping - so the selection survives every future sync without re-picking. + const [reconfig, setReconfig] = useState>({}) + // Wizard step: 0 is the overview; 1..N edit one resource kind each, entered via + // "Edit mappings". Backing out of step 1 returns to the overview. + const [step, setStep] = useState(0) + const [confirmSyncOpen, setConfirmSyncOpen] = useState(false) + const [submitting, setSubmitting] = useState(false) + + useEffect(() => { + if (open) { + setSelectedKey(edgeOptions[0]?.value ?? '') + } + }, [open, edgeOptions]) + + // Restart at the overview and drop in-session overrides whenever it (re)opens or + // the direction changes - the mapping set, and therefore the steps, depend on the + // direction. + useEffect(() => { + setStep(0) + setTargets({}) + setReconfig({}) + }, [open, selectedKey]) + + const selected = edgeOptions.find((option) => option.value === selectedKey) + const otherWorkspaceId = selected?.otherWorkspaceId + const direction = selected?.direction ?? 'push' + + 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]) + const dependentReconfigs = useMemo( + () => diff.data?.dependentReconfigs ?? [], + [diff.data?.dependentReconfigs] + ) + const resourceUsages = useMemo(() => diff.data?.resourceUsages ?? [], [diff.data?.resourceUsages]) + + // Group dependents by their parent (kind:sourceId) once, so each mapping entry below gets a + // STABLE `dependents` array reference - a fresh `.filter` per render would defeat + // ResourceReconfigure's grouping memo. + const dependentsByParent = useMemo(() => { + const map = new Map() + for (const dependent of dependentReconfigs) { + const key = `${dependent.parentKind}:${dependent.parentSourceId}` + const list = map.get(key) + if (list) list.push(dependent) + else map.set(key, [dependent]) + } + return map + }, [dependentReconfigs]) + + // Effective target for an entry: the user's in-session override if present, + // else the persisted mapping from the server. Read directly from `entries` so + // a reopened edge reflects stored mappings without a seeding effect. + const targetFor = (entry: ForkMappingEntry) => targets[entryKey(entry)] ?? entry.targetId ?? '' + + const requiredComplete = entries.every((entry) => !entry.required || targetFor(entry) !== '') + + // Every workflow a mapping entry's resource is used in, for the always-on reconfigure + // listing rendered beneath that mapping (so the credential/KB stays in context). + const usagesForEntry = (entry: ForkMappingEntry): ForkResourceUsage['workflows'] => + resourceUsages.find( + (usage) => usage.parentKind === entry.kind && usage.parentSourceId === entry.sourceId + )?.workflows ?? EMPTY_USAGES + + // This entry's dependent fields (its credential/KB's selectors), from the memoized grouping. + const dependentsForEntry = (entry: ForkMappingEntry): ForkDependentReconfig[] => + dependentsByParent.get(entryKey(entry)) ?? EMPTY_DEPENDENTS + + // Group mappings by resource type - one step per kind, required types first. + 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]) + + // The mapping entry each dependent hangs off, indexed by `kind:sourceId` (matching `entryKey`) + // so the per-field lookups below are O(1) instead of rescanning `entries` for every dependent - + // and several times per field across the Sync gate, the value helper, and the payload build. + const entriesByParent = useMemo(() => { + const map = new Map() + for (const entry of entries) map.set(entryKey(entry), entry) + return map + }, [entries]) + + // The mapping entry a dependent field hangs off (its credential/KB), for change + target lookups. + const entryForDependent = (field: ForkDependentReconfig) => + entriesByParent.get(`${field.parentKind}:${field.parentSourceId}`) + + // The value sent + displayed for a dependent (delegates to the shared rule): the user's + // re-pick, else the stored value - blank when this field's parent target changed in-session. + // Callers that already resolved the parent pass it in to skip a second lookup. + const dependentValueFor = ( + field: ForkDependentReconfig, + parent = entryForDependent(field) + ): string => + effectiveDependentValue( + field, + reconfig, + parent ? shouldReconfigureEntry(parent, targets) : false + ) + + // Every required dependent whose parent IS mapped must have a value before sync. A dependent + // whose parent target is still empty can't be picked yet (its selector is disabled) and is + // gated by `requiredComplete` on the parent instead, so it's skipped here. + const reconfigComplete = dependentReconfigs.every((field) => { + if (!field.required) return true + const parent = entryForDependent(field) + if (!parent || targetFor(parent) === '') return true + return dependentValueFor(field, parent) !== '' + }) + + // Per-kind status for the overview listing: "Fully mapped" or "n/total mapped", + // flagged when a REQUIRED target is still missing (which blocks Sync). Reads the + // effective (override-or-persisted) target so it reflects both remembered mappings + // and in-session edits. + const kindSummaries = groupedEntries.map((group) => { + const total = group.items.length + const mapped = group.items.filter((entry) => targetFor(entry) !== '').length + const requiredPending = group.items.some((entry) => entry.required && targetFor(entry) === '') + return { kind: group.kind, label: group.label, total, mapped, requiredPending } + }) + + // Step 0 is the overview; each subsequent step edits one resource kind, entered via + // "Edit mappings". Reconfigure cards render inline under the changed mapping (not as + // their own steps) so the credential/KB context stays visible. `safeStep` guards + // against a group count that shrank on refetch. + const stepCount = 1 + groupedEntries.length + const safeStep = Math.min(step, Math.max(0, stepCount - 1)) + const isLastStep = safeStep >= stepCount - 1 + const currentGroup = safeStep >= 1 ? (groupedEntries[safeStep - 1] ?? null) : null + // Gate Sync on the diff being loaded too, not just the mapping: until `diff.data` arrives + // `dependentReconfigs` is empty, so `reconfigComplete` is vacuously true and `runPromote` would + // omit `dependentValues` - i.e. Sync before the diff loads would bypass dependent gating. + const syncDisabled = + submitting || + !otherWorkspaceId || + !requiredComplete || + !reconfigComplete || + mapping.isLoading || + !diff.data + const headsUp = + (diff.data?.mcpReauthServerIds.length ?? 0) > 0 || + (diff.data?.inlineSecretSources.length ?? 0) > 0 + + const runPromote = async () => { + if (!otherWorkspaceId) return + setSubmitting(true) + try { + await updateMapping.mutateAsync({ + workspaceId, + body: { + otherWorkspaceId, + direction, + entries: entries.map((entry) => ({ + resourceType: entry.resourceType, + sourceId: entry.sourceId, + targetId: targetFor(entry) || null, + })), + }, + }) + + // Send the full stored mapping for every dependent whose parent is mapped (its effective + // value - re-pick, stored, or blank-after-change). Promote persists this verbatim as the + // stored mapping and applies it; fields whose parent isn't mapped yet are omitted (they + // can't be configured). This is the whole "what's in the mapping goes in" contract. + const dependentValues = dependentReconfigs.flatMap((field) => { + const parent = entryForDependent(field) + if (!parent || targetFor(parent) === '') return [] + return [ + { + workflowId: field.targetWorkflowId, + blockId: field.targetBlockId, + subBlockKey: field.subBlockKey, + value: dependentValueFor(field, parent), + }, + ] + }) + + const result = await promote.mutateAsync({ + workspaceId, + body: { + otherWorkspaceId, + direction, + // Once the diff has loaded, ALWAYS send the full effective set - including `[]`, which + // means "every dependent went away" and must reconcile/clear the live replace targets' + // stored rows. Collapsing `[]` into omission would make the backend PRESERVE stale rows. + // Only omit before the diff loads (set unknown), so the existing store is left untouched. + ...(diff.data ? { dependentValues } : {}), + }, + }) + + if (!result.promoteRunId) { + if (result.unmappedRequired.length > 0) { + toast.error('Map all required credentials and secrets first') + return + } + toast.error('Sync did not complete') + return + } + + const target = parent?.name ?? 'the workspace' + const label = direction === 'pull' ? `Pulled from "${target}"` : `Pushed to "${target}"` + const needsConfig = result.needsConfiguration + const clearedOptional = result.clearedOptional + // List the affected blocks, naming the workflow for a single one and falling back to + // a count across many. Block names ("Gmail 2") are far more actionable than the + // generic field titles ("Label") behind them. + const formatWhere = (list: Array<{ workflowName: string; blocks: string[] }>) => { + const totalBlocks = list.reduce((sum, workflow) => sum + workflow.blocks.length, 0) + if (list.length === 1) return `${list[0].blocks.join(', ')} in ${list[0].workflowName}` + return `${totalBlocks} block${totalBlocks === 1 ? '' : 's'} across ${list.length} workflows` + } + const optionalBlocks = clearedOptional.reduce( + (sum, workflow) => sum + workflow.blocks.length, + 0 + ) + // Appended to a higher-priority warning so a cleared optional filter is never hidden. + const optionalSuffix = + optionalBlocks > 0 + ? ` (+${optionalBlocks} block${optionalBlocks === 1 ? '' : 's'} with optional fields cleared)` + : '' + if (needsConfig.length > 0) { + toast.warning(`${label}. Re-check ${formatWhere(needsConfig)}.${optionalSuffix}`) + } else if (result.deployFailed > 0) { + const n = result.deployFailed + toast.warning( + `${label}, but ${n} workflow${n === 1 ? '' : 's'} failed to deploy — open and redeploy ${n === 1 ? 'it' : 'them'}.${optionalSuffix}` + ) + } else if (clearedOptional.length > 0) { + toast.warning( + `${label}. Optional settings cleared — re-check ${formatWhere(clearedOptional)}.` + ) + } else { + toast.success(label) + } + onOpenChange(false) + } catch (error) { + toast.error(getErrorMessage(error, 'Sync 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]) + + // Right-cluster action sitting immediately left of the primary. The overview pairs + // "Edit mappings" with Sync (entering the step walk); every editing step pairs Back + // with Next (or with Sync on the last step). Back out of step 1 lands on the + // overview, restoring the "Edit mappings · Sync" pair. + const syncPrimaryAdjacent: ChipModalFooterSlotAction | undefined = + safeStep === 0 + ? groupedEntries.length > 0 + ? { label: 'Edit mappings', onClick: () => setStep(1), disabled: submitting } + : undefined + : { label: 'Back', onClick: () => setStep(safeStep - 1), disabled: submitting } + + return ( + <> + + onOpenChange(false)}> + {currentGroup ? `Sync workspace: ${currentGroup.label}` : 'Sync workspace'} + + + {safeStep === 0 ? ( +
+ + + + + {/* Always shown once the diff loads so the user sees the section even with nothing + deployed - an empty change list means the source has no deployed workflows (every + deployed workflow appears here, changed or not), so the muted state nudges a deploy. */} + {diff.data ? ( + + {workflowChanges.length > 0 ? ( +
+ {workflowChanges.map((change, index) => { + const renamed = change.currentName !== change.otherName + return ( +
+ + {change.currentName} + + {renamed ? ( + <> + + + {change.otherName} + + + ) : null} +
+ ) + })} +
+ ) : ( +
+ {direction === 'push' + ? `No deployed workflows. Deploy workflows to push changes to ${parent?.name ?? 'the parent'}.` + : `No deployed workflows in ${parent?.name ?? 'the parent'} to pull.`} +
+ )} +
+ ) : null} + + {headsUp ? ( + + {(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} + + {kindSummaries.length > 0 ? ( + +
+ {kindSummaries.map(({ kind, label, total, mapped, requiredPending }) => { + const complete = mapped === total + return ( +
+ {label} + + {complete ? 'Fully mapped' : `${mapped}/${total} mapped`} + +
+ ) + })} +
+
+ ) : null} +
+ ) : currentGroup ? ( +
+ {currentGroup.items.map((entry) => { + // On push, a parent target can back only one source; disable any target + // another source already took (named in the hint) so the user can't create a + // mapping that would be silently dropped on save. Pull allows sharing a target. + const takenOwners = + direction === 'push' + ? takenTargetOwners(currentGroup.items, targets, entry) + : EMPTY_TARGET_OWNERS + return ( + + * + + ) : undefined + } + > + { + const owner = takenOwners.get(candidate.id) + return { + label: owner + ? `${candidate.label} · mapped to ${owner}` + : candidate.label, + value: candidate.id, + disabled: owner !== undefined, + } + })} + value={targetFor(entry) || undefined} + onChange={(value) => { + setTargets((prev) => ({ ...prev, [entryKey(entry)]: value })) + // Changing the parent invalidates any in-session re-picks of its + // dependents - they were chosen against the old account and won't resolve + // against the new one, so drop them; otherwise a stale re-pick (which + // wins over the parent-changed check) would be sent to the new account. + setReconfig((prev) => { + let changed = false + const next = { ...prev } + for (const dependent of dependentsForEntry(entry)) { + const key = dependentKey(dependent) + if (key in next) { + delete next[key] + changed = true + } + } + return changed ? next : prev + }) + }} + placeholder='Select target' + /> + {entry.candidatesTruncated ? ( +
+ This workspace has more options than shown here. If you don't see the right + one, narrow it down by name. +
+ ) : null} + {/* Always-on: every workflow this resource is used in, each expandable to + its blocks + dependent selectors (greyed when nothing to configure). */} + +
+ ) + })} +
+ ) : null} +
+ onOpenChange(false)} + hideCancel + primaryAdjacentAction={syncPrimaryAdjacent} + primaryAction={ + safeStep >= 1 && !isLastStep + ? { label: 'Next', onClick: () => setStep(safeStep + 1), disabled: submitting } + : { + label: submitting ? 'Working...' : 'Sync', + onClick: () => setConfirmSyncOpen(true), + disabled: syncDisabled, + disabledTooltip: !requiredComplete + ? 'Map all required secrets first' + : !reconfigComplete + ? 'Reconfigure all required fields first' + : undefined, + } + } + /> +
+ + { + setConfirmSyncOpen(false) + void runPromote() + }, + pending: submitting, + pendingLabel: 'Syncing...', + }} + /> + + ) +} 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..534bd0ccd39 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 @@ -18,12 +18,17 @@ import { } from '@/components/emcn' import { ManageWorkspace, PanelLeft } 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 +101,8 @@ function WorkspaceHeaderImpl({ }: WorkspaceHeaderProps) { const [isCreateModalOpen, setIsCreateModalOpen] = useState(false) const [isInviteModalOpen, setIsInviteModalOpen] = useState(false) + const [isForkModalOpen, setIsForkModalOpen] = useState(false) + const [isPromoteModalOpen, setIsPromoteModalOpen] = useState(false) const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false) const [deleteTarget, setDeleteTarget] = useState(null) const [isLeaveModalOpen, setIsLeaveModalOpen] = useState(false) @@ -120,6 +127,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 @@ -238,6 +253,16 @@ function WorkspaceHeaderImpl({ onUploadLogo(capturedWorkspaceRef.current.id) } + // Always open Manage Forks - rollback and the durable Activity log live here and must + // stay reachable at a workspace cap. Only creating a NEW fork is gated (in the modal). + const handleForkAction = () => { + setIsForkModalOpen(true) + } + + const handleSyncAction = () => { + setIsPromoteModalOpen(true) + } + /** * Handle leave workspace after confirmation */ @@ -635,6 +660,9 @@ function WorkspaceHeaderImpl({ const contextCanAdmin = capturedPermissions === 'admin' const capturedWorkspace = workspaces.find((w) => w.id === capturedWorkspaceRef.current?.id) const isOwner = capturedWorkspace && sessionUserId === capturedWorkspace.ownerId + // Only the active row can offer fork actions: its lineage/availability is the + // data loaded for `workspaceId`. Sync needs a parent; Manage needs children. + const showForkInContext = capturedWorkspaceRef.current?.id === workspaceId && canUseForking return ( + { + if (isBillingEnabled) navigateToSettings({ section: 'billing' }) + }} + /> + 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/components/emcn/components/chip-modal/chip-modal.tsx b/apps/sim/components/emcn/components/chip-modal/chip-modal.tsx index 6d98dd2d875..433cc78ad70 100644 --- a/apps/sim/components/emcn/components/chip-modal/chip-modal.tsx +++ b/apps/sim/components/emcn/components/chip-modal/chip-modal.tsx @@ -927,10 +927,12 @@ export type ChipModalFooterSlotAction = ChipModalFooterAction | ChipModalFooterC export interface ChipModalFooterProps { /** - * Dismiss handler for the always-present Cancel button. Like the header's - * close (X), Cancel is structural — it always reads "Cancel" and there is no - * prop to relabel or remove it. Its enabled state can be controlled via - * {@link ChipModalFooterProps.cancelDisabled}. + * Dismiss handler for the Cancel button. For standard form footers Cancel is + * structural — it always reads "Cancel" and cannot be relabeled. Its enabled + * state is controlled via {@link ChipModalFooterProps.cancelDisabled}. A + * multi-step/wizard footer whose own Back navigation plus the header close (X) + * already cover dismissal may suppress it via + * {@link ChipModalFooterProps.hideCancel}. */ onCancel: () => void /** @@ -940,13 +942,33 @@ export interface ChipModalFooterProps { * @default false */ cancelDisabled?: boolean + /** + * Suppresses the Cancel button entirely. Reserve for multi-step/wizard footers + * where the in-footer Back navigation plus the header close (X) already provide + * dismissal, so a third dismiss affordance is redundant. Standard one-shot form + * footers keep Cancel — do not hide it merely to declutter. + * @default false + */ + hideCancel?: boolean /** Primary action, anchored bottom-right (e.g. Save, Create, Delete). */ primaryAction: ChipModalFooterAction + /** + * An action rendered immediately to the LEFT of the {@link primaryAction}, + * inside the right-anchored cluster (after the structural Cancel). Use for the + * trailing half of a paired control that reads as ONE unit with the primary — + * canonically a wizard's `Back` sitting beside `Next`, or a "skip ahead" + * shortcut beside the primary — where docking it to the far-left + * {@link secondaryActions} slot would visually divorce it from the primary it + * pairs with. Rendered as a bare {@link Chip} (same chrome as Cancel) so the + * filled primary stays the sole emphasized control; accepts a + * {@link ChipModalFooterCustomAction} for chip-chrome controls. + */ + primaryAdjacentAction?: ChipModalFooterSlotAction /** * Auxiliary actions docked to the far-left, opposite the Cancel/primary * cluster, rendered in order on the cluster's `gap-2` rhythm — e.g. Delete - * in an edit flow, Back in a wizard, or chip-chrome controls (a date + time - * picker pair in a scheduling footer) via + * in an edit flow, a wizard's "skip ahead" shortcut, or chip-chrome controls + * (a date + time picker pair in a scheduling footer) via * {@link ChipModalFooterCustomAction}. Like a `Resource` header's actions, * each entry is a constrained {@link ChipModalFooterSlotAction} — consumers * describe intent, never chrome. @@ -1013,7 +1035,9 @@ function renderFooterSlotAction(action: ChipModalFooterSlotAction): React.ReactN function ChipModalFooter({ onCancel, cancelDisabled, + hideCancel = false, primaryAction, + primaryAdjacentAction, secondaryActions, }: ChipModalFooterProps) { const showsDisabledTooltip = Boolean(primaryAction.disabled && primaryAction.disabledTooltip) @@ -1041,9 +1065,12 @@ function ChipModalFooter({ ) : undefined } > - - Cancel - + {hideCancel ? null : ( + + Cancel + + )} + {primaryAdjacentAction ? renderFooterSlotAction(primaryAdjacentAction) : null} {showsDisabledTooltip ? ( diff --git a/apps/sim/hooks/queries/background-work.ts b/apps/sim/hooks/queries/background-work.ts new file mode 100644 index 00000000000..e3b2b1a1d76 --- /dev/null +++ b/apps/sim/hooks/queries/background-work.ts @@ -0,0 +1,43 @@ +import { useQuery } from '@tanstack/react-query' +import { requestJson } from '@/lib/api/client/request' +import { + type BackgroundWorkItem, + getWorkspaceBackgroundWorkContract, +} from '@/lib/api/contracts/workspace-fork' + +export const backgroundWorkKeys = { + all: ['backgroundWork'] as const, + lists: () => [...backgroundWorkKeys.all, 'list'] as const, + list: (workspaceId?: string) => [...backgroundWorkKeys.lists(), workspaceId ?? ''] as const, +} + +async function fetchWorkspaceBackgroundWork( + workspaceId: string, + signal?: AbortSignal +): Promise { + const data = await requestJson(getWorkspaceBackgroundWorkContract, { + params: { id: workspaceId }, + signal, + }) + return data.items +} + +const isActive = (item: BackgroundWorkItem) => + item.status === 'pending' || item.status === 'processing' + +/** + * Durable "background work in progress" status for a workspace (fork content copy + + * any deployment side-effects). Poll-first per the best-practice for long jobs: the + * status survives a reload (it's a DB row), and we only keep polling while something is + * still running, then stop. Refetch on focus catches changes after the tab was away. + */ +export function useWorkspaceBackgroundWork(workspaceId?: string) { + return useQuery({ + queryKey: backgroundWorkKeys.list(workspaceId), + queryFn: ({ signal }) => fetchWorkspaceBackgroundWork(workspaceId as string, signal), + enabled: Boolean(workspaceId), + staleTime: 5_000, + refetchInterval: (query) => ((query.state.data ?? []).some(isActive) ? 5_000 : false), + refetchOnWindowFocus: true, + }) +} diff --git a/apps/sim/hooks/queries/workspace-fork.ts b/apps/sim/hooks/queries/workspace-fork.ts new file mode 100644 index 00000000000..2fdc7be954c --- /dev/null +++ b/apps/sim/hooks/queries/workspace-fork.ts @@ -0,0 +1,180 @@ +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 { backgroundWorkKeys } from '@/hooks/queries/background-work' +import { deploymentKeys } from '@/hooks/queries/deployments' +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() }) + queryClient.invalidateQueries({ queryKey: backgroundWorkKeys.lists() }) + }, + }) +} + +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: () => { + // A sync changes lineage (undoable run), mappings, and the diff - not the + // workspace's copyable resource inventory, so leave `resources` cached. + queryClient.invalidateQueries({ queryKey: forkKeys.lineages() }) + queryClient.invalidateQueries({ queryKey: forkKeys.mappings() }) + queryClient.invalidateQueries({ queryKey: forkKeys.diffs() }) + queryClient.invalidateQueries({ queryKey: backgroundWorkKeys.lists() }) + // A sync rewrites the target workflows' drafts AND redeploys them. The promote + // result doesn't expose the affected ids, so invalidate all deployment caches: + // otherwise a target workflow whose deployed state was already cached compares its + // fresh draft against the stale (pre-sync) deployed snapshot and falsely shows + // "Update" instead of "Live". + queryClient.invalidateQueries({ queryKey: deploymentKeys.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: () => { + // Rollback changes lineage, mappings, and the diff - not the copyable resource + // inventory, so leave `resources` cached (mirrors usePromoteFork). + queryClient.invalidateQueries({ queryKey: forkKeys.lineages() }) + queryClient.invalidateQueries({ queryKey: forkKeys.mappings() }) + queryClient.invalidateQueries({ queryKey: forkKeys.diffs() }) + queryClient.invalidateQueries({ queryKey: backgroundWorkKeys.lists() }) + // Rollback restores the target workflows' drafts + reactivates a prior deployment, + // so the cached deployed snapshots are stale - refresh them so change detection + // doesn't falsely show "Update" (mirrors usePromoteFork). + queryClient.invalidateQueries({ queryKey: deploymentKeys.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..d7f183001ec --- /dev/null +++ b/apps/sim/lib/api/contracts/workspace-fork.ts @@ -0,0 +1,477 @@ +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(), + /** 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(), + /** True when `targetId` is an unconfirmed auto-suggestion (no persisted mapping yet). */ + suggested: z.boolean(), + required: z.boolean(), + candidates: z.array(forkMappingCandidateSchema), + /** + * True when the target workspace has more candidates of this kind than the picker + * loads, so the list shown is partial. The UI surfaces a "refine to find more" + * notice; the chosen target is validated by exact id on save (never capped). + */ + candidatesTruncated: z.boolean(), +}) +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(), +}) + +/** + * A configured selector field (Gmail label, Slack channel, KB document, ...) that + * `dependsOn` a remappable parent resource - a credential or a knowledge base - so a + * sync clears it whenever that parent's target changes. The modal renders a controlled + * selector against the newly-chosen parent (using `selectorKey` + `context` + the new + * parent value under `parentContextKey`) so the user re-picks the value in place, + * pre-sync, instead of having it cleared and reconfigured after. `targetBlockId` is the + * deterministic fork block id, matching the engine, so the re-pick lands on the right + * block. Only fields the source actually had set are emitted (the active operation's), + * so blocks aren't padded with every operation variant. + */ +export const forkDependentReconfigSchema = z.object({ + /** The remappable parent resource kind whose target swap clears this field. */ + parentKind: z.enum(['credential', 'knowledge-base', 'table']), + /** Source id of that parent (matches a mapping entry's `sourceId`). */ + parentSourceId: z.string(), + /** SelectorContext key the new parent value is supplied under (`oauthCredential` | `knowledgeBaseId` | `tableId`). */ + parentContextKey: z.string(), + targetWorkflowId: z.string(), + targetBlockId: z.string(), + blockName: z.string(), + subBlockKey: z.string(), + selectorKey: z.string(), + title: z.string(), + /** + * The field's stored value (from the persisted mapping), so the always-on reconfigure listing + * pre-fills the selector with what the user last set. Empty string when unset; for an edge + * that predates the store the TARGET's currently-configured value is the fallback (never the + * source's, which would overwrite the target's own selection on the first sync). After a + * parent target CHANGE the modal ignores this and starts blank, since the old value no longer + * resolves against the new parent. + */ + currentValue: z.string(), + /** Whether the field is required - a required empty field blocks Sync. */ + required: z.boolean(), + /** + * SelectorContext key this field's own value supplies to its in-block descendants + * (e.g. a spreadsheet feeds `spreadsheetId` to the sheet selector). Lets the modal + * chain re-picks: a re-picked parent updates its children's selector context. + */ + providesContextKey: z.string().optional(), + /** + * SelectorContext keys this field needs from in-block siblings (excluding the parent + * the modal already supplies). The modal keeps the field disabled until each sibling + * that provides one of these keys has been re-picked, so a child never queries a stale + * upstream value. + */ + consumesContextKeys: z.array(z.string()), + /** Source-derived selector context (sans the parent key the modal supplies). */ + context: z.record(z.string(), z.string()), +}) +export type ForkDependentReconfig = z.output + +/** + * Every workflow a mapped resource (any kind) is used in, for the always-on reconfigure + * listing under each mapping entry. Joined to the entry by `(parentKind, parentSourceId)`; + * the modal cross-references {@link forkDependentReconfigSchema} per workflow to decide which + * rows are expandable (have configurable dependents) vs. greyed (used here, nothing to tune). + */ +export const forkResourceUsageSchema = z.object({ + parentKind: forkRemapKindSchema, + parentSourceId: z.string(), + workflows: z.array( + z.object({ + /** Deterministic fork (target) workflow id - matches a dependent's `targetWorkflowId`. */ + workflowId: z.string(), + workflowName: z.string(), + }) + ), +}) +export type ForkResourceUsage = z.output + +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()), + /** Configured selector fields per parent (credential/KB), for the pre-sync reconfigure. */ + dependentReconfigs: z.array(forkDependentReconfigSchema), + /** Every workflow each mapped resource is used in, for the always-on reconfigure listing. */ + resourceUsages: z.array(forkResourceUsageSchema), + }), + }, +}) +export type GetForkDiffResponse = z.output +export type ForkWorkflowChange = z.output + +/** + * A workflow whose required dependent fields a sync cleared because their parent + * (e.g. a credential) was changed - the target must re-pick them. The workflow's + * draft holds the synced state but it was NOT redeployed (the prior version keeps + * running), so nothing runs broken until the user reconfigures. + */ +export const forkNeedsConfigurationSchema = z.object({ + workflowName: z.string(), + /** Names of the blocks in the workflow that need a re-check (deduplicated). */ + blocks: z.array(z.string()).min(1), +}) +export type ForkNeedsConfiguration = z.output + +/** + * One dependent field's value in the stored mapping. The sync modal sends the full set for + * every dependent whose parent is mapped; promote persists them to + * `workspace_fork_dependent_value` and applies them verbatim to the target blocks, so the + * user's selection survives every future sync without re-picking. `blockId` is the + * deterministic fork block id, so the value lands on the right block. + */ +export const forkDependentValueEntrySchema = z.object({ + workflowId: nonEmptyIdSchema, + blockId: nonEmptyIdSchema, + subBlockKey: z.string().min(1, 'subBlockKey is required'), + value: z.string(), +}) +export type ForkDependentValueEntry = z.input + +export const promoteForkBodySchema = z.object({ + otherWorkspaceId: workspaceIdSchema, + direction: forkDirectionSchema, + /** + * The full stored mapping of dependent-field values; persisted to + * `workspace_fork_dependent_value` and applied to the target blocks verbatim. Omitting the + * field leaves the stored mapping untouched (the store stays the source of truth); sending + * an explicit `[]` clears it for the written replace targets. + */ + dependentValues: z.array(forkDependentValueEntrySchema).max(2000).optional(), +}) +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(), + deployFailed: z.number().int(), + unmappedRequired: z.array(forkUnmappedReferenceSchema), + /** Workflows whose required dependent fields the target must re-pick post-sync. */ + needsConfiguration: z.array(forkNeedsConfigurationSchema), + /** Workflows whose optional dependent fields a swap cleared (surfaced, not gated). */ + clearedOptional: z.array(forkNeedsConfigurationSchema), + }), + }, +}) +export type PromoteForkBody = z.input +export type PromoteForkResponse = z.output + +/** Structured detail for a background job, surfaced in the Activity tab's expand row. */ +export const backgroundWorkMetadataSchema = z + .object({ + /** Display name of the user who performed the action (denormalized at write time). */ + actorName: z.string().optional(), + // Fork content copy + childWorkspaceId: z.string().optional(), + childWorkspaceName: z.string().optional(), + workflowsCopied: z.number().int().optional(), + tables: z.number().int().optional(), + knowledgeBases: z.number().int().optional(), + files: z.number().int().optional(), + copied: z.number().int().optional(), + failed: z.number().int().optional(), + /** Names of the resources a fork copied, by kind, for the report breakdown. */ + workflowNames: z.array(z.string()).optional(), + tableNames: z.array(z.string()).optional(), + knowledgeBaseNames: z.array(z.string()).optional(), + fileNames: z.array(z.string()).optional(), + customToolNames: z.array(z.string()).optional(), + skillNames: z.array(z.string()).optional(), + mcpServerNames: z.array(z.string()).optional(), + // Sync / rollback + otherWorkspaceName: z.string().optional(), + direction: z.enum(['push', 'pull']).optional(), + updated: z.number().int().optional(), + created: z.number().int().optional(), + archived: z.number().int().optional(), + redeployed: z.number().int().optional(), + deployFailed: z.number().int().optional(), + restored: z.number().int().optional(), + unarchived: z.number().int().optional(), + removed: z.number().int().optional(), + skipped: z.number().int().optional(), + /** Names of the workflows a sync changed, by action, for the report breakdown. */ + updatedNames: z.array(z.string()).optional(), + createdNames: z.array(z.string()).optional(), + archivedNames: z.array(z.string()).optional(), + /** Workflows whose required dependent fields a sync left for the target to re-pick. */ + needsConfiguration: z.array(forkNeedsConfigurationSchema).optional(), + /** Workflows whose optional dependent fields a sync cleared (FYI, non-blocking). */ + clearedOptional: z.array(forkNeedsConfigurationSchema).optional(), + }) + .nullable() +export const backgroundWorkItemSchema = z.object({ + id: z.string(), + workspaceId: z.string(), + workflowId: z.string().nullable(), + kind: z.enum(['deployment_side_effects', 'fork_content_copy', 'fork_sync', 'fork_rollback']), + status: z.enum(['pending', 'processing', 'completed', 'completed_with_warnings', 'failed']), + message: z.string().nullable(), + error: z.string().nullable(), + metadata: backgroundWorkMetadataSchema, + startedAt: z.string(), + completedAt: z.string().nullable(), +}) +export type BackgroundWorkMetadata = z.output +export const getWorkspaceBackgroundWorkContract = defineRouteContract({ + method: 'GET', + path: '/api/workspaces/[id]/background-work', + params: workspaceIdParamsSchema, + response: { + mode: 'json', + schema: z.object({ items: z.array(backgroundWorkItemSchema) }), + }, +}) +export type BackgroundWorkItem = z.output +export type GetWorkspaceBackgroundWorkResponse = z.output< + typeof getWorkspaceBackgroundWorkContract.response.schema +> + +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 38bbab06471..55156c46bc1 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -406,6 +406,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) @@ -505,6 +506,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) @@ -544,6 +546,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..e15c07a17ce --- /dev/null +++ b/apps/sim/lib/workflows/persistence/remap-internal-ids.test.ts @@ -0,0 +1,150 @@ +/** + * @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') + }) + + it('remaps a comma-separated manualWorkflowIds list', () => { + const subBlocks: SubBlockRecord = { + manualWorkflowIds: { id: 'manualWorkflowIds', type: 'short-input', value: 'wf-src, sub-src' }, + } + const result = remapWorkflowReferencesInSubBlocks(subBlocks, map) + expect(result.manualWorkflowIds.value).toBe('wf-dst,sub-dst') + }) + + it('drops unmapped ids from a manualWorkflowIds list when clearUnmapped is set', () => { + const subBlocks: SubBlockRecord = { + manualWorkflowIds: { + id: 'manualWorkflowIds', + type: 'short-input', + value: 'wf-src,wf-unknown', + }, + } + const result = remapWorkflowReferencesInSubBlocks(subBlocks, map, { clearUnmapped: true }) + expect(result.manualWorkflowIds.value).toBe('wf-dst') + }) + + it('remaps a multi-select workflowSelector array', () => { + const subBlocks: SubBlockRecord = { + workflowSelector: { id: 'workflowSelector', type: 'dropdown', value: ['wf-src', 'sub-src'] }, + } + const result = remapWorkflowReferencesInSubBlocks(subBlocks, map) + expect(result.workflowSelector.value).toEqual(['wf-dst', 'sub-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..e71b9497367 --- /dev/null +++ b/apps/sim/lib/workflows/persistence/remap-internal-ids.ts @@ -0,0 +1,292 @@ +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: single + * `workflow-selector` / `manualWorkflowId` values, multi-workflow lists + * (`workflowSelector` multi-select + comma-separated `manualWorkflowIds`, as used + * by the logs block), 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 (baseKey === 'manualWorkflowIds' || baseKey === 'workflowSelector') { + const remapped = remapWorkflowIdList(subBlock.value, workflowIdMap, clearUnmapped) + if (remapped !== subBlock.value) { + updated[key] = { ...subBlock, value: remapped } + 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 a multi-workflow value (comma-separated string or array of workflow ids) + * through a workflow id map. Unmapped ids are dropped when `clearUnmapped` is set + * (cross-workspace) and preserved otherwise. Returns the original reference when + * nothing changed. + */ +function remapWorkflowIdList( + value: unknown, + workflowIdMap: Map, + clearUnmapped: boolean +): unknown { + const remapId = (id: string): string | null => { + const mapped = workflowIdMap.get(id) + if (mapped) return mapped + return clearUnmapped ? null : id + } + if (Array.isArray(value)) { + let changed = false + const next: unknown[] = [] + for (const item of value) { + if (typeof item !== 'string' || !item) { + next.push(item) + continue + } + const mapped = remapId(item) + if (mapped === null) { + changed = true + continue + } + if (mapped !== item) changed = true + next.push(mapped) + } + return changed ? next : value + } + if (typeof value === 'string' && value) { + const next: string[] = [] + for (const id of value.split(',').map((entry) => entry.trim())) { + if (!id) continue + const mapped = remapId(id) + if (mapped !== null) next.push(mapped) + } + const joined = next.join(',') + return joined === value ? value : joined + } + return value +} + +/** + * 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/persistence/utils.ts b/apps/sim/lib/workflows/persistence/utils.ts index 895a87f6013..8f3df9326d4 100644 --- a/apps/sim/lib/workflows/persistence/utils.ts +++ b/apps/sim/lib/workflows/persistence/utils.ts @@ -637,6 +637,23 @@ export async function deployWorkflow(params: { } } + // Refuse to deploy an archived (soft-deleted) workflow. Checked under the row + // lock so it's atomic with a concurrent fork rollback that archives a + // promote-created workflow: a stale promote deploy can never resurrect it into + // an archived-but-deployed (incoherent) state. + const [archivedRow] = await tx + .select({ archivedAt: workflow.archivedAt }) + .from(workflow) + .where(eq(workflow.id, workflowId)) + .limit(1) + if (archivedRow?.archivedAt != null) { + return { + success: false as const, + error: 'Cannot deploy an archived workflow', + errorCode: 'validation' as const, + } + } + currentState = params.workflowState ?? (await loadWorkflowDeploymentSnapshot(workflowId, tx)) if (!currentState) { return { 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 { workspaceId, workflowId = null, kind, message, metadata, supersede = true } = params + if (supersede) { + await executor + .delete(backgroundWorkStatus) + .where( + and( + eq(backgroundWorkStatus.workspaceId, workspaceId), + eq(backgroundWorkStatus.kind, kind), + workflowId == null + ? isNull(backgroundWorkStatus.workflowId) + : eq(backgroundWorkStatus.workflowId, workflowId) + ) + ) + } + const id = generateId() + const now = new Date() + await executor.insert(backgroundWorkStatus).values({ + id, + workspaceId, + workflowId, + kind, + status: 'processing', + message: message ?? null, + metadata: metadata ?? null, + startedAt: now, + updatedAt: now, + }) + return id +} + +/** + * Record a synchronous operation directly as a terminal audit entry (no `processing` + * phase). Append-only - used by sync/rollback, which complete in-request, so they show + * up in the same workspace audit log as fork jobs. + */ +export async function recordBackgroundWork( + executor: DbOrTx, + params: { + workspaceId: string + kind: BackgroundWorkKind + status: Extract + message?: string + error?: string + metadata?: unknown + } +): Promise { + const now = new Date() + await executor.insert(backgroundWorkStatus).values({ + id: generateId(), + workspaceId: params.workspaceId, + workflowId: null, + kind: params.kind, + status: params.status, + message: params.message ?? null, + error: params.error ?? null, + metadata: params.metadata ?? null, + startedAt: now, + completedAt: now, + updatedAt: now, + }) +} + +/** Mark a tracked unit of work terminal (completed / completed_with_warnings / failed). */ +export async function finishBackgroundWork( + executor: DbOrTx, + id: string, + params: { + status: Extract + message?: string + error?: string + metadata?: unknown + } +): Promise { + const now = new Date() + // Merge metadata so terminal counts (copied/failed) augment what start recorded + // (child name, per-kind plan) instead of replacing it. + let metadata: unknown + if (params.metadata !== undefined) { + const [existing] = await executor + .select({ metadata: backgroundWorkStatus.metadata }) + .from(backgroundWorkStatus) + .where(eq(backgroundWorkStatus.id, id)) + .limit(1) + metadata = { ...toMetadataRecord(existing?.metadata), ...toMetadataRecord(params.metadata) } + } + await executor + .update(backgroundWorkStatus) + .set({ + status: params.status, + message: params.message ?? null, + error: params.error ?? null, + ...(params.metadata !== undefined ? { metadata } : {}), + completedAt: now, + updatedAt: now, + }) + .where(eq(backgroundWorkStatus.id, id)) +} + +/** Coerce an unknown jsonb metadata value to a plain record for safe merging. */ +function toMetadataRecord(value: unknown): Record { + return value && typeof value === 'object' && !Array.isArray(value) + ? (value as Record) + : {} +} + +/** + * Recent background-work jobs for a workspace - the durable audit record the Activity + * tab renders, most-recent first and capped. Fork jobs are append-only (one row per + * fork), so this is the workspace's fork history; older rows are pruned by the cron. + */ +export async function listSurfacedBackgroundWork( + executor: DbOrTx, + workspaceId: string +): Promise { + const rows = await executor + .select() + .from(backgroundWorkStatus) + .where( + and( + eq(backgroundWorkStatus.workspaceId, workspaceId), + inArray(backgroundWorkStatus.status, SURFACED_STATUSES) + ) + ) + .orderBy(desc(backgroundWorkStatus.updatedAt)) + .limit(BACKGROUND_WORK_LIST_LIMIT) + return rows as BackgroundWorkRow[] +} + +/** + * Fail background-work rows stuck in an active state past {@link STALE_ACTIVE_MS}: the + * worker crashed or restarted before writing a terminal status, and a hard crash has + * no in-task hook to recover from. Marks them `failed` so the UI shows the failure + * rather than an indefinitely-spinning banner. Touches ONLY `background_work_status`. + * Returns the count reaped; invoked from the outbox-processor cron. + */ +export async function reapStaleBackgroundWork(executor: DbOrTx): Promise { + const now = new Date() + const cutoff = new Date(now.getTime() - STALE_ACTIVE_MS) + const reaped = await executor + .update(backgroundWorkStatus) + .set({ + status: 'failed', + error: 'Background work did not finish in time (the worker may have restarted).', + completedAt: now, + updatedAt: now, + }) + .where( + and( + inArray(backgroundWorkStatus.status, ['pending', 'processing']), + lte(backgroundWorkStatus.startedAt, cutoff) + ) + ) + .returning({ id: backgroundWorkStatus.id }) + + // Retention: the append-only fork audit trail would otherwise grow forever, so drop + // terminal rows past the retention window. The Activity tab caps display separately. + await executor + .delete(backgroundWorkStatus) + .where( + and( + inArray(backgroundWorkStatus.status, ['completed', 'completed_with_warnings', 'failed']), + lte(backgroundWorkStatus.updatedAt, new Date(now.getTime() - RETENTION_MS)) + ) + ) + + if (reaped.length > 0) { + logger.warn('Reaped stale background-work rows', { + count: reaped.length, + thresholdMs: STALE_ACTIVE_MS, + }) + } + return reaped.length +} diff --git a/apps/sim/lib/workspaces/fork/copy/content-copy-runner.ts b/apps/sim/lib/workspaces/fork/copy/content-copy-runner.ts new file mode 100644 index 00000000000..16f76dbf3d0 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/copy/content-copy-runner.ts @@ -0,0 +1,59 @@ +import { db } from '@sim/db' +import { getErrorMessage } from '@sim/utils/errors' +import { finishBackgroundWork } from '@/lib/workspaces/fork/background-work/store' +import type { BlobCopyTask } from '@/lib/workspaces/fork/copy/copy-files' +import { executeForkFileBlobCopies } from '@/lib/workspaces/fork/copy/copy-files' +import type { ForkContentPlan } from '@/lib/workspaces/fork/copy/copy-resources' +import { copyForkResourceContent } from '@/lib/workspaces/fork/copy/copy-resources' + +/** + * Serializable payload for the post-fork heavy-content copy. Runs either as a + * Trigger.dev task (`background/fork-content-copy`) or inline via `runDetached` + * when Trigger.dev is disabled - both call {@link runForkContentCopy}. + */ +export interface ForkContentCopyPayload { + contentPlan: ForkContentPlan + blobTasks: BlobCopyTask[] + /** + * `background_work_status` row to finish when the copy ends, so the source workspace's + * Manage Forks -> Activity entry resolves (completed / warning / error). Started right + * after the fork commits so it's visible immediately. + */ + statusId?: string + requestId?: string +} + +/** + * Copy the heavy fork content after the fork transaction has committed: table + * rows, KB documents + embeddings (keyset-paginated), and file blobs. Best-effort + * and idempotency-unsafe (per-row inserts use fresh ids), so it must run at most + * once - never blindly retried. Per-resource failures are counted (not thrown), so + * the run finishes `completed_with_warnings` rather than failing the whole copy. + */ +export async function runForkContentCopy(payload: ForkContentCopyPayload): Promise { + const { contentPlan, blobTasks, statusId, requestId } = payload + try { + const resourceCounts = await copyForkResourceContent({ contentPlan, requestId }) + const fileCounts = await executeForkFileBlobCopies(blobTasks, requestId) + const copied = resourceCounts.copied + fileCounts.copied + const failed = resourceCounts.failed + fileCounts.failed + if (statusId) { + await finishBackgroundWork(db, statusId, { + status: failed > 0 ? 'completed_with_warnings' : 'completed', + message: + failed > 0 + ? `Copied ${copied} item${copied === 1 ? '' : 's'}; ${failed} could not be copied` + : `Copied ${copied} item${copied === 1 ? '' : 's'}`, + metadata: { copied, failed }, + }) + } + } catch (error) { + if (statusId) { + await finishBackgroundWork(db, statusId, { + status: 'failed', + error: getErrorMessage(error, 'Background resource copy failed'), + }).catch(() => {}) + } + throw error + } +} 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..20bf90c59ad --- /dev/null +++ b/apps/sim/lib/workspaces/fork/copy/copy-files.ts @@ -0,0 +1,146 @@ +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: non-deleted, scoped to the source workspace, and restricted to durable + // `workspace` files. Only workspace files are forkable - chat/copilot/mothership + // uploads are session-scoped and their chat-bound unique index can't be duplicated - + // so any non-workspace id passed here is ignored rather than copied. + const metas = await tx + .select() + .from(workspaceFiles) + .where( + and( + inArray(workspaceFiles.id, fileIds), + eq(workspaceFiles.workspaceId, sourceWorkspaceId), + eq(workspaceFiles.context, 'workspace'), + 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<{ copied: number; failed: number }> { + let copied = 0 + let failed = 0 + 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, + }, + }) + copied += 1 + } catch (error) { + failed += 1 + logger.warn(`[${requestId}] Failed to copy file blob during fork`, { + targetKey: task.targetKey, + error: getErrorMessage(error), + }) + } + } + return { copied, failed } +} 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..2fbde548ece --- /dev/null +++ b/apps/sim/lib/workspaces/fork/copy/copy-resources.ts @@ -0,0 +1,435 @@ +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[] +} + +/** Display names of the copied resources, by kind, for the fork activity report. */ +export interface ForkCopiedResourceNames { + tables: string[] + knowledgeBases: string[] + customTools: string[] + skills: string[] + mcpServers: string[] +} + +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 + /** Names of the copied resources, by kind, for the fork report breakdown. */ + names: ForkCopiedResourceNames +} + +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 names: ForkCopiedResourceNames = { + tables: [], + knowledgeBases: [], + customTools: [], + skills: [], + mcpServers: [], + } + + 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) + ) + ) + const inserts: (typeof customTools.$inferInsert)[] = [] + for (const row of rows) { + const childId = generateId() + inserts.push({ + ...row, + id: childId, + workspaceId: childWorkspaceId, + userId, + createdAt: now, + updatedAt: now, + }) + record('custom_tool', row.id, childId) + names.customTools.push(row.title) + } + if (inserts.length > 0) await tx.insert(customTools).values(inserts) + } + + if (selection.skills.length > 0) { + const rows = await tx + .select() + .from(skill) + .where(and(inArray(skill.id, selection.skills), eq(skill.workspaceId, sourceWorkspaceId))) + const inserts: (typeof skill.$inferInsert)[] = [] + for (const row of rows) { + const childId = generateId() + inserts.push({ + ...row, + id: childId, + workspaceId: childWorkspaceId, + userId, + createdAt: now, + updatedAt: now, + }) + record('skill', row.id, childId) + names.skills.push(row.name) + } + if (inserts.length > 0) await tx.insert(skill).values(inserts) + } + + 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 insertsByChildId = new Map() + for (const row of rows) { + const childId = row.url ? generateMcpServerId(childWorkspaceId, row.url) : generateId() + record('mcp_server', row.id, childId) + if (insertsByChildId.has(childId)) continue + names.mcpServers.push(row.name) + insertsByChildId.set(childId, { + ...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, + }) + } + if (insertsByChildId.size > 0) { + await tx + .insert(mcpServers) + .values([...insertsByChildId.values()]) + .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) + ) + ) + const inserts: (typeof userTableDefinitions.$inferInsert)[] = [] + for (const definition of definitions) { + const childTableId = generateId() + const remappedSchema = remapForkTableWorkflowGroups( + definition.schema as TableSchema, + workflowIdMap + ) + inserts.push({ + ...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 }) + names.tables.push(definition.name) + } + if (inserts.length > 0) await tx.insert(userTableDefinitions).values(inserts) + } + + 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) + ) + ) + const inserts: (typeof knowledgeBase.$inferInsert)[] = [] + for (const base of bases) { + const childKbId = generateId() + inserts.push({ + ...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 }) + names.knowledgeBases.push(base.name) + } + if (inserts.length > 0) await tx.insert(knowledgeBase).values(inserts) + } + + return { idMap, mappingEntries, contentPlan, names } +} + +/** + * 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<{ copied: number; failed: number }> { + const { contentPlan, requestId = 'unknown' } = params + const { childWorkspaceId } = contentPlan + + let copiedResources = 0 + let failedResources = 0 + + 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)) + copiedResources += 1 + } catch (error) { + failedResources += 1 + 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 + } + copiedResources += 1 + } catch (error) { + failedResources += 1 + logger.warn(`[${requestId}] Failed to copy knowledge base content during fork`, { + sourceKnowledgeBaseId: kb.sourceId, + error: getErrorMessage(error), + }) + } + } + + return { copied: copiedResources, failed: failedResources } +} + +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.test.ts b/apps/sim/lib/workspaces/fork/copy/copy-workflows.test.ts new file mode 100644 index 00000000000..e141d65e7b4 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/copy/copy-workflows.test.ts @@ -0,0 +1,62 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { buildWorkflowNameRegistry } from '@/lib/workspaces/fork/copy/copy-workflows' + +describe('buildWorkflowNameRegistry', () => { + it('reports a name as taken by another workflow in the same folder', () => { + const reg = buildWorkflowNameRegistry([{ id: 'w1', folderId: 'f1', name: 'Onboarding' }]) + expect(reg.isTaken('f1', 'Onboarding', null)).toBe(true) + expect(reg.isTaken('f1', 'Onboarding', 'w2')).toBe(true) + }) + + it('excludes the workflow itself so a replace can keep its own name', () => { + const reg = buildWorkflowNameRegistry([{ id: 'w1', folderId: 'f1', name: 'Onboarding' }]) + expect(reg.isTaken('f1', 'Onboarding', 'w1')).toBe(false) + }) + + it('is folder-scoped: the same name in another folder is free', () => { + const reg = buildWorkflowNameRegistry([{ id: 'w1', folderId: 'f1', name: 'Onboarding' }]) + expect(reg.isTaken('f2', 'Onboarding', null)).toBe(false) + expect(reg.isTaken(null, 'Onboarding', null)).toBe(false) + }) + + it('treats the root (null) folder distinctly, matching coalesce(folderId, "")', () => { + const reg = buildWorkflowNameRegistry([{ id: 'w1', folderId: null, name: 'Root WF' }]) + expect(reg.isTaken(null, 'Root WF', null)).toBe(true) + expect(reg.isTaken('f1', 'Root WF', null)).toBe(false) + }) + + it('claims a new name so a later workflow in the same copy loop sees it taken', () => { + const reg = buildWorkflowNameRegistry([]) + expect(reg.isTaken('f1', 'Report', null)).toBe(false) + reg.claim('f1', 'Report', 'wA') + expect(reg.isTaken('f1', 'Report', null)).toBe(true) + expect(reg.isTaken('f1', 'Report', 'wA')).toBe(false) + }) + + it('releases the prior name when a workflow is renamed (claim moves keys)', () => { + const reg = buildWorkflowNameRegistry([{ id: 'w1', folderId: 'f1', name: 'Old' }]) + reg.claim('f1', 'New', 'w1') + expect(reg.isTaken('f1', 'Old', null)).toBe(false) + expect(reg.isTaken('f1', 'New', null)).toBe(true) + }) + + it('re-claiming the same (folder, name) is a no-op', () => { + const reg = buildWorkflowNameRegistry([{ id: 'w1', folderId: 'f1', name: 'Same' }]) + reg.claim('f1', 'Same', 'w1') + expect(reg.isTaken('f1', 'Same', 'w1')).toBe(false) + expect(reg.isTaken('f1', 'Same', null)).toBe(true) + }) + + it('handles multiple holders (legacy duplicates) and partial release', () => { + const reg = buildWorkflowNameRegistry([ + { id: 'w1', folderId: 'f1', name: 'Dup' }, + { id: 'w2', folderId: 'f1', name: 'Dup' }, + ]) + expect(reg.isTaken('f1', 'Dup', 'w1')).toBe(true) + reg.claim('f1', 'Other', 'w2') + expect(reg.isTaken('f1', 'Dup', 'w1')).toBe(false) + }) +}) 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..19167a0b50e --- /dev/null +++ b/apps/sim/lib/workspaces/fork/copy/copy-workflows.ts @@ -0,0 +1,548 @@ +import { workflow, workflowBlocks, workflowFolder } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { generateId } from '@sim/utils/id' +import { and, eq, inArray, isNull } 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, + type ForkBlockIdResolver, +} from '@/lib/workspaces/fork/remap/block-identity' +import { + applyDependentOverrides, + collectClearedDependents, + type NeedsConfigurationField, +} from '@/lib/workspaces/fork/remap/remap-references' +import type { + BlockData, + BlockState, + Loop, + Parallel, + SubBlockState, + WorkflowState, + Variable as WorkflowStateVariable, +} from '@/stores/workflows/workflow/types' + +const logger = createLogger('WorkspaceForkCopyWorkflows') + +type SubBlockTransform = (subBlocks: SubBlockRecord, blockType: string) => 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 +} + +// `\u0000` (a NUL byte) can never appear in a Postgres text column, so it is a +// collision-free separator between the folder id and the name. +const workflowNameKey = (folderId: string | null, name: string) => `${folderId ?? ''}\u0000${name}` + +/** + * In-memory registry of a workspace's active workflow names, keyed by + * (folderId, name) - the exact columns of the `workflow_workspace_folder_name_active_unique` + * partial index. Lets fork/promote resolve name collisions across many copied workflows + * from a single load instead of one `nameTaken` query per workflow inside the (locked) + * transaction. + * + * Correctness is still guaranteed by that DB unique index; this is only a proactive + * collision avoider. A stale snapshot (e.g. a concurrent non-fork rename mid-promote) can + * therefore only cause a rare, retry-able unique violation - never a duplicate name - + * exactly as the prior per-workflow check-then-write already could. + */ +export interface WorkflowNameRegistry { + /** True when (folderId, name) is held by any active workflow other than `excludeWorkflowId`. */ + isTaken(folderId: string | null, name: string, excludeWorkflowId: string | null): boolean + /** Record that `workflowId` now holds (folderId, name), releasing any name it held before. */ + claim(folderId: string | null, name: string, workflowId: string): void +} + +/** Build a {@link WorkflowNameRegistry} from already-loaded rows (pure - unit-testable). */ +export function buildWorkflowNameRegistry( + rows: Array<{ id: string; folderId: string | null; name: string }> +): WorkflowNameRegistry { + const holdersByKey = new Map>() + const keyByWorkflow = new Map() + for (const row of rows) { + const key = workflowNameKey(row.folderId, row.name) + const holders = holdersByKey.get(key) + if (holders) holders.add(row.id) + else holdersByKey.set(key, new Set([row.id])) + keyByWorkflow.set(row.id, key) + } + + return { + isTaken(folderId, name, excludeWorkflowId) { + const holders = holdersByKey.get(workflowNameKey(folderId, name)) + if (!holders) return false + for (const id of holders) if (id !== excludeWorkflowId) return true + return false + }, + claim(folderId, name, workflowId) { + const newKey = workflowNameKey(folderId, name) + const prevKey = keyByWorkflow.get(workflowId) + if (prevKey === newKey) return + if (prevKey) holdersByKey.get(prevKey)?.delete(workflowId) + const holders = holdersByKey.get(newKey) + if (holders) holders.add(workflowId) + else holdersByKey.set(newKey, new Set([workflowId])) + keyByWorkflow.set(workflowId, newKey) + }, + } +} + +/** Load every active workflow name in a workspace into a {@link WorkflowNameRegistry}. */ +export async function loadWorkflowNameRegistry( + executor: DbOrTx, + workspaceId: string +): Promise { + const rows = await executor + .select({ id: workflow.id, folderId: workflow.folderId, name: workflow.name }) + .from(workflow) + .where(and(eq(workflow.workspaceId, workspaceId), isNull(workflow.archivedAt))) + return buildWorkflowNameRegistry(rows) +} + +/** + * Batched read of the current DRAFT subBlocks for a set of (replace) target + * workflows, keyed `workflowId -> blockId -> subBlocks`. One query for the whole + * promote so the locked apply phase doesn't do N per-workflow loads; called + * pre-write so it reflects the target state the user configured before this sync + * overwrites it. Promote uses it to detect required dependents the sync left empty + * (see {@link collectClearedDependents}); fork-create has no prior target and skips it. + */ +export async function loadTargetDraftSubBlocks( + executor: DbOrTx, + workflowIds: string[] +): Promise>> { + const byWorkflow = new Map>() + if (workflowIds.length === 0) return byWorkflow + const rows = await executor + .select({ + workflowId: workflowBlocks.workflowId, + blockId: workflowBlocks.id, + subBlocks: workflowBlocks.subBlocks, + }) + .from(workflowBlocks) + .where(inArray(workflowBlocks.workflowId, workflowIds)) + for (const row of rows) { + let blocks = byWorkflow.get(row.workflowId) + if (!blocks) { + blocks = new Map() + byWorkflow.set(row.workflowId, blocks) + } + blocks.set(row.blockId, (row.subBlocks ?? {}) as SubBlockRecord) + } + return byWorkflow +} + +/** + * Pick a non-colliding name for a copied workflow against the preloaded registry, which + * mirrors the workspace's (folder, name, not-archived, exclude-self) predicate from one + * query instead of one per candidate. Mirrors {@link deduplicateWorkflowName}'s ` (n)` + * numbering, but reads from memory so the copy loop issues no per-workflow name queries. + */ +function resolveTargetWorkflowName( + registry: WorkflowNameRegistry, + folderId: string | null, + name: string, + excludeWorkflowId: string | null +): string { + const taken = (candidate: string) => registry.isTaken(folderId, candidate, excludeWorkflowId) + if (!taken(name)) return name + for (let i = 2; i < 100; i++) { + const candidate = `${name} (${i})` + if (!taken(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 + /** + * `dependsOn` fields (top-level and nested tool-input) a remapped parent left empty + * that weren't restored from the target draft - the parent legitimately changed. + * Carries `required` per field: promote skips redeploy + gates on required ones and + * surfaces optional ones so a cleared filter never broadens behavior silently. + */ + clearedDependents: NeedsConfigurationField[] + /** + * Source block id -> assigned target block id, so the caller can persist the + * block-identity pairs (see `recordForkBlockPairs`) that keep promotes reversible. + */ + blockIdMapping: Map +} + +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 + /** + * The target workflow's current draft subBlocks (block id -> subBlocks), for + * `replace` mode only. When present, required dependents that the sync left empty + * (the parent change cleared and the stored mapping didn't fill) are reported in + * {@link CopyWorkflowResult.needsConfiguration}. + */ + targetCurrentBlocks?: Map + /** + * Per-block (block id -> subBlock key -> value) stored dependent values applied last, + * after the reference transform cleared the source's, so the stored mapping is the sole + * source of truth for what each dependent selector resolves to. + */ + dependentOverrides?: Map> + /** + * Preloaded name registry so name-collision resolution reads from memory instead of one + * query per workflow inside the tx. Build once per copy loop via {@link loadWorkflowNameRegistry}. + */ + nameRegistry: WorkflowNameRegistry + /** + * Resolve each source block to its target block id, reusing the persisted counterpart + * when one exists so a push keeps the parent's original block ids (and webhook URLs) + * instead of re-deriving them (see {@link buildForkBlockIdResolver}). Omitted on fork + * creation, where every id is derived fresh. + */ + resolveBlockId?: ForkBlockIdResolver + 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, + targetCurrentBlocks, + dependentOverrides, + nameRegistry, + resolveBlockId, + 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, + resolveBlockId + ? resolveBlockId(targetWorkflowId, oldBlockId) + : deriveForkBlockId(targetWorkflowId, oldBlockId) + ) + } + + const newBlocks: Record = {} + const clearedDependents: NeedsConfigurationField[] = [] + 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 + const sanitizedSource = sanitizeSubBlocksForDuplicate(sourceSubBlocks) + let subBlocks: SubBlockRecord = sanitizedSource + if (transformSubBlocks) { + subBlocks = transformSubBlocks(subBlocks, block.type) + } + 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 + + // Apply the stored dependent values for this block (the modal's mapping). The reference + // transform already cleared the source's dependent values when their parent was remapped, + // so the stored mapping is the SOLE source of truth - no implicit "preserve the target's + // value" path. Allowlisted (top-level + nested tool params) inside applyDependentOverrides + // so a crafted value can't touch a parent/credential field or inject a bogus subblock. + const targetCurrent = targetCurrentBlocks?.get(newBlockId) + const blockOverrides = dependentOverrides?.get(newBlockId) + if (blockOverrides && blockOverrides.size > 0) { + subBlocks = applyDependentOverrides(subBlocks, block.type, blockOverrides) + } + + // Dependents the TARGET had configured that the parent change cleared and nothing + // restored: the target must re-pick required ones (promote skips this workflow's + // redeploy) and is told about optional ones. Keyed on the target draft so a field the + // source carried but the target never set isn't flagged. + if (mode === 'replace' && targetCurrent) { + clearedDependents.push( + ...collectClearedDependents(block.type, newBlockId, block.name, targetCurrent, subBlocks) + ) + } + + 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 = resolveTargetWorkflowName( + nameRegistry, + targetFolderId, + sourceMeta.name, + mode === 'replace' ? targetWorkflowId : null + ) + // Claim the resolved name so the next workflow in this copy loop sees it taken. The DB + // write below uses the same (folderId, name), so the registry stays consistent with it. + nameRegistry.claim(targetFolderId, resolvedName, targetWorkflowId) + + 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, + clearedDependents, + blockIdMapping, + } +} 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..01c2aaedd23 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/copy/deploy-bridge.ts @@ -0,0 +1,188 @@ +import { db, runOutsideTransactionContext } from '@sim/db' +import { workflow, workflowDeploymentVersion } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, eq, exists, inArray, isNull, sql } 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. Requires an actually-active deployment version, not just the + * `isDeployed` flag: a workflow flagged deployed with no active version (a "ghost" + * left by an inconsistent state) has nothing to copy, so excluding it here keeps the + * diff/plan counts aligned with what apply actually writes instead of over-reporting + * then silently skipping it. Correlated `exists` (not a join) so a workflow is never + * double-listed if more than one active version row ever exists. + */ +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), + exists( + db + .select({ one: sql`1` }) + .from(workflowDeploymentVersion) + .where( + and( + eq(workflowDeploymentVersion.workflowId, workflow.id), + eq(workflowDeploymentVersion.isActive, true) + ) + ) + ) + ) + ) +} + +/** 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 +} + +/** + * Batched {@link getActiveDeploymentVersionNumber}: the active deployed version per + * workflow id, so promote's apply phase resolves every prior version in one query + * instead of N round-trips inside the (locked) transaction. Workflows with no active + * version are absent from the map. + */ +export async function getActiveDeploymentVersionNumbers( + executor: DbOrTx, + workflowIds: string[] +): Promise> { + if (workflowIds.length === 0) return new Map() + const rows = await executor + .select({ + workflowId: workflowDeploymentVersion.workflowId, + version: workflowDeploymentVersion.version, + }) + .from(workflowDeploymentVersion) + .where( + and( + inArray(workflowDeploymentVersion.workflowId, workflowIds), + eq(workflowDeploymentVersion.isActive, true) + ) + ) + return new Map(rows.map((row) => [row.workflowId, row.version])) +} + +/** + * 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 + ) + } + // Read states in bounded-concurrency batches instead of one serial await per workflow: + // serial cost is O(workflows) round trips (this also runs on the diff preview, refetched + // while the sync modal is open). The cap keeps concurrent global-pool checkouts well + // under the pool max even at the workflow ceiling, and this runs BEFORE any transaction. + const sourceStates = new Map() + const READ_CONCURRENCY = 5 + for (let i = 0; i < deployedWorkflows.length; i += READ_CONCURRENCY) { + const batch = deployedWorkflows.slice(i, i + READ_CONCURRENCY) + const states = await Promise.all(batch.map((wf) => readDeployedState(wf.id, sourceWorkspaceId))) + batch.forEach((wf, index) => { + const state = states[index] + 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..6cdf43dace7 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/create-fork.ts @@ -0,0 +1,406 @@ +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 { getErrorMessage } from '@sim/utils/errors' +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 { + finishBackgroundWork, + startBackgroundWork, +} from '@/lib/workspaces/fork/background-work/store' +import { + type ForkContentCopyPayload, + runForkContentCopy, +} from '@/lib/workspaces/fork/copy/content-copy-runner' +import { planForkFileCopies } from '@/lib/workspaces/fork/copy/copy-files' +import { + copyForkResourceContainers, + type ForkCopiedResourceNames, +} from '@/lib/workspaces/fork/copy/copy-resources' +import { + copyWorkflowStateIntoTarget, + loadWorkflowNameRegistry, + resolveForkFolderMapping, +} from '@/lib/workspaces/fork/copy/copy-workflows' +import { loadSourceDeployedStates } from '@/lib/workspaces/fork/copy/deploy-bridge' +import { setForkLockTimeout } from '@/lib/workspaces/fork/lineage/lineage' +import { + type ForkBlockPair, + reconcileForkBlockPairs, + toForkBlockPairs, +} from '@/lib/workspaces/fork/mapping/block-map-store' +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 + /** Display name of the user forking, recorded on the activity entry. */ + actorName?: 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 forkedWorkflowNames: string[] = [] + let forkedResourceNames: ForkCopiedResourceNames = { + tables: [], + knowledgeBases: [], + customTools: [], + skills: [], + mcpServers: [], + } + const { result, blobTasks, contentPlan } = await db.transaction(async (tx) => { + await setForkLockTimeout(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, + }) + forkedResourceNames = resourceResult.names + + 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, + }) + + // The child is brand new, so this loads an empty registry; name collisions can only + // arise among the copied workflows themselves, which the in-loop claims resolve. + const nameRegistry = await loadWorkflowNameRegistry(tx, childWorkspaceId) + + let workflowsCopied = 0 + // Seed the block-identity map (parent block -> derived child block) so a later push of + // this fork resolves each child block back to the parent's ORIGINAL id instead of + // re-deriving and re-keying the parent's webhook URLs. + const blockPairs: ForkBlockPair[] = [] + const sourceWorkflowIds: string[] = [] + for (const wf of deployedWorkflows) { + const sourceState = sourceStates.get(wf.id) + if (!sourceState) continue + const targetWorkflowId = workflowIdMap.get(wf.id)! + const copyResult = await copyWorkflowStateIntoTarget({ + tx, + targetWorkflowId, + targetWorkspaceId: childWorkspaceId, + userId, + mode: 'create', + now, + sourceState, + sourceMeta: { + name: wf.name, + description: wf.description, + folderId: wf.folderId, + sortOrder: wf.sortOrder, + }, + workflowIdMap, + folderIdMap, + transformSubBlocks: transform, + nameRegistry, + requestId, + }) + // Creation copies parent -> child, so the source side is the parent. + blockPairs.push(...toForkBlockPairs(copyResult.blockIdMapping, true, wf.id, targetWorkflowId)) + sourceWorkflowIds.push(wf.id) + workflowsCopied += 1 + forkedWorkflowNames.push(wf.name) + } + await reconcileForkBlockPairs(tx, childWorkspaceId, true, sourceWorkflowIds, blockPairs) + + // 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 + + // Record a durable job for EVERY fork (the fork already committed), scoped to the + // SOURCE workspace - that's where the fork was initiated and where its Activity tab + // lives, so the record survives a reload of the fork modal. When there is heavy + // content to copy in the background the row stays `processing` until the runner + // finishes it (merging in copied/failed); otherwise the fork is already complete. + const forkedName = result.workspace.name + // The fork already committed; failing to record the tracking row must not turn it into + // a 500. Log and continue without a status row - the background content copy below still + // runs (its runner no-ops the status update when statusId is absent). + let statusId: string | undefined + try { + statusId = await startBackgroundWork(db, { + workspaceId: source.id, + kind: 'fork_content_copy', + // Append-only: each fork is a distinct entry in the source workspace's fork history. + supersede: false, + message: hasContent ? `Copying resources to "${forkedName}"` : `Forked into "${forkedName}"`, + metadata: { + childWorkspaceId: result.workspace.id, + childWorkspaceName: forkedName, + actorName: params.actorName, + workflowsCopied: result.workflowsCopied, + tables: contentPlan.tables.length, + knowledgeBases: contentPlan.knowledgeBases.length, + files: blobTasks.length, + workflowNames: forkedWorkflowNames, + tableNames: forkedResourceNames.tables, + knowledgeBaseNames: forkedResourceNames.knowledgeBases, + fileNames: blobTasks.map((task) => task.fileName), + customToolNames: forkedResourceNames.customTools, + skillNames: forkedResourceNames.skills, + mcpServerNames: forkedResourceNames.mcpServers, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Failed to record fork background-work status`, { + childWorkspaceId: result.workspace.id, + error: getErrorMessage(error), + }) + } + + if (!hasContent) { + if (statusId) { + await finishBackgroundWork(db, statusId, { + status: 'completed', + message: `Forked into "${forkedName}"`, + metadata: { copied: 0, failed: 0 }, + }).catch(() => {}) + } + return result + } + + const payload: ForkContentCopyPayload = { contentPlan, blobTasks, statusId, requestId } + try { + 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)) + } + } catch (error) { + // The fork itself succeeded; only scheduling the background copy failed. Surface + // it on the status row instead of failing the (committed) fork response. + logger.error(`[${requestId}] Failed to schedule fork content copy`, { + childWorkspaceId: result.workspace.id, + error: getErrorMessage(error), + }) + if (statusId) { + await finishBackgroundWork(db, statusId, { + status: 'failed', + error: getErrorMessage(error, 'Could not start the background copy'), + }).catch(() => {}) + } + } + + 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..a7d4d84dcf7 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/lineage/authz.ts @@ -0,0 +1,142 @@ +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; 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, canAdmin: access.canAdmin } +} + +/** 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. + * + * `pinOrganization` makes the child ALWAYS land in the source's org - including a + * personal source (org `null`) - rather than the acting user's membership org, + * which the policy would otherwise fall back to when the source is personal. + */ +export async function assertCanFork( + sourceWorkspaceId: string, + userId: string +): Promise { + const source = await assertWorkspaceAdminAccess(sourceWorkspaceId, userId) + const policy = await getWorkspaceCreationPolicy({ + userId, + activeOrganizationId: source.organizationId, + pinOrganization: true, + }) + 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 admin on BOTH the source and the target: a sync + * reads the source's deployed workflows/resources and force-replaces the target's, + * and the sync surface is only ever offered to workspace admins. `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 assertWorkspaceAdminAccess(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..588bf2e433c --- /dev/null +++ b/apps/sim/lib/workspaces/fork/lineage/lineage.ts @@ -0,0 +1,108 @@ +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 +} + +/** + * 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 +} + +/** + * How long a fork transaction waits for a lock before aborting. Bounds the wait on the + * target/edge advisory locks (and any incidental row lock) so a contended sync into the + * same target fails fast and returns its pooled connection instead of piling waiters up + * and stagnating the pool at scale. 10s favors completing a legit sync queued behind an + * in-flight one, while still tripping on a pathological hold. Connection-level timeouts + * are not used (PlanetScale rejects them) - this is transaction-scoped only. + */ +const FORK_LOCK_TIMEOUT_MS = 10_000 + +/** + * Apply {@link FORK_LOCK_TIMEOUT_MS} to the current transaction (`set_config(local)`), + * so it covers `pg_advisory_xact_lock` waits too. Call at the very start of a fork + * transaction, before acquiring any lock. + */ +export async function setForkLockTimeout(tx: DbOrTx): Promise { + await tx.execute(sql`select set_config('lock_timeout', ${`${FORK_LOCK_TIMEOUT_MS}ms`}, true)`) +} + +/** + * 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/block-map-store.test.ts b/apps/sim/lib/workspaces/fork/mapping/block-map-store.test.ts new file mode 100644 index 00000000000..55f33423302 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/mapping/block-map-store.test.ts @@ -0,0 +1,50 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { toForkBlockPairs } from '@/lib/workspaces/fork/mapping/block-map-store' + +describe('toForkBlockPairs', () => { + const mapping = new Map([ + ['src-1', 'tgt-1'], + ['src-2', 'tgt-2'], + ]) + + it('orients source->target as parent->child on pull/create (source = parent)', () => { + expect(toForkBlockPairs(mapping, true, 'wf-parent', 'wf-child')).toEqual([ + { + parentWorkflowId: 'wf-parent', + childWorkflowId: 'wf-child', + parentBlockId: 'src-1', + childBlockId: 'tgt-1', + }, + { + parentWorkflowId: 'wf-parent', + childWorkflowId: 'wf-child', + parentBlockId: 'src-2', + childBlockId: 'tgt-2', + }, + ]) + }) + + it('orients source->target as child->parent on push (source = child)', () => { + expect(toForkBlockPairs(mapping, false, 'wf-child', 'wf-parent')).toEqual([ + { + parentWorkflowId: 'wf-parent', + childWorkflowId: 'wf-child', + parentBlockId: 'tgt-1', + childBlockId: 'src-1', + }, + { + parentWorkflowId: 'wf-parent', + childWorkflowId: 'wf-child', + parentBlockId: 'tgt-2', + childBlockId: 'src-2', + }, + ]) + }) + + it('returns an empty list for an empty mapping', () => { + expect(toForkBlockPairs(new Map(), true, 'wf-parent', 'wf-child')).toEqual([]) + }) +}) diff --git a/apps/sim/lib/workspaces/fork/mapping/block-map-store.ts b/apps/sim/lib/workspaces/fork/mapping/block-map-store.ts new file mode 100644 index 00000000000..68f8b2e1eb6 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/mapping/block-map-store.ts @@ -0,0 +1,120 @@ +import { workspaceForkBlockMap } from '@sim/db/schema' +import { generateId } from '@sim/utils/id' +import { and, eq, inArray } from 'drizzle-orm' +import type { DbOrTx } from '@/lib/db/types' +import type { ForkBlockMap } from '@/lib/workspaces/fork/remap/block-identity' + +/** One persisted block-identity pair for an edge (carries both workflow sides). */ +export interface ForkBlockPair { + parentWorkflowId: string + parentBlockId: string + childWorkflowId: string + childBlockId: string +} + +/** + * Load an edge's persisted block-identity pairs into both lookup directions, each entry + * carrying its target-side workflow so the resolver can scope a reuse to the right workflow + * (see {@link buildForkBlockIdResolver}). Empty for an edge that predates the map (every + * block id then derives, exactly as before). + */ +export async function loadForkBlockMap( + executor: DbOrTx, + childWorkspaceId: string +): Promise { + const rows = await executor + .select({ + parentWorkflowId: workspaceForkBlockMap.parentWorkflowId, + parentBlockId: workspaceForkBlockMap.parentBlockId, + childWorkflowId: workspaceForkBlockMap.childWorkflowId, + childBlockId: workspaceForkBlockMap.childBlockId, + }) + .from(workspaceForkBlockMap) + .where(eq(workspaceForkBlockMap.childWorkspaceId, childWorkspaceId)) + const parentToChild = new Map() + const childToParent = new Map() + for (const row of rows) { + parentToChild.set(row.parentBlockId, { + targetBlockId: row.childBlockId, + targetWorkflowId: row.childWorkflowId, + }) + childToParent.set(row.childBlockId, { + targetBlockId: row.parentBlockId, + targetWorkflowId: row.parentWorkflowId, + }) + } + return { parentToChild, childToParent } +} + +/** + * Orient one workflow's copy mapping (`sourceBlockId -> targetBlockId`) into block pairs. + * On pull/create the source is the parent; on push it's the child. The workflow ids are + * fixed for the whole mapping (one source workflow copied into one target workflow). + */ +export function toForkBlockPairs( + blockIdMapping: ReadonlyMap, + sourceIsParent: boolean, + sourceWorkflowId: string, + targetWorkflowId: string +): ForkBlockPair[] { + const parentWorkflowId = sourceIsParent ? sourceWorkflowId : targetWorkflowId + const childWorkflowId = sourceIsParent ? targetWorkflowId : sourceWorkflowId + const pairs: ForkBlockPair[] = [] + for (const [sourceBlockId, targetBlockId] of blockIdMapping) { + pairs.push({ + parentWorkflowId, + childWorkflowId, + parentBlockId: sourceIsParent ? sourceBlockId : targetBlockId, + childBlockId: sourceIsParent ? targetBlockId : sourceBlockId, + }) + } + return pairs +} + +/** + * Replace the persisted pairs for the promoted SOURCE workflows with the live ones. Deleting + * by the (stable) source-side workflow id first does two things: it nukes pairs for blocks + * the source deleted since the last sync (e.g. a removed trigger), and it clears the old pair + * for a workflow whose target was archived + re-created - so the map always reflects the live + * lineage and a stale pair can never re-home a block onto an archived workflow's id. Block + * identity is otherwise immutable, so on a steady sync this just deletes and re-inserts the + * same pairs. `sourceIsParent` is true on pull/create, false on push. + */ +export async function reconcileForkBlockPairs( + executor: DbOrTx, + childWorkspaceId: string, + sourceIsParent: boolean, + sourceWorkflowIds: string[], + pairs: ForkBlockPair[] +): Promise { + if (sourceWorkflowIds.length > 0) { + const sourceWorkflowColumn = sourceIsParent + ? workspaceForkBlockMap.parentWorkflowId + : workspaceForkBlockMap.childWorkflowId + await executor + .delete(workspaceForkBlockMap) + .where( + and( + eq(workspaceForkBlockMap.childWorkspaceId, childWorkspaceId), + inArray(sourceWorkflowColumn, sourceWorkflowIds) + ) + ) + } + if (pairs.length === 0) return + const now = new Date() + await executor + .insert(workspaceForkBlockMap) + .values( + pairs.map((pair) => ({ + id: generateId(), + childWorkspaceId, + parentWorkflowId: pair.parentWorkflowId, + parentBlockId: pair.parentBlockId, + childWorkflowId: pair.childWorkflowId, + childBlockId: pair.childBlockId, + createdAt: now, + updatedAt: now, + })) + ) + .onConflictDoNothing() +} 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/dependent-reconfigs.test.ts b/apps/sim/lib/workspaces/fork/mapping/dependent-reconfigs.test.ts new file mode 100644 index 00000000000..7ed51e03b19 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/mapping/dependent-reconfigs.test.ts @@ -0,0 +1,516 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it, vi } from 'vitest' +import { + collectForkDependentReconfigs, + collectForkResourceUsages, +} from '@/lib/workspaces/fork/mapping/dependent-reconfigs' +import { + buildForkBlockIdResolver, + deriveForkBlockId, + EMPTY_FORK_BLOCK_MAP, +} from '@/lib/workspaces/fork/remap/block-identity' +import { getBlock } from '@/blocks/registry' +import type { BlockConfig, SubBlockConfig } from '@/blocks/types' +import type { WorkflowState } from '@/stores/workflows/workflow/types' + +const blockWith = (subBlocks: SubBlockConfig[]): BlockConfig => + ({ name: 'Test', description: '', subBlocks, outputs: {} }) as unknown as BlockConfig + +const sourceState = ( + blockType: string, + subBlocks: Record +): WorkflowState => + ({ + blocks: { 'block-1': { id: 'block-1', type: blockType, name: 'Block', subBlocks } }, + edges: [], + loops: {}, + parallels: {}, + variables: {}, + }) as unknown as WorkflowState + +const replaceItem = { + sourceWorkflowId: 'wf-src', + targetWorkflowId: 'wf-tgt', + mode: 'replace' as const, +} + +// No persisted block map in these unit tests, so the resolver derives - matching the +// `deriveForkBlockId(...)` ids the expectations assert. +const resolve = buildForkBlockIdResolver(true, EMPTY_FORK_BLOCK_MAP) + +describe('collectForkDependentReconfigs', () => { + it("emits the active operation's credential-dependent selector (condition-gated)", () => { + vi.mocked(getBlock).mockReturnValue( + blockWith([ + { id: 'credential', title: 'Credential', type: 'oauth-input' }, + { id: 'operation', title: 'Operation', type: 'dropdown' }, + { + id: 'folder', + title: 'Label', + type: 'folder-selector', + dependsOn: ['credential'], + selectorKey: 'gmail.labels', + required: true, + condition: { field: 'operation', value: 'read' }, + }, + // A different operation's variant -> excluded by its condition, not by emptiness. + { + id: 'otherFolder', + title: 'Move To Label', + type: 'folder-selector', + dependsOn: ['credential'], + selectorKey: 'gmail.labels', + condition: { field: 'operation', value: 'move' }, + }, + ]) + ) + const states = new Map([ + [ + 'wf-src', + sourceState('gmail', { + credential: { value: 'cred-src' }, + operation: { value: 'read' }, + folder: { value: 'INBOX' }, + }), + ], + ]) + const result = collectForkDependentReconfigs([replaceItem], states, resolve) + expect(result).toEqual([ + { + parentKind: 'credential', + parentSourceId: 'cred-src', + parentContextKey: 'oauthCredential', + targetWorkflowId: 'wf-tgt', + targetBlockId: deriveForkBlockId('wf-tgt', 'block-1'), + blockName: 'Block', + subBlockKey: 'folder', + selectorKey: 'gmail.labels', + title: 'Label', + currentValue: 'INBOX', + required: true, + consumesContextKeys: [], + context: {}, + }, + ]) + }) + + it('emits a knowledge-base-dependent document selector', () => { + vi.mocked(getBlock).mockReturnValue( + blockWith([ + { + id: 'knowledgeBaseSelector', + title: 'Knowledge Base', + type: 'knowledge-base-selector', + canonicalParamId: 'knowledgeBaseId', + }, + { + id: 'documentSelector', + title: 'Document', + type: 'document-selector', + canonicalParamId: 'documentId', + selectorKey: 'knowledge.documents', + dependsOn: ['knowledgeBaseSelector'], + required: true, + }, + ]) + ) + const states = new Map([ + [ + 'wf-src', + sourceState('knowledge', { + knowledgeBaseSelector: { value: 'kb-src' }, + documentSelector: { value: 'doc-src' }, + }), + ], + ]) + const result = collectForkDependentReconfigs([replaceItem], states, resolve) + expect(result).toEqual([ + { + parentKind: 'knowledge-base', + parentSourceId: 'kb-src', + parentContextKey: 'knowledgeBaseId', + targetWorkflowId: 'wf-tgt', + targetBlockId: deriveForkBlockId('wf-tgt', 'block-1'), + blockName: 'Block', + subBlockKey: 'documentSelector', + selectorKey: 'knowledge.documents', + title: 'Document', + currentValue: 'doc-src', + required: true, + consumesContextKeys: [], + context: {}, + }, + ]) + }) + + it('offers an active credential selector even when the source left it empty', () => { + vi.mocked(getBlock).mockReturnValue( + blockWith([ + { id: 'credential', title: 'Credential', type: 'oauth-input' }, + { + id: 'folder', + title: 'Label', + type: 'folder-selector', + dependsOn: ['credential'], + selectorKey: 'gmail.labels', + }, + ]) + ) + // The source has the credential but no label - the user must still be able to set one + // during the swap (a prior sync may have cleared it), so the selector is offered. + const states = new Map([ + [ + 'wf-src', + sourceState('gmail', { credential: { value: 'cred-src' }, folder: { value: '' } }), + ], + ]) + const result = collectForkDependentReconfigs([replaceItem], states, resolve) + expect(result).toHaveLength(1) + expect(result[0]).toMatchObject({ subBlockKey: 'folder', parentSourceId: 'cred-src' }) + }) + + it('still skips a selector whose parent credential is unset', () => { + vi.mocked(getBlock).mockReturnValue( + blockWith([ + { id: 'credential', title: 'Credential', type: 'oauth-input' }, + { + id: 'folder', + title: 'Label', + type: 'folder-selector', + dependsOn: ['credential'], + selectorKey: 'gmail.labels', + }, + ]) + ) + const states = new Map([ + ['wf-src', sourceState('gmail', { credential: { value: '' }, folder: { value: 'INBOX' } })], + ]) + expect(collectForkDependentReconfigs([replaceItem], states, resolve)).toEqual([]) + }) + + it('skips create-mode targets and credentialSet refs', () => { + vi.mocked(getBlock).mockReturnValue( + blockWith([ + { id: 'credential', title: 'Credential', type: 'oauth-input' }, + { + id: 'folder', + title: 'Label', + type: 'folder-selector', + dependsOn: ['credential'], + selectorKey: 'gmail.labels', + }, + ]) + ) + const created = new Map([ + [ + 'wf-src', + sourceState('gmail', { credential: { value: 'cred-src' }, folder: { value: 'INBOX' } }), + ], + ]) + expect( + collectForkDependentReconfigs( + [{ sourceWorkflowId: 'wf-src', targetWorkflowId: 'wf-tgt', mode: 'create' }], + created, + resolve + ) + ).toEqual([]) + + const orgSet = new Map([ + [ + 'wf-src', + sourceState('gmail', { + credential: { value: 'credentialSet:cs-1' }, + folder: { value: 'INBOX' }, + }), + ], + ]) + expect(collectForkDependentReconfigs([replaceItem], orgSet, resolve)).toEqual([]) + }) + + it('walks the transitive chain and tags the context key a re-pick provides', () => { + vi.mocked(getBlock).mockReturnValue( + blockWith([ + { id: 'credential', title: 'Credential', type: 'oauth-input' }, + { + id: 'spreadsheetId', + title: 'Spreadsheet', + type: 'file-selector', + canonicalParamId: 'spreadsheetId', + selectorKey: 'google.drive', + dependsOn: ['credential'], + }, + { + id: 'sheetName', + title: 'Sheet', + type: 'sheet-selector', + selectorKey: 'google.sheets', + dependsOn: ['spreadsheetId'], + required: true, + }, + ]) + ) + const states = new Map([ + [ + 'wf-src', + sourceState('google_sheets', { + credential: { value: 'cred-src' }, + spreadsheetId: { value: 'ss-src' }, + sheetName: { value: 'Sheet1' }, + }), + ], + ]) + const result = collectForkDependentReconfigs([replaceItem], states, resolve) + // Both the spreadsheet (direct) and its sheet (transitive) are offered, in order. + expect(result.map((entry) => entry.subBlockKey)).toEqual(['spreadsheetId', 'sheetName']) + const spreadsheet = result.find((entry) => entry.subBlockKey === 'spreadsheetId') + expect(spreadsheet?.parentKind).toBe('credential') + expect(spreadsheet?.providesContextKey).toBe('spreadsheetId') + const sheet = result.find((entry) => entry.subBlockKey === 'sheetName') + expect(sheet?.parentKind).toBe('credential') + expect(sheet?.required).toBe(true) + // The sheet consumes the spreadsheet's key, so the modal gates it on that re-pick. + expect(sheet?.consumesContextKeys).toEqual(['spreadsheetId']) + // The source spreadsheet rides in context; the modal overlays the re-picked one. + expect(sheet?.context.spreadsheetId).toBe('ss-src') + }) + + it('emits a credential-dependent selector nested inside a tool-input tool', () => { + vi.mocked(getBlock).mockImplementation((type) => { + if (type === 'agent') return blockWith([{ id: 'tools', title: 'Tools', type: 'tool-input' }]) + if (type === 'gmail') + return blockWith([ + { id: 'credential', title: 'Credential', type: 'oauth-input' }, + { + id: 'folder', + title: 'Label', + type: 'folder-selector', + dependsOn: ['credential'], + selectorKey: 'gmail.labels', + required: true, + }, + ]) + return undefined as unknown as BlockConfig + }) + const states = new Map([ + [ + 'wf-src', + sourceState('agent', { + tools: { + value: [ + { + type: 'gmail', + title: 'Gmail 1', + params: { credential: 'cred-src', folder: 'INBOX' }, + }, + ], + }, + }), + ], + ]) + const result = collectForkDependentReconfigs([replaceItem], states, resolve) + expect(result).toEqual([ + { + parentKind: 'credential', + parentSourceId: 'cred-src', + parentContextKey: 'oauthCredential', + targetWorkflowId: 'wf-tgt', + targetBlockId: deriveForkBlockId('wf-tgt', 'block-1'), + blockName: 'Block', + subBlockKey: 'tools[0].folder', + selectorKey: 'gmail.labels', + title: 'Gmail 1: Label', + currentValue: 'INBOX', + required: true, + consumesContextKeys: [], + context: {}, + }, + ]) + }) + + it('offers a nested tool selector even when the source left it empty', () => { + vi.mocked(getBlock).mockImplementation((type) => { + if (type === 'agent') return blockWith([{ id: 'tools', title: 'Tools', type: 'tool-input' }]) + if (type === 'gmail') + return blockWith([ + { id: 'credential', title: 'Credential', type: 'oauth-input' }, + { + id: 'folder', + title: 'Label', + type: 'folder-selector', + dependsOn: ['credential'], + selectorKey: 'gmail.labels', + }, + ]) + return undefined as unknown as BlockConfig + }) + const states = new Map([ + [ + 'wf-src', + sourceState('agent', { + tools: { + value: [ + { type: 'gmail', title: 'Gmail 1', params: { credential: 'cred-src', folder: '' } }, + ], + }, + }), + ], + ]) + const result = collectForkDependentReconfigs([replaceItem], states, resolve) + expect(result).toHaveLength(1) + expect(result[0]).toMatchObject({ subBlockKey: 'tools[0].folder', title: 'Gmail 1: Label' }) + }) + + it('evaluates a nested tool selector condition against the tool-level operation', () => { + vi.mocked(getBlock).mockImplementation((type) => { + if (type === 'agent') return blockWith([{ id: 'tools', title: 'Tools', type: 'tool-input' }]) + if (type === 'gmail') + return blockWith([ + { id: 'credential', title: 'Credential', type: 'oauth-input' }, + { + id: 'folder', + title: 'Label', + type: 'folder-selector', + dependsOn: ['credential'], + selectorKey: 'gmail.labels', + // Active only under read - and `operation` lives at the tool level, not params. + condition: { field: 'operation', value: 'read_gmail' }, + }, + ]) + return undefined as unknown as BlockConfig + }) + const reading = new Map([ + [ + 'wf-src', + sourceState('agent', { + tools: { + value: [ + { + type: 'gmail', + title: 'Gmail', + operation: 'read_gmail', + params: { credential: 'cred-src', folder: 'INBOX' }, + }, + ], + }, + }), + ], + ]) + expect(collectForkDependentReconfigs([replaceItem], reading, resolve)).toHaveLength(1) + + // Same tool under a different operation -> the read-only label is gated off. + const sending = new Map([ + [ + 'wf-src', + sourceState('agent', { + tools: { + value: [ + { + type: 'gmail', + title: 'Gmail', + operation: 'send_gmail', + params: { credential: 'cred-src', folder: 'INBOX' }, + }, + ], + }, + }), + ], + ]) + expect(collectForkDependentReconfigs([replaceItem], sending, resolve)).toEqual([]) + }) + + it('anchors on a table selector for its column dependents', () => { + vi.mocked(getBlock).mockReturnValue( + blockWith([ + { + id: 'tableSelector', + title: 'Table', + type: 'table-selector', + canonicalParamId: 'tableId', + }, + { + id: 'conflictColumnSelector', + title: 'Column', + type: 'column-selector', + canonicalParamId: 'conflictColumn', + selectorKey: 'table.columns', + dependsOn: ['tableSelector'], + }, + ]) + ) + const states = new Map([ + [ + 'wf-src', + sourceState('table', { + tableSelector: { value: 'tbl-src' }, + conflictColumnSelector: { value: 'col1' }, + }), + ], + ]) + const result = collectForkDependentReconfigs([replaceItem], states, resolve) + expect(result).toHaveLength(1) + expect(result[0]).toMatchObject({ + parentKind: 'table', + parentSourceId: 'tbl-src', + parentContextKey: 'tableId', + subBlockKey: 'conflictColumnSelector', + selectorKey: 'table.columns', + }) + }) +}) + +describe('collectForkResourceUsages', () => { + const usageItem = ( + sourceWorkflowId: string, + targetWorkflowId: string, + name: string, + mode: 'create' | 'replace' = 'replace' + ) => ({ sourceWorkflowId, targetWorkflowId, mode, sourceMeta: { name } }) + + // The reference scan reads each subblock entry's own `type`, so credential usages need + // typed entries (unlike the dependent collector, which keys off the block config). + const credentialState = (credentialId: string): WorkflowState => + ({ + blocks: { + 'block-1': { + id: 'block-1', + type: 'gmail', + name: 'Block', + subBlocks: { credential: { id: 'credential', type: 'oauth-input', value: credentialId } }, + }, + }, + edges: [], + loops: {}, + parallels: {}, + variables: {}, + }) as unknown as WorkflowState + + it('lists each replace workflow a resource is used in, with its (target) name', () => { + const states = new Map([ + ['wf-a', credentialState('cred-src')], + ['wf-b', credentialState('cred-src')], + ]) + const result = collectForkResourceUsages( + [usageItem('wf-a', 'wf-tgt-a', 'Workflow A'), usageItem('wf-b', 'wf-tgt-b', 'Workflow B')], + states + ) + expect(result).toEqual([ + { + parentKind: 'credential', + parentSourceId: 'cred-src', + workflows: [ + { workflowId: 'wf-tgt-a', workflowName: 'Workflow A' }, + { workflowId: 'wf-tgt-b', workflowName: 'Workflow B' }, + ], + }, + ]) + }) + + it('skips create-mode targets (the source config carries over on first sync)', () => { + const states = new Map([['wf-a', credentialState('cred-src')]]) + expect( + collectForkResourceUsages([usageItem('wf-a', 'wf-tgt-a', 'A', 'create')], states) + ).toEqual([]) + }) +}) diff --git a/apps/sim/lib/workspaces/fork/mapping/dependent-reconfigs.ts b/apps/sim/lib/workspaces/fork/mapping/dependent-reconfigs.ts new file mode 100644 index 00000000000..fa54d294e2d --- /dev/null +++ b/apps/sim/lib/workspaces/fork/mapping/dependent-reconfigs.ts @@ -0,0 +1,331 @@ +import type { ForkDependentReconfig, ForkResourceUsage } from '@/lib/api/contracts/workspace-fork' +import { coerceObjectArray, isRecord } from '@/lib/workflows/persistence/remap-internal-ids' +import { getWorkflowSearchDependentClears } from '@/lib/workflows/search-replace/dependencies' +import { + buildSelectorContextFromBlock, + SELECTOR_CONTEXT_FIELDS, +} from '@/lib/workflows/subblocks/context' +import { + buildCanonicalIndex, + buildSubBlockValues, + evaluateSubBlockCondition, +} from '@/lib/workflows/subblocks/visibility' +import type { ForkBlockIdResolver } from '@/lib/workspaces/fork/remap/block-identity' +import { + isSubBlockRequired, + scanWorkflowReferences, +} from '@/lib/workspaces/fork/remap/remap-references' +import { getBlock } from '@/blocks/registry' +import type { SubBlockConfig } from '@/blocks/types' +import { getDependsOnFields } from '@/blocks/utils' +import type { WorkflowState } from '@/stores/workflows/workflow/types' + +const isSelectorContextKey = ( + key: string +): key is Parameters[0] => + SELECTOR_CONTEXT_FIELDS.has(key as Parameters[0]) + +interface ReconfigItem { + sourceWorkflowId: string + targetWorkflowId: string + mode: 'create' | 'replace' +} + +/** + * Parent anchor types a dependent selector can hang off, with the SelectorContext key + * the new parent value is supplied under. A parent is a remappable resource (rewritten + * source->target on sync) whose target swap clears its dependents. MCP servers are + * intentionally excluded: their tool dependent has no `selectorKey` and a separate + * (non-`useSelectorOptions`) stack, so it falls back to the needs-config surfacing. + */ +const PARENT_ANCHORS: ReadonlyArray<{ + subBlockType: string + parentKind: ForkDependentReconfig['parentKind'] + parentContextKey: string +}> = [ + { subBlockType: 'oauth-input', parentKind: 'credential', parentContextKey: 'oauthCredential' }, + { + subBlockType: 'knowledge-base-selector', + parentKind: 'knowledge-base', + parentContextKey: 'knowledgeBaseId', + }, + { subBlockType: 'table-selector', parentKind: 'table', parentContextKey: 'tableId' }, +] + +interface EmitAnchoredParams { + /** The block (top-level) or tool config whose subblocks are scanned for anchors. */ + config: NonNullable> + /** Flat id -> value map for that config (top-level subblock values, or a tool's params). */ + values: Record + /** Block/tool type + its subblock shape, for building the source selector context. */ + contextBlockType: string + contextSubBlocks: Record + blockName: string + targetWorkflowId: string + /** Memoized so the deterministic target block id is derived at most once per block. */ + resolveTargetBlockId: () => string + /** Map a dependent's config id to its wire `subBlockKey` (identity, or nested `tools[i].id`). */ + makeSubBlockKey: (dependentId: string) => string + makeTitle: (dependent: SubBlockConfig) => string + /** + * Emit `providesContextKey`/`consumesContextKeys` so the modal can chain in-block + * re-picks. Top-level chains; nested tool params don't (a tool's chain would need + * per-tool context scoping - out of scope - and the common nested case is a single + * credential-anchored field). + */ + chaining: boolean + out: ForkDependentReconfig[] +} + +/** + * Emit one config's credential/KB/table-anchored selector dependents that the source had + * configured. Shared by the top-level subblock scan and the nested `tool-input` tool scan. + * Walks the FULL transitive dependent chain (parent -> child -> grandchild) per anchor and + * dedups fields reachable via multiple anchors/paths. + */ +function emitAnchoredDependents(params: EmitAnchoredParams): void { + const { + config, + values, + contextBlockType, + contextSubBlocks, + blockName, + targetWorkflowId, + resolveTargetBlockId, + makeSubBlockKey, + makeTitle, + chaining, + out, + } = params + const fullContext = buildSelectorContextFromBlock(contextBlockType, contextSubBlocks) + const canonicalIndex = buildCanonicalIndex(config.subBlocks) + const configById = new Map(config.subBlocks.filter((cfg) => cfg.id).map((cfg) => [cfg.id, cfg])) + // A field could hang off two anchors (or be reachable via two paths); emit it once. + const seen = new Set() + + for (const anchor of PARENT_ANCHORS) { + for (const anchorCfg of config.subBlocks) { + if (anchorCfg.type !== anchor.subBlockType || !anchorCfg.id) continue + const rawValue = values[anchorCfg.id] + const parentSourceId = typeof rawValue === 'string' ? rawValue : '' + // Skip empty and org-scoped credential sets (those carry over unchanged). + if (!parentSourceId || parentSourceId.startsWith('credentialSet:')) continue + // Multi-value parents (comma-joined) can't match a single mapping entry; skip + // (the field falls back to needs-config) rather than mis-bind to one of several. + if (parentSourceId.includes(',')) continue + + // Context the dependents need (spreadsheetId, ...) minus the parent key the modal supplies. + const context: Record = {} + for (const [key, value] of Object.entries(fullContext)) { + if (key === anchor.parentContextKey) continue + if (typeof value === 'string' && value) context[key] = value + } + + for (const clear of getWorkflowSearchDependentClears(config.subBlocks, anchorCfg.id)) { + const dependent = configById.get(clear.subBlockId) + if (!dependent?.id || !dependent.selectorKey) continue + // Skip fields gated off by their `condition` - a selector under a now-inactive + // operation (e.g. a move-only label while the block reads) isn't in play. We do + // NOT require a source value: an active selector the source left empty is still + // offered, so the user can set a label/sheet during the swap even when the source + // (or a prior sync) cleared it - the whole point of the in-place re-pick. + if (dependent.condition && !evaluateSubBlockCondition(dependent.condition, values)) continue + // The SelectorContext key this field supplies to its own descendants, so the + // modal can chain re-picks (re-picked spreadsheet feeds the sheet selector). + const canonicalKey = canonicalIndex.canonicalIdBySubBlockId[dependent.id] ?? dependent.id + // Dedup by canonical key so a basic/advanced pair (or two paths to the same field) + // is offered exactly once. + if (seen.has(canonicalKey)) continue + seen.add(canonicalKey) + const providesContextKey = + chaining && isSelectorContextKey(canonicalKey) ? canonicalKey : undefined + // The SelectorContext keys this field needs from in-block siblings (e.g. a sheet + // needs the spreadsheet), excluding the anchor key the modal already supplies, so + // the modal can keep a child disabled until its re-picked parent is chosen. + const consumesContextKeys = chaining + ? [ + ...new Set( + getDependsOnFields(dependent.dependsOn) + .map((parent) => canonicalIndex.canonicalIdBySubBlockId[parent] ?? parent) + .filter((key) => key !== anchor.parentContextKey && isSelectorContextKey(key)) + ), + ] + : [] + // Carry the selector's static `mimeType` filter (Drive/Sheets pickers) so the + // modal selector loads the same filtered list the editor would, not all files. + const dependentContext = + typeof dependent.mimeType === 'string' && dependent.mimeType + ? { ...context, mimeType: dependent.mimeType } + : context + out.push({ + parentKind: anchor.parentKind, + parentSourceId, + parentContextKey: anchor.parentContextKey, + targetWorkflowId, + targetBlockId: resolveTargetBlockId(), + blockName, + subBlockKey: makeSubBlockKey(dependent.id), + selectorKey: dependent.selectorKey, + title: makeTitle(dependent), + // Source value, so the always-on listing pre-fills a stable parent's selector. + currentValue: + typeof values[dependent.id] === 'string' ? (values[dependent.id] as string) : '', + required: isSubBlockRequired(dependent.required, values), + providesContextKey, + consumesContextKeys, + context: dependentContext, + }) + } + } + } +} + +/** + * Scan the source's deployed workflows for configured selector fields that `dependsOn` + * a remappable parent (a credential, knowledge base, or table) - the fields a sync clears + * whenever that parent's target changes. Covers top-level block subblocks AND selectors + * nested inside `tool-input` tools (Agent/tool blocks), so a Gmail tool's label inside an + * Agent block is offered for re-pick too. Each entry carries the deterministic target + * block id, the parent it hangs off (so the modal can bind it to the newly-chosen target), + * and the source-derived selector context. Every selector active for the source's current + * operation is emitted - including ones the source left empty - so the user can set a + * value in place during the swap even when the source (or a prior sync) had none; only + * selectors gated off by their `condition` (a different operation's variant) are skipped. + * Replace targets only: a freshly created target has nothing configured to swap yet. + * + * `resolveTargetBlockId` MUST be the same resolver `copyWorkflowStateIntoTarget` uses for + * this promote (see {@link buildForkBlockIdResolver}); otherwise the modal would key a + * re-pick by a derived id while the sync writes the block under its persisted counterpart, + * and the override would silently miss. + */ +export function collectForkDependentReconfigs( + items: ReconfigItem[], + sourceStates: Map, + resolveTargetBlockId: ForkBlockIdResolver +): ForkDependentReconfig[] { + const out: ForkDependentReconfig[] = [] + for (const item of items) { + if (item.mode !== 'replace') continue + const state = sourceStates.get(item.sourceWorkflowId) + if (!state) continue + for (const [sourceBlockId, block] of Object.entries(state.blocks)) { + const config = getBlock(block.type) + if (!config) continue + const subBlocks = (block.subBlocks ?? {}) as Record + const sourceValues = buildSubBlockValues(subBlocks) + let cachedTargetBlockId: string | null = null + const resolveBlockId = () => + (cachedTargetBlockId ??= resolveTargetBlockId(item.targetWorkflowId, sourceBlockId)) + + // Top-level credential/KB/table-anchored selectors. + emitAnchoredDependents({ + config, + values: sourceValues, + contextBlockType: block.type, + contextSubBlocks: subBlocks, + blockName: block.name, + targetWorkflowId: item.targetWorkflowId, + resolveTargetBlockId: resolveBlockId, + makeSubBlockKey: (id) => id, + makeTitle: (dependent) => dependent.title ?? dependent.id ?? '', + chaining: true, + out, + }) + + // Nested `tool-input` tools: each selected tool's own credential-anchored selectors, + // keyed `toolInput[index].paramId` (matching the needs-config key) and titled with the + // tool so the modal re-picks them under the same block card. + for (const cfg of config.subBlocks) { + if (cfg.type !== 'tool-input' || !cfg.id) continue + const { array: tools } = coerceObjectArray(subBlocks[cfg.id]?.value) + if (!tools) continue + for (let index = 0; index < tools.length; index++) { + const tool = tools[index] + if (!isRecord(tool) || typeof tool.type !== 'string') continue + const toolConfig = getBlock(tool.type) + if (!toolConfig) continue + const toolParams = isRecord(tool.params) ? tool.params : {} + // A tool's `operation` is stored at the tool level, not in params, but subblock + // conditions reference it (e.g. a Gmail label only under `read_gmail`). Merge it + // in so condition-gating matches the editor's `{ operation, ...params }`. + const toolValues = + typeof tool.operation === 'string' + ? { operation: tool.operation, ...toolParams } + : toolParams + const toolContextSubBlocks: Record = {} + for (const [key, value] of Object.entries(toolValues)) { + toolContextSubBlocks[key] = { value } + } + const toolLabel = + typeof tool.title === 'string' && tool.title ? tool.title : toolConfig.name + const toolInputKey = cfg.id + const toolIndex = index + emitAnchoredDependents({ + config: toolConfig, + values: toolValues, + contextBlockType: tool.type, + contextSubBlocks: toolContextSubBlocks, + blockName: block.name, + targetWorkflowId: item.targetWorkflowId, + resolveTargetBlockId: resolveBlockId, + makeSubBlockKey: (id) => `${toolInputKey}[${toolIndex}].${id}`, + makeTitle: (dependent) => `${toolLabel}: ${dependent.title ?? dependent.id ?? ''}`, + chaining: false, + out, + }) + } + } + } + } + return out +} + +interface ResourceUsageItem { + sourceWorkflowId: string + targetWorkflowId: string + mode: 'create' | 'replace' + /** Source workflow name, shown as the (renamed-aware) target name in the listing. */ + sourceMeta: { name: string } +} + +/** + * Every workflow each mapped resource (any kind) is used in - the spine of the always-on + * reconfigure listing under a mapping entry. Scans each source workflow's references + * (deduped per workflow, so a resource used by several blocks is one workflow usage) and + * groups them by `(kind, sourceId)`. Unlike {@link collectForkDependentReconfigs} this is + * NOT anchor-limited: it includes resources with no configurable dependent (env vars, files, + * a Gmail block with no active label) so the modal can still list - greyed - the workflows + * they appear in. Replace targets only, mirroring the dependent collector (a freshly created + * target carries the source config over and has nothing to reconcile yet). + */ +export function collectForkResourceUsages( + items: ResourceUsageItem[], + sourceStates: Map +): ForkResourceUsage[] { + const byResource = new Map() + for (const item of items) { + if (item.mode !== 'replace') continue + const state = sourceStates.get(item.sourceWorkflowId) + if (!state) continue + const blocks = Object.values(state.blocks).map((block) => ({ + id: block.id, + name: block.name, + subBlocks: block.subBlocks as unknown, + })) + // scanWorkflowReferences already dedups by `${kind}:${sourceId}` across the workflow, + // so each resource appears once per workflow here. + for (const reference of scanWorkflowReferences(blocks, () => null).references) { + const key = `${reference.kind}\u0000${reference.sourceId}` + let usage = byResource.get(key) + if (!usage) { + usage = { parentKind: reference.kind, parentSourceId: reference.sourceId, workflows: [] } + byResource.set(key, usage) + } + usage.workflows.push({ + workflowId: item.targetWorkflowId, + workflowName: item.sourceMeta.name, + }) + } + } + return Array.from(byResource.values()) +} diff --git a/apps/sim/lib/workspaces/fork/mapping/dependent-value-store.test.ts b/apps/sim/lib/workspaces/fork/mapping/dependent-value-store.test.ts new file mode 100644 index 00000000000..29bde91f43c --- /dev/null +++ b/apps/sim/lib/workspaces/fork/mapping/dependent-value-store.test.ts @@ -0,0 +1,131 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it, vi } from 'vitest' +import type { DbOrTx } from '@/lib/db/types' +import { + forkDependentValueKey, + loadForkDependentValues, + reconcileForkDependentValues, +} from '@/lib/workspaces/fork/mapping/dependent-value-store' + +describe('forkDependentValueKey', () => { + it('builds a stable triple key', () => { + expect(forkDependentValueKey('wf', 'blk', 'folder')).toBe('wf\u0000blk\u0000folder') + }) + + it("doesn't collide when an id contains a printable separator", () => { + // 'a:b' + 'c' must differ from 'a' + 'b:c' - the NUL separator guarantees it. + expect(forkDependentValueKey('a:b', 'c', 'd')).not.toBe(forkDependentValueKey('a', 'b:c', 'd')) + }) +}) + +describe('loadForkDependentValues', () => { + it('selects the edge rows', async () => { + const where = vi + .fn() + .mockResolvedValue([ + { targetWorkflowId: 'wf', targetBlockId: 'b', subBlockKey: 'folder', value: 'INBOX' }, + ]) + const from = vi.fn(() => ({ where })) + const executor = { select: vi.fn(() => ({ from })) } + const rows = await loadForkDependentValues(executor as unknown as DbOrTx, 'ws-1') + expect(executor.select).toHaveBeenCalledTimes(1) + expect(rows).toEqual([ + { targetWorkflowId: 'wf', targetBlockId: 'b', subBlockKey: 'folder', value: 'INBOX' }, + ]) + }) + + it('scopes the read to the given target workflows', async () => { + const where = vi.fn().mockResolvedValue([]) + const from = vi.fn(() => ({ where })) + const executor = { select: vi.fn(() => ({ from })) } + await loadForkDependentValues(executor as unknown as DbOrTx, 'ws-1', ['wf-1', 'wf-2']) + expect(executor.select).toHaveBeenCalledTimes(1) + expect(where).toHaveBeenCalledTimes(1) + }) + + it('short-circuits an empty target filter without querying', async () => { + const executor = { select: vi.fn() } + const rows = await loadForkDependentValues(executor as unknown as DbOrTx, 'ws-1', []) + expect(executor.select).not.toHaveBeenCalled() + expect(rows).toEqual([]) + }) +}) + +describe('reconcileForkDependentValues', () => { + function fakeExecutor() { + const deleteWhere = vi.fn().mockResolvedValue(undefined) + const insertValues = vi.fn().mockResolvedValue(undefined) + const executor = { + delete: vi.fn(() => ({ where: deleteWhere })), + insert: vi.fn(() => ({ values: insertValues })), + } + return { executor: executor as unknown as DbOrTx, deleteWhere, insertValues } + } + + it('deletes the given workflows then inserts only non-empty values', async () => { + const { executor, deleteWhere, insertValues } = fakeExecutor() + await reconcileForkDependentValues( + executor, + 'ws-1', + ['wf-1'], + [ + { targetWorkflowId: 'wf-1', targetBlockId: 'b1', subBlockKey: 'folder', value: 'INBOX' }, + { targetWorkflowId: 'wf-1', targetBlockId: 'b2', subBlockKey: 'folder', value: '' }, + ] + ) + expect(deleteWhere).toHaveBeenCalledTimes(1) + expect(insertValues).toHaveBeenCalledTimes(1) + const rows = insertValues.mock.calls[0][0] as Array> + expect(rows).toHaveLength(1) + expect(rows[0]).toMatchObject({ + childWorkspaceId: 'ws-1', + targetWorkflowId: 'wf-1', + targetBlockId: 'b1', + subBlockKey: 'folder', + value: 'INBOX', + }) + }) + + it('skips the delete when no workflows are given, and skips insert when all values are empty', async () => { + const { executor, deleteWhere, insertValues } = fakeExecutor() + await reconcileForkDependentValues( + executor, + 'ws-1', + [], + [{ targetWorkflowId: 'wf-1', targetBlockId: 'b1', subBlockKey: 'folder', value: '' }] + ) + expect(deleteWhere).not.toHaveBeenCalled() + expect(insertValues).not.toHaveBeenCalled() + }) + + it('clears a workflow (delete, no insert) when its full set is now empty', async () => { + const { executor, deleteWhere, insertValues } = fakeExecutor() + await reconcileForkDependentValues(executor, 'ws-1', ['wf-1'], []) + expect(deleteWhere).toHaveBeenCalledTimes(1) + expect(insertValues).not.toHaveBeenCalled() + }) + + it('dedupes duplicate field entries (last value wins) so a retried payload cannot trip the unique index', async () => { + const { executor, insertValues } = fakeExecutor() + await reconcileForkDependentValues( + executor, + 'ws-1', + ['wf-1'], + [ + { targetWorkflowId: 'wf-1', targetBlockId: 'b1', subBlockKey: 'folder', value: 'INBOX' }, + { targetWorkflowId: 'wf-1', targetBlockId: 'b1', subBlockKey: 'folder', value: 'SENT' }, + ] + ) + expect(insertValues).toHaveBeenCalledTimes(1) + const rows = insertValues.mock.calls[0][0] as Array> + expect(rows).toHaveLength(1) + expect(rows[0]).toMatchObject({ + targetWorkflowId: 'wf-1', + targetBlockId: 'b1', + subBlockKey: 'folder', + value: 'SENT', + }) + }) +}) diff --git a/apps/sim/lib/workspaces/fork/mapping/dependent-value-store.ts b/apps/sim/lib/workspaces/fork/mapping/dependent-value-store.ts new file mode 100644 index 00000000000..aa60dbdc102 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/mapping/dependent-value-store.ts @@ -0,0 +1,99 @@ +import { workspaceForkDependentValue } from '@sim/db/schema' +import { generateId } from '@sim/utils/id' +import { and, eq, inArray } from 'drizzle-orm' +import type { DbOrTx } from '@/lib/db/types' + +/** One stored dependent-field value for an edge. */ +export interface ForkDependentValue { + targetWorkflowId: string + targetBlockId: string + subBlockKey: string + value: string +} + +/** Stable key for a stored value (target workflow + block + subblock). */ +export function forkDependentValueKey( + targetWorkflowId: string, + targetBlockId: string, + subBlockKey: string +): string { + // NUL separators so ids/keys containing ':' can't be confused for a different triple. + return `${targetWorkflowId}\u0000${targetBlockId}\u0000${subBlockKey}` +} + +/** + * Load an edge's stored dependent values - the single source of truth for what each dependent + * selector (Gmail label, KB document, sheet tab) is set to. Consumed two ways: the diff + * overlays them as the modal's pre-filled value, and a promote applies them verbatim. Pass + * `targetWorkflowIds` to scope the read to a plan's replace targets (matching the + * `(childWorkspaceId, targetWorkflowId)` index) instead of loading the whole edge; an empty + * array short-circuits to no rows. + */ +export async function loadForkDependentValues( + executor: DbOrTx, + childWorkspaceId: string, + targetWorkflowIds?: string[] +): Promise { + if (targetWorkflowIds && targetWorkflowIds.length === 0) return [] + const where = targetWorkflowIds + ? and( + eq(workspaceForkDependentValue.childWorkspaceId, childWorkspaceId), + inArray(workspaceForkDependentValue.targetWorkflowId, targetWorkflowIds) + ) + : eq(workspaceForkDependentValue.childWorkspaceId, childWorkspaceId) + return executor + .select({ + targetWorkflowId: workspaceForkDependentValue.targetWorkflowId, + targetBlockId: workspaceForkDependentValue.targetBlockId, + subBlockKey: workspaceForkDependentValue.subBlockKey, + value: workspaceForkDependentValue.value, + }) + .from(workspaceForkDependentValue) + .where(where) +} + +/** + * Replace the stored dependent values for the given target workflows with `values` (the full + * set the modal sent). Deletes those workflows' rows first, then inserts the non-empty values, + * so the store always equals exactly what the user configured - cleared fields drop out, and + * blocks/fields that no longer exist are pruned. Empty values aren't stored (an empty store + * entry and a missing one mean the same thing: unset). + */ +export async function reconcileForkDependentValues( + executor: DbOrTx, + childWorkspaceId: string, + targetWorkflowIds: string[], + values: ForkDependentValue[] +): Promise { + if (targetWorkflowIds.length > 0) { + await executor + .delete(workspaceForkDependentValue) + .where( + and( + eq(workspaceForkDependentValue.childWorkspaceId, childWorkspaceId), + inArray(workspaceForkDependentValue.targetWorkflowId, targetWorkflowIds) + ) + ) + } + // Dedupe by the stored (workflow, block, subblock) triple (last value wins) before building + // insert rows, so a duplicated/retried payload entry can't trip the `..._field_unique` index + // and abort the whole sync transaction. Empty values aren't stored. + const deduped = new Map() + for (const entry of values) { + if (entry.value === '') continue + deduped.set( + forkDependentValueKey(entry.targetWorkflowId, entry.targetBlockId, entry.subBlockKey), + entry + ) + } + const rows = Array.from(deduped.values()).map((entry) => ({ + id: generateId(), + childWorkspaceId, + targetWorkflowId: entry.targetWorkflowId, + targetBlockId: entry.targetBlockId, + subBlockKey: entry.subBlockKey, + value: entry.value, + })) + if (rows.length === 0) return + await executor.insert(workspaceForkDependentValue).values(rows) +} 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..f349f3ea2c0 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/mapping/mapping-service.test.ts @@ -0,0 +1,182 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { ForkRemapKind } from '@/lib/workspaces/fork/remap/remap-references' + +const { mockFilterExisting, mockGetCredentialProviders, mockGetEnvKeys } = vi.hoisted(() => ({ + mockFilterExisting: vi.fn(), + mockGetCredentialProviders: vi.fn(), + mockGetEnvKeys: vi.fn(), +})) + +vi.mock('@/lib/workspaces/fork/mapping/resources', () => ({ + listForkResourceCandidates: vi.fn(), + classifyCredentialResourceType: vi.fn(), + getWorkspaceEnvKeys: mockGetEnvKeys, + filterExistingForkTargets: mockFilterExisting, + getCredentialProvidersByIds: mockGetCredentialProviders, + CANDIDATE_LIMIT: 1000, +})) + +import { ForkError } from '@/lib/workspaces/fork/lineage/authz' +import { + findDuplicateTargetEntry, + validateForkMappingTargets, +} from '@/lib/workspaces/fork/mapping/mapping-service' + +type ExistingByKind = Partial>> + +describe('validateForkMappingTargets', () => { + beforeEach(() => { + vi.clearAllMocks() + mockFilterExisting.mockResolvedValue({} as ExistingByKind) + mockGetEnvKeys.mockResolvedValue(new Set()) + mockGetCredentialProviders.mockResolvedValue(new Map()) + }) + + it('rejects a workflow-type entry with a target (identity is system-managed)', async () => { + await expect( + validateForkMappingTargets('ws-source', 'ws-target', [ + { resourceType: 'workflow', sourceId: 'wf-src', targetId: 'wf-tgt' }, + ]) + ).rejects.toBeInstanceOf(ForkError) + }) + + it('short-circuits without querying when no entry has a target', async () => { + await expect( + validateForkMappingTargets('ws-source', 'ws-target', [ + { resourceType: 'env_var', sourceId: 'API_KEY', targetId: null }, + ]) + ).resolves.toBeUndefined() + expect(mockFilterExisting).not.toHaveBeenCalled() + expect(mockGetEnvKeys).not.toHaveBeenCalled() + }) + + it('accepts an env-var whose target key exists in the target workspace', async () => { + mockGetEnvKeys.mockResolvedValue(new Set(['API_KEY'])) + await expect( + validateForkMappingTargets('ws-source', 'ws-target', [ + { resourceType: 'env_var', sourceId: 'API_KEY', targetId: 'API_KEY' }, + ]) + ).resolves.toBeUndefined() + }) + + it('rejects an env-var whose target key is not in the target workspace', async () => { + mockGetEnvKeys.mockResolvedValue(new Set()) + await expect( + validateForkMappingTargets('ws-source', 'ws-target', [ + { resourceType: 'env_var', sourceId: 'API_KEY', targetId: 'missing' }, + ]) + ).rejects.toBeInstanceOf(ForkError) + }) + + it('accepts a target validated by exact id even when picker lists are capped', async () => { + // filterExistingForkTargets checks by exact id (cap-free), so a target that would + // sit past the candidate cap still validates. + mockFilterExisting.mockResolvedValue({ table: new Set(['table-1001']) }) + await expect( + validateForkMappingTargets('ws-source', 'ws-target', [ + { resourceType: 'table', sourceId: 'table-src', targetId: 'table-1001' }, + ]) + ).resolves.toBeUndefined() + }) + + it('rejects a target that does not exist in the target workspace', async () => { + mockFilterExisting.mockResolvedValue({ table: new Set() }) + await expect( + validateForkMappingTargets('ws-source', 'ws-target', [ + { resourceType: 'table', sourceId: 'table-src', targetId: 'table-gone' }, + ]) + ).rejects.toBeInstanceOf(ForkError) + }) + + it('rejects a credential whose target provider differs from the source provider', async () => { + mockFilterExisting.mockResolvedValue({ credential: new Set(['cred-tgt']) }) + mockGetCredentialProviders.mockImplementation(async (_db: unknown, workspaceId: string) => + workspaceId === 'ws-source' + ? new Map([['cred-src', 'google-email']]) + : new Map([['cred-tgt', 'google-calendar']]) + ) + await expect( + validateForkMappingTargets('ws-source', 'ws-target', [ + { resourceType: 'oauth_credential', sourceId: 'cred-src', targetId: 'cred-tgt' }, + ]) + ).rejects.toBeInstanceOf(ForkError) + }) + + it('accepts a credential whose target provider matches the source provider', async () => { + mockFilterExisting.mockResolvedValue({ credential: new Set(['cred-tgt']) }) + mockGetCredentialProviders.mockImplementation(async (_db: unknown, workspaceId: string) => + workspaceId === 'ws-source' + ? new Map([['cred-src', 'google-email']]) + : new Map([['cred-tgt', 'google-email']]) + ) + await expect( + validateForkMappingTargets('ws-source', 'ws-target', [ + { resourceType: 'oauth_credential', sourceId: 'cred-src', targetId: 'cred-tgt' }, + ]) + ).resolves.toBeUndefined() + }) + + it('rejects a credential whose source is not a credential in the source workspace', async () => { + mockFilterExisting.mockResolvedValue({ credential: new Set(['cred-tgt']) }) + mockGetCredentialProviders.mockImplementation(async (_db: unknown, workspaceId: string) => + workspaceId === 'ws-source' + ? new Map() // cred-foreign is not in the source + : new Map([['cred-tgt', 'google-email']]) + ) + await expect( + validateForkMappingTargets('ws-source', 'ws-target', [ + { resourceType: 'oauth_credential', sourceId: 'cred-foreign', targetId: 'cred-tgt' }, + ]) + ).rejects.toBeInstanceOf(ForkError) + }) +}) + +describe('findDuplicateTargetEntry', () => { + it('returns null when every target is used by at most one source', () => { + expect( + findDuplicateTargetEntry([ + { resourceType: 'oauth_credential', sourceId: 'c1', targetId: 't1' }, + { resourceType: 'oauth_credential', sourceId: 'c2', targetId: 't2' }, + ]) + ).toBeNull() + }) + + it('flags two distinct sources mapped to the same target', () => { + expect( + findDuplicateTargetEntry([ + { resourceType: 'oauth_credential', sourceId: 'c1', targetId: 'shared' }, + { resourceType: 'oauth_credential', sourceId: 'c2', targetId: 'shared' }, + ]) + ).toEqual({ resourceType: 'oauth_credential', targetId: 'shared' }) + }) + + it('ignores cleared (null target) entries', () => { + expect( + findDuplicateTargetEntry([ + { resourceType: 'oauth_credential', sourceId: 'c1', targetId: null }, + { resourceType: 'oauth_credential', sourceId: 'c2', targetId: null }, + ]) + ).toBeNull() + }) + + it('does not flag the same source+target repeated', () => { + expect( + findDuplicateTargetEntry([ + { resourceType: 'table', sourceId: 'c1', targetId: 't1' }, + { resourceType: 'table', sourceId: 'c1', targetId: 't1' }, + ]) + ).toBeNull() + }) + + it('does not conflate the same target id across resource types', () => { + expect( + findDuplicateTargetEntry([ + { resourceType: 'oauth_credential', sourceId: 'c1', targetId: 'same' }, + { resourceType: 'table', sourceId: 'c2', targetId: 'same' }, + ]) + ).toBeNull() + }) +}) 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..a7700aea2d5 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/mapping/mapping-service.ts @@ -0,0 +1,421 @@ +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 ForkResourceType, + getEdgeMappingRows, + nonCredentialForkKindToResourceType, + resourceTypeToForkKind, + upsertEdgeMappings, +} from '@/lib/workspaces/fork/mapping/mapping-store' +import { + CANDIDATE_LIMIT, + classifyCredentialResourceType, + type ForkResourceCandidate, + filterExistingForkTargets, + getCredentialProvidersByIds, + 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()) + + // First pass: resolve each reference's stored target + the data to build its entry, + // collecting stored target ids so existence is checked by exact id (cap-free) - a + // valid mapping to a target past the display cap must be RETAINED, not shown unmapped. + interface PendingEntry { + reference: ForkReference + resourceType: Exclude + sourceLabel: string + sourceProviderId: string | undefined + candidates: ForkResourceCandidate[] + storedTargetId: string | null + } + const pending: PendingEntry[] = [] + const storedTargetIdsByKind: Partial>> = {} + + 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 sourceProviderId = sourceCandidate?.providerId + // A credential reference only maps to a target credential of the SAME OAuth + // provider - a Gmail (google-email) reference must never offer a Google Calendar + // credential. Non-credential kinds carry no provider, so their full list stands. + const candidates = + reference.kind === 'credential' && sourceProviderId + ? targetCandidates[reference.kind].filter( + (candidate) => candidate.providerId === sourceProviderId + ) + : targetCandidates[reference.kind] + const storedTargetId = resolver(reference.kind, reference.sourceId) ?? null + if (storedTargetId && reference.kind !== 'env-var') { + ;(storedTargetIdsByKind[reference.kind] ??= new Set()).add(storedTargetId) + } + pending.push({ + reference, + resourceType, + sourceLabel, + sourceProviderId, + candidates, + storedTargetId, + }) + } + + // Cap-free existence of every stored target (env vars validated against env keys). + const existingStoredTargets = await filterExistingForkTargets( + db, + targetWorkspaceId, + storedTargetIdsByKind + ) + + const entries: ForkMappingEntry[] = [] + for (const p of pending) { + const targetExists = + p.storedTargetId != null && + (p.reference.kind === 'env-var' + ? targetEnvKeys.has(p.storedTargetId) + : (existingStoredTargets[p.reference.kind]?.has(p.storedTargetId) ?? false)) + const currentTargetId = targetExists ? p.storedTargetId : null + + // If the retained current target isn't in the (capped) candidate list, append it + // so the picker can still display the current selection. + let candidates = p.candidates + if (currentTargetId && !candidates.some((candidate) => candidate.id === currentTargetId)) { + candidates = [...candidates, { id: currentTargetId, label: currentTargetId }] + } + + const targetId = + currentTargetId ?? + suggestTarget(p.reference.kind, p.sourceLabel, p.sourceProviderId, candidates) + // True when `targetId` is an unconfirmed name/provider suggestion (no persisted + // mapping). The modal treats a suggestion as a pending change so it shows the + // pre-sync reconfigure rather than letting an accepted suggestion silently clear + // dependents and surface them only after the sync. + const suggested = currentTargetId == null && targetId != null + + entries.push({ + kind: p.reference.kind, + resourceType: p.resourceType, + sourceId: p.reference.sourceId, + sourceLabel: p.sourceLabel, + targetId, + suggested, + required: p.reference.required, + candidates, + // The full (unfiltered) target list for this kind hit the cap, so the picker is + // showing a partial list - the UI tells the user to refine. + candidatesTruncated: targetCandidates[p.reference.kind].length >= CANDIDATE_LIMIT, + }) + } + + return { entries } +} + +export interface ApplyForkMappingEntry { + resourceType: ForkResourceType + sourceId: string + targetId: string | null +} + +/** + * The first target two distinct sources are mapped to (same resourceType + targetId, + * different sourceId), or null when every target is used by at most one source. Cleared + * entries (null target) are ignored. Used by the PUSH path only: a push row is unique on + * the parent (target) side, so such a pair collides on that unique index and one mapping + * would be silently dropped - the caller rejects it instead. Pull is the inverse (many + * parent sources may share one child target, which the resolver handles), so pull does not + * use this guard. + */ +export function findDuplicateTargetEntry( + entries: ApplyForkMappingEntry[] +): { resourceType: ForkResourceType; targetId: string } | null { + const sourcesByTarget = new Map>() + for (const entry of entries) { + if (entry.targetId == null) continue + // Null-byte separator so a targetId containing ':' (e.g. credentialSet:...) can't + // be confused with a different (resourceType, targetId) pair. + const key = `${entry.resourceType}\u0000${entry.targetId}` + const sources = sourcesByTarget.get(key) + if (!sources) { + sourcesByTarget.set(key, new Set([entry.sourceId])) + continue + } + sources.add(entry.sourceId) + if (sources.size > 1) return { resourceType: entry.resourceType, targetId: entry.targetId } + } + return 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 unique on the parent (target) side, so two distinct sources mapped to + // the same target would collide on that index and one would be silently dropped (its + // reference then resolves unmapped). Reject loudly - on push each parent target can back + // only one source. (Pull is the inverse: many parent sources may share one child target, + // which the resolver handles, so pull skips this guard. The modal also disables an + // already-taken target on push so users never reach this error normally.) + const collision = findDuplicateTargetEntry(entries) + if (collision) { + const kind = resourceTypeToForkKind(collision.resourceType) ?? collision.resourceType + throw new ForkError( + `Two sources are mapped to the same ${kind} target. Each target can be mapped from only one source.`, + 400 + ) + } + // 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. Credential targets must additionally share + * the source credential's OAuth provider, so a Gmail reference can never be pointed + * at a Google Calendar credential (the UI enforces this; this is the write-side + * boundary that catches direct API calls and stale rows). + */ +export async function validateForkMappingTargets( + sourceWorkspaceId: string, + targetWorkspaceId: string, + entries: ApplyForkMappingEntry[] +): Promise { + const withTarget = entries.filter((entry) => entry.targetId != null) + if (withTarget.length === 0) return + + // Collect the exact target ids per kind so existence is checked by id, NOT against + // the display-capped candidate list - a valid target that simply sits past the cap + // must never be rejected on save. + const targetIdsByKind: Partial>> = {} + let hasEnvVar = false + for (const entry of withTarget) { + 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. + throw new ForkError( + `Resource type "${entry.resourceType}" cannot be mapped via the mapping editor`, + 400 + ) + } + if (kind === 'env-var') { + hasEnvVar = true + continue + } + ;(targetIdsByKind[kind] ??= new Set()).add(entry.targetId as string) + } + + const credentialEntries = withTarget.filter( + (entry) => resourceTypeToForkKind(entry.resourceType) === 'credential' + ) + + const [existingTargets, targetEnvKeys, sourceProviders, targetProviders] = await Promise.all([ + filterExistingForkTargets(db, targetWorkspaceId, targetIdsByKind), + hasEnvVar ? getWorkspaceEnvKeys(db, targetWorkspaceId) : Promise.resolve(new Set()), + getCredentialProvidersByIds( + db, + sourceWorkspaceId, + credentialEntries.map((entry) => entry.sourceId) + ), + getCredentialProvidersByIds( + db, + targetWorkspaceId, + credentialEntries.map((entry) => entry.targetId as string) + ), + ]) + + for (const entry of withTarget) { + const kind = resourceTypeToForkKind(entry.resourceType) + if (!kind) continue + const targetId = entry.targetId as string + + if (kind === 'env-var') { + if (!targetEnvKeys.has(targetId)) { + throw new ForkError( + `Mapping target "${targetId}" is not an environment variable in the target workspace`, + 400 + ) + } + continue + } + + if (!existingTargets[kind]?.has(targetId)) { + throw new ForkError( + `Mapping target "${targetId}" is not a valid ${kind} in the target workspace`, + 400 + ) + } + + if (kind === 'credential') { + // The source must be a real credential in the source workspace. A foreign id + // (not present) would skip the provider check and let a crafted mapping drive + // cross-workspace credential-access propagation on promote. + if (!sourceProviders.has(entry.sourceId)) { + throw new ForkError( + `Source credential "${entry.sourceId}" is not a credential in the source workspace`, + 400 + ) + } + const sourceProviderId = sourceProviders.get(entry.sourceId) + const targetProviderId = targetProviders.get(targetId) ?? null + if (sourceProviderId && targetProviderId !== sourceProviderId) { + throw new ForkError( + `Mapping target "${targetId}" must use the same provider as the source credential`, + 400 + ) + } + } + } +} diff --git a/apps/sim/lib/workspaces/fork/mapping/mapping-store.test.ts b/apps/sim/lib/workspaces/fork/mapping/mapping-store.test.ts new file mode 100644 index 00000000000..491256729c1 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/mapping/mapping-store.test.ts @@ -0,0 +1,75 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { buildForkResolver, type ForkMappingRow } from '@/lib/workspaces/fork/mapping/mapping-store' + +const credentialRow: ForkMappingRow = { + id: 'm1', + childWorkspaceId: 'ws-child', + resourceType: 'oauth_credential', + parentResourceId: 'cred-parent', + childResourceId: 'cred-child', +} + +describe('buildForkResolver', () => { + it('resolves source->target for a pull (source is parent)', () => { + const resolve = buildForkResolver([credentialRow], { sourceIsParent: true }) + expect(resolve('credential', 'cred-parent')).toBe('cred-child') + }) + + it('resolves source->target for a push (source is child)', () => { + const resolve = buildForkResolver([credentialRow], { sourceIsParent: false }) + expect(resolve('credential', 'cred-child')).toBe('cred-parent') + }) + + it('skips unmapped rows (null childResourceId)', () => { + const resolve = buildForkResolver([{ ...credentialRow, childResourceId: null }], { + sourceIsParent: true, + }) + expect(resolve('credential', 'cred-parent')).toBeNull() + }) + + it('drops a mapped target that no longer exists in the target workspace', () => { + const resolve = buildForkResolver([credentialRow], { + sourceIsParent: true, + // target cred-child was deleted after the mapping was saved + validTargetIdsByKind: { credential: new Set() }, + }) + expect(resolve('credential', 'cred-parent')).toBeNull() + }) + + it('keeps a mapped target that still exists in the target workspace', () => { + const resolve = buildForkResolver([credentialRow], { + sourceIsParent: true, + validTargetIdsByKind: { credential: new Set(['cred-child']) }, + }) + expect(resolve('credential', 'cred-parent')).toBe('cred-child') + }) + + it('does not existence-check kinds absent from validTargetIdsByKind', () => { + const resolve = buildForkResolver([credentialRow], { + sourceIsParent: true, + validTargetIdsByKind: { table: new Set() }, + }) + expect(resolve('credential', 'cred-parent')).toBe('cred-child') + }) + + it('falls back to identity for a workspace env key present in the target', () => { + const resolve = buildForkResolver([], { + sourceIsParent: true, + sourceEnvKeys: new Set(['API_KEY']), + targetEnvKeys: new Set(['API_KEY']), + }) + expect(resolve('env-var', 'API_KEY')).toBe('API_KEY') + }) + + it('leaves a personal (non-source-workspace) env key as-is', () => { + const resolve = buildForkResolver([], { + sourceIsParent: true, + sourceEnvKeys: new Set(['WORKSPACE_KEY']), + targetEnvKeys: new Set(), + }) + expect(resolve('env-var', 'PERSONAL_KEY')).toBe('PERSONAL_KEY') + }) +}) 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..c37016584b0 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/mapping/mapping-store.ts @@ -0,0 +1,309 @@ +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 + /** + * Target ids that still EXIST in the target workspace, per kind, among the mapped + * targets. When a kind is present, a mapped target NOT in its set is treated as + * unmapped (the target was deleted after the mapping was saved), so a dead id is + * never written into the promoted workflow. Kinds absent here are not existence- + * checked (resolved as before). + */ + validTargetIdsByKind?: Partial>> +} + +/** + * 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) { + const validSet = options.validTargetIdsByKind?.[kind] + if (!validSet || validSet.has(mapped)) return mapped + // The mapped target was deleted from the target workspace after the mapping was + // saved. Fall through so the reference resolves as unmapped (surfaced as required + // / cleared if optional) instead of writing a dead id into the promoted workflow. + } + 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..0fa56338d95 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/mapping/resources.ts @@ -0,0 +1,366 @@ +import { + credential, + customTools, + knowledgeBase, + mcpServers, + skill, + userTableDefinitions, + workflow, + workflowDeploymentVersion, + workspaceEnvironment, + workspaceFiles, +} from '@sim/db/schema' +import { and, count, eq, exists, inArray, 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 +} + +export 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)) +} + +// Shared `{ id, label }` candidate queries for the content resource kinds that BOTH the +// mapping-target picker and the fork-copy picker list - one source of the archived/deleted +// filters so the two pickers can never drift apart. Credentials, env vars (mapping-only), +// and files (copy-only) stay inline in their respective functions. +const tableCandidatesQuery = (executor: DbOrTx, workspaceId: string) => + executor + .select({ id: userTableDefinitions.id, label: userTableDefinitions.name }) + .from(userTableDefinitions) + .where( + and( + eq(userTableDefinitions.workspaceId, workspaceId), + isNull(userTableDefinitions.archivedAt) + ) + ) + .limit(CANDIDATE_LIMIT) + +const knowledgeBaseCandidatesQuery = (executor: DbOrTx, workspaceId: string) => + executor + .select({ id: knowledgeBase.id, label: knowledgeBase.name }) + .from(knowledgeBase) + .where(and(eq(knowledgeBase.workspaceId, workspaceId), isNull(knowledgeBase.deletedAt))) + .limit(CANDIDATE_LIMIT) + +const customToolCandidatesQuery = (executor: DbOrTx, workspaceId: string) => + executor + .select({ id: customTools.id, label: customTools.title }) + .from(customTools) + .where(eq(customTools.workspaceId, workspaceId)) + .limit(CANDIDATE_LIMIT) + +const skillCandidatesQuery = (executor: DbOrTx, workspaceId: string) => + executor + .select({ id: skill.id, label: skill.name }) + .from(skill) + .where(eq(skill.workspaceId, workspaceId)) + .limit(CANDIDATE_LIMIT) + +const mcpServerCandidatesQuery = (executor: DbOrTx, workspaceId: string) => + executor + .select({ id: mcpServers.id, label: mcpServers.name }) + .from(mcpServers) + .where(and(eq(mcpServers.workspaceId, workspaceId), isNull(mcpServers.deletedAt))) + .limit(CANDIDATE_LIMIT) + +/** + * 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) + // Only real connections are mappable credentials. `env_workspace`/`env_personal` + // rows live in the same table but are environment variables (surfaced via the + // 'env-var' kind), so they must never appear as credential targets. + .where( + and( + eq(credential.workspaceId, workspaceId), + inArray(credential.type, ['oauth', 'service_account']) + ) + ) + .limit(CANDIDATE_LIMIT), + executor + .select({ variables: workspaceEnvironment.variables }) + .from(workspaceEnvironment) + .where(eq(workspaceEnvironment.workspaceId, workspaceId)) + .limit(1), + tableCandidatesQuery(executor, workspaceId), + knowledgeBaseCandidatesQuery(executor, workspaceId), + mcpServerCandidatesQuery(executor, workspaceId), + customToolCandidatesQuery(executor, workspaceId), + skillCandidatesQuery(executor, workspaceId), + ]) + + 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, + 'knowledge-base': kbs, + 'mcp-server': servers, + 'custom-tool': tools, + skill: skills, + 'knowledge-document': [], + file: [], + } +} + +/** + * Given mapped target ids grouped by kind, return the subset that still EXISTS in the + * target workspace (same archived/deleted filters as `listForkResourceCandidates`). + * Used at promote time so a mapping whose target was deleted after it was saved + * resolves as unmapped (surfaced/cleared) instead of writing a dead id into the + * promoted workflow. Queries the exact ids (not the capped candidate list) so a valid + * target is never wrongly dropped, and only the DB-backed kinds are checked - env-var + * existence is handled by the resolver's `targetEnvKeys`, and `file`/`workflow` are + * resolved by other paths. + */ +export async function filterExistingForkTargets( + executor: DbOrTx, + workspaceId: string, + idsByKind: Partial>> +): Promise>>> { + const ids = (kind: ForkRemapKind): string[] => { + const set = idsByKind[kind] + return set && set.size > 0 ? Array.from(set) : [] + } + const credIds = ids('credential') + const tableIds = ids('table') + const kbIds = ids('knowledge-base') + const mcpIds = ids('mcp-server') + const toolIds = ids('custom-tool') + const skillIds = ids('skill') + + const [creds, tables, kbs, servers, tools, skills] = await Promise.all([ + credIds.length === 0 + ? Promise.resolve([] as Array<{ id: string }>) + : executor + .select({ id: credential.id }) + .from(credential) + .where( + and( + eq(credential.workspaceId, workspaceId), + inArray(credential.type, ['oauth', 'service_account']), + inArray(credential.id, credIds) + ) + ), + tableIds.length === 0 + ? Promise.resolve([] as Array<{ id: string }>) + : executor + .select({ id: userTableDefinitions.id }) + .from(userTableDefinitions) + .where( + and( + eq(userTableDefinitions.workspaceId, workspaceId), + isNull(userTableDefinitions.archivedAt), + inArray(userTableDefinitions.id, tableIds) + ) + ), + kbIds.length === 0 + ? Promise.resolve([] as Array<{ id: string }>) + : executor + .select({ id: knowledgeBase.id }) + .from(knowledgeBase) + .where( + and( + eq(knowledgeBase.workspaceId, workspaceId), + isNull(knowledgeBase.deletedAt), + inArray(knowledgeBase.id, kbIds) + ) + ), + mcpIds.length === 0 + ? Promise.resolve([] as Array<{ id: string }>) + : executor + .select({ id: mcpServers.id }) + .from(mcpServers) + .where( + and( + eq(mcpServers.workspaceId, workspaceId), + isNull(mcpServers.deletedAt), + inArray(mcpServers.id, mcpIds) + ) + ), + toolIds.length === 0 + ? Promise.resolve([] as Array<{ id: string }>) + : executor + .select({ id: customTools.id }) + .from(customTools) + .where(and(eq(customTools.workspaceId, workspaceId), inArray(customTools.id, toolIds))), + skillIds.length === 0 + ? Promise.resolve([] as Array<{ id: string }>) + : executor + .select({ id: skill.id }) + .from(skill) + .where(and(eq(skill.workspaceId, workspaceId), inArray(skill.id, skillIds))), + ]) + + const result: Partial>> = {} + if (credIds.length > 0) result.credential = new Set(creds.map((r) => r.id)) + if (tableIds.length > 0) result.table = new Set(tables.map((r) => r.id)) + if (kbIds.length > 0) result['knowledge-base'] = new Set(kbs.map((r) => r.id)) + if (mcpIds.length > 0) result['mcp-server'] = new Set(servers.map((r) => r.id)) + if (toolIds.length > 0) result['custom-tool'] = new Set(tools.map((r) => r.id)) + if (skillIds.length > 0) result.skill = new Set(skills.map((r) => r.id)) + return result +} + +/** + * Provider id for each given credential id in a workspace, looked up by exact id (no + * candidate cap). Presence in the returned map means the credential exists in the + * workspace, so this doubles as a cap-free existence + provider check for validation. + */ +export async function getCredentialProvidersByIds( + executor: DbOrTx, + workspaceId: string, + ids: string[] +): Promise> { + if (ids.length === 0) return new Map() + const rows = await executor + .select({ id: credential.id, providerId: credential.providerId }) + .from(credential) + .where( + and( + eq(credential.workspaceId, workspaceId), + inArray(credential.type, ['oauth', 'service_account']), + inArray(credential.id, ids) + ) + ) + return new Map(rows.map((row) => [row.id, row.providerId ?? null])) +} + +export interface ForkCopyableResources { + files: ForkResourceCandidate[] + tables: ForkResourceCandidate[] + knowledgeBases: ForkResourceCandidate[] + customTools: ForkResourceCandidate[] + skills: ForkResourceCandidate[] + mcpServers: ForkResourceCandidate[] + /** + * Count of deployed workflows that the fork would copy. When 0, the fork modal shows an + * informational note (forking is never blocked) - create-fork seeds a blank starter + * workflow so the child is still a usable 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) + // Only durable workspace files are forkable - chat/copilot/mothership uploads are + // session-scoped attachments (and their chat-bound unique index can't be copied). + .where( + and( + eq(workspaceFiles.workspaceId, workspaceId), + eq(workspaceFiles.context, 'workspace'), + isNull(workspaceFiles.deletedAt) + ) + ) + .limit(CANDIDATE_LIMIT), + tableCandidatesQuery(executor, workspaceId), + knowledgeBaseCandidatesQuery(executor, workspaceId), + customToolCandidatesQuery(executor, workspaceId), + skillCandidatesQuery(executor, workspaceId), + mcpServerCandidatesQuery(executor, workspaceId), + executor + .select({ value: count() }) + .from(workflow) + // Match listDeployedWorkflows: a workflow only counts as copyable when it has an + // actually-active deployment version, not just the isDeployed flag, so the fork + // modal's preflight count never over-reports "ghost" deployed workflows. + .where( + and( + eq(workflow.workspaceId, workspaceId), + eq(workflow.isDeployed, true), + isNull(workflow.archivedAt), + exists( + executor + .select({ one: sql`1` }) + .from(workflowDeploymentVersion) + .where( + and( + eq(workflowDeploymentVersion.workflowId, workflow.id), + eq(workflowDeploymentVersion.isActive, true) + ) + ) + ) + ) + ), + ]) + 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..e0dde4eb7db --- /dev/null +++ b/apps/sim/lib/workspaces/fork/promote/promote-plan.ts @@ -0,0 +1,282 @@ +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, + resourceTypeToForkKind, +} from '@/lib/workspaces/fork/mapping/mapping-store' +import { + filterExistingForkTargets, + getWorkspaceEnvKeys, +} from '@/lib/workspaces/fork/mapping/resources' +import { + type ForkReference, + type ForkReferenceResolver, + type ForkRemapKind, + 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 +} + +/** + * 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 + + // Collect each mapping's chosen target id (per kind) and keep only those that still + // exist in the target workspace, so a target deleted after the mapping was saved + // resolves as unmapped instead of writing a dead id into the promoted workflow. + const mappedTargetIdsByKind: Partial>> = {} + for (const row of mappingRows) { + const kind = resourceTypeToForkKind(row.resourceType) + if (!kind) continue + const targetId = sourceIsParent ? row.childResourceId : row.parentResourceId + if (targetId == null) continue + const set = mappedTargetIdsByKind[kind] ?? new Set() + set.add(targetId) + mappedTargetIdsByKind[kind] = set + } + const validTargetIdsByKind = await filterExistingForkTargets( + executor, + targetWorkspaceId, + mappedTargetIdsByKind + ) + + const resolver = buildForkResolver(mappingRows, { + sourceIsParent, + targetEnvKeys, + sourceEnvKeys, + validTargetIdsByKind, + }) + + 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 }) + .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 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, + } +} 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..f263bfcaa6f --- /dev/null +++ b/apps/sim/lib/workspaces/fork/promote/promote-run-store.ts @@ -0,0 +1,128 @@ +import { workspaceForkPromoteRun } from '@sim/db/schema' +import { generateId } from '@sim/utils/id' +import { 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 +} + +/** 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..9694f279bf5 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/promote/promote.ts @@ -0,0 +1,767 @@ +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 type { DbOrTx } from '@/lib/db/types' +import { + enqueueWorkflowUndeploySideEffects, + processWorkflowDeploymentOutboxEvent, +} from '@/lib/workflows/deployment-outbox' +import { performFullDeploy } from '@/lib/workflows/orchestration/deploy' +import { undeployWorkflow } from '@/lib/workflows/persistence/utils' +import { + copyWorkflowStateIntoTarget, + loadTargetDraftSubBlocks, + loadWorkflowNameRegistry, + resolveForkFolderMapping, +} from '@/lib/workspaces/fork/copy/copy-workflows' +import { + getActiveDeploymentVersionNumbers, + loadSourceDeployedStates, +} from '@/lib/workspaces/fork/copy/deploy-bridge' +import { + acquireForkEdgeLock, + acquireForkTargetLock, + type ForkEdge, + setForkLockTimeout, +} from '@/lib/workspaces/fork/lineage/lineage' +import { + type ForkBlockPair, + loadForkBlockMap, + reconcileForkBlockPairs, + toForkBlockPairs, +} from '@/lib/workspaces/fork/mapping/block-map-store' +import { + type ForkDependentValue, + loadForkDependentValues, + reconcileForkDependentValues, +} from '@/lib/workspaces/fork/mapping/dependent-value-store' +import { + deleteWorkflowIdentityByIds, + 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 { buildForkBlockIdResolver } from '@/lib/workspaces/fork/remap/block-identity' +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' + userId: string + /** + * The full stored mapping of dependent-field values the caller is committing (target + * workflow id + deterministic block id + subblock key -> value). Applied to the target + * blocks during the merge and persisted as the stored mapping. OMITTING the field (passing + * `undefined`) leaves the existing stored mapping untouched - the store is the sole source + * of truth and is loaded + applied as-is; an explicit `[]` clears the written replace + * targets' mapping. This distinction keeps a programmatic promote that omits the field from + * silently wiping the user's saved selections. + */ + dependentValues?: Array<{ + workflowId: string + blockId: string + subBlockKey: string + value: string + }> + requestId?: string +} + +export interface PromoteForkResult { + promoteRunId: string + updated: number + created: number + archived: number + redeployed: number + /** + * Targets whose state was written but whose post-transaction deploy failed. The + * draft holds the synced state; the active deployment still runs the prior version + * until a redeploy. Surfaced (rather than swallowed) so the caller can warn. + */ + deployFailed: number + unmappedRequired: Array> + blocked: 'unmapped' | null + /** Names of the workflows the sync changed, by action, for the activity report. */ + updatedNames: string[] + createdNames: string[] + archivedNames: string[] + /** + * Workflows whose required dependent fields a parent change cleared - the target + * must re-pick them. These were written but intentionally NOT redeployed (the prior + * version keeps running), so the sync never deploys a broken workflow. + */ + needsConfiguration: Array<{ workflowName: string; blocks: string[] }> + /** + * Workflows whose OPTIONAL dependent fields a parent change cleared (e.g. a trigger + * label filter). Redeployed as-is, but surfaced so a cleared filter that would broaden + * behavior is never silent. + */ + clearedOptional: Array<{ workflowName: string; blocks: string[] }> +} + +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()) +} + +/** + * Grant each mapped target credential the same active members its source credential has, + * intersected with the target workspace's members, so a promoted workflow's collaborators + * can use the remapped credential. Both sides are validated to actually exist in their + * workspace first (so a crafted/stale mapping can't drive cross-workspace access), member + * reads are batched, and inserts are conflict-safe. Side-effect only on `credentialMember`. + */ +async function propagateCredentialAccess( + tx: DbOrTx, + params: { + plan: ForkPromotePlan + sourceWorkspaceId: string + targetWorkspaceId: string + targetMembers: string[] + now: Date + } +): Promise { + const { plan, sourceWorkspaceId, targetWorkspaceId, targetMembers, now } = params + const credentialPairs = collectCredentialPairs(plan) + const propagationTargetIds = credentialPairs.map(([, targetCredId]) => targetCredId) + const propagationSourceIds = credentialPairs.map(([sourceCredId]) => sourceCredId) + + 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 validSourceCredentialIds = new Set() + if (propagationSourceIds.length > 0) { + const validRows = await tx + .select({ id: credential.id }) + .from(credential) + .where( + and( + inArray(credential.id, propagationSourceIds), + eq(credential.workspaceId, sourceWorkspaceId) + ) + ) + for (const row of validRows) validSourceCredentialIds.add(row.id) + } + + const validPairs = credentialPairs.filter( + ([sourceCredId, targetCredId]) => + validSourceCredentialIds.has(sourceCredId) && validTargetCredentialIds.has(targetCredId) + ) + if (validPairs.length === 0) return + + // 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], + }) + } +} + +interface PromoteTxBlocked { + blocked: 'unmapped' + unmappedRequired: PromoteForkResult['unmappedRequired'] +} + +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 + /** Source workflows skipped because their deployment vanished between plan and apply. */ + skippedItems: Array<{ id: string; name: string }> + /** Target workflow id -> source name, so the deploy-failure report can show names. */ + writtenNames: Record + /** Names of the changed workflows, by action, for the activity report. */ + updatedNames: string[] + createdNames: string[] + archivedNames: string[] + /** Outbox event ids enqueued for archived orphans' undeploy side-effects. */ + undeployEventIds: string[] + /** + * Per-target required dependents a parent change cleared (with workflow id so the + * post-commit deploy loop can skip them, keeping the prior version running). + */ + needsConfiguration: Array<{ workflowId: string; workflowName: string; blocks: string[] }> + /** Per-workflow optional dependents a parent change cleared (surfaced, not gated). */ + clearedOptional: Array<{ workflowName: string; blocks: string[] }> +} + +/** + * Group flat dependent values into the apply map `target workflow -> block id -> subblock -> value` + * that {@link copyWorkflowStateIntoTarget} consumes. Pure (no DB), so the provided-value path can + * build it before the transaction (it doesn't depend on the plan); the omitted path feeds it the + * loaded store rows inside the tx, where the plan's replace targets are known. + */ +function groupDependentOverrides( + values: ForkDependentValue[] +): Map>> { + const byWorkflow = new Map>>() + for (const entry of values) { + let byBlock = byWorkflow.get(entry.targetWorkflowId) + if (!byBlock) { + byBlock = new Map() + byWorkflow.set(entry.targetWorkflowId, byBlock) + } + let byKey = byBlock.get(entry.targetBlockId) + if (!byKey) { + byKey = new Map() + byBlock.set(entry.targetBlockId, byKey) + } + byKey.set(entry.subBlockKey, entry.value) + } + return byWorkflow +} + +/** + * 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. A sync always force-replaces the + * target's deployed state (the modal confirms the overwrite up front); it blocks + * without mutating only when required references are unmapped. + */ +export async function promoteFork(params: PromoteForkParams): Promise { + const { edge, sourceWorkspaceId, targetWorkspaceId, direction, userId } = params + const requestId = params.requestId ?? 'unknown' + + // Distinguish an OMITTED dependent mapping (leave the store as-is) from an explicit empty + // array (clear it). When values are PROVIDED the apply map is plan-independent, so build it + // here - BEFORE the transaction - to keep the advisory lock tight (pure in-memory, no DB), + // mirroring how the source states are pre-loaded above. The OMITTED path needs the plan's + // replace targets, so it loads + builds inside the tx below. + const dependentValuesProvided = params.dependentValues !== undefined + const providedOverridesByWorkflow = dependentValuesProvided + ? groupDependentOverrides( + (params.dependentValues ?? []).map((entry) => ({ + targetWorkflowId: entry.workflowId, + targetBlockId: entry.blockId, + subBlockKey: entry.subBlockKey, + value: entry.value, + })) + ) + : null + + 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) => { + // Bound lock waits so a contended sync into this target fails fast instead of + // stagnating the pool. Must run before acquiring the advisory locks below. + await setForkLockTimeout(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, + })), + } + } + + const now = new Date() + const transform = createForkSubBlockTransform(plan.resolver) + const folderIdMap = await resolveForkFolderMapping({ + tx, + sourceWorkspaceId, + targetWorkspaceId, + userId, + now, + }) + + // Batch every prior-version read (replace + archive targets) into one query before any + // write, so the locked apply phase doesn't do N round-trips. Reads are pre-write, so + // they still reflect the active version each target had before this sync. + const priorVersionByTarget = await getActiveDeploymentVersionNumbers(tx, [ + ...plan.items.filter((item) => item.mode === 'replace').map((item) => item.targetWorkflowId), + ...plan.archivedTargetIds, + ]) + + // Preload the target's active workflow names so per-workflow collision checks read from + // memory instead of one query each inside this locked tx. The DB unique index remains + // the correctness backstop (a stale snapshot only risks a rare, retry-able conflict). + const nameRegistry = await loadWorkflowNameRegistry(tx, targetWorkspaceId) + + // Replace targets (the only mode with a prior target state) - reused by the draft preload + // and the dependent-value apply/load below. + const replaceTargetIds = plan.items + .filter((item) => item.mode === 'replace') + .map((item) => item.targetWorkflowId) + + // Preload the target's current draft subBlocks (replace targets only) so the copy can + // detect dependent fields a parent change cleared that the stored mapping didn't refill + // (surfaced as needs-configuration). One batched query pre-write, so it reflects the + // pre-sync target state. + const targetDraftByWorkflow = await loadTargetDraftSubBlocks(tx, replaceTargetIds) + + // The dependent-value apply map (target workflow -> block id -> subblock -> value). When the + // caller PROVIDED values it was built pre-tx (plan-independent); apply it and reconcile the + // store below. When OMITTED the store is the sole source of truth - load the existing values + // for the plan's replace targets (one indexed query) and build the map, skipping the reconcile + // below so an omitted field never wipes the saved mapping. + const overridesByWorkflow = + providedOverridesByWorkflow ?? + groupDependentOverrides( + await loadForkDependentValues(tx, edge.childWorkspaceId, replaceTargetIds) + ) + + // Resolve each source block to its counterpart's EXISTING id (via the persisted block + // map) instead of re-deriving, so a push keeps the parent's original block ids - and the + // webhook URLs derived from them - stable. Falls back to derive for blocks with no pair + // yet (added since the last sync), which are recorded below. + const sourceIsParent = sourceWorkspaceId === edge.parentWorkspaceId + const blockMap = await loadForkBlockMap(tx, edge.childWorkspaceId) + const resolveBlockId = buildForkBlockIdResolver(sourceIsParent, blockMap) + const blockPairs: ForkBlockPair[] = [] + + const updatedSnapshots: PromoteRunWorkflowSnapshot[] = [] + const createdTargetIds: string[] = [] + const writtenItems: typeof plan.items = [] + const needsConfiguration: PromoteTxApplied['needsConfiguration'] = [] + const clearedOptional: PromoteTxApplied['clearedOptional'] = [] + 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 = priorVersionByTarget.get(item.targetWorkflowId) ?? null + updatedSnapshots.push({ workflowId: item.targetWorkflowId, priorVersion }) + } else { + createdTargetIds.push(item.targetWorkflowId) + } + const copyResult = await copyWorkflowStateIntoTarget({ + tx, + targetWorkflowId: item.targetWorkflowId, + targetWorkspaceId, + userId, + mode: item.mode, + now, + sourceState, + sourceMeta: item.sourceMeta, + workflowIdMap: plan.workflowIdMap, + folderIdMap, + transformSubBlocks: transform, + targetCurrentBlocks: + item.mode === 'replace' ? targetDraftByWorkflow.get(item.targetWorkflowId) : undefined, + dependentOverrides: overridesByWorkflow.get(item.targetWorkflowId), + nameRegistry, + resolveBlockId, + requestId, + }) + blockPairs.push( + ...toForkBlockPairs( + copyResult.blockIdMapping, + sourceIsParent, + item.sourceWorkflowId, + item.targetWorkflowId + ) + ) + const requiredCleared = copyResult.clearedDependents.filter((field) => field.required) + const optionalCleared = copyResult.clearedDependents.filter((field) => !field.required) + if (requiredCleared.length > 0) { + needsConfiguration.push({ + workflowId: item.targetWorkflowId, + workflowName: item.sourceMeta.name, + // Surface the block names (deduped) - the field titles ("Label") aren't useful. + blocks: [...new Set(requiredCleared.map((field) => field.blockName))], + }) + } + if (optionalCleared.length > 0) { + clearedOptional.push({ + workflowName: item.sourceMeta.name, + blocks: [...new Set(optionalCleared.map((field) => field.blockName))], + }) + } + writtenItems.push(item) + } + + // Reconcile block-identity pairs for the written source workflows: clears pairs for + // blocks the source dropped (e.g. a deleted trigger) and any stale pair from a re-created + // target, then records the live ones - so the next promote resolves these blocks to these + // same ids and never re-homes one onto an archived workflow's block. + await reconcileForkBlockPairs( + tx, + edge.childWorkspaceId, + sourceIsParent, + writtenItems.map((item) => item.sourceWorkflowId), + blockPairs + ) + + // Persist / prune the stored dependent mapping. When the caller PROVIDED values, replace + // every written replace-target's stored set (cleared/removed fields drop out so the store + // equals exactly what was sent) AND prune the archived targets' now-dead rows (their + // workflow no longer exists and has no FK to cascade). Scope the inserted values to the + // delete's workflows so a value for a workflow skipped this pass (its source state + // vanished) can't be inserted without first clearing its old row and trip the unique + // constraint. When OMITTED, the store stays the source of truth (already applied above) - + // only prune archived targets, never touch the live replace targets' mapping. + const dependentTargetIds = new Set( + writtenItems.filter((item) => item.mode === 'replace').map((item) => item.targetWorkflowId) + ) + if (dependentValuesProvided) { + await reconcileForkDependentValues( + tx, + edge.childWorkspaceId, + [...dependentTargetIds, ...plan.archivedTargetIds], + (params.dependentValues ?? []) + .filter((entry) => dependentTargetIds.has(entry.workflowId)) + .map((entry) => ({ + targetWorkflowId: entry.workflowId, + targetBlockId: entry.blockId, + subBlockKey: entry.subBlockKey, + value: entry.value, + })) + ) + } else if (plan.archivedTargetIds.length > 0) { + await reconcileForkDependentValues(tx, edge.childWorkspaceId, plan.archivedTargetIds, []) + } + + const archivedNames = + plan.archivedTargetIds.length > 0 + ? ( + await tx + .select({ name: workflow.name }) + .from(workflow) + .where(inArray(workflow.id, plan.archivedTargetIds)) + ).map((row) => row.name) + : [] + + const undeployEventIds: string[] = [] + const archivedSnapshots: PromoteRunWorkflowSnapshot[] = [] + for (const targetWorkflowId of plan.archivedTargetIds) { + const priorVersion = priorVersionByTarget.get(targetWorkflowId) ?? null + archivedSnapshots.push({ workflowId: targetWorkflowId, priorVersion }) + // Enqueue undeploy side-effects (webhook + MCP-tool cleanup) so an archived orphan + // doesn't leak its subscriptions/registrations - mirrors rollback's undeploy path. + await undeployWorkflow({ + workflowId: targetWorkflowId, + tx, + onUndeployTransaction: async (innerTx, { deploymentVersionIds }) => { + if (deploymentVersionIds.length === 0) return + const eventId = await enqueueWorkflowUndeploySideEffects(innerTx, { + workflowId: targetWorkflowId, + deploymentVersionIds, + userId, + requestId, + }) + undeployEventIds.push(eventId) + }, + }) + 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, + })) + // The identity upsert keys on the parent side, which on push is the TARGET. A + // source whose previously-mapped target was archived gets a freshly-generated + // target id here, so its old (stale-target) identity row wouldn't be overwritten + // and would leak a second mapping for the same source. Delete every prior identity + // row for these sources (by the source side) first so exactly one row per source + // remains - this also converges any pre-existing duplicates. + await deleteWorkflowIdentityByIds( + tx, + edge.childWorkspaceId, + direction === 'pull' ? 'parent' : 'child', + writtenItems.map((item) => item.sourceWorkflowId) + ) + await upsertEdgeMappings(tx, edge.childWorkspaceId, userId, identityEntries) + + await propagateCredentialAccess(tx, { + plan, + sourceWorkspaceId, + targetWorkspaceId, + targetMembers, + now, + }) + + 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 writtenSourceIds = new Set(writtenItems.map((item) => item.sourceWorkflowId)) + const skippedItems = plan.items + .filter((item) => !writtenSourceIds.has(item.sourceWorkflowId)) + .map((item) => ({ id: item.sourceWorkflowId, name: item.sourceMeta.name })) + if (skippedItems.length > 0) { + logger.warn( + `[${requestId}] Promote skipped ${skippedItems.length} source workflow(s) whose deployment disappeared between plan and apply`, + { sourceWorkspaceId, targetWorkspaceId, skipped: skippedItems.length } + ) + } + + return { + blocked: null, + promoteRunId, + deployTargetIds: writtenItems.map((item) => item.targetWorkflowId), + updated: updatedSnapshots.length, + created: createdTargetIds.length, + archived: archivedSnapshots.length, + skippedItems, + writtenNames: Object.fromEntries( + writtenItems.map((item) => [item.targetWorkflowId, item.sourceMeta.name]) + ), + updatedNames: writtenItems + .filter((item) => item.mode === 'replace') + .map((item) => item.sourceMeta.name), + createdNames: writtenItems + .filter((item) => item.mode !== 'replace') + .map((item) => item.sourceMeta.name), + archivedNames, + undeployEventIds, + needsConfiguration, + clearedOptional, + } + }) + + if (txResult.blocked !== null) { + const unmappedRequired = txResult.blocked === 'unmapped' ? txResult.unmappedRequired : [] + return { + promoteRunId: '', + updated: 0, + created: 0, + archived: 0, + redeployed: 0, + deployFailed: 0, + unmappedRequired, + blocked: txResult.blocked, + updatedNames: [], + createdNames: [], + archivedNames: [], + needsConfiguration: [], + clearedOptional: [], + } + } + + // Process archived orphans' undeploy side-effects after commit (durably retried by the + // outbox cron if this dies first), so the locked transaction never held a network call. + for (const eventId of txResult.undeployEventIds) { + try { + await processWorkflowDeploymentOutboxEvent(eventId) + } catch (error) { + logger.warn(`[${requestId}] Deferred archive undeploy side-effect failed (will retry)`, { + eventId, + error: getErrorMessage(error), + }) + } + } + + let redeployed = 0 + const deployFailures: string[] = [] + const deployWarnings: string[] = [] + // Targets whose required dependents a parent change cleared: their draft holds the + // synced state, but we intentionally skip the redeploy so the prior deployed version + // keeps running instead of going live with an empty required field. The user re-picks + // the field (surfaced via `needsConfiguration`), then a redeploy/next sync deploys it. + const needsConfigTargetIds = new Set(txResult.needsConfiguration.map((n) => n.workflowId)) + // Deploy in a deterministic (sorted) order so this UNLOCKED loop acquires workflow + // row locks in the same order as a concurrent rollback's atomic tx (and a sibling + // promote's deploy loop), avoiding deadlocks - see rollback.ts lock ordering. + const deployTargetIds = [...txResult.deployTargetIds].sort((a, b) => a.localeCompare(b)) + for (const targetWorkflowId of deployTargetIds) { + // The transaction already force-replaced this target's draft state, so connected + // canvas clients must adopt it (mothership-edit semantics) whether or not the + // subsequent deploy succeeds - otherwise they keep, and may clobber, stale state. + void notifyForkWorkflowChanged(targetWorkflowId) + if (needsConfigTargetIds.has(targetWorkflowId)) continue + try { + const result = await performFullDeploy({ workflowId: targetWorkflowId, userId, requestId }) + if (result.success) { + redeployed += 1 + // A deploy can succeed but defer/queue some side-effects (trigger/schedule/MCP + // sync). Surface those instead of swallowing them into a clean success. + if (result.warnings?.length) { + const name = txResult.writtenNames[targetWorkflowId] ?? targetWorkflowId + for (const warning of result.warnings) deployWarnings.push(`${name}: ${warning}`) + } + } else { + deployFailures.push(targetWorkflowId) + logger.warn(`[${requestId}] Deploy after promote failed`, { + workflowId: targetWorkflowId, + error: result.error, + }) + } + } catch (error) { + deployFailures.push(targetWorkflowId) + logger.error(`[${requestId}] Deploy after promote threw`, { + workflowId: targetWorkflowId, + error: getErrorMessage(error), + }) + } + } + + if (deployFailures.length > 0) { + logger.warn(`[${requestId}] Promote wrote state but some targets failed to deploy`, { + sourceWorkspaceId, + targetWorkspaceId, + deployFailed: deployFailures.length, + deployFailures, + }) + } + + if (deployWarnings.length > 0) { + logger.warn(`[${requestId}] Promote deploys emitted warnings`, { deployWarnings }) + } + if (txResult.skippedItems.length > 0) { + logger.warn(`[${requestId}] Promote skipped undeployed source workflows`, { + skipped: txResult.skippedItems.map((item) => item.name), + }) + } + if (txResult.needsConfiguration.length > 0) { + logger.warn(`[${requestId}] Promote left required dependent fields needing configuration`, { + sourceWorkspaceId, + targetWorkspaceId, + needsConfiguration: txResult.needsConfiguration.map((n) => n.workflowName), + }) + } + logger.info(`[${requestId}] Promoted ${sourceWorkspaceId} -> ${targetWorkspaceId}`, { + updated: txResult.updated, + created: txResult.created, + archived: txResult.archived, + redeployed, + deployFailed: deployFailures.length, + needsConfiguration: txResult.needsConfiguration.length, + }) + + return { + promoteRunId: txResult.promoteRunId, + updated: txResult.updated, + created: txResult.created, + archived: txResult.archived, + redeployed, + deployFailed: deployFailures.length, + unmappedRequired: [], + blocked: null, + updatedNames: txResult.updatedNames, + createdNames: txResult.createdNames, + archivedNames: txResult.archivedNames, + needsConfiguration: txResult.needsConfiguration.map(({ workflowName, blocks }) => ({ + workflowName, + blocks, + })), + clearedOptional: txResult.clearedOptional, + } +} diff --git a/apps/sim/lib/workspaces/fork/promote/reactivate-in-tx.ts b/apps/sim/lib/workspaces/fork/promote/reactivate-in-tx.ts new file mode 100644 index 00000000000..f5b129401d1 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/promote/reactivate-in-tx.ts @@ -0,0 +1,138 @@ +import { workflow, workflowDeploymentVersion } from '@sim/db/schema' +import { and, eq } from 'drizzle-orm' +import type { DbOrTx } from '@/lib/db/types' +import { enqueueWorkflowDeploymentSideEffects } from '@/lib/workflows/deployment-outbox' +import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils' +import type { WorkflowState } from '@/stores/workflows/workflow/types' + +interface ReactivateDeployedVersionParams { + tx: DbOrTx + workflowId: string + version: number + userId: string + requestId: string +} + +export interface ReactivateDeployedVersionResult { + deploymentVersionId: string + /** + * Outbox event id enqueued inside the transaction. Process it AFTER the tx commits + * (or rely on the outbox cron/reaper if the process dies first). + */ + outboxEventId: string +} + +/** + * Reactivate a prior deployment version AND restore the workflow's draft to it using + * ONLY DB writes against the provided transaction, enqueuing the deployment + * side-effect (webhook / schedule / MCP re-subscription) to the outbox for processing + * AFTER the tx commits. This composes the DB halves of {@link activateWorkflowVersion} + * and `performRevertToVersion` so a fork rollback can run atomically under its fork + * advisory lock - the heavy side-effects never run inside the locked tx. + * + * Deliberately does NOT call `assertWorkflowMutable`: a rollback is an admin force-undo + * and must not be blocked by a workflow/folder lock (that check is also not tx-safe). + * Idempotent: deactivate-all + activate-target + overwrite-draft yield the same state + * on retry. + * + * Returns null when the target version row no longer exists, so the caller can mark the + * workflow skipped rather than failing the whole rollback. + */ +export async function reactivateDeployedVersionInTx( + params: ReactivateDeployedVersionParams +): Promise { + const { tx, workflowId, version, userId, requestId } = params + const now = new Date() + + // Lock the workflow row so this serializes with a concurrent (unlocked) promote + // deploy loop, which locks the same row in deployWorkflow - guaranteeing the final + // (active version, draft) pair is always coherent regardless of commit order. + await tx + .select({ id: workflow.id }) + .from(workflow) + .where(eq(workflow.id, workflowId)) + .limit(1) + .for('update') + + const [versionRow] = await tx + .select({ id: workflowDeploymentVersion.id, state: workflowDeploymentVersion.state }) + .from(workflowDeploymentVersion) + .where( + and( + eq(workflowDeploymentVersion.workflowId, workflowId), + eq(workflowDeploymentVersion.version, version) + ) + ) + .limit(1) + + if (!versionRow) return null + + const deployedState = versionRow.state as { + blocks?: Record + edges?: unknown[] + loops?: Record + parallels?: Record + variables?: WorkflowState['variables'] + } + if (!deployedState.blocks || !deployedState.edges) { + throw new Error( + `Deployment version ${version} for workflow ${workflowId} has an invalid state structure` + ) + } + + // Activate the target version (deactivate every other), mark the workflow deployed. + await tx + .update(workflowDeploymentVersion) + .set({ isActive: false }) + .where(eq(workflowDeploymentVersion.workflowId, workflowId)) + await tx + .update(workflowDeploymentVersion) + .set({ isActive: true }) + .where( + and( + eq(workflowDeploymentVersion.workflowId, workflowId), + eq(workflowDeploymentVersion.version, version) + ) + ) + await tx + .update(workflow) + .set({ isDeployed: true, deployedAt: now }) + .where(eq(workflow.id, workflowId)) + + // Restore the draft to the deployed version's state. + const hasVariables = Object.hasOwn(deployedState, 'variables') + const restoredState: WorkflowState = { + blocks: deployedState.blocks, + edges: deployedState.edges, + loops: deployedState.loops || {}, + parallels: deployedState.parallels || {}, + lastSaved: now.getTime(), + } as WorkflowState + if (hasVariables) { + restoredState.variables = deployedState.variables || {} + } + + const saveResult = await saveWorkflowToNormalizedTables(workflowId, restoredState, tx) + if (!saveResult.success) { + throw new Error(saveResult.error || `Failed to restore draft for workflow ${workflowId}`) + } + + await tx + .update(workflow) + .set({ + ...(hasVariables ? { variables: deployedState.variables || {} } : {}), + lastSynced: now, + updatedAt: now, + }) + .where(eq(workflow.id, workflowId)) + + const outboxEventId = await enqueueWorkflowDeploymentSideEffects(tx, { + workflowId, + deploymentVersionId: versionRow.id, + userId, + requestId, + forceRecreateSubscriptions: true, + }) + + return { deploymentVersionId: versionRow.id, outboxEventId } +} diff --git a/apps/sim/lib/workspaces/fork/promote/rollback.test.ts b/apps/sim/lib/workspaces/fork/promote/rollback.test.ts new file mode 100644 index 00000000000..e7d8bc58ba7 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/promote/rollback.test.ts @@ -0,0 +1,279 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { + mockResolveForkEdge, + mockAcquireTargetLock, + mockAcquireEdgeLock, + mockGetLatestRun, + mockDeleteAllRuns, + mockReactivate, + mockUndeploy, + mockDeleteIdentity, + mockEnqueueUndeploy, + mockProcessOutbox, + mockNotify, +} = vi.hoisted(() => ({ + mockResolveForkEdge: vi.fn(), + mockAcquireTargetLock: vi.fn(), + mockAcquireEdgeLock: vi.fn(), + mockGetLatestRun: vi.fn(), + mockDeleteAllRuns: vi.fn(), + mockReactivate: vi.fn(), + mockUndeploy: vi.fn(), + mockDeleteIdentity: vi.fn(), + mockEnqueueUndeploy: vi.fn(), + mockProcessOutbox: vi.fn(), + mockNotify: vi.fn(), +})) + +vi.mock('@/lib/workspaces/fork/lineage/lineage', () => ({ + resolveForkEdge: mockResolveForkEdge, + acquireForkTargetLock: mockAcquireTargetLock, + acquireForkEdgeLock: mockAcquireEdgeLock, + setForkLockTimeout: vi.fn(), +})) + +vi.mock('@/lib/workspaces/fork/promote/promote-run-store', () => ({ + getLatestPromoteRunForTarget: mockGetLatestRun, + deleteAllPromoteRunsForTarget: mockDeleteAllRuns, +})) + +vi.mock('@/lib/workspaces/fork/promote/reactivate-in-tx', () => ({ + reactivateDeployedVersionInTx: mockReactivate, +})) + +vi.mock('@/lib/workflows/persistence/utils', () => ({ + undeployWorkflow: mockUndeploy, +})) + +vi.mock('@/lib/workspaces/fork/mapping/mapping-store', () => ({ + deleteWorkflowIdentityByIds: mockDeleteIdentity, +})) + +vi.mock('@/lib/workflows/deployment-outbox', () => ({ + enqueueWorkflowUndeploySideEffects: mockEnqueueUndeploy, + processWorkflowDeploymentOutboxEvent: mockProcessOutbox, +})) + +vi.mock('@/lib/workspaces/fork/socket', () => ({ + notifyForkWorkflowChanged: mockNotify, +})) + +import { db } from '@sim/db' +import { rollbackFork } from '@/lib/workspaces/fork/promote/rollback' + +const EDGE = { childWorkspaceId: 'child-ws', parentWorkspaceId: 'parent-ws' } + +/** A fake transaction whose existence query returns the given undeploy ids. */ +function makeTx(existingUndeployIds: string[] = []) { + return { + select: vi.fn(() => ({ + from: vi.fn(() => ({ + where: vi.fn(() => Promise.resolve(existingUndeployIds.map((id) => ({ id })))), + })), + })), + update: vi.fn(() => ({ + set: vi.fn(() => ({ where: vi.fn(() => Promise.resolve(undefined)) })), + })), + } +} + +function setTx(existingUndeployIds: string[] = []) { + vi.mocked(db.transaction).mockImplementation( + async (cb: (tx: unknown) => unknown) => cb(makeTx(existingUndeployIds)) as never + ) +} + +function makeRun(overrides: Partial> = {}) { + return { + id: 'run-1', + childWorkspaceId: EDGE.childWorkspaceId, + direction: 'push' as const, + snapshot: { updated: [], created: [], archived: [] }, + ...overrides, + } +} + +describe('rollbackFork', () => { + beforeEach(() => { + vi.clearAllMocks() + mockResolveForkEdge.mockResolvedValue(EDGE) + mockReactivate.mockResolvedValue({ deploymentVersionId: 'dv', outboxEventId: 'evt' }) + mockUndeploy.mockResolvedValue({ success: true }) + mockProcessOutbox.mockResolvedValue('completed') + setTx([]) + }) + + it('reactivates updated workflows and processes side-effects after commit', async () => { + const run = makeRun({ + snapshot: { + updated: [ + { workflowId: 'wf-b', priorVersion: 5 }, + { workflowId: 'wf-a', priorVersion: 3 }, + ], + created: [], + archived: [], + }, + }) + mockGetLatestRun.mockResolvedValue(run) + mockReactivate.mockImplementation(async ({ workflowId }: { workflowId: string }) => ({ + deploymentVersionId: `dv-${workflowId}`, + outboxEventId: `evt-${workflowId}`, + })) + + const result = await rollbackFork({ + targetWorkspaceId: 'target-ws', + otherWorkspaceId: 'other-ws', + userId: 'user-1', + }) + + expect(result).toMatchObject({ + restored: 2, + archived: 0, + unarchived: 0, + skipped: 0, + skippedIds: [], + }) + // Deterministic (sorted) order: wf-a before wf-b. + expect(mockReactivate.mock.calls.map((c) => c[0].workflowId)).toEqual(['wf-a', 'wf-b']) + expect(mockProcessOutbox).toHaveBeenCalledWith('evt-wf-a') + expect(mockProcessOutbox).toHaveBeenCalledWith('evt-wf-b') + expect(mockDeleteAllRuns).toHaveBeenCalledTimes(1) + expect(mockNotify).toHaveBeenCalledWith('wf-a') + expect(mockNotify).toHaveBeenCalledWith('wf-b') + }) + + it('un-archives and reactivates an archived orphan (prior version restored)', async () => { + const run = makeRun({ + snapshot: { updated: [], created: [], archived: [{ workflowId: 'wf-x', priorVersion: 2 }] }, + }) + mockGetLatestRun.mockResolvedValue(run) + mockReactivate.mockResolvedValue({ deploymentVersionId: 'dv-x', outboxEventId: 'evt-x' }) + + const result = await rollbackFork({ + targetWorkspaceId: 'target-ws', + otherWorkspaceId: 'other-ws', + userId: 'user-1', + }) + + expect(result.unarchived).toBe(1) + expect(result.restored).toBe(0) + expect(result.archived).toBe(0) + expect(result.skipped).toBe(0) + expect(mockReactivate).toHaveBeenCalledWith( + expect.objectContaining({ workflowId: 'wf-x', version: 2 }) + ) + expect(mockProcessOutbox).toHaveBeenCalledWith('evt-x') + expect(mockNotify).toHaveBeenCalledWith('wf-x') + }) + + it('aborts with 409 and writes nothing when a newer sync supersedes it mid-flight', async () => { + const run = makeRun({ + snapshot: { updated: [{ workflowId: 'wf-a', priorVersion: 3 }], created: [], archived: [] }, + }) + // Unlocked read returns our run; the in-tx re-check sees a newer run. + mockGetLatestRun.mockResolvedValueOnce(run).mockResolvedValueOnce(makeRun({ id: 'run-2' })) + + await expect( + rollbackFork({ + targetWorkspaceId: 'target-ws', + otherWorkspaceId: 'other-ws', + userId: 'user-1', + }) + ).rejects.toMatchObject({ statusCode: 409 }) + + // No partial restore: nothing reactivated, no undo point consumed. + expect(mockReactivate).not.toHaveBeenCalled() + expect(mockUndeploy).not.toHaveBeenCalled() + expect(mockDeleteAllRuns).not.toHaveBeenCalled() + expect(mockProcessOutbox).not.toHaveBeenCalled() + }) + + it('surfaces a skipped reactivation when the version is gone (never silent)', async () => { + const run = makeRun({ + snapshot: { + updated: [ + { workflowId: 'wf-a', priorVersion: 3 }, + { workflowId: 'wf-b', priorVersion: 5 }, + ], + created: [], + archived: [], + }, + }) + mockGetLatestRun.mockResolvedValue(run) + mockReactivate.mockImplementation(async ({ workflowId }: { workflowId: string }) => + workflowId === 'wf-b' ? null : { deploymentVersionId: 'dv', outboxEventId: 'evt-wf-a' } + ) + + const result = await rollbackFork({ + targetWorkspaceId: 'target-ws', + otherWorkspaceId: 'other-ws', + userId: 'user-1', + }) + + expect(result.restored).toBe(1) + expect(result.skipped).toBe(1) + expect(result.skippedIds).toEqual(['wf-b']) + expect(mockNotify).not.toHaveBeenCalledWith('wf-b') + }) + + it('undeploys + archives created workflows and dissolves their identity rows', async () => { + setTx(['wf-c']) + const run = makeRun({ + direction: 'push', + snapshot: { updated: [], created: ['wf-c'], archived: [] }, + }) + mockGetLatestRun.mockResolvedValue(run) + mockUndeploy.mockImplementation( + async ({ + onUndeployTransaction, + }: { + onUndeployTransaction?: ( + tx: unknown, + r: { deploymentVersionIds: string[] } + ) => Promise + }) => { + await onUndeployTransaction?.(makeTx(), { deploymentVersionIds: ['dv-c'] }) + return { success: true } + } + ) + mockEnqueueUndeploy.mockResolvedValue('undeploy-evt') + + const result = await rollbackFork({ + targetWorkspaceId: 'target-ws', + otherWorkspaceId: 'other-ws', + userId: 'user-1', + }) + + expect(result.archived).toBe(1) + expect(result.skipped).toBe(0) + expect(mockUndeploy).toHaveBeenCalledTimes(1) + expect(mockDeleteIdentity).toHaveBeenCalledWith( + expect.anything(), + EDGE.childWorkspaceId, + 'parent', + ['wf-c'] + ) + expect(mockProcessOutbox).toHaveBeenCalledWith('undeploy-evt') + }) + + it('skips a created workflow that was hard-deleted (not archived, surfaced)', async () => { + setTx([]) // wf-c no longer exists + const run = makeRun({ snapshot: { updated: [], created: ['wf-c'], archived: [] } }) + mockGetLatestRun.mockResolvedValue(run) + + const result = await rollbackFork({ + targetWorkspaceId: 'target-ws', + otherWorkspaceId: 'other-ws', + userId: 'user-1', + }) + + expect(mockUndeploy).not.toHaveBeenCalled() + expect(result.skipped).toBe(1) + expect(result.skippedIds).toEqual(['wf-c']) + expect(result.archived).toBe(0) + }) +}) 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..86fe6d7996f --- /dev/null +++ b/apps/sim/lib/workspaces/fork/promote/rollback.ts @@ -0,0 +1,292 @@ +import { db } from '@sim/db' +import { workflow } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { inArray } from 'drizzle-orm' +import { + enqueueWorkflowUndeploySideEffects, + processWorkflowDeploymentOutboxEvent, +} from '@/lib/workflows/deployment-outbox' +import { undeployWorkflow } from '@/lib/workflows/persistence/utils' +import { ForkError } from '@/lib/workspaces/fork/lineage/authz' +import { + acquireForkEdgeLock, + acquireForkTargetLock, + resolveForkEdge, + setForkLockTimeout, +} 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 { reactivateDeployedVersionInTx } from '@/lib/workspaces/fork/promote/reactivate-in-tx' +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 restored. */ + skipped: number + /** Ids of the skipped workflows (surfaced so the partial restore is never silent). */ + skippedIds: string[] +} + +/** A single restore action, sorted by workflow id for a deterministic lock order. */ +type RollbackOp = + | { workflowId: string; kind: 'reactivate'; version: number } + | { workflowId: string; kind: 'undeploy' } + +/** + * Undo the most recent promote into `targetWorkspaceId` in ONE atomic, fork-locked, + * DB-only transaction. Because a concurrent promote takes the same target advisory + * lock for its write transaction, it cannot interleave with the rollback: it runs + * fully before or fully after. If a newer sync superseded our undo point, we abort + * with 409 BEFORE any write, so the operation is strictly all-or-nothing - it never + * leaves a partially reverted target. + * + * The heavy webhook / schedule / MCP re-subscription work is enqueued to the + * deployment outbox INSIDE the transaction and processed AFTER commit (and durably + * retried by the outbox cron/reaper if this process dies first), so the locked + * transaction never holds across a network call. 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. + 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 + + // Build the restore ops: reactivate a prior version, or undeploy (created targets + + // updated targets that had no prior deployment). Sort by workflow id so the locked + // transaction acquires workflow row locks in a deterministic order, avoiding + // deadlocks with the (unlocked) promote deploy loop, which locks the same rows. + const undeployIds = [ + ...created, + ...updated.filter((i) => i.priorVersion == null).map((i) => i.workflowId), + ] + const toReactivateOps = ( + list: Array<{ workflowId: string; priorVersion: number | null }> + ): RollbackOp[] => + list + .filter((item) => item.priorVersion != null) + .map((item) => ({ + workflowId: item.workflowId, + kind: 'reactivate' as const, + version: item.priorVersion as number, + })) + const ops: RollbackOp[] = [ + ...toReactivateOps(updated), + ...toReactivateOps(archived), + ...undeployIds.map((workflowId) => ({ workflowId, kind: 'undeploy' as const })), + ].sort((a, b) => a.workflowId.localeCompare(b.workflowId)) + + const skipped = new Set() + const outboxEventIds: string[] = [] + + await db.transaction(async (tx) => { + await setForkLockTimeout(tx) + await acquireForkTargetLock(tx, targetWorkspaceId) + await acquireForkEdgeLock(tx, edge.childWorkspaceId) + + // Re-confirm our run is still the newest sync, now under the lock. If a promote + // landed since the unlocked read above, abort with NO writes (tx rolls back). + const current = await getLatestPromoteRunForTarget(tx, targetWorkspaceId) + if (!current || current.id !== run.id) { + throw new ForkError( + 'A newer sync into this workspace exists; reopen and undo the most recent sync.', + 409 + ) + } + + const now = new Date() + + // Un-archive the orphans the promote archived BEFORE reactivating them. + if (archived.length > 0) { + await tx + .update(workflow) + .set({ archivedAt: null, updatedAt: now }) + .where( + inArray( + workflow.id, + archived.map((i) => i.workflowId) + ) + ) + } + + // Which undeploy targets still exist (created targets can be hard-deleted after the + // promote; a missing one is already gone, so skip rather than fail the rollback). + const existingUndeploy = + undeployIds.length === 0 + ? new Set() + : new Set( + ( + await tx + .select({ id: workflow.id }) + .from(workflow) + .where(inArray(workflow.id, undeployIds)) + ).map((row) => row.id) + ) + + for (const op of ops) { + if (op.kind === 'reactivate') { + const result = await reactivateDeployedVersionInTx({ + tx, + workflowId: op.workflowId, + version: op.version, + userId, + requestId, + }) + // A null result means the workflow / version was hard-deleted since the + // promote - record it so the partial restore is surfaced, never silent. + if (!result) { + skipped.add(op.workflowId) + continue + } + outboxEventIds.push(result.outboxEventId) + continue + } + + if (!existingUndeploy.has(op.workflowId)) { + skipped.add(op.workflowId) + continue + } + const undeployResult = await undeployWorkflow({ + workflowId: op.workflowId, + tx, + onUndeployTransaction: async (innerTx, { deploymentVersionIds }) => { + if (deploymentVersionIds.length === 0) return + const eventId = await enqueueWorkflowUndeploySideEffects(innerTx, { + workflowId: op.workflowId, + deploymentVersionIds, + userId, + requestId, + }) + outboxEventIds.push(eventId) + }, + }) + if (!undeployResult.success) { + // The workflow exists but couldn't be undeployed - abort so we never leave a + // partial undo. The whole tx rolls back and the undo point is preserved. + throw new ForkError( + `Rollback could not undeploy workflow ${op.workflowId}: ${undeployResult.error ?? 'unknown error'}. The undo point is preserved - retry the rollback.`, + 500 + ) + } + } + + // Archive the workflows the promote created and dissolve their identity rows. + 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 so no older sibling + // sync becomes undoable once this one is undone. + await deleteAllPromoteRunsForTarget(tx, targetWorkspaceId) + }) + + // After commit: process the enqueued side-effects (webhooks / schedules / MCP). These + // are durable outbox rows, so a crash here is recovered by the outbox cron/reaper - + // failures only warn, they never undo the (committed) restore. + for (const eventId of outboxEventIds) { + try { + await processWorkflowDeploymentOutboxEvent(eventId) + } catch (error) { + logger.warn( + `[${requestId}] Deferred rollback side-effect processing failed (will retry via outbox)`, + { eventId, error } + ) + } + } + + if (skipped.size > 0) { + logger.warn( + `[${requestId}] Rollback skipped ${skipped.size} workflow(s) no longer in the database`, + { + targetWorkspaceId, + skipped: Array.from(skipped), + } + ) + } + + // Notify connected canvases to adopt the restored state (reactivated drafts + the + // undeployed/archived created targets). Skipped (gone) workflows have no room. + const notifyIds = new Set() + for (const op of ops) { + if (!skipped.has(op.workflowId)) notifyIds.add(op.workflowId) + } + for (const workflowId of notifyIds) void notifyForkWorkflowChanged(workflowId) + + // Attribute each skip to its bucket (a workflow is in exactly one) so the counts + // reflect what was actually restored, not the snapshot size. + const createdSet = new Set(created) + const archivedSet = new Set(archived.map((i) => i.workflowId)) + const updatedSet = new Set(updated.map((i) => i.workflowId)) + let skippedUpdated = 0 + let skippedCreated = 0 + let skippedArchived = 0 + for (const id of skipped) { + if (updatedSet.has(id)) skippedUpdated += 1 + else if (createdSet.has(id)) skippedCreated += 1 + else if (archivedSet.has(id)) skippedArchived += 1 + } + + const restored = updated.length - skippedUpdated + const archivedCount = created.length - skippedCreated + const unarchived = archived.length - skippedArchived + + const result: RollbackForkResult = { + restored, + archived: archivedCount, + unarchived, + skipped: skipped.size, + skippedIds: Array.from(skipped), + } + + logger.info(`[${requestId}] Rolled back promote into ${targetWorkspaceId}`, { + restored: result.restored, + archived: result.archived, + unarchived: result.unarchived, + skipped: result.skipped, + }) + + 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..fc782916caa --- /dev/null +++ b/apps/sim/lib/workspaces/fork/remap/block-identity.test.ts @@ -0,0 +1,88 @@ +/** + * @vitest-environment node + */ +import { isValidUuid } from '@sim/utils/id' +import { describe, expect, it } from 'vitest' +import { + buildForkBlockIdResolver, + deriveForkBlockId, + EMPTY_FORK_BLOCK_MAP, + type ForkBlockMap, +} 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')) + }) +}) + +describe('buildForkBlockIdResolver', () => { + const parentWf = 'wf-parent' + const childWf = 'wf-child' + const parentBlock = 'block-parent' + // The pair the fork created: child block derived from the parent block. + const childBlock = deriveForkBlockId(childWf, parentBlock) + const seededMap: ForkBlockMap = { + parentToChild: new Map([ + [parentBlock, { targetBlockId: childBlock, targetWorkflowId: childWf }], + ]), + childToParent: new Map([ + [childBlock, { targetBlockId: parentBlock, targetWorkflowId: parentWf }], + ]), + } + + it('push maps a child block back to the parent ORIGINAL id (keeps the webhook URL stable)', () => { + const pushResolve = buildForkBlockIdResolver(false, seededMap) + expect(pushResolve(parentWf, childBlock)).toBe(parentBlock) + // The bug this fixes: without the map, push would re-derive and re-key the parent block. + expect(pushResolve(parentWf, childBlock)).not.toBe(deriveForkBlockId(parentWf, childBlock)) + }) + + it('pull maps a parent block to its existing child id', () => { + const pullResolve = buildForkBlockIdResolver(true, seededMap) + expect(pullResolve(childWf, parentBlock)).toBe(childBlock) + }) + + it('derives (does NOT reuse) when the target workflow was re-created (different id)', () => { + // Parent workflow archived + re-created as wf-parent-2: the pair points at the old + // workflow, so reusing parentBlock there would collide on the global block PK. Derive. + const pushResolve = buildForkBlockIdResolver(false, seededMap) + expect(pushResolve('wf-parent-2', childBlock)).toBe( + deriveForkBlockId('wf-parent-2', childBlock) + ) + expect(pushResolve('wf-parent-2', childBlock)).not.toBe(parentBlock) + }) + + it('derives a fresh id for a source block with no recorded pair (added since last sync)', () => { + const pushResolve = buildForkBlockIdResolver(false, seededMap) + expect(pushResolve(parentWf, 'block-new')).toBe(deriveForkBlockId(parentWf, 'block-new')) + }) + + it('derives everything when the map is empty (fork creation)', () => { + const resolve = buildForkBlockIdResolver(true, EMPTY_FORK_BLOCK_MAP) + expect(resolve(childWf, parentBlock)).toBe(deriveForkBlockId(childWf, parentBlock)) + }) +}) 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..fd659d4a646 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/remap/block-identity.ts @@ -0,0 +1,95 @@ +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) +} + +/** A persisted counterpart: the target block id plus the workflow it belongs to. */ +export interface ForkBlockMapEntry { + targetBlockId: string + /** The target-side workflow the pair belongs to (childWorkflowId for parentToChild). */ + targetWorkflowId: string +} + +/** Persisted block-identity pairs for an edge, indexed for both promote directions. */ +export interface ForkBlockMap { + /** parent block id -> { child block, child workflow } (pull/create resolve source=parent). */ + parentToChild: ReadonlyMap + /** child block id -> { parent block, parent workflow } (push resolves source=child). */ + childToParent: ReadonlyMap +} + +/** An empty map - fork creation has no prior pairs, so every block id is derived fresh. */ +export const EMPTY_FORK_BLOCK_MAP: ForkBlockMap = { + parentToChild: new Map(), + childToParent: new Map(), +} + +/** Resolve a source block to its target block id for a promote (map-or-derive). */ +export type ForkBlockIdResolver = (targetWorkflowId: string, sourceBlockId: string) => string + +/** + * Build the block-id resolver a promote uses to assign target block ids. It reuses the + * persisted counterpart when one exists AND that pair belongs to the workflow being written, + * else falls back to {@link deriveForkBlockId} (blocks added since the last sync; fork + * creation, which has no map). `sourceIsParent` is true on pull/create (source = parent) and + * false on push (source = child); it selects the lookup direction. + * + * The workflow guard is what makes a re-created target safe: if the original target workflow + * was archived and the promote creates a new one, the recorded pair points at the OLD + * workflow, so it no longer matches and we derive a fresh id - never reusing the archived + * workflow's block id (which would collide on the global `workflow_blocks` primary key). + * + * For a stable workflow this still maps each child block back to the parent's ORIGINAL id on + * push, keeping its trigger webhook URL fixed. The SAME resolver must back + * `copyWorkflowStateIntoTarget` (which writes the blocks) and `collectForkDependentReconfigs` + * (which keys the modal's override by target block id), or the two would disagree. + */ +export function buildForkBlockIdResolver( + sourceIsParent: boolean, + map: ForkBlockMap +): ForkBlockIdResolver { + const existing = sourceIsParent ? map.parentToChild : map.childToParent + return (targetWorkflowId, sourceBlockId) => { + const entry = existing.get(sourceBlockId) + if (entry && entry.targetWorkflowId === targetWorkflowId) return entry.targetBlockId + return deriveForkBlockId(targetWorkflowId, sourceBlockId) + } +} 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..a913ada3143 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/remap/fork-bootstrap.ts @@ -0,0 +1,30 @@ +import type { SubBlockRecord } from '@/lib/workflows/persistence/remap-internal-ids' +import { + clearDependentsOnRemap, + 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, blockType: string) => SubBlockRecord { + return (subBlocks, blockType) => { + const result = remapForkSubBlocks(subBlocks, resolveCopied, 'create') + return clearDependentsOnRemap(result.subBlocks, blockType, result.remappedKeys) + } +} 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..f607a1fb70f --- /dev/null +++ b/apps/sim/lib/workspaces/fork/remap/remap-references.test.ts @@ -0,0 +1,642 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it, vi } from 'vitest' +import type { BlockConfig, 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 { + applyDependentOverrides, + collectClearedDependents, + parseNestedDependentKey, + readTargetDraftDependentValue, + remapForkSubBlocks, + remapToolBlockResources, +} from '@/lib/workspaces/fork/remap/remap-references' +import { getBlock } from '@/blocks/registry' + +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('leaves an advanced-mode manualCredential id untouched (escape hatch)', () => { + 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: 'mc-src', knowledgeBaseId: 'kb-dst' }) + }) + + it('preserves an org-scoped credentialSet ref without remapping or recording it', () => { + const tool = { + type: 'testblock', + toolId: 'testblock_run', + params: { credential: 'credentialSet:cs-1' }, + } + 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: true, + blockConfigs, + }) + expect((result.params as Record).credential).toBe('credentialSet:cs-1') + expect(recorded).toHaveLength(0) + }) + + 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') + }) + + it('clears a dependent tool param when its parent resource is remapped', () => { + const tool = { + type: 'depblock', + toolId: 'depblock_run', + params: { knowledgeBaseId: 'kb-src', documentId: 'doc-src' }, + } + const result = remapToolBlockResources(tool, { + resolve: (kind, id) => (kind === 'knowledge-base' && id === 'kb-src' ? 'kb-dst' : null), + resolveFileKey: () => null, + clearUnresolved: false, + blockConfigs: { + depblock: { + subBlocks: [ + { id: 'knowledgeBaseId', title: 'KB', type: 'knowledge-base-selector' }, + { + id: 'documentId', + title: 'Doc', + type: 'document-selector', + dependsOn: ['knowledgeBaseId'], + }, + ], + }, + }, + }) + expect(result.params).toEqual({ knowledgeBaseId: 'kb-dst', documentId: '' }) + }) +}) + +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('mc-src') + expect(result.references).toHaveLength(0) + }) + + it('promote mode: keeps + records the basic credential; manual id is escape hatch', () => { + const result = remapForkSubBlocks( + subBlocks(), + (kind, id) => (kind === 'knowledge-base' && id === 'kb-src' ? 'kb-dst' : null), + 'promote' + ) + // The basic credential is cleared (never carry an invalid cross-workspace id) but + // still surfaced as required so the sync blocks; the advanced manualCredential is an + // escape hatch - preserved verbatim, not recorded. + expect(result.subBlocks.credential.value).toBe('') + 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).not.toContain('credential:mc-src') + expect(result.unmapped.every((r) => r.kind !== 'knowledge-base')).toBe(true) + }) + + it('promote mode: preserves a credentialSet ref without flagging it', () => { + const sb: SubBlockRecord = { + triggerCredentials: { + id: 'triggerCredentials', + type: 'oauth-input', + value: 'credentialSet:cs-1', + }, + } + const result = remapForkSubBlocks(sb, () => null, 'promote') + expect(result.subBlocks.triggerCredentials.value).toBe('credentialSet:cs-1') + expect(result.references).toHaveLength(0) + expect(result.unmapped).toHaveLength(0) + }) + + it('create mode: keeps a credentialSet ref (org-scoped, not cleared)', () => { + const sb: SubBlockRecord = { + triggerCredentials: { + id: 'triggerCredentials', + type: 'oauth-input', + value: 'credentialSet:cs-1', + }, + } + const result = remapForkSubBlocks(sb, () => null, 'create') + expect(result.subBlocks.triggerCredentials.value).toBe('credentialSet:cs-1') + expect(result.references).toHaveLength(0) + }) + + 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}}') + }) +}) + +const blockWith = (subBlocks: SubBlockConfig[]): BlockConfig => + ({ name: 'Test', description: '', subBlocks, outputs: {} }) as unknown as BlockConfig + +const entry = (id: string, type: string, value: unknown) => ({ id, type, value }) + +describe('collectClearedDependents', () => { + it('flags a required dependent the target had set but the merge left empty', () => { + vi.mocked(getBlock).mockReturnValue( + blockWith([ + { id: 'credential', title: 'Credential', type: 'oauth-input' }, + { + id: 'folder', + title: 'Label', + type: 'folder-selector', + dependsOn: ['credential'], + required: true, + }, + ]) + ) + const targetDraft: SubBlockRecord = { + credential: entry('credential', 'oauth-input', 'c-target'), + folder: entry('folder', 'folder-selector', 'INBOX'), + } + const merged: SubBlockRecord = { + credential: entry('credential', 'oauth-input', 'c-new'), + folder: entry('folder', 'folder-selector', ''), + } + expect(collectClearedDependents('gmail', 'b1', 'Send Email', targetDraft, merged)).toEqual([ + { + blockId: 'b1', + blockName: 'Send Email', + subBlockKey: 'folder', + title: 'Label', + required: true, + }, + ]) + }) + + it('returns an optional cleared dependent flagged required:false', () => { + vi.mocked(getBlock).mockReturnValue( + blockWith([ + { id: 'credential', title: 'Credential', type: 'oauth-input' }, + { id: 'folder', title: 'Label', type: 'folder-selector', dependsOn: ['credential'] }, + ]) + ) + const targetDraft: SubBlockRecord = { + credential: entry('credential', 'oauth-input', 'c-target'), + folder: entry('folder', 'folder-selector', 'INBOX'), + } + const merged: SubBlockRecord = { + credential: entry('credential', 'oauth-input', 'c-new'), + folder: entry('folder', 'folder-selector', ''), + } + expect(collectClearedDependents('gmail', 'b1', 'Send Email', targetDraft, merged)).toEqual([ + { + blockId: 'b1', + blockName: 'Send Email', + subBlockKey: 'folder', + title: 'Label', + required: false, + }, + ]) + }) + + it('does not flag a dependent the target never configured (only the source carried it)', () => { + vi.mocked(getBlock).mockReturnValue( + blockWith([ + { id: 'credential', title: 'Credential', type: 'oauth-input' }, + { id: 'folder', title: 'Label', type: 'folder-selector', dependsOn: ['credential'] }, + ]) + ) + // The target's fork never set this label, so the merge leaving it empty is not a loss - + // this is the pull case where the parent carried a filter the fork never had. + const targetDraft: SubBlockRecord = { + credential: entry('credential', 'oauth-input', 'c-target'), + folder: entry('folder', 'folder-selector', ''), + } + const merged: SubBlockRecord = { + credential: entry('credential', 'oauth-input', 'c-new'), + folder: entry('folder', 'folder-selector', ''), + } + expect(collectClearedDependents('gmail', 'b1', 'Send Email', targetDraft, merged)).toEqual([]) + }) + + it('does not flag a dependent that ended up with a value (preserved or overridden)', () => { + vi.mocked(getBlock).mockReturnValue( + blockWith([ + { id: 'credential', title: 'Credential', type: 'oauth-input' }, + { + id: 'folder', + title: 'Label', + type: 'folder-selector', + dependsOn: ['credential'], + required: true, + }, + ]) + ) + const targetDraft: SubBlockRecord = { + credential: entry('credential', 'oauth-input', 'c-target'), + folder: entry('folder', 'folder-selector', 'INBOX'), + } + const merged: SubBlockRecord = { + credential: entry('credential', 'oauth-input', 'c-target'), + folder: entry('folder', 'folder-selector', 'INBOX'), + } + expect(collectClearedDependents('gmail', 'b1', 'Send Email', targetDraft, merged)).toEqual([]) + }) + + it('does not flag a cleared dependent gated off by its condition', () => { + vi.mocked(getBlock).mockReturnValue( + blockWith([ + { id: 'credential', title: 'Credential', type: 'oauth-input' }, + { id: 'operation', title: 'Operation', type: 'dropdown' }, + { + id: 'folder', + title: 'Label', + type: 'folder-selector', + dependsOn: ['credential'], + required: true, + condition: { field: 'operation', value: 'read' }, + }, + ]) + ) + // The operation is 'send', so the read-only folder field is inactive - a stale value + // it carried must not be flagged as required. + const targetDraft: SubBlockRecord = { + credential: entry('credential', 'oauth-input', 'c-target'), + operation: entry('operation', 'dropdown', 'send'), + folder: entry('folder', 'folder-selector', 'INBOX'), + } + const merged: SubBlockRecord = { + credential: entry('credential', 'oauth-input', 'c-new'), + operation: entry('operation', 'dropdown', 'send'), + folder: entry('folder', 'folder-selector', ''), + } + expect(collectClearedDependents('gmail', 'b1', 'Send Email', targetDraft, merged)).toEqual([]) + }) + + it('flags a cleared dependent nested inside a tool-input tool', () => { + vi.mocked(getBlock).mockImplementation((type) => { + if (type === 'agent') return blockWith([{ id: 'tools', title: 'Tools', type: 'tool-input' }]) + if (type === 'gmail') + return blockWith([ + { id: 'credential', title: 'Credential', type: 'oauth-input' }, + { + id: 'folder', + title: 'Label', + type: 'folder-selector', + dependsOn: ['credential'], + required: true, + }, + ]) + return undefined as unknown as BlockConfig + }) + const targetDraft: SubBlockRecord = { + tools: entry('tools', 'tool-input', [ + { type: 'gmail', title: 'Gmail', params: { credential: 'c-target', folder: 'INBOX' } }, + ]), + } + const merged: SubBlockRecord = { + tools: entry('tools', 'tool-input', [ + { type: 'gmail', title: 'Gmail', params: { credential: 'c-new', folder: '' } }, + ]), + } + expect(collectClearedDependents('agent', 'b1', 'Agent', targetDraft, merged)).toEqual([ + { + blockId: 'b1', + blockName: 'Agent', + subBlockKey: 'tools[0].folder', + title: 'Gmail: Label', + required: true, + }, + ]) + }) +}) + +describe('applyDependentOverrides', () => { + const gmailConfig = () => + blockWith([ + { id: 'credential', title: 'Credential', type: 'oauth-input' }, + { + id: 'folder', + title: 'Label', + type: 'folder-selector', + dependsOn: ['credential'], + selectorKey: 'gmail.labels', + }, + ]) + + it('applies a top-level re-pick value', () => { + vi.mocked(getBlock).mockReturnValue(gmailConfig()) + const subBlocks: SubBlockRecord = { + credential: entry('credential', 'oauth-input', 'c-new'), + folder: entry('folder', 'folder-selector', ''), + } + const result = applyDependentOverrides(subBlocks, 'gmail', new Map([['folder', 'Label_42']])) + expect((result.folder as { value: unknown }).value).toBe('Label_42') + }) + + it('rejects an override for a non-dependent / parent key (allowlist)', () => { + vi.mocked(getBlock).mockReturnValue(gmailConfig()) + const subBlocks: SubBlockRecord = { + credential: entry('credential', 'oauth-input', 'c-new'), + folder: entry('folder', 'folder-selector', ''), + } + // 'credential' is a parent (no selectorKey) - must never be writable via override. + const result = applyDependentOverrides(subBlocks, 'gmail', new Map([['credential', 'evil']])) + expect(result).toBe(subBlocks) + expect((subBlocks.credential as { value: unknown }).value).toBe('c-new') + }) + + it('applies a nested tool-input re-pick onto the matching tool param', () => { + vi.mocked(getBlock).mockImplementation((type) => { + if (type === 'agent') return blockWith([{ id: 'tools', title: 'Tools', type: 'tool-input' }]) + if (type === 'gmail') return gmailConfig() + return undefined as unknown as BlockConfig + }) + const subBlocks: SubBlockRecord = { + tools: entry('tools', 'tool-input', [ + { type: 'gmail', title: 'Gmail', params: { credential: 'c-new', folder: '' } }, + ]), + } + const result = applyDependentOverrides( + subBlocks, + 'agent', + new Map([['tools[0].folder', 'Label_99']]) + ) + const tools = (result.tools as { value: Array<{ params: { folder: string } }> }).value + expect(tools[0].params.folder).toBe('Label_99') + }) + + it('rejects a nested override for a non-allowlisted tool param', () => { + vi.mocked(getBlock).mockImplementation((type) => { + if (type === 'agent') return blockWith([{ id: 'tools', title: 'Tools', type: 'tool-input' }]) + if (type === 'gmail') return gmailConfig() + return undefined as unknown as BlockConfig + }) + const subBlocks: SubBlockRecord = { + tools: entry('tools', 'tool-input', [ + { type: 'gmail', title: 'Gmail', params: { credential: 'c-new', folder: '' } }, + ]), + } + // 'credential' inside the tool is a parent - not overridable. + const result = applyDependentOverrides( + subBlocks, + 'agent', + new Map([['tools[0].credential', 'evil']]) + ) + expect(result).toBe(subBlocks) + }) + + it('ignores a nested override whose tool index is out of range', () => { + vi.mocked(getBlock).mockImplementation((type) => { + if (type === 'agent') return blockWith([{ id: 'tools', title: 'Tools', type: 'tool-input' }]) + if (type === 'gmail') return gmailConfig() + return undefined as unknown as BlockConfig + }) + const subBlocks: SubBlockRecord = { + tools: entry('tools', 'tool-input', [ + { type: 'gmail', title: 'Gmail', params: { credential: 'c-new', folder: '' } }, + ]), + } + const result = applyDependentOverrides( + subBlocks, + 'agent', + new Map([['tools[5].folder', 'Label_99']]) + ) + expect(result).toBe(subBlocks) + }) +}) + +describe('parseNestedDependentKey', () => { + it('parses a nested tool-input key with a numeric index', () => { + expect(parseNestedDependentKey('tools[0].folder')).toEqual({ + toolInputId: 'tools', + index: 0, + paramId: 'folder', + }) + expect(parseNestedDependentKey('tools[12].channel')).toEqual({ + toolInputId: 'tools', + index: 12, + paramId: 'channel', + }) + }) + + it('returns null for a plain top-level key', () => { + expect(parseNestedDependentKey('folder')).toBeNull() + expect(parseNestedDependentKey('credential')).toBeNull() + }) +}) + +describe('readTargetDraftDependentValue', () => { + it('reads a top-level draft value', () => { + const draft: SubBlockRecord = { folder: { value: 'INBOX' } } + expect(readTargetDraftDependentValue(draft, undefined, 'folder')).toBe('INBOX') + }) + + it('returns empty for a missing or non-string top-level value', () => { + expect(readTargetDraftDependentValue({ folder: { value: 42 } }, undefined, 'folder')).toBe('') + expect(readTargetDraftDependentValue({}, undefined, 'folder')).toBe('') + expect(readTargetDraftDependentValue(undefined, undefined, 'folder')).toBe('') + }) + + it('reads the target draft nested param when the source/target tool types match at that index', () => { + const target: SubBlockRecord = { + tools: { value: [{ type: 'gmail', params: { folder: 'INBOX' } }] }, + } + const source: SubBlockRecord = { + tools: { value: [{ type: 'gmail', params: { folder: 'SENT' } }] }, + } + // Reads the TARGET draft's value (INBOX), gated on a same-type tool at the index. + expect(readTargetDraftDependentValue(target, source, 'tools[0].folder')).toBe('INBOX') + }) + + it('identity guard: returns empty when the target draft tool type differs from the source dependent tool', () => { + // The source dependent hangs off a Gmail tool at index 0, but the target draft holds a Slack + // tool there - its param value is not this field's value, so nothing is seeded. + const target: SubBlockRecord = { + tools: { value: [{ type: 'slack', params: { folder: 'INBOX' } }] }, + } + const source: SubBlockRecord = { + tools: { value: [{ type: 'gmail', params: { folder: 'SENT' } }] }, + } + expect(readTargetDraftDependentValue(target, source, 'tools[0].folder')).toBe('') + }) + + it('returns empty when the target draft has no tool at the index', () => { + const target: SubBlockRecord = { tools: { value: [] } } + const source: SubBlockRecord = { + tools: { value: [{ type: 'gmail', params: { folder: 'SENT' } }] }, + } + expect(readTargetDraftDependentValue(target, source, 'tools[0].folder')).toBe('') + }) + + it('returns empty when the source tool type cannot be verified', () => { + const target: SubBlockRecord = { + tools: { value: [{ type: 'gmail', params: { folder: 'INBOX' } }] }, + } + // No source subBlocks (or no tool at the index) -> identity unverifiable -> do not seed. + expect(readTargetDraftDependentValue(target, undefined, 'tools[0].folder')).toBe('') + expect(readTargetDraftDependentValue(target, { tools: { value: [] } }, 'tools[0].folder')).toBe( + '' + ) + }) + + it('handles the JSON-string stored tool array shape', () => { + const target: SubBlockRecord = { + tools: { value: JSON.stringify([{ type: 'gmail', params: { folder: 'INBOX' } }]) }, + } + const source: SubBlockRecord = { + tools: { value: JSON.stringify([{ type: 'gmail', params: { folder: 'SENT' } }]) }, + } + expect(readTargetDraftDependentValue(target, source, 'tools[0].folder')).toBe('INBOX') + }) +}) 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..7df375febaa --- /dev/null +++ b/apps/sim/lib/workspaces/fork/remap/remap-references.ts @@ -0,0 +1,921 @@ +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 { getWorkflowSearchDependentClears } from '@/lib/workflows/search-replace/dependencies' +import { getToolInputParamConfigs } from '@/lib/workflows/search-replace/indexer' +import { + getWorkflowSearchSubBlockResourceDefinition, + parseWorkflowSearchSubBlockResources, + type StructuredWorkflowSearchResourceKind, +} from '@/lib/workflows/search-replace/resources/registry' +import { + buildSubBlockValues, + evaluateSubBlockCondition, + isNonEmptyValue, +} from '@/lib/workflows/subblocks/visibility' +import type { ParsedStoredTool } from '@/lib/workflows/tool-input/types' +import { remapForkFileUploadValue } from '@/lib/workspaces/fork/remap/remap-files' +import { getBlock } from '@/blocks/registry' +import type { SubBlockConfig } from '@/blocks/types' + +/** + * 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']) + +/** + * Id-based override kind for a TOOL param's credential, resolved by subblock id so a + * basic `credential` / `triggerCredentials` is caught even when its config is filtered + * out by a reactive condition (the registry path would otherwise skip it). Advanced + * `manual*` ids are an escape hatch - the user owns them (e.g. via a `{{SECRET}}`), so + * they are never auto-remapped. + */ +function getToolParamOverrideKind(paramId: string): ForkRemapKind | null { + const base = paramId.replace(/_\d+$/, '') + if (base === 'manualCredential') return null + if (CREDENTIAL_SUBBLOCK_IDS.has(base)) return 'credential' + 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` is cleared by the +// `dependsOn` rule (clearDependentsOnRemap) when its parent knowledge base is remapped. +// `mcp-tool-selector` is likewise cleared by `dependsOn` when its `mcp-server-selector` +// parent is remapped - the tool list is server-scoped and may differ in the target. + +/** 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 + +/** + * A `credentialSet:` reference points at an ORG-scoped credential set. A fork + * inherits its parent's org, so the set is already valid in the target — these refs + * are preserved verbatim and never treated as a workspace credential to remap/flag. + */ +function isCredentialSetRef(value: string): boolean { + return value.startsWith('credentialSet:') +} + +/** + * 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[] + /** Subblock keys whose resource id was rewritten/cleared this pass (the `dependsOn` parents). */ + remappedKeys: Set +} + +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. + * After remapping, dependent params (via `dependsOn`) of any changed resource are + * cleared with the same {@link getWorkflowSearchDependentClears} walk search-replace + * uses, so a child scoped to the old parent isn't left stale. + */ +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 + } + const remappedParamIds = new Set() + + // 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 + if (overrideKind === 'credential' && isCredentialSetRef(currentValue)) continue + const target = opts.resolve(overrideKind, currentValue) + opts.record?.(overrideKind, currentValue, target != null) + if (target != null) { + if (target !== currentValue) { + setParam(paramId, target) + remappedParamIds.add(paramId) + } + } else if (opts.clearUnresolved) { + setParam(paramId, '') + remappedParamIds.add(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) + remappedParamIds.add(paramId) + } + 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) + if (forkKind === 'credential' && isCredentialSetRef(ref.rawValue)) continue + 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) + remappedParamIds.add(paramId) + } + } + + if (remappedParamIds.size > 0) { + const toolBlockConfig = opts.blockConfigs?.[tool.type] ?? getBlock(tool.type) + const toolSubBlocks = toolBlockConfig?.subBlocks + if (toolSubBlocks) { + const currentParams = nextParams ?? params + for (const paramId of remappedParamIds) { + for (const clear of getWorkflowSearchDependentClears(toolSubBlocks, paramId)) { + if (remappedParamIds.has(clear.subBlockId)) continue + const existing = currentParams[clear.subBlockId] + if (existing === '' || existing == null) continue + setParam(clear.subBlockId, '') + } + } + } + } + + 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 +} + +/** + * 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 { + const clearUnresolved = true + const result: SubBlockRecord = {} + const references = new Map() + const unmapped = new Map() + const remappedKeys = new Set() + + 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 valueBeforeResource = 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) + if (forkKind === 'credential' && isCredentialSetRef(ref.rawValue)) continue + 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 (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 + } + } + } + + 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 === '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 }) + } + + if (value !== valueBeforeResource) remappedKeys.add(subBlockKey) + + // 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 } + } + + return { + subBlocks: result, + references: Array.from(references.values()), + unmapped: Array.from(unmapped.values()), + remappedKeys, + } +} + +/** + * Clear every subblock whose `dependsOn` parent was remapped to a different + * target this pass, so a child scoped to the old parent (a KB's document, a + * Slack channel, a sheet tab) never carries a stale id into the target. Reuses + * the search-replace dependent-clear walk (canonical-pair aware, transitive over + * `dependsOn` chains) so fork/promote and in-editor search-replace clear + * identically. Children of an unchanged parent are preserved; a no-op for + * unknown block types or when nothing was remapped. + */ +export function clearDependentsOnRemap( + subBlocks: SubBlockRecord, + blockType: string, + remappedKeys: ReadonlySet +): SubBlockRecord { + if (remappedKeys.size === 0) return subBlocks + const config = getBlock(blockType) + if (!config) return subBlocks + + const toClear = new Set() + for (const key of remappedKeys) { + for (const clear of getWorkflowSearchDependentClears(config.subBlocks, key)) { + if (!remappedKeys.has(clear.subBlockId)) toClear.add(clear.subBlockId) + } + } + + let next: SubBlockRecord | null = null + for (const id of toClear) { + const existing = subBlocks[id] + if (!existing || typeof existing !== 'object') continue + if (existing.value === '' || existing.value == null) continue + next ??= { ...subBlocks } + next[id] = { ...existing, value: '' } + } + return next ?? subBlocks +} + +/** + * A dependent field a sync left empty (it had a source value). `required` ones gate Sync + * and skip redeploy; optional ones are surfaced so a cleared filter never broadens silently. + */ +export interface NeedsConfigurationField { + blockId: string + blockName: string + subBlockKey: string + title: string + required: boolean +} + +/** Evaluate a subblock's `required` (boolean | condition | fn) against a value map. */ +export function isSubBlockRequired( + required: SubBlockConfig['required'], + values: Record +): boolean { + if (required === true) return true + if (!required) return false + // The object/function forms are structurally a SubBlockCondition. + return evaluateSubBlockCondition( + required as Parameters[0], + values + ) +} + +/** Nested `tool-input` dependents (Agent/tool blocks) the TARGET configured that a remap cleared. */ +function collectClearedToolParamDependents( + toolInputKey: string, + blockId: string, + blockName: string, + targetCurrentValue: unknown, + mergedValue: unknown, + out: NeedsConfigurationField[] +): void { + const { array: targetTools } = coerceObjectArray(targetCurrentValue) + const { array: mergedTools } = coerceObjectArray(mergedValue) + if (!mergedTools || !targetTools) return + // Index pairing is only safe when the tool sets line up; otherwise skip rather than + // pair a cleared param against the wrong tool. + if (targetTools.length !== mergedTools.length) return + for (let index = 0; index < mergedTools.length; index++) { + const tool = mergedTools[index] + const targetTool = targetTools[index] + if (!isRecord(tool) || typeof tool.type !== 'string') continue + if (!isRecord(targetTool) || targetTool.type !== tool.type) continue + const toolConfig = getBlock(tool.type) + if (!toolConfig) continue + const targetParams = isRecord(targetTool.params) ? targetTool.params : {} + const mergedParams = isRecord(tool.params) ? tool.params : {} + // A tool's `operation` lives at the tool level, not in params, but conditions + // reference it - merge it in so condition/required gating matches the editor. + const mergedValues = + typeof tool.operation === 'string' + ? { operation: tool.operation, ...mergedParams } + : mergedParams + const toolLabel = typeof tool.title === 'string' && tool.title ? tool.title : toolConfig.name + for (const cfg of toolConfig.subBlocks) { + if (!cfg.dependsOn || !cfg.id) continue + // Only flag a param the TARGET tool had configured (not one the source carried in). + if (!isNonEmptyValue(targetParams[cfg.id])) continue + if (isNonEmptyValue(mergedParams[cfg.id])) continue + // Skip fields gated off by their `condition` (a stale value under an inactive + // operation isn't actually required now). + if (cfg.condition && !evaluateSubBlockCondition(cfg.condition, mergedValues)) continue + out.push({ + blockId, + blockName, + subBlockKey: `${toolInputKey}[${index}].${cfg.id}`, + title: `${toolLabel}: ${cfg.title ?? cfg.id}`, + required: isSubBlockRequired(cfg.required, mergedValues), + }) + } + } +} + +/** + * `dependsOn` children the TARGET workspace had configured (in its draft) that the merge + * left empty - the parent they hang off was swapped/remapped and the value wasn't restored + * or re-picked. Keyed on the TARGET draft (not the source) so a field the source carries + * but the target never configured is NOT flagged (e.g. a pull bringing in the parent's + * label filter the fork never set). Covers top-level subblocks AND nested `tool-input` + * params. `required` ones must be re-picked before the workflow can run (promote skips + * their redeploy); optional ones are surfaced so a filter the swap cleared never broadens + * behavior silently. Pure; `mergedSubBlocks` is the final state about to be written, + * `targetCurrentSubBlocks` the target's pre-sync draft. + */ +export function collectClearedDependents( + blockType: string, + blockId: string, + blockName: string, + targetCurrentSubBlocks: SubBlockRecord, + mergedSubBlocks: SubBlockRecord +): NeedsConfigurationField[] { + const config = getBlock(blockType) + if (!config) return [] + const targetValues = buildSubBlockValues(targetCurrentSubBlocks) + const mergedValues = buildSubBlockValues(mergedSubBlocks) + const fields: NeedsConfigurationField[] = [] + for (const cfg of config.subBlocks) { + if (!cfg.id) continue + // Only flag a field the target had configured (so the user lost their own selection), + // still empty after merge, and currently active (a value under a now-inactive + // `condition`/operation isn't really in play). + if ( + cfg.dependsOn && + isNonEmptyValue(targetValues[cfg.id]) && + !isNonEmptyValue(mergedValues[cfg.id]) && + (!cfg.condition || evaluateSubBlockCondition(cfg.condition, mergedValues)) + ) { + fields.push({ + blockId, + blockName, + subBlockKey: cfg.id, + title: cfg.title ?? cfg.id, + required: isSubBlockRequired(cfg.required, mergedValues), + }) + } + if (cfg.type === 'tool-input') { + collectClearedToolParamDependents( + cfg.id, + blockId, + blockName, + targetValues[cfg.id], + mergedValues[cfg.id], + fields + ) + } + } + return fields +} + +/** + * Parse a nested dependent/override key `toolInput[index].paramId` into its parts (the index + * coerced to a number). Returns null for a plain top-level subblock key. Shared by the diff's + * first-sync draft reader and the override apply so both parse the `toolInput[index].paramId` + * shape identically. + */ +export function parseNestedDependentKey( + key: string +): { toolInputId: string; index: number; paramId: string } | null { + const match = /^([^[]+)\[(\d+)\]\.(.+)$/.exec(key) + if (!match) return null + const [, toolInputId, indexStr, paramId] = match + return { toolInputId, index: Number(indexStr), paramId } +} + +/** + * Read a dependent field's currently-configured value from a target block's draft subBlocks - + * the first-sync fallback used when the stored mapping has no entry yet. Seeds the diff pre-fill + * from the TARGET (never the source, which would overwrite the target's own selection on an edge + * that predates the store). Identity-aware: for a nested `toolInput[index].paramId` key it only + * reads the target draft's param when the target tool at that index is the SAME tool type the + * SOURCE dependent hangs off; otherwise that index holds a different tool whose value isn't this + * field's. Returns '' when unset or when identity can't be verified. + * + * Both records are read structurally (only each subblock's `value`), so callers can pass either a + * persisted {@link SubBlockRecord} (the target draft) or an in-memory `WorkflowState` block's + * subblocks (the source) without a cast. + */ +export function readTargetDraftDependentValue( + targetDraftSubBlocks: Record | undefined, + sourceSubBlocks: Record | undefined, + subBlockKey: string +): string { + if (!targetDraftSubBlocks) return '' + const nested = parseNestedDependentKey(subBlockKey) + if (nested) { + const { toolInputId, index, paramId } = nested + const targetTool = coerceObjectArray(targetDraftSubBlocks[toolInputId]?.value).array?.[index] + if (!isRecord(targetTool) || typeof targetTool.type !== 'string') return '' + const sourceTool = coerceObjectArray(sourceSubBlocks?.[toolInputId]?.value).array?.[index] + if (!isRecord(sourceTool) || sourceTool.type !== targetTool.type) return '' + const params = isRecord(targetTool.params) ? targetTool.params : {} + const value = params[paramId] + return typeof value === 'string' ? value : '' + } + // TODO(fork): identity-guard top-level reads too - only seed when the target draft's parent + // (credential/KB/table) still equals the mapped target. Threading the parent subblock id and + // mapped target value here is invasive, and a changed parent is already blanked by the modal's + // `parentChanged` logic, leaving only a narrow same-index/different-parent first-sync edge. + const value = targetDraftSubBlocks[subBlockKey]?.value + return typeof value === 'string' ? value : '' +} + +/** + * Apply nested `tool-input` overrides onto one tool array, matching by index and + * allowlisting each param to the tool's own reconfigurable dependents (dependsOn + + * selectorKey). Returns the same reference when nothing applied. Handles the array and + * JSON-string stored shapes. + */ +function applyNestedToolOverrides( + value: unknown, + items: ReadonlyArray<{ index: number; paramId: string; value: string }> +): unknown { + const { array, wasString } = coerceObjectArray(value) + if (!array) return value + let changed = false + const merged = array.map((tool, index) => { + const forTool = items.filter((item) => item.index === index) + if (forTool.length === 0) return tool + if (!isRecord(tool) || typeof tool.type !== 'string') return tool + const toolConfig = getBlock(tool.type) + if (!toolConfig) return tool + const allowed = new Set( + toolConfig.subBlocks + .filter((cfg) => cfg.id && cfg.dependsOn && cfg.selectorKey) + .map((cfg) => cfg.id) + ) + const params = isRecord(tool.params) ? tool.params : {} + let nextParams: Record | null = null + for (const item of forTool) { + if (!allowed.has(item.paramId)) continue + nextParams ??= { ...params } + nextParams[item.paramId] = item.value + } + if (!nextParams) return tool + changed = true + return { ...tool, params: nextParams } + }) + if (!changed) return value + return wasString ? JSON.stringify(merged) : merged +} + +/** + * Apply the stored dependent mapping onto the merged subBlocks (called last, after the + * reference transform cleared the source's dependent values, so the stored mapping is the sole + * source of truth for what each selector resolves to). Allowlisted to the block's + * reconfigurable dependent selectors (`dependsOn` + `selectorKey`) - top-level subblocks AND + * nested `tool-input` params keyed `toolInput[index].paramId` - so a crafted value can never + * set a parent/credential field (bypassing mapping validation) or inject a bogus subblock. + * Returns a new record only when something applied. + */ +export function applyDependentOverrides( + subBlocks: SubBlockRecord, + blockType: string, + overrides: ReadonlyMap +): SubBlockRecord { + const config = getBlock(blockType) + if (!config || overrides.size === 0) return subBlocks + + const allowedTopLevel = new Set() + const toolInputIds = new Set() + for (const cfg of config.subBlocks) { + if (!cfg.id) continue + if (cfg.dependsOn && cfg.selectorKey) allowedTopLevel.add(cfg.id) + if (cfg.type === 'tool-input') toolInputIds.add(cfg.id) + } + + const nestedByTool = new Map>() + let next: SubBlockRecord | null = null + + for (const [key, value] of overrides) { + const nested = parseNestedDependentKey(key) + if (nested) { + if (!toolInputIds.has(nested.toolInputId)) continue + const list = nestedByTool.get(nested.toolInputId) ?? [] + list.push({ index: nested.index, paramId: nested.paramId, value }) + nestedByTool.set(nested.toolInputId, list) + continue + } + if (!allowedTopLevel.has(key)) continue + next ??= { ...subBlocks } + // The allowlist already proves this is a real dependent selector, so create the entry + // if the merge dropped it (don't silently skip a legitimate re-pick). + const existing = next[key] + next[key] = existing && typeof existing === 'object' ? { ...existing, value } : { value } + } + + for (const [toolInputId, items] of nestedByTool) { + const existing = (next ?? subBlocks)[toolInputId] + if (!existing || typeof existing !== 'object') continue + const updated = applyNestedToolOverrides(existing.value, items) + if (updated === existing.value) continue + next ??= { ...subBlocks } + next[toolInputId] = { ...existing, value: updated } + } + + return next ?? subBlocks +} + +/** + * 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, blockType: string) => SubBlockRecord { + return (subBlocks, blockType) => { + const result = remapSubBlocks(subBlocks, resolve) + return clearDependentsOnRemap(result.subBlocks, blockType, result.remappedKeys) + } +} + +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..24f96c046f0 --- /dev/null +++ b/apps/sim/lib/workspaces/fork/socket.ts @@ -0,0 +1,43 @@ +import { createLogger } from '@sim/logger' +import { env } from '@/lib/core/config/env' +import { getSocketServerUrl } from '@/lib/core/utils/urls' + +const logger = createLogger('WorkspaceForkSocket') + +async function postToRealtime(path: string, workflowId: string): Promise { + const response = await fetch(`${getSocketServerUrl()}${path}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'x-api-key': env.INTERNAL_API_SECRET }, + body: JSON.stringify({ workflowId }), + }) + if (!response.ok) { + throw new Error(`${path} responded ${response.status}`) + } +} + +/** + * Notify connected canvas clients that a fork promote/rollback force-replaced a + * workflow's state. This mirrors a mothership edit rather than a passive ping: + * + * - `workflow-updated` makes each client reload the workflow from the API the same + * way it reacts to an external full-state edit (copilot / state route), deferring + * while a local diff or unsaved operations are pending so it never clobbers + * in-flight work. Without this the canvas keeps the stale state and a later local + * edit would overwrite the freshly-synced state. + * - `workflow-deployed` refreshes the deployment indicator (the promote/rollback also + * changed which version is deployed). + * + * Best-effort and independent: each notification is attempted regardless of the + * other, and a failure only warns - it never blocks the promote/rollback. + */ +export async function notifyForkWorkflowChanged(workflowId: string): Promise { + const results = await Promise.allSettled([ + postToRealtime('/api/workflow-updated', workflowId), + postToRealtime('/api/workflow-deployed', workflowId), + ]) + for (const result of results) { + if (result.status === 'rejected') { + logger.warn('Fork sync socket notification failed', { workflowId, error: result.reason }) + } + } +} diff --git a/apps/sim/lib/workspaces/policy.test.ts b/apps/sim/lib/workspaces/policy.test.ts index 0828cff4288..31ed0ed7e26 100644 --- a/apps/sim/lib/workspaces/policy.test.ts +++ b/apps/sim/lib/workspaces/policy.test.ts @@ -152,6 +152,45 @@ describe('getWorkspaceCreationPolicy', () => { expect(mockGetHighestPrioritySubscription).not.toHaveBeenCalled() }) + it('without pinning, a null active org falls back to the caller membership org', async () => { + mockFeatureFlags.isBillingEnabled = false + mockGetUserOrganization.mockResolvedValue({ + organizationId: 'user-org', + role: 'admin', + memberId: 'member-1', + }) + mockDbResults.value = [[{ userId: 'owner-1' }]] + + const result = await getWorkspaceCreationPolicy({ + userId: 'user-1', + activeOrganizationId: null, + }) + + expect(result.workspaceMode).toBe(WORKSPACE_MODE.ORGANIZATION) + expect(result.organizationId).toBe('user-org') + }) + + it('pins to the source org: a personal source (null) stays personal regardless of caller org', async () => { + mockFeatureFlags.isBillingEnabled = false + mockGetUserOrganization.mockResolvedValue({ + organizationId: 'user-org', + role: 'admin', + memberId: 'member-1', + }) + mockDbResults.value = [[{ value: 0 }]] + + const result = await getWorkspaceCreationPolicy({ + userId: 'user-1', + activeOrganizationId: null, + pinOrganization: true, + }) + + expect(result.canCreate).toBe(true) + expect(result.workspaceMode).toBe(WORKSPACE_MODE.PERSONAL) + expect(result.organizationId).toBeNull() + expect(result.billedAccountUserId).toBe('user-1') + }) + it('allows org admins on a team plan to create organization workspaces', async () => { mockGetUserOrganization.mockResolvedValueOnce({ organizationId: 'org-1', diff --git a/apps/sim/lib/workspaces/policy.ts b/apps/sim/lib/workspaces/policy.ts index addb1fcad10..b6ec9c3ca13 100644 --- a/apps/sim/lib/workspaces/policy.ts +++ b/apps/sim/lib/workspaces/policy.ts @@ -54,6 +54,13 @@ export interface WorkspaceCreationPolicy { interface GetWorkspaceCreationPolicyParams { userId: string activeOrganizationId?: string | null + /** + * When true, `activeOrganizationId` is authoritative: it is used exactly as given + * (including `null`, which means a personal workspace) and never falls back to the + * caller's membership org. Forks set this so the child always lands in the SOURCE's + * org, not whatever org the acting user happens to belong to. + */ + pinOrganization?: boolean } export function isOrganizationWorkspace( @@ -215,9 +222,12 @@ export async function getInvitePlanCategoryForUser(userId: string): Promise { const membership = await getUserOrganization(userId) - const organizationId = activeOrganizationId ?? membership?.organizationId ?? null + const organizationId = pinOrganization + ? (activeOrganizationId ?? null) + : (activeOrganizationId ?? membership?.organizationId ?? null) const orgRole = organizationId == null ? undefined diff --git a/bun.lock b/bun.lock index b383873e093..652b7e7b02d 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "simstudio", diff --git a/helm/sim/values.yaml b/helm/sim/values.yaml index 0e4e84517f2..4ddbd5bc28b 100644 --- a/helm/sim/values.yaml +++ b/helm/sim/values.yaml @@ -1312,6 +1312,18 @@ cronjobs: successfulJobsHistoryLimit: 3 failedJobsHistoryLimit: 1 + # Processes the transactional outbox (deployment side-effects retry / dead-letter) + # and reaps stale workspace-fork background-work rows. Both are safety nets behind + # the immediate post-commit processing, so frequent runs keep retries timely. + outboxProcess: + enabled: true + name: outbox-process + schedule: "*/1 * * * *" + path: "/api/webhooks/outbox/process" + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 1 + # Global CronJob settings image: repository: curlimages/curl 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/0251_wakeful_smiling_tiger.sql b/packages/db/migrations/0251_wakeful_smiling_tiger.sql new file mode 100644 index 00000000000..905e65937a8 --- /dev/null +++ b/packages/db/migrations/0251_wakeful_smiling_tiger.sql @@ -0,0 +1,50 @@ +CREATE TYPE "public"."background_work_kind" AS ENUM('deployment_side_effects', 'fork_content_copy', 'fork_sync', 'fork_rollback');--> statement-breakpoint +CREATE TYPE "public"."background_work_status_value" AS ENUM('pending', 'processing', 'completed', 'completed_with_warnings', 'failed');--> statement-breakpoint +CREATE TABLE "background_work_status" ( + "id" text PRIMARY KEY NOT NULL, + "workspace_id" text NOT NULL, + "workflow_id" text, + "kind" "background_work_kind" NOT NULL, + "status" "background_work_status_value" NOT NULL, + "message" text, + "error" text, + "metadata" jsonb, + "started_at" timestamp DEFAULT now() NOT NULL, + "completed_at" timestamp, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "workspace_fork_block_map" ( + "id" text PRIMARY KEY NOT NULL, + "child_workspace_id" text NOT NULL, + "parent_workflow_id" text NOT NULL, + "parent_block_id" text NOT NULL, + "child_workflow_id" text NOT NULL, + "child_block_id" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "workspace_fork_dependent_value" ( + "id" text PRIMARY KEY NOT NULL, + "child_workspace_id" text NOT NULL, + "target_workflow_id" text NOT NULL, + "target_block_id" text NOT NULL, + "sub_block_key" text NOT NULL, + "value" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "background_work_status" ADD CONSTRAINT "background_work_status_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "background_work_status" ADD CONSTRAINT "background_work_status_workflow_id_workflow_id_fk" FOREIGN KEY ("workflow_id") REFERENCES "public"."workflow"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "workspace_fork_block_map" ADD CONSTRAINT "workspace_fork_block_map_child_workspace_id_workspace_id_fk" FOREIGN KEY ("child_workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "workspace_fork_dependent_value" ADD CONSTRAINT "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk" FOREIGN KEY ("child_workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "background_work_status_workspace_status_idx" ON "background_work_status" USING btree ("workspace_id","status");--> statement-breakpoint +CREATE INDEX "background_work_status_workflow_status_idx" ON "background_work_status" USING btree ("workflow_id","status");--> statement-breakpoint +CREATE UNIQUE INDEX "workspace_fork_block_map_child_ws_parent_unique" ON "workspace_fork_block_map" USING btree ("child_workspace_id","parent_block_id");--> statement-breakpoint +CREATE UNIQUE INDEX "workspace_fork_block_map_child_ws_child_unique" ON "workspace_fork_block_map" USING btree ("child_workspace_id","child_block_id");--> statement-breakpoint +CREATE INDEX "workspace_fork_block_map_child_ws_parent_wf_idx" ON "workspace_fork_block_map" USING btree ("child_workspace_id","parent_workflow_id");--> statement-breakpoint +CREATE INDEX "workspace_fork_block_map_child_ws_child_wf_idx" ON "workspace_fork_block_map" USING btree ("child_workspace_id","child_workflow_id");--> statement-breakpoint +CREATE INDEX "workspace_fork_dependent_value_child_ws_wf_idx" ON "workspace_fork_dependent_value" USING btree ("child_workspace_id","target_workflow_id");--> statement-breakpoint +CREATE UNIQUE INDEX "workspace_fork_dependent_value_field_unique" ON "workspace_fork_dependent_value" USING btree ("child_workspace_id","target_workflow_id","target_block_id","sub_block_key"); \ No newline at end of file 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/0251_snapshot.json b/packages/db/migrations/meta/0251_snapshot.json new file mode 100644 index 00000000000..2a10bef5a6d --- /dev/null +++ b/packages/db/migrations/meta/0251_snapshot.json @@ -0,0 +1,17528 @@ +{ + "id": "f65305bb-18e8-4196-bf00-de66de42deec", + "prevId": "5e9026af-bce0-4abc-b4f6-82ca3b396ed1", + "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.background_work_status": { + "name": "background_work_status", + "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": false + }, + "kind": { + "name": "kind", + "type": "background_work_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "background_work_status_value", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "background_work_status_workspace_status_idx": { + "name": "background_work_status_workspace_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": {} + }, + "background_work_status_workflow_status_idx": { + "name": "background_work_status_workflow_status_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "background_work_status_workspace_id_workspace_id_fk": { + "name": "background_work_status_workspace_id_workspace_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "background_work_status_workflow_id_workflow_id_fk": { + "name": "background_work_status_workflow_id_workflow_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "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_block_map": { + "name": "workspace_fork_block_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 + }, + "parent_workflow_id": { + "name": "parent_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_block_id": { + "name": "parent_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_workflow_id": { + "name": "child_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_block_id": { + "name": "child_block_id", + "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": { + "workspace_fork_block_map_child_ws_parent_unique": { + "name": "workspace_fork_block_map_child_ws_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_unique": { + "name": "workspace_fork_block_map_child_ws_child_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_parent_wf_idx": { + "name": "workspace_fork_block_map_child_ws_parent_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_wf_idx": { + "name": "workspace_fork_block_map_child_ws_child_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_block_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_block_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_block_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_dependent_value": { + "name": "workspace_fork_dependent_value", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workflow_id": { + "name": "target_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sub_block_key": { + "name": "sub_block_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "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": { + "workspace_fork_dependent_value_child_ws_wf_idx": { + "name": "workspace_fork_dependent_value_child_ws_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_dependent_value_field_unique": { + "name": "workspace_fork_dependent_value_field_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sub_block_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_dependent_value", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_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.background_work_kind": { + "name": "background_work_kind", + "schema": "public", + "values": ["deployment_side_effects", "fork_content_copy", "fork_sync", "fork_rollback"] + }, + "public.background_work_status_value": { + "name": "background_work_status_value", + "schema": "public", + "values": ["pending", "processing", "completed", "completed_with_warnings", "failed"] + }, + "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..d398ff03f79 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1744,6 +1744,20 @@ "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 + }, + { + "idx": 251, + "version": "7", + "when": 1782606413439, + "tag": "0251_wakeful_smiling_tiger", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index fb0cd842e1a..58d4d7682ca 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,230 @@ 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 + ), + }) +) + +/** + * Stable 1:1 block-identity map between a fork (child) and its parent, per edge. Seeded at + * fork creation (parent block -> derived child block) and reconciled on every promote. + * Promote looks a source block up here to reuse its counterpart's EXISTING id instead of + * re-deriving: without it, pushing a fork's workflow over the parent would re-key the + * parent's blocks and change their webhook URLs (the path falls back to the block id). + * + * Each pair records BOTH workflow ids so a lookup can be scoped to the workflow it belongs + * to: a target workflow that was archived and re-created gets a fresh id (the pair no longer + * matches), which avoids reusing an archived workflow's block id and colliding on the global + * `workflow_blocks` primary key. Block ids are plain text (no FK to `workflow_blocks`, which + * is rewritten on every deploy); only the edge (`child_workspace_id`) cascades. A parent + * block can map to different children across sibling forks, so uniqueness is per (edge, + * parent) and per (edge, child). + */ +export const workspaceForkBlockMap = pgTable( + 'workspace_fork_block_map', + { + id: text('id').primaryKey(), + childWorkspaceId: text('child_workspace_id') + .notNull() + .references(() => workspace.id, { onDelete: 'cascade' }), + parentWorkflowId: text('parent_workflow_id').notNull(), + parentBlockId: text('parent_block_id').notNull(), + childWorkflowId: text('child_workflow_id').notNull(), + childBlockId: text('child_block_id').notNull(), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (table) => ({ + // Pull resolves parent source block -> child target; one child per parent block per edge. + childWsParentBlockUnique: uniqueIndex('workspace_fork_block_map_child_ws_parent_unique').on( + table.childWorkspaceId, + table.parentBlockId + ), + // Push resolves child source block -> parent target; one parent per child block per edge. + childWsChildBlockUnique: uniqueIndex('workspace_fork_block_map_child_ws_child_unique').on( + table.childWorkspaceId, + table.childBlockId + ), + // Reconcile deletes a source workflow's pairs by its (stable) workflow id before + // re-inserting the live ones, so index both workflow sides for that sweep. + childWsParentWorkflowIdx: index('workspace_fork_block_map_child_ws_parent_wf_idx').on( + table.childWorkspaceId, + table.parentWorkflowId + ), + childWsChildWorkflowIdx: index('workspace_fork_block_map_child_ws_child_wf_idx').on( + table.childWorkspaceId, + table.childWorkflowId + ), + }) +) + +/** + * The user's stored dependent-field re-picks for an edge: a (target workflow, target block, + * subblock) -> selected value mapping (a Gmail label, a KB document, a sheet tab). The sync + * modal reads and writes this, and every promote applies it verbatim - it is the single + * source of truth for dependent values, replacing the old implicit "preserve the target's + * value if the credential is unchanged" path. Block ids are plain text (no FK to + * `workflow_blocks`, which is rewritten on every deploy); only the edge (`child_workspace_id`) + * cascades. The target workflow id encodes direction (push -> parent workflow, pull -> child + * workflow), so no separate direction column is needed. + */ +export const workspaceForkDependentValue = pgTable( + 'workspace_fork_dependent_value', + { + id: text('id').primaryKey(), + childWorkspaceId: text('child_workspace_id') + .notNull() + .references(() => workspace.id, { onDelete: 'cascade' }), + targetWorkflowId: text('target_workflow_id').notNull(), + targetBlockId: text('target_block_id').notNull(), + subBlockKey: text('sub_block_key').notNull(), + value: text('value').notNull(), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (table) => ({ + // Reconcile replaces a workflow's stored values by its id, so index that sweep. + childWsWorkflowIdx: index('workspace_fork_dependent_value_child_ws_wf_idx').on( + table.childWorkspaceId, + table.targetWorkflowId + ), + // One stored value per (edge, target workflow, target block, subblock). + childWsFieldUnique: uniqueIndex('workspace_fork_dependent_value_field_unique').on( + table.childWorkspaceId, + table.targetWorkflowId, + table.targetBlockId, + table.subBlockKey + ), + }) +) + +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 + ), + }) +) + +export const backgroundWorkKindEnum = pgEnum('background_work_kind', [ + 'deployment_side_effects', + 'fork_content_copy', + 'fork_sync', + 'fork_rollback', +]) + +export const backgroundWorkStatusValueEnum = pgEnum('background_work_status_value', [ + 'pending', + 'processing', + 'completed', + 'completed_with_warnings', + 'failed', +]) + +/** + * Durable status for asynchronous background work (post-sync/rollback deployment + * side-effects and fork content copy), so the canvas can show a "work in progress" + * banner that survives a reload. A row scoped to a single workflow sets `workflowId`; + * workspace-spanning work (fork content copy) leaves it null. + */ +export const backgroundWorkStatus = pgTable( + 'background_work_status', + { + id: text('id').primaryKey(), + workspaceId: text('workspace_id') + .notNull() + .references(() => workspace.id, { onDelete: 'cascade' }), + workflowId: text('workflow_id').references(() => workflow.id, { onDelete: 'cascade' }), + kind: backgroundWorkKindEnum('kind').notNull(), + status: backgroundWorkStatusValueEnum('status').notNull(), + message: text('message'), + error: text('error'), + metadata: jsonb('metadata'), + startedAt: timestamp('started_at').notNull().defaultNow(), + completedAt: timestamp('completed_at'), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (table) => ({ + workspaceStatusIdx: index('background_work_status_workspace_status_idx').on( + table.workspaceId, + table.status + ), + workflowStatusIdx: index('background_work_status_workflow_status_idx').on( + table.workflowId, + table.status + ), }) ) 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/packages/testing/src/mocks/schema.mock.ts b/packages/testing/src/mocks/schema.mock.ts index a175c179581..153a4fc429f 100644 --- a/packages/testing/src/mocks/schema.mock.ts +++ b/packages/testing/src/mocks/schema.mock.ts @@ -1240,6 +1240,16 @@ export const schemaMock = { error: 'error', locators: 'locators', }, + workspaceForkDependentValue: { + id: 'id', + childWorkspaceId: 'childWorkspaceId', + targetWorkflowId: 'targetWorkflowId', + targetBlockId: 'targetBlockId', + subBlockKey: 'subBlockKey', + value: 'value', + createdAt: 'createdAt', + updatedAt: 'updatedAt', + }, /** Custom type export for tsvector */ tsvector: 'tsvector', } diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index f595d845c85..11072e29a2e 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: 864, - zodRoutes: 864, + totalRoutes: 872, + zodRoutes: 872, nonZodRoutes: 0, } as const