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
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:
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* Flatten workflow block outputs into pickable paths.
|
||||
*
|
||||
* Shared helper used by any UI that needs to let a user pick one (or many) of a
|
||||
* workflow's block outputs — deploy modal's OutputSelect, the table workflow-column
|
||||
* config sidebar, etc. Keeping this in one place means the "what counts as an
|
||||
* output" rules (excluded types, trigger-mode handling, recursion into nested
|
||||
* output shapes, BFS sort order) don't drift between consumers.
|
||||
*/
|
||||
|
||||
import { getEffectiveBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
|
||||
|
||||
/**
|
||||
* Block types whose "outputs" are really workflow inputs (Start/starter) or flow
|
||||
* control and should never appear in an output picker.
|
||||
*/
|
||||
export const EXCLUDED_OUTPUT_TYPES = new Set(['starter', 'start_trigger', 'human_in_the_loop'])
|
||||
|
||||
export interface FlattenedBlockOutput {
|
||||
blockId: string
|
||||
blockName: string
|
||||
blockType: string
|
||||
/** Dot-path into the block's output (e.g. `content`, `content.text`). */
|
||||
path: string
|
||||
/** Type from the block's output schema (e.g. `string`, `number`, `json`).
|
||||
* Used by the table column-sidebar to pick the right column type. */
|
||||
leafType?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal shape we need off each block — compatible with both the normalized
|
||||
* WorkflowState.blocks entries and the editor's live block state.
|
||||
*/
|
||||
export interface FlattenOutputsBlockInput {
|
||||
id: string
|
||||
type: string
|
||||
name?: string
|
||||
triggerMode?: boolean
|
||||
subBlocks?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface FlattenOutputsEdgeInput {
|
||||
source: string
|
||||
target: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a flat list of pickable output paths across every eligible block in
|
||||
* a workflow.
|
||||
*
|
||||
* @param blocks Iterable of blocks from the workflow state.
|
||||
* @param edges Optional edge list — when provided, results are sorted by BFS
|
||||
* distance from a start/trigger block (descending), so terminal blocks
|
||||
* appear first. This matches the deploy modal's grouping order.
|
||||
*/
|
||||
export function flattenWorkflowOutputs(
|
||||
blocks: Iterable<FlattenOutputsBlockInput>,
|
||||
edges: Iterable<FlattenOutputsEdgeInput> = []
|
||||
): FlattenedBlockOutput[] {
|
||||
const blockList = Array.from(blocks)
|
||||
const results: FlattenedBlockOutput[] = []
|
||||
|
||||
for (const block of blockList) {
|
||||
if (!block?.id || !block?.type) continue
|
||||
if (EXCLUDED_OUTPUT_TYPES.has(block.type)) continue
|
||||
|
||||
const normalizedSubBlocks: Record<string, { value: unknown }> = {}
|
||||
if (block.subBlocks && typeof block.subBlocks === 'object') {
|
||||
for (const [k, v] of Object.entries(block.subBlocks)) {
|
||||
normalizedSubBlocks[k] =
|
||||
v && typeof v === 'object' && 'value' in (v as object)
|
||||
? (v as { value: unknown })
|
||||
: { value: v }
|
||||
}
|
||||
}
|
||||
|
||||
const effectiveTriggerMode = Boolean(block.triggerMode)
|
||||
let outs: Record<string, unknown> = {}
|
||||
try {
|
||||
outs = getEffectiveBlockOutputs(block.type, normalizedSubBlocks, {
|
||||
triggerMode: effectiveTriggerMode,
|
||||
preferToolOutputs: !effectiveTriggerMode,
|
||||
}) as Record<string, unknown>
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
if (!outs || Object.keys(outs).length === 0) continue
|
||||
|
||||
const blockName = block.name || `Block ${block.id}`
|
||||
const add = (path: string, outputObj: unknown, prefix = ''): void => {
|
||||
const fullPath = prefix ? `${prefix}.${path}` : path
|
||||
const declaredType =
|
||||
outputObj &&
|
||||
typeof outputObj === 'object' &&
|
||||
!Array.isArray(outputObj) &&
|
||||
'type' in (outputObj as object) &&
|
||||
typeof (outputObj as { type: unknown }).type === 'string'
|
||||
? (outputObj as { type: string }).type
|
||||
: undefined
|
||||
const isLeaf =
|
||||
typeof outputObj !== 'object' ||
|
||||
outputObj === null ||
|
||||
declaredType !== undefined ||
|
||||
Array.isArray(outputObj)
|
||||
if (isLeaf) {
|
||||
results.push({
|
||||
blockId: block.id,
|
||||
blockName,
|
||||
blockType: block.type,
|
||||
path: fullPath,
|
||||
leafType: declaredType,
|
||||
})
|
||||
return
|
||||
}
|
||||
for (const [key, value] of Object.entries(outputObj as Record<string, unknown>)) {
|
||||
add(key, value, fullPath)
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(outs)) add(key, value)
|
||||
}
|
||||
|
||||
const edgeList = Array.from(edges)
|
||||
if (edgeList.length === 0 || results.length === 0) return results
|
||||
|
||||
// Sort by BFS distance from the first start/trigger block, descending — so terminal
|
||||
// blocks group to the top. Matches the deploy modal's OutputSelect ordering.
|
||||
const startBlock = blockList.find(
|
||||
(b) => b.type === 'starter' || b.type === 'start_trigger' || !!b.triggerMode
|
||||
)
|
||||
if (!startBlock) return results
|
||||
|
||||
const adj: Record<string, string[]> = {}
|
||||
for (const e of edgeList) {
|
||||
if (!adj[e.source]) adj[e.source] = []
|
||||
adj[e.source].push(e.target)
|
||||
}
|
||||
const distances: Record<string, number> = {}
|
||||
const visited = new Set<string>()
|
||||
const queue: Array<[string, number]> = [[startBlock.id, 0]]
|
||||
while (queue.length > 0) {
|
||||
const [id, d] = queue.shift()!
|
||||
if (visited.has(id)) continue
|
||||
visited.add(id)
|
||||
distances[id] = d
|
||||
for (const t of adj[id] ?? []) queue.push([t, d + 1])
|
||||
}
|
||||
|
||||
return results
|
||||
.map((r, i) => ({ r, d: distances[r.blockId] ?? -1, i }))
|
||||
.sort((a, b) => {
|
||||
if (b.d !== a.d) return b.d - a.d
|
||||
return a.i - b.i // preserve discovery order within the same distance
|
||||
})
|
||||
.map(({ r }) => r)
|
||||
}
|
||||
|
||||
/**
|
||||
* BFS distance from the workflow's start/trigger block to every reachable block,
|
||||
* keyed by blockId. Blocks unreachable from start get `-1`. Pure function over
|
||||
* the same `blocks`/`edges` shape `flattenWorkflowOutputs` accepts; callers that
|
||||
* want a *start-first* (execution-order ASC) sort use this map directly instead
|
||||
* of `flattenWorkflowOutputs` (which sorts terminal-blocks-first for picker UX).
|
||||
*/
|
||||
export function getBlockExecutionOrder(
|
||||
blocks: Iterable<FlattenOutputsBlockInput>,
|
||||
edges: Iterable<FlattenOutputsEdgeInput>
|
||||
): Record<string, number> {
|
||||
const blockList = Array.from(blocks)
|
||||
const startBlock = blockList.find(
|
||||
(b) => b.type === 'starter' || b.type === 'start_trigger' || !!b.triggerMode
|
||||
)
|
||||
const distances: Record<string, number> = {}
|
||||
for (const b of blockList) {
|
||||
if (b?.id) distances[b.id] = -1
|
||||
}
|
||||
if (!startBlock) return distances
|
||||
|
||||
const adj: Record<string, string[]> = {}
|
||||
for (const e of edges) {
|
||||
if (!adj[e.source]) adj[e.source] = []
|
||||
adj[e.source].push(e.target)
|
||||
}
|
||||
const visited = new Set<string>()
|
||||
const queue: Array<[string, number]> = [[startBlock.id, 0]]
|
||||
while (queue.length > 0) {
|
||||
const [id, d] = queue.shift()!
|
||||
if (visited.has(id)) continue
|
||||
visited.add(id)
|
||||
distances[id] = d
|
||||
for (const t of adj[id] ?? []) queue.push([t, d + 1])
|
||||
}
|
||||
return distances
|
||||
}
|
||||
Reference in New Issue
Block a user