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
+613
View File
@@ -0,0 +1,613 @@
import { db } from '@sim/db'
import { workflowFolder, workflow as workflowTable } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow'
import { generateId } from '@sim/utils/id'
import { and, asc, eq, inArray, isNull, max, min, sql } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { ensureWorkflowAliasBacking } from '@/lib/copilot/vfs/workflow-alias-backing'
import { materializeInlineExecutionValue } from '@/lib/execution/payloads/inline-materialization.server'
import type { ExecutionMaterializationContext } from '@/lib/execution/payloads/materialization.server'
import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults'
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils'
import { listAccessibleWorkspaceRowsForUser } from '@/lib/workspaces/utils'
import type { ExecutionResult } from '@/executor/types'
const logger = createLogger('WorkflowUtils')
export type WorkflowScope = 'active' | 'archived' | 'all'
export async function getWorkflowById(id: string, options?: { includeArchived?: boolean }) {
const { includeArchived = false } = options ?? {}
const rows = await db
.select()
.from(workflowTable)
.where(
includeArchived
? eq(workflowTable.id, id)
: and(eq(workflowTable.id, id), isNull(workflowTable.archivedAt))
)
.limit(1)
return rows[0]
}
export async function listWorkflows(workspaceId: string, options?: { scope?: WorkflowScope }) {
const { scope = 'active' } = options ?? {}
return db
.select()
.from(workflowTable)
.where(
scope === 'all'
? eq(workflowTable.workspaceId, workspaceId)
: scope === 'archived'
? and(
eq(workflowTable.workspaceId, workspaceId),
sql`${workflowTable.archivedAt} IS NOT NULL`
)
: and(eq(workflowTable.workspaceId, workspaceId), isNull(workflowTable.archivedAt))
)
.orderBy(asc(workflowTable.sortOrder), asc(workflowTable.createdAt))
}
/**
* Generates a unique workflow name within a workspace+folder scope.
* If the name already exists among active workflows, appends (2), (3), etc.
*
* Pass a transaction as `executor` when running inside an open tx so the
* lookup observes workflows inserted earlier in the same transaction.
*/
export async function deduplicateWorkflowName(
name: string,
workspaceId: string,
folderId: string | null | undefined,
executor: Pick<typeof db, 'select'> = db
): Promise<string> {
const folderCondition = folderId
? eq(workflowTable.folderId, folderId)
: isNull(workflowTable.folderId)
const [existing] = await executor
.select({ id: workflowTable.id })
.from(workflowTable)
.where(
and(
eq(workflowTable.workspaceId, workspaceId),
folderCondition,
eq(workflowTable.name, name),
isNull(workflowTable.archivedAt)
)
)
.limit(1)
if (!existing) {
return name
}
for (let i = 2; i < 100; i++) {
const candidate = `${name} (${i})`
const [dup] = await executor
.select({ id: workflowTable.id })
.from(workflowTable)
.where(
and(
eq(workflowTable.workspaceId, workspaceId),
folderCondition,
eq(workflowTable.name, candidate),
isNull(workflowTable.archivedAt)
)
)
.limit(1)
if (!dup) {
return candidate
}
}
return `${name} (${generateId().slice(0, 6)})`
}
export type WorkflowResolutionResult =
| {
status: 'resolved'
workflowId: string
workspaceId: string
workflowName?: string
}
| {
status: 'not_found'
message: string
}
| {
status: 'ambiguous'
message: string
candidates: Array<{
workflowId: string
workflowName?: string
folderId?: string | null
}>
}
export async function resolveWorkflowIdForUser(
userId: string,
workflowId?: string,
workflowName?: string,
workspaceId?: string
): Promise<WorkflowResolutionResult> {
if (workflowId) {
const authorization = await authorizeWorkflowByWorkspacePermission({
workflowId,
userId,
action: 'read',
})
if (!authorization.allowed) {
return {
status: 'not_found',
message: 'No workflows found. Create a workflow first or provide a valid workflowId.',
}
}
const wf = await getWorkflowById(workflowId)
if (!wf?.workspaceId) {
return {
status: 'not_found',
message: 'No workflows found. Create a workflow first or provide a valid workflowId.',
}
}
return {
status: 'resolved',
workflowId,
workspaceId: wf.workspaceId,
workflowName: wf.name || undefined,
}
}
const accessibleRows = await listAccessibleWorkspaceRowsForUser(userId, 'all')
const workspaceIdList = accessibleRows.map((row) => row.workspace.id)
const allowedWorkspaceIds = workspaceId
? workspaceIdList.filter((candidateWorkspaceId) => candidateWorkspaceId === workspaceId)
: workspaceIdList
if (allowedWorkspaceIds.length === 0) {
return {
status: 'not_found',
message: 'No workflows found. Create a workflow first or provide a valid workflowId.',
}
}
const workflowRows = await db
.select()
.from(workflowTable)
.where(
and(inArray(workflowTable.workspaceId, allowedWorkspaceIds), isNull(workflowTable.archivedAt))
)
.orderBy(asc(workflowTable.sortOrder), asc(workflowTable.createdAt), asc(workflowTable.id))
const workflows = workflowRows.filter(
(workflow): workflow is (typeof workflowRows)[number] & { workspaceId: string } =>
workflow.workspaceId !== null
)
if (workflows.length === 0) {
return {
status: 'not_found',
message: 'No workflows found. Create a workflow first or provide a valid workflowId.',
}
}
if (workflowName) {
const matches = workflows.filter(
(w) =>
String(w.name || '')
.trim()
.toLowerCase() === workflowName.toLowerCase()
)
if (matches.length === 1) {
const [match] = matches
return {
status: 'resolved',
workflowId: match.id,
workspaceId: match.workspaceId,
workflowName: match.name || undefined,
}
}
if (matches.length > 1) {
return {
status: 'ambiguous',
message: `Multiple workflows named "${workflowName}" were found. Provide workflowId to disambiguate.`,
candidates: matches.map((match) => ({
workflowId: match.id,
workflowName: match.name || undefined,
folderId: match.folderId,
})),
}
}
return {
status: 'not_found',
message: `No workflow named "${workflowName}" was found.`,
}
}
if (workflows.length === 1) {
return {
status: 'resolved',
workflowId: workflows[0].id,
workspaceId: workflows[0].workspaceId,
workflowName: workflows[0].name || undefined,
}
}
return {
status: 'ambiguous',
message:
'Multiple workflows are available. Provide workflowId or workflowName to disambiguate.',
candidates: workflows.slice(0, 20).map((workflow) => ({
workflowId: workflow.id,
workflowName: workflow.name || undefined,
folderId: workflow.folderId,
})),
}
}
export async function updateWorkflowRunCounts(workflowId: string, runs = 1) {
try {
const workflow = await getWorkflowById(workflowId)
if (!workflow) {
logger.error(`Workflow ${workflowId} not found`)
throw new Error(`Workflow ${workflowId} not found`)
}
await db
.update(workflowTable)
.set({
runCount: workflow.runCount + runs,
lastRunAt: new Date(),
})
.where(eq(workflowTable.id, workflowId))
return {
success: true,
runsAdded: runs,
newTotal: workflow.runCount + runs,
}
} catch (error) {
logger.error(`Error updating workflow stats for ${workflowId}`, error)
throw error
}
}
export const workflowHasResponseBlock = (
executionResult: Pick<ExecutionResult, 'success' | 'logs'>
): boolean => {
if (!executionResult?.logs || !Array.isArray(executionResult.logs) || !executionResult.success) {
return false
}
const responseBlock = executionResult.logs.find(
(log) => log?.blockType === 'response' && log?.success
)
return responseBlock !== undefined
}
export const createHttpResponseFromBlock = async (
executionResult: Pick<ExecutionResult, 'output'>,
context?: ExecutionMaterializationContext
): Promise<NextResponse> => {
const { data = {}, status = 200, headers = {} } = executionResult.output
const responseData = await materializeInlineExecutionValue(data, context)
const responseHeaders = new Headers({
'Content-Type': 'application/json',
...headers,
})
return NextResponse.json(responseData, {
status: status,
headers: responseHeaders,
})
}
/**
* Validates that the current user has permission to access/modify a workflow
* Returns session and workflow info if authorized, or error response if not
*/
export async function validateWorkflowPermissions(
workflowId: string,
requestId: string,
action: 'read' | 'write' | 'admin' = 'read'
) {
const session = await getSession()
if (!session?.user?.id) {
logger.warn(`[${requestId}] No authenticated user session for workflow ${action}`)
return {
error: { message: 'Unauthorized', status: 401 },
session: null,
workflow: null,
}
}
const authorization = await authorizeWorkflowByWorkspacePermission({
workflowId,
userId: session.user.id,
action,
})
if (!authorization.workflow) {
logger.warn(`[${requestId}] Workflow ${workflowId} not found`)
return {
error: { message: 'Workflow not found', status: 404 },
session: null,
workflow: null,
}
}
if (!authorization.allowed) {
const message =
authorization.message || `Unauthorized: Access denied to ${action} this workflow`
logger.warn(
`[${requestId}] User ${session.user.id} unauthorized to ${action} workflow ${workflowId}`,
{
action,
workflowId,
}
)
return {
error: { message, status: authorization.status },
session: null,
workflow: null,
}
}
return {
error: null,
session,
workflow: authorization.workflow,
}
}
// ── Workflow CRUD ──
export interface CreateWorkflowInput {
userId: string
workspaceId: string
name: string
description?: string | null
folderId?: string | null
}
export async function createWorkflowRecord(params: CreateWorkflowInput) {
const { userId, workspaceId, name, description = null, folderId = null } = params
const workflowId = generateId()
const now = new Date()
const duplicateConditions = [
eq(workflowTable.workspaceId, workspaceId),
isNull(workflowTable.archivedAt),
eq(workflowTable.name, name),
...(folderId ? [eq(workflowTable.folderId, folderId)] : [isNull(workflowTable.folderId)]),
]
const [duplicateWorkflow] = await db
.select({ id: workflowTable.id })
.from(workflowTable)
.where(and(...duplicateConditions))
.limit(1)
if (duplicateWorkflow) {
throw new Error(
`A workflow named "${name}" already exists in this folder. Use a different name.`
)
}
const workflowParentCondition = folderId
? eq(workflowTable.folderId, folderId)
: isNull(workflowTable.folderId)
const folderParentCondition = folderId
? eq(workflowFolder.parentId, folderId)
: isNull(workflowFolder.parentId)
const [[workflowMinResult], [folderMinResult]] = await Promise.all([
db
.select({ minOrder: min(workflowTable.sortOrder) })
.from(workflowTable)
.where(
and(
eq(workflowTable.workspaceId, workspaceId),
workflowParentCondition,
isNull(workflowTable.archivedAt)
)
),
db
.select({ minOrder: min(workflowFolder.sortOrder) })
.from(workflowFolder)
.where(and(eq(workflowFolder.workspaceId, workspaceId), folderParentCondition)),
])
const minSortOrder = [workflowMinResult?.minOrder, folderMinResult?.minOrder].reduce<
number | null
>((currentMin, candidate) => {
if (candidate == null) return currentMin
if (currentMin == null) return candidate
return Math.min(currentMin, candidate)
}, null)
const sortOrder = minSortOrder != null ? minSortOrder - 1 : 0
await db.insert(workflowTable).values({
id: workflowId,
userId,
workspaceId,
folderId,
sortOrder,
name,
description,
lastSynced: now,
createdAt: now,
updatedAt: now,
isDeployed: false,
runCount: 0,
variables: {},
})
const { workflowState } = buildDefaultWorkflowArtifacts()
const saveResult = await saveWorkflowToNormalizedTables(workflowId, workflowState)
if (!saveResult.success) {
throw new Error(saveResult.error || 'Failed to save workflow state')
}
await ensureWorkflowAliasBacking({ workspaceId, userId, workflowId, workflowName: name })
return { workflowId, name, workspaceId, folderId, sortOrder, createdAt: now, updatedAt: now }
}
export async function updateWorkflowRecord(
workflowId: string,
updates: { name?: string; description?: string; folderId?: string | null }
) {
const setData: Record<string, unknown> = { updatedAt: new Date() }
if (updates.name !== undefined) setData.name = updates.name
if (updates.description !== undefined) setData.description = updates.description
if (updates.folderId !== undefined) setData.folderId = updates.folderId
await db.update(workflowTable).set(setData).where(eq(workflowTable.id, workflowId))
}
export async function deleteWorkflowRecord(workflowId: string) {
const { archiveWorkflow } = await import('@/lib/workflows/lifecycle')
await archiveWorkflow(workflowId, {
requestId: `workflow-record-${workflowId}`,
notifySocket: false,
})
}
export async function setWorkflowVariables(workflowId: string, variables: Record<string, unknown>) {
await db
.update(workflowTable)
.set({ variables, updatedAt: new Date() })
.where(eq(workflowTable.id, workflowId))
}
// ── Folder CRUD ──
export interface CreateFolderInput {
userId: string
workspaceId: string
name: string
parentId?: string | null
}
export async function createFolderRecord(params: CreateFolderInput) {
const { userId, workspaceId, name, parentId = null } = params
const [maxResult] = await db
.select({ maxOrder: max(workflowFolder.sortOrder) })
.from(workflowFolder)
.where(
and(
eq(workflowFolder.workspaceId, workspaceId),
parentId ? eq(workflowFolder.parentId, parentId) : isNull(workflowFolder.parentId)
)
)
const sortOrder = (maxResult?.maxOrder ?? 0) + 1
const folderId = generateId()
await db.insert(workflowFolder).values({
id: folderId,
userId,
workspaceId,
parentId,
name,
sortOrder,
createdAt: new Date(),
updatedAt: new Date(),
})
return { folderId, name, workspaceId, parentId }
}
export async function updateFolderRecord(
folderId: string,
updates: { name?: string; parentId?: string | null }
) {
const setData: Record<string, unknown> = { updatedAt: new Date() }
if (updates.name !== undefined) setData.name = updates.name
if (updates.parentId !== undefined) setData.parentId = updates.parentId
await db.update(workflowFolder).set(setData).where(eq(workflowFolder.id, folderId))
}
export async function verifyFolderWorkspace(
folderId: string,
workspaceId: string
): Promise<boolean> {
const [row] = await db
.select({ id: workflowFolder.id })
.from(workflowFolder)
.where(and(eq(workflowFolder.id, folderId), eq(workflowFolder.workspaceId, workspaceId)))
.limit(1)
return Boolean(row)
}
export async function deleteFolderRecord(folderId: string): Promise<boolean> {
const [folder] = await db
.select({ parentId: workflowFolder.parentId })
.from(workflowFolder)
.where(eq(workflowFolder.id, folderId))
.limit(1)
if (!folder) return false
await db
.update(workflowTable)
.set({ folderId: folder.parentId, updatedAt: new Date() })
.where(eq(workflowTable.folderId, folderId))
await db
.update(workflowFolder)
.set({ parentId: folder.parentId, updatedAt: new Date() })
.where(eq(workflowFolder.parentId, folderId))
await db.delete(workflowFolder).where(eq(workflowFolder.id, folderId))
return true
}
/**
* Checks whether setting `parentId` as the parent of `folderId` would
* create a circular reference in the folder tree.
*/
export async function checkForCircularReference(
folderId: string,
parentId: string
): Promise<boolean> {
let currentParentId: string | null = parentId
const visited = new Set<string>()
while (currentParentId) {
if (visited.has(currentParentId) || currentParentId === folderId) {
return true
}
visited.add(currentParentId)
const [parent] = await db
.select({ parentId: workflowFolder.parentId })
.from(workflowFolder)
.where(eq(workflowFolder.id, currentParentId))
.limit(1)
currentParentId = parent?.parentId || null
}
return false
}
export async function listFolders(workspaceId: string) {
return db
.select({
folderId: workflowFolder.id,
folderName: workflowFolder.name,
parentId: workflowFolder.parentId,
sortOrder: workflowFolder.sortOrder,
locked: workflowFolder.locked,
})
.from(workflowFolder)
.where(and(eq(workflowFolder.workspaceId, workspaceId), isNull(workflowFolder.archivedAt)))
.orderBy(asc(workflowFolder.sortOrder), asc(workflowFolder.createdAt))
}