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

620 lines
20 KiB
TypeScript

import { useCallback } from 'react'
import { createLogger } from '@sim/logger'
import type { QueryClient } from '@tanstack/react-query'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { requestJson, requestRaw } from '@/lib/api/client/request'
import {
type ActivateDeploymentVersionResponse,
activateDeploymentVersionContract,
type ChatDeploymentStatus,
type ChatDetail,
type DeploymentInfoResponse,
type DeploymentVersionsResponse,
type DeployWorkflowResponse,
deployWorkflowContract,
getChatDeploymentStatusContract,
getChatDetailContract,
getDeployedWorkflowStateContract,
getDeploymentInfoContract,
listDeploymentVersionsContract,
type UpdateDeploymentVersionMetadataResponse,
undeployWorkflowContract,
updateDeploymentVersionMetadataContract,
updatePublicApiContract,
} from '@/lib/api/contracts/deployments'
import { wandGenerateStreamContract } from '@/lib/api/contracts/hotspots'
import { fetchDeploymentVersionState } from '@/hooks/queries/utils/fetch-deployment-version-state'
import { workflowKeys } from '@/hooks/queries/utils/workflow-keys'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
const logger = createLogger('DeploymentQueries')
export type { ChatDetail, DeploymentVersionsResponse }
export const DEPLOYMENT_INFO_STALE_TIME = 30 * 1000
export const DEPLOYED_WORKFLOW_STATE_STALE_TIME = 30 * 1000
export const DEPLOYMENT_VERSIONS_STALE_TIME = 30 * 1000
export const CHAT_DEPLOYMENT_STATUS_STALE_TIME = 30 * 1000
export const CHAT_DETAIL_STALE_TIME = 30 * 1000
/**
* Query key factory for deployment-related queries
*/
export const deploymentKeys = {
all: ['deployments'] as const,
infos: () => [...deploymentKeys.all, 'info'] as const,
info: (workflowId: string | null) => [...deploymentKeys.infos(), workflowId ?? ''] as const,
deployedState: (workflowId: string | null) =>
[...deploymentKeys.all, 'deployedState', workflowId ?? ''] as const,
allVersions: () => [...deploymentKeys.all, 'versions'] as const,
versions: (workflowId: string | null) =>
[...deploymentKeys.allVersions(), workflowId ?? ''] as const,
chatStatuses: () => [...deploymentKeys.all, 'chatStatus'] as const,
chatStatus: (workflowId: string | null) =>
[...deploymentKeys.chatStatuses(), workflowId ?? ''] as const,
chatDetails: () => [...deploymentKeys.all, 'chatDetail'] as const,
chatDetail: (chatId: string | null) => [...deploymentKeys.chatDetails(), chatId ?? ''] as const,
}
/**
* Invalidates the core deployment queries (info, deployedState, versions) for a workflow.
* Used by mutation onSuccess callbacks and manual invalidation after chat deployments.
*/
export function invalidateDeploymentQueries(queryClient: QueryClient, workflowId: string) {
return Promise.all([
queryClient.invalidateQueries({ queryKey: deploymentKeys.info(workflowId) }),
queryClient.invalidateQueries({ queryKey: deploymentKeys.deployedState(workflowId) }),
queryClient.invalidateQueries({ queryKey: deploymentKeys.versions(workflowId) }),
queryClient.invalidateQueries({ queryKey: deploymentKeys.chatStatus(workflowId) }),
])
}
export async function refetchDeploymentBoundary(queryClient: QueryClient, workflowId: string) {
await invalidateDeploymentQueries(queryClient, workflowId)
await Promise.all([
queryClient.refetchQueries({ queryKey: deploymentKeys.info(workflowId) }),
queryClient.refetchQueries({ queryKey: deploymentKeys.deployedState(workflowId) }),
queryClient.refetchQueries({ queryKey: workflowKeys.state(workflowId) }),
])
}
export type WorkflowDeploymentInfo = DeploymentInfoResponse & {
deployedAt: string | null
apiKey: string | null
needsRedeployment: boolean
isPublicApi: boolean
}
/**
* Fetches deployment info for a workflow
*/
async function fetchDeploymentInfo(
workflowId: string,
signal?: AbortSignal
): Promise<WorkflowDeploymentInfo> {
const data = await requestJson(getDeploymentInfoContract, {
params: { id: workflowId },
signal,
})
return {
isDeployed: data.isDeployed ?? false,
deployedAt: data.deployedAt ?? null,
apiKey: data.apiKey ?? null,
needsRedeployment: data.needsRedeployment ?? false,
isPublicApi: data.isPublicApi ?? false,
}
}
/**
* Hook to fetch deployment info for a workflow.
* Provides isDeployed status, deployedAt timestamp, apiKey info, and needsRedeployment flag.
*/
export function useDeploymentInfo(
workflowId: string | null,
options?: { enabled?: boolean; refetchOnMount?: boolean | 'always' }
) {
return useQuery({
queryKey: deploymentKeys.info(workflowId),
queryFn: ({ signal }) => fetchDeploymentInfo(workflowId!, signal),
enabled: Boolean(workflowId) && (options?.enabled ?? true),
staleTime: DEPLOYMENT_INFO_STALE_TIME,
...(options?.refetchOnMount !== undefined && { refetchOnMount: options.refetchOnMount }),
})
}
/**
* Fetches the deployed workflow state snapshot for a workflow
*/
async function fetchDeployedWorkflowState(
workflowId: string,
signal?: AbortSignal
): Promise<WorkflowState | null> {
const data = await requestJson(getDeployedWorkflowStateContract, {
params: { id: workflowId },
signal,
})
return data.deployedState || null
}
/**
* Hook to fetch the deployed workflow state snapshot.
* Returns the full workflow state at the time of the last active deployment.
*/
export function useDeployedWorkflowState(
workflowId: string | null,
options?: { enabled?: boolean }
) {
return useQuery({
queryKey: deploymentKeys.deployedState(workflowId),
queryFn: ({ signal }) => fetchDeployedWorkflowState(workflowId!, signal),
enabled: Boolean(workflowId) && (options?.enabled ?? true),
staleTime: DEPLOYED_WORKFLOW_STATE_STALE_TIME,
})
}
/**
* Fetches all deployment versions for a workflow
*/
async function fetchDeploymentVersions(
workflowId: string,
signal?: AbortSignal
): Promise<DeploymentVersionsResponse> {
const data = await requestJson(listDeploymentVersionsContract, {
params: { id: workflowId },
signal,
})
return {
versions: Array.isArray(data.versions) ? data.versions : [],
}
}
/**
* Hook to fetch deployment versions for a workflow.
* Returns a list of all deployment versions with their metadata.
*/
export function useDeploymentVersions(workflowId: string | null, options?: { enabled?: boolean }) {
return useQuery({
queryKey: deploymentKeys.versions(workflowId),
queryFn: ({ signal }) => fetchDeploymentVersions(workflowId!, signal),
enabled: Boolean(workflowId) && (options?.enabled ?? true),
staleTime: DEPLOYMENT_VERSIONS_STALE_TIME,
})
}
/**
* Fetches chat deployment status for a workflow
*/
async function fetchChatDeploymentStatus(
workflowId: string,
signal?: AbortSignal
): Promise<ChatDeploymentStatus> {
const data = await requestJson(getChatDeploymentStatusContract, {
params: { id: workflowId },
signal,
})
return {
isDeployed: data.isDeployed ?? false,
deployment: data.deployment ?? null,
}
}
/**
* Hook to fetch chat deployment status for a workflow.
* Returns whether a chat is deployed and basic deployment info.
*/
export function useChatDeploymentStatus(
workflowId: string | null,
options?: { enabled?: boolean }
) {
return useQuery({
queryKey: deploymentKeys.chatStatus(workflowId),
queryFn: ({ signal }) => fetchChatDeploymentStatus(workflowId!, signal),
enabled: Boolean(workflowId) && (options?.enabled ?? true),
staleTime: CHAT_DEPLOYMENT_STATUS_STALE_TIME,
})
}
/**
* Fetches chat detail by chat ID
*/
async function fetchChatDetail(chatId: string, signal?: AbortSignal): Promise<ChatDetail> {
return requestJson(getChatDetailContract, {
params: { id: chatId },
signal,
})
}
/**
* Hook to fetch chat detail by chat ID.
* Returns full chat configuration including customizations and auth settings.
*/
export function useChatDetail(chatId: string | null, options?: { enabled?: boolean }) {
return useQuery({
queryKey: deploymentKeys.chatDetail(chatId),
queryFn: ({ signal }) => fetchChatDetail(chatId!, signal),
enabled: Boolean(chatId) && (options?.enabled ?? true),
staleTime: CHAT_DETAIL_STALE_TIME,
})
}
/**
* Combined hook to fetch chat deployment info for a workflow.
* First fetches the chat status, then if deployed, fetches the chat detail.
* Returns the combined result.
*/
export function useChatDeploymentInfo(workflowId: string | null, options?: { enabled?: boolean }) {
const queryClient = useQueryClient()
const statusQuery = useChatDeploymentStatus(workflowId, options)
const chatId = statusQuery.data?.deployment?.id ?? null
const detailQuery = useChatDetail(chatId, {
enabled: Boolean(chatId) && statusQuery.isSuccess && (options?.enabled ?? true),
})
const refetch = useCallback(async () => {
const statusResult = await statusQuery.refetch()
const nextChatId = statusResult.data?.deployment?.id
if (nextChatId) {
await queryClient.fetchQuery({
queryKey: deploymentKeys.chatDetail(nextChatId),
queryFn: ({ signal }) => fetchChatDetail(nextChatId, signal),
staleTime: CHAT_DETAIL_STALE_TIME,
})
}
}, [queryClient, statusQuery.refetch])
return {
isLoading:
statusQuery.isLoading || Boolean(statusQuery.data?.isDeployed && detailQuery.isLoading),
isError: statusQuery.isError || detailQuery.isError,
error: statusQuery.error ?? detailQuery.error,
chatExists: statusQuery.data?.isDeployed ?? false,
existingChat: detailQuery.data ?? null,
refetch,
}
}
/**
* Variables for deploy workflow mutation
*/
interface DeployWorkflowVariables {
workflowId: string
}
type DeployWorkflowResult = Omit<DeployWorkflowResponse, 'deployedAt' | 'apiKey'> & {
deployedAt?: string
apiKey?: string
}
/**
* Mutation hook for deploying a workflow.
* Invalidates deployment info and versions queries on success.
*/
export function useDeployWorkflow() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ workflowId }: DeployWorkflowVariables): Promise<DeployWorkflowResult> => {
const data = await requestJson(deployWorkflowContract, {
params: { id: workflowId },
})
return {
isDeployed: data.isDeployed ?? false,
deployedAt: data.deployedAt ?? undefined,
apiKey: data.apiKey ?? undefined,
warnings: data.warnings,
}
},
onSettled: (_data, error, variables) => {
if (error) {
logger.error('Failed to deploy workflow', { error })
return invalidateDeploymentQueries(queryClient, variables.workflowId)
}
logger.info('Workflow deployed successfully', { workflowId: variables.workflowId })
return refetchDeploymentBoundary(queryClient, variables.workflowId)
},
})
}
/**
* Variables for undeploy workflow mutation
*/
interface UndeployWorkflowVariables {
workflowId: string
}
/**
* Mutation hook for undeploying a workflow.
* Invalidates deployment info and versions queries on success.
*/
export function useUndeployWorkflow() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ workflowId }: UndeployWorkflowVariables) => {
return requestJson(undeployWorkflowContract, {
params: { id: workflowId },
})
},
onSettled: (_data, error, variables) => {
if (error) {
logger.error('Failed to undeploy workflow', { error })
} else {
logger.info('Workflow undeployed successfully', { workflowId: variables.workflowId })
}
return Promise.all([
invalidateDeploymentQueries(queryClient, variables.workflowId),
queryClient.invalidateQueries({
queryKey: deploymentKeys.chatStatus(variables.workflowId),
}),
])
},
})
}
/**
* Variables for update deployment version mutation
*/
interface UpdateDeploymentVersionVariables {
workflowId: string
version: number
name?: string
description?: string | null
}
type UpdateDeploymentVersionResult = UpdateDeploymentVersionMetadataResponse
/**
* Mutation hook for updating a deployment version's name or description.
* Invalidates versions query on success.
*/
export function useUpdateDeploymentVersion() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({
workflowId,
version,
name,
description,
}: UpdateDeploymentVersionVariables): Promise<UpdateDeploymentVersionResult> => {
return requestJson(updateDeploymentVersionMetadataContract, {
params: { id: workflowId, version },
body: { name, description },
})
},
onSettled: (_data, error, variables) => {
if (!error) {
logger.info('Deployment version updated', {
workflowId: variables.workflowId,
version: variables.version,
})
} else {
logger.error('Failed to update deployment version', { error })
}
return queryClient.invalidateQueries({
queryKey: deploymentKeys.versions(variables.workflowId),
})
},
})
}
/**
* Variables for generating a version description
*/
interface GenerateVersionDescriptionVariables {
workflowId: string
version: number
onStreamChunk?: (accumulated: string) => void
/** Aborts the diff fetches + SSE stream when the modal unmounts mid-generation. */
signal?: AbortSignal
}
const VERSION_DESCRIPTION_SYSTEM_PROMPT = `You are writing deployment version descriptions for a workflow automation platform.
Write a brief, factual description (1-3 sentences, under 2000 characters) that states what changed between versions.
Guidelines:
- Use the specific values provided (credential names, channel names, model names)
- Be precise: "Changes Slack channel from #general to #alerts" not "Updates channel configuration"
- Combine related changes: "Updates Agent model to claude-sonnet-4-5 and increases temperature to 0.8"
- For added/removed blocks, mention their purpose if clear from the type
Format rules:
- Plain text only, no quotes around the response
- No markdown formatting
- No filler phrases ("for improved efficiency", "streamlining the workflow")
- No version numbers or "This version" prefixes
Examples:
- Switches Agent model from gpt-4o to claude-sonnet-4-5. Changes Slack credential to Production OAuth.
- Adds Gmail notification block for sending alerts. Removes unused Function block. Updates Router conditions.
- Updates system prompt for more concise responses. Reduces temperature from 0.7 to 0.3.
- Connects Slack block to Router. Adds 2 new workflow connections. Configures error handling path.`
/**
* Hook for generating a version description using AI based on workflow diff
*/
export function useGenerateVersionDescription() {
return useMutation({
mutationFn: async ({
workflowId,
version,
onStreamChunk,
signal,
}: GenerateVersionDescriptionVariables): Promise<string> => {
const { generateWorkflowDiffSummary, formatDiffSummaryForDescriptionAsync } = await import(
'@/lib/workflows/comparison/compare'
)
const currentState = await fetchDeploymentVersionState(workflowId, version, signal)
let previousState = null
if (version > 1) {
try {
previousState = await fetchDeploymentVersionState(workflowId, version - 1, signal)
} catch {
// Previous version may not exist, continue without it
}
}
const diffSummary = generateWorkflowDiffSummary(currentState, previousState)
const diffText = await formatDiffSummaryForDescriptionAsync(
diffSummary,
currentState,
workflowId
)
const wandResponse = await requestRaw(
wandGenerateStreamContract,
{
body: {
prompt: `Generate a deployment version description based on these changes:\n\n${diffText}`,
systemPrompt: VERSION_DESCRIPTION_SYSTEM_PROMPT,
stream: true,
workflowId,
},
signal,
},
{
headers: {
'Cache-Control': 'no-cache, no-transform',
},
cache: 'no-store',
}
)
if (!wandResponse.body) {
throw new Error('Response body is null')
}
const { readSSEStream } = await import('@/lib/core/utils/sse')
const accumulatedContent = await readSSEStream(wandResponse.body, {
onAccumulated: onStreamChunk,
signal,
})
if (!accumulatedContent) {
throw new Error('Failed to generate description')
}
return accumulatedContent.trim()
},
onSuccess: (content) => {
logger.info('Generated version description', { length: content.length })
},
onError: (error) => {
if (error.name === 'AbortError') return
logger.error('Failed to generate version description', { error })
},
})
}
/**
* Variables for activate version mutation
*/
interface ActivateVersionVariables {
workflowId: string
version: number
}
type ActivateVersionResult = ActivateDeploymentVersionResponse
/**
* Mutation hook for activating (promoting) a specific deployment version.
* Invalidates deployment info and versions queries on success.
*/
export function useActivateDeploymentVersion() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({
workflowId,
version,
}: ActivateVersionVariables): Promise<ActivateVersionResult> => {
return requestJson(activateDeploymentVersionContract, {
params: { id: workflowId, version },
body: { isActive: true },
})
},
onMutate: async ({ workflowId, version }) => {
await queryClient.cancelQueries({ queryKey: deploymentKeys.versions(workflowId) })
const previousVersions = queryClient.getQueryData<DeploymentVersionsResponse>(
deploymentKeys.versions(workflowId)
)
if (previousVersions) {
queryClient.setQueryData<DeploymentVersionsResponse>(deploymentKeys.versions(workflowId), {
versions: previousVersions.versions.map((v) => ({
...v,
isActive: v.version === version,
})),
})
}
return { previousVersions }
},
onError: (_, variables, context) => {
logger.error('Failed to activate deployment version')
if (context?.previousVersions) {
queryClient.setQueryData(
deploymentKeys.versions(variables.workflowId),
context.previousVersions
)
}
},
onSettled: (_data, error, variables) => {
if (!error) {
logger.info('Deployment version activated', {
workflowId: variables.workflowId,
version: variables.version,
})
}
return invalidateDeploymentQueries(queryClient, variables.workflowId)
},
})
}
/**
* Variables for updating public API access
*/
interface UpdatePublicApiVariables {
workflowId: string
isPublicApi: boolean
}
/**
* Mutation hook for toggling a workflow's public API access.
* Invalidates deployment info query on success.
*/
export function useUpdatePublicApi() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ workflowId, isPublicApi }: UpdatePublicApiVariables) => {
return requestJson(updatePublicApiContract, {
params: { id: workflowId },
body: { isPublicApi },
})
},
onSettled: (_data, error, variables) => {
if (!error) {
logger.info('Public API setting updated', {
workflowId: variables.workflowId,
isPublicApi: variables.isPublicApi,
})
} else {
logger.error('Failed to update public API setting', { error })
}
return queryClient.invalidateQueries({
queryKey: deploymentKeys.info(variables.workflowId),
})
},
})
}