d25d482dc2
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
198 lines
7.0 KiB
TypeScript
198 lines
7.0 KiB
TypeScript
import type { SubBlockType } from '@sim/workflow-types/blocks'
|
|
import type { WorkflowInputField } from '@/lib/workflows/input-format'
|
|
import type { BlockConfig, BlockIcon, SubBlockConfig } from '@/blocks/types'
|
|
|
|
/**
|
|
* The block-type prefix that identifies a custom (deploy-as-block) block. Shared
|
|
* by the registry overlay, the executor handler dispatch, and access control.
|
|
*/
|
|
export const CUSTOM_BLOCK_TYPE_PREFIX = 'custom_block_'
|
|
|
|
/** Whether a block type is a published custom block. */
|
|
export function isCustomBlockType(type: string | undefined | null): type is string {
|
|
return typeof type === 'string' && type.startsWith(CUSTOM_BLOCK_TYPE_PREFIX)
|
|
}
|
|
|
|
/** Tile background for custom-block icons (the uploaded image renders on top). */
|
|
export const CUSTOM_BLOCK_TILE_COLOR = '#6F6F6F'
|
|
|
|
/** A curated output exposed on the block, mapped from a child block output. */
|
|
export interface CustomBlockOutput {
|
|
blockId: string
|
|
path: string
|
|
name: string
|
|
}
|
|
|
|
/**
|
|
* A curated input the admin chose to expose on the block, keyed by the source
|
|
* Start field's stable `id`, with optional consumer-facing hints.
|
|
*/
|
|
export interface CustomBlockInput {
|
|
id: string
|
|
name: string
|
|
type: string
|
|
placeholder?: string
|
|
description?: string
|
|
required?: boolean
|
|
}
|
|
|
|
/**
|
|
* The DB-backed identity + presentation of a custom block. `workflowId` is the
|
|
* bound source workflow whose LATEST deployment this block always executes.
|
|
*/
|
|
export interface CustomBlockRow {
|
|
type: string
|
|
name: string
|
|
description: string
|
|
workflowId: string
|
|
/** Curated exposed outputs; empty/absent exposes the child's whole `result`. */
|
|
exposedOutputs?: CustomBlockOutput[]
|
|
}
|
|
|
|
/**
|
|
* Params that carry the block's own wiring rather than a mapped Start input.
|
|
* Everything else on the block is collected into the child `inputMapping`. Shared
|
|
* with the serializer so "does this custom block declare input sub-blocks?" reads
|
|
* from one source instead of re-listing the structural ids.
|
|
*/
|
|
export const RESERVED_PARAMS = new Set([
|
|
'workflowId',
|
|
'inputMapping',
|
|
'triggerMode',
|
|
'advancedMode',
|
|
])
|
|
|
|
/** Map a Start input field type to the editor sub-block type used to collect it. */
|
|
function subBlockTypeForField(fieldType: string): SubBlockType {
|
|
switch (fieldType) {
|
|
case 'boolean':
|
|
return 'switch'
|
|
case 'object':
|
|
case 'array':
|
|
return 'code'
|
|
case 'file[]':
|
|
return 'file-upload'
|
|
default:
|
|
return 'short-input'
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Synthesize a `BlockConfig` for a published custom block from its DB row and the
|
|
* live-derived Start input fields. Shared by the client (real icon + per-field
|
|
* editors) and the server (placeholder icon + `inputFields: []`, since the
|
|
* `inputMapping` wiring is schema-agnostic).
|
|
*
|
|
* Execution reuses the `workflow_executor` tool: the bound `workflowId` and the
|
|
* assembled `inputMapping` are hidden, baked sub-blocks; each Start input becomes
|
|
* its own editable sub-block whose value is collected into `inputMapping`.
|
|
* `<refs>` inside those values resolve at execution exactly like the
|
|
* `workflow_input` block.
|
|
*
|
|
* The sub-block id is the field's stable id (`field.id`), NOT its display name, so
|
|
* renaming a Start input in the source workflow and redeploying never orphans a
|
|
* consumer's placed value. The name is shown as the sub-block title and is what
|
|
* the child workflow ultimately receives — the id→name remap happens at execution
|
|
* in `WorkflowBlockHandler` against the loaded child's current field names. Legacy
|
|
* fields without an id fall back to keying on the name.
|
|
*/
|
|
export function buildCustomBlockConfig(
|
|
row: CustomBlockRow,
|
|
inputFields: WorkflowInputField[],
|
|
opts: { icon: BlockIcon; bgColor?: string; hideFromToolbar?: boolean }
|
|
): BlockConfig {
|
|
const fieldSubBlocks: SubBlockConfig[] = inputFields.map((field) => {
|
|
const type = subBlockTypeForField(field.type)
|
|
const sub: SubBlockConfig = {
|
|
id: field.id ?? field.name,
|
|
title: field.name,
|
|
type,
|
|
description: field.description,
|
|
placeholder: field.placeholder,
|
|
// Serializer Loop-B (required subBlocks not covered by tool params) and the
|
|
// editor asterisk both read this — same enforcement path as regular blocks.
|
|
required: field.required === true,
|
|
}
|
|
if (field.type === 'object' || field.type === 'array') sub.language = 'json'
|
|
if (field.type === 'file[]') sub.multiple = true
|
|
return sub
|
|
})
|
|
|
|
return {
|
|
type: row.type,
|
|
name: row.name,
|
|
description: row.description,
|
|
category: 'tools',
|
|
longDescription:
|
|
'A published workflow packaged as a reusable, self-contained block. Fill its input ' +
|
|
'fields; it runs the underlying workflow and returns the outputs below. The bound ' +
|
|
'workflow is baked in — no workflow id or input mapping to configure.',
|
|
bgColor: opts.bgColor ?? CUSTOM_BLOCK_TILE_COLOR,
|
|
icon: opts.icon,
|
|
// A disabled block stays resolvable (so a still-placed instance survives
|
|
// serialization and fails loudly at run via `getCustomBlockAuthority`, instead
|
|
// of silently vanishing from the graph) but is hidden from the palette so no
|
|
// new instance can be placed.
|
|
hideFromToolbar: opts.hideFromToolbar,
|
|
subBlocks: [
|
|
{
|
|
id: 'workflowId',
|
|
type: 'short-input',
|
|
hidden: true,
|
|
value: () => row.workflowId,
|
|
},
|
|
{
|
|
id: 'inputMapping',
|
|
type: 'code',
|
|
language: 'json',
|
|
hidden: true,
|
|
value: (params) => {
|
|
const mapping: Record<string, unknown> = {}
|
|
for (const [key, val] of Object.entries(params)) {
|
|
if (RESERVED_PARAMS.has(key)) continue
|
|
if (val === undefined || val === '') continue
|
|
mapping[key] = val
|
|
}
|
|
return JSON.stringify(mapping)
|
|
},
|
|
},
|
|
...fieldSubBlocks,
|
|
],
|
|
tools: {
|
|
access: ['workflow_executor'],
|
|
config: {
|
|
tool: () => 'workflow_executor',
|
|
params: (params) => ({
|
|
workflowId: params.workflowId,
|
|
inputMapping: params.inputMapping,
|
|
}),
|
|
},
|
|
},
|
|
inputs: {
|
|
workflowId: { type: 'string', description: 'Bound source workflow id' },
|
|
inputMapping: { type: 'json', description: 'Mapping of input fields to values' },
|
|
},
|
|
outputs: buildOutputs(row.exposedOutputs),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The block's declared outputs. Internal plumbing (child workflow id/name, trace
|
|
* spans) is never exposed. With curated `exposedOutputs`, each becomes its own
|
|
* named output; otherwise the whole child `result` is exposed.
|
|
*/
|
|
function buildOutputs(exposed: CustomBlockOutput[] | undefined): BlockConfig['outputs'] {
|
|
const outputs: BlockConfig['outputs'] = {
|
|
success: { type: 'boolean', description: 'Execution success status' },
|
|
error: { type: 'string', description: 'Error message' },
|
|
}
|
|
if (exposed && exposed.length > 0) {
|
|
for (const out of exposed) {
|
|
outputs[out.name] = { type: 'json', description: `Output: ${out.path}` }
|
|
}
|
|
} else {
|
|
outputs.result = { type: 'json', description: 'Workflow execution result' }
|
|
}
|
|
return outputs
|
|
}
|