chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
+370
View File
@@ -0,0 +1,370 @@
import { db } from '@sim/db'
import {
apiKey,
chat,
webhook,
workflow,
workflowDeploymentVersion,
workflowFolder,
workflowMcpTool,
workflowSchedule,
workspace,
} from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq, inArray, isNull } from 'drizzle-orm'
import { cleanupWorkflowAliasBacking } from '@/lib/copilot/vfs/workflow-alias-backing'
import { env } from '@/lib/core/config/env'
import { PlatformEvents } from '@/lib/core/telemetry'
import { generateRequestId } from '@/lib/core/utils/request'
import { getSocketServerUrl } from '@/lib/core/utils/urls'
import { mcpPubSub } from '@/lib/mcp/pubsub'
import { getWorkflowById } from '@/lib/workflows/utils'
const logger = createLogger('WorkflowLifecycle')
interface ArchiveWorkflowOptions {
requestId: string
notifySocket?: boolean
archivedAt?: Date
}
async function notifyWorkflowArchived(workflowId: string, requestId: string): Promise<void> {
try {
const socketResponse = await fetch(`${getSocketServerUrl()}/api/workflow-deleted`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': env.INTERNAL_API_SECRET,
},
body: JSON.stringify({ workflowId }),
})
if (!socketResponse.ok) {
logger.warn(`[${requestId}] Failed to notify Socket.IO about archived workflow ${workflowId}`)
}
} catch (error) {
logger.warn(`[${requestId}] Error notifying Socket.IO about archived workflow ${workflowId}`, {
error,
})
}
}
async function cleanupExternalWebhooksForWorkflow(
workflowId: string,
requestId: string
): Promise<void> {
try {
const { cleanupExternalWebhook } = await import('@/lib/webhooks/provider-subscriptions')
const webhooksToCleanup = await db
.select({
webhook: webhook,
workflow: {
id: workflow.id,
userId: workflow.userId,
workspaceId: workflow.workspaceId,
},
})
.from(webhook)
.innerJoin(workflow, eq(webhook.workflowId, workflow.id))
.where(eq(webhook.workflowId, workflowId))
for (const webhookData of webhooksToCleanup) {
try {
await cleanupExternalWebhook(webhookData.webhook, webhookData.workflow, requestId)
} catch (error) {
logger.warn(
`[${requestId}] Failed to cleanup external webhook ${webhookData.webhook.id} for workflow ${workflowId}`,
{ error }
)
}
}
} catch (error) {
logger.warn(`[${requestId}] Error during external webhook cleanup for workflow ${workflowId}`, {
error,
})
}
}
export async function archiveWorkflow(
workflowId: string,
options: ArchiveWorkflowOptions
): Promise<{ archived: boolean; workflow: Awaited<ReturnType<typeof getWorkflowById>> | null }> {
const existingWorkflow = await getWorkflowById(workflowId, { includeArchived: true })
if (!existingWorkflow) {
return { archived: false, workflow: null }
}
if (existingWorkflow.archivedAt) {
return { archived: false, workflow: existingWorkflow }
}
const now = options.archivedAt ?? new Date()
const affectedWorkflowMcpServers = await db
.select({ serverId: workflowMcpTool.serverId })
.from(workflowMcpTool)
.where(and(eq(workflowMcpTool.workflowId, workflowId), isNull(workflowMcpTool.archivedAt)))
await db.transaction(async (tx) => {
await tx
.update(workflowSchedule)
.set({
archivedAt: now,
updatedAt: now,
status: 'disabled',
nextRunAt: null,
lastQueuedAt: null,
})
.where(and(eq(workflowSchedule.workflowId, workflowId), isNull(workflowSchedule.archivedAt)))
await tx
.update(webhook)
.set({
archivedAt: now,
updatedAt: now,
isActive: false,
})
.where(and(eq(webhook.workflowId, workflowId), isNull(webhook.archivedAt)))
await tx
.update(chat)
.set({
archivedAt: now,
updatedAt: now,
isActive: false,
})
.where(and(eq(chat.workflowId, workflowId), isNull(chat.archivedAt)))
await tx
.update(workflowMcpTool)
.set({
archivedAt: now,
updatedAt: now,
})
.where(and(eq(workflowMcpTool.workflowId, workflowId), isNull(workflowMcpTool.archivedAt)))
await tx
.update(workflowDeploymentVersion)
.set({
isActive: false,
})
.where(eq(workflowDeploymentVersion.workflowId, workflowId))
await tx
.update(workflow)
.set({
archivedAt: now,
updatedAt: now,
isDeployed: false,
isPublicApi: false,
})
.where(and(eq(workflow.id, workflowId), isNull(workflow.archivedAt)))
})
try {
PlatformEvents.workflowDeleted({
workflowId,
workspaceId: existingWorkflow.workspaceId || undefined,
})
} catch {}
if (options.notifySocket !== false) {
await notifyWorkflowArchived(workflowId, options.requestId)
}
if (existingWorkflow.workspaceId) {
try {
await cleanupWorkflowAliasBacking({
workspaceId: existingWorkflow.workspaceId,
workflowId,
deletedAt: now,
})
} catch (error) {
logger.warn(`[${options.requestId}] Failed to clean up workflow alias backing`, {
workflowId,
workspaceId: existingWorkflow.workspaceId,
error,
})
}
}
await cleanupExternalWebhooksForWorkflow(workflowId, options.requestId)
if (existingWorkflow.workspaceId && mcpPubSub && affectedWorkflowMcpServers.length > 0) {
const uniqueServerIds = [...new Set(affectedWorkflowMcpServers.map((row) => row.serverId))]
for (const serverId of uniqueServerIds) {
mcpPubSub.publishWorkflowToolsChanged({
serverId,
workspaceId: existingWorkflow.workspaceId,
})
}
}
return {
archived: true,
workflow: await getWorkflowById(workflowId, { includeArchived: true }),
}
}
interface RestoreWorkflowOptions {
requestId: string
}
export async function restoreWorkflow(
workflowId: string,
options: RestoreWorkflowOptions
): Promise<{ restored: boolean; workflow: Awaited<ReturnType<typeof getWorkflowById>> | null }> {
const existingWorkflow = await getWorkflowById(workflowId, { includeArchived: true })
if (!existingWorkflow) {
return { restored: false, workflow: null }
}
if (!existingWorkflow.archivedAt) {
return { restored: false, workflow: existingWorkflow }
}
if (existingWorkflow.workspaceId) {
const { getWorkspaceWithOwner } = await import('@/lib/workspaces/permissions/utils')
const ws = await getWorkspaceWithOwner(existingWorkflow.workspaceId)
if (!ws || ws.archivedAt) {
throw new Error('Cannot restore workflow into an archived workspace')
}
}
let clearFolderId = false
if (existingWorkflow.folderId) {
const [folder] = await db
.select({ archivedAt: workflowFolder.archivedAt })
.from(workflowFolder)
.where(eq(workflowFolder.id, existingWorkflow.folderId))
if (!folder || folder.archivedAt) {
clearFolderId = true
}
}
const now = new Date()
await db.transaction(async (tx) => {
await tx
.update(workflow)
.set({
archivedAt: null,
updatedAt: now,
...(clearFolderId && { folderId: null }),
})
.where(eq(workflow.id, workflowId))
await tx
.update(workflowSchedule)
.set({ archivedAt: null, updatedAt: now })
.where(eq(workflowSchedule.workflowId, workflowId))
await tx
.update(webhook)
.set({ archivedAt: null, updatedAt: now })
.where(eq(webhook.workflowId, workflowId))
await tx
.update(chat)
.set({ archivedAt: null, updatedAt: now })
.where(eq(chat.workflowId, workflowId))
await tx
.update(workflowMcpTool)
.set({ archivedAt: null, updatedAt: now })
.where(eq(workflowMcpTool.workflowId, workflowId))
})
logger.info(`[${options.requestId}] Restored workflow ${workflowId}`)
return {
restored: true,
workflow: await getWorkflowById(workflowId),
}
}
async function archiveWorkflows(
workflowIds: string[],
options: ArchiveWorkflowOptions
): Promise<number> {
const uniqueWorkflowIds = Array.from(new Set(workflowIds))
let archivedCount = 0
for (const workflowId of uniqueWorkflowIds) {
const result = await archiveWorkflow(workflowId, options)
if (result.archived) {
archivedCount += 1
}
}
return archivedCount
}
export async function archiveWorkflowsForWorkspace(
workspaceId: string,
options: ArchiveWorkflowOptions
): Promise<number> {
const workflows = await db
.select({ id: workflow.id })
.from(workflow)
.where(and(eq(workflow.workspaceId, workspaceId), isNull(workflow.archivedAt)))
return archiveWorkflows(
workflows.map((entry) => entry.id),
options
)
}
export async function archiveWorkflowsByIdsInWorkspace(
workspaceId: string,
workflowIds: string[],
options: ArchiveWorkflowOptions
): Promise<number> {
if (workflowIds.length === 0) {
return 0
}
const workflows = await db
.select({ id: workflow.id })
.from(workflow)
.where(
and(
eq(workflow.workspaceId, workspaceId),
isNull(workflow.archivedAt),
inArray(workflow.id, workflowIds)
)
)
return archiveWorkflows(
workflows.map((entry) => entry.id),
options
)
}
/**
* Disables all resources owned by a banned user by archiving every workspace
* they own (cascading to workflows, chats, KBs, tables, files, etc.)
* and deleting their personal API keys.
*/
export async function disableUserResources(userId: string): Promise<void> {
const requestId = generateRequestId()
logger.info(`[${requestId}] Disabling resources for banned user ${userId}`)
const { archiveWorkspace } = await import('@/lib/workspaces/lifecycle')
const ownedWorkspaces = await db
.select({ id: workspace.id })
.from(workspace)
.where(and(eq(workspace.ownerId, userId), isNull(workspace.archivedAt)))
await Promise.all([
...ownedWorkspaces.map((w) => archiveWorkspace(w.id, { requestId })),
db.delete(apiKey).where(eq(apiKey.userId, userId)),
])
logger.info(
`[${requestId}] Disabled resources for user ${userId}: archived ${ownedWorkspaces.length} workspaces, deleted API keys`
)
}