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,632 @@
|
||||
import { getBlock } from '@/blocks'
|
||||
import type { BlockState } from '@/stores/workflows/workflow/types'
|
||||
|
||||
/**
|
||||
* Unified trigger type definitions
|
||||
*/
|
||||
export const TRIGGER_TYPES = {
|
||||
INPUT: 'input_trigger',
|
||||
MANUAL: 'manual_trigger',
|
||||
CHAT: 'chat_trigger',
|
||||
API: 'api_trigger',
|
||||
WEBHOOK: 'webhook',
|
||||
GENERIC_WEBHOOK: 'generic_webhook',
|
||||
SCHEDULE: 'schedule',
|
||||
SIM: 'sim_workspace_event',
|
||||
START: 'start_trigger',
|
||||
STARTER: 'starter', // Legacy
|
||||
} as const
|
||||
|
||||
export type TriggerType = (typeof TRIGGER_TYPES)[keyof typeof TRIGGER_TYPES]
|
||||
|
||||
export enum StartBlockPath {
|
||||
UNIFIED = 'unified_start',
|
||||
LEGACY_STARTER = 'legacy_starter',
|
||||
SPLIT_INPUT = 'legacy_input_trigger',
|
||||
SPLIT_API = 'legacy_api_trigger',
|
||||
SPLIT_CHAT = 'legacy_chat_trigger',
|
||||
SPLIT_MANUAL = 'legacy_manual_trigger',
|
||||
EXTERNAL_TRIGGER = 'external_trigger',
|
||||
}
|
||||
|
||||
type StartExecutionKind = 'chat' | 'manual' | 'api' | 'external'
|
||||
|
||||
const EXECUTION_PRIORITIES: Record<StartExecutionKind, StartBlockPath[]> = {
|
||||
chat: [StartBlockPath.UNIFIED, StartBlockPath.SPLIT_CHAT, StartBlockPath.LEGACY_STARTER],
|
||||
manual: [
|
||||
StartBlockPath.UNIFIED,
|
||||
StartBlockPath.SPLIT_API,
|
||||
StartBlockPath.SPLIT_INPUT,
|
||||
StartBlockPath.SPLIT_MANUAL,
|
||||
StartBlockPath.LEGACY_STARTER,
|
||||
StartBlockPath.EXTERNAL_TRIGGER,
|
||||
],
|
||||
api: [
|
||||
StartBlockPath.UNIFIED,
|
||||
StartBlockPath.SPLIT_API,
|
||||
StartBlockPath.SPLIT_INPUT,
|
||||
StartBlockPath.LEGACY_STARTER,
|
||||
],
|
||||
external: [StartBlockPath.EXTERNAL_TRIGGER],
|
||||
}
|
||||
|
||||
const CHILD_PRIORITIES: StartBlockPath[] = [
|
||||
StartBlockPath.UNIFIED,
|
||||
StartBlockPath.SPLIT_INPUT,
|
||||
StartBlockPath.LEGACY_STARTER,
|
||||
]
|
||||
|
||||
const START_CONFLICT_TYPES: TriggerType[] = [
|
||||
TRIGGER_TYPES.START,
|
||||
TRIGGER_TYPES.API,
|
||||
TRIGGER_TYPES.INPUT,
|
||||
TRIGGER_TYPES.MANUAL,
|
||||
TRIGGER_TYPES.CHAT,
|
||||
TRIGGER_TYPES.STARTER, // Legacy starter also conflicts with start_trigger
|
||||
]
|
||||
|
||||
type MinimalBlock = { type: string; subBlocks?: Record<string, unknown> | undefined }
|
||||
|
||||
export interface StartBlockCandidate<T extends MinimalBlock> {
|
||||
blockId: string
|
||||
block: T
|
||||
path: StartBlockPath
|
||||
}
|
||||
|
||||
type ClassifyStartOptions = {
|
||||
category?: string
|
||||
triggerModeEnabled?: boolean
|
||||
}
|
||||
|
||||
export function classifyStartBlockType(
|
||||
type: string,
|
||||
opts?: ClassifyStartOptions
|
||||
): StartBlockPath | null {
|
||||
switch (type) {
|
||||
case TRIGGER_TYPES.START:
|
||||
return StartBlockPath.UNIFIED
|
||||
case TRIGGER_TYPES.STARTER:
|
||||
return StartBlockPath.LEGACY_STARTER
|
||||
case TRIGGER_TYPES.INPUT:
|
||||
return StartBlockPath.SPLIT_INPUT
|
||||
case TRIGGER_TYPES.API:
|
||||
return StartBlockPath.SPLIT_API
|
||||
case TRIGGER_TYPES.CHAT:
|
||||
return StartBlockPath.SPLIT_CHAT
|
||||
case TRIGGER_TYPES.MANUAL:
|
||||
return StartBlockPath.SPLIT_MANUAL
|
||||
case TRIGGER_TYPES.WEBHOOK:
|
||||
case TRIGGER_TYPES.SCHEDULE:
|
||||
case TRIGGER_TYPES.SIM:
|
||||
return StartBlockPath.EXTERNAL_TRIGGER
|
||||
default:
|
||||
if (opts?.category === 'triggers' || opts?.triggerModeEnabled) {
|
||||
return StartBlockPath.EXTERNAL_TRIGGER
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function classifyStartBlock<T extends MinimalBlock>(block: T): StartBlockPath | null {
|
||||
const blockState = block as Partial<BlockState>
|
||||
|
||||
// Try to get metadata from the block itself first
|
||||
let category: string | undefined
|
||||
const triggerModeEnabled = Boolean(blockState.triggerMode)
|
||||
|
||||
// If not available on the block, fetch from registry
|
||||
const blockConfig = getBlock(block.type)
|
||||
|
||||
if (blockConfig) {
|
||||
category = blockConfig.category
|
||||
}
|
||||
|
||||
return classifyStartBlockType(block.type, { category, triggerModeEnabled })
|
||||
}
|
||||
|
||||
export function isLegacyStartPath(path: StartBlockPath): boolean {
|
||||
return path !== StartBlockPath.UNIFIED
|
||||
}
|
||||
|
||||
function toEntries<T extends MinimalBlock>(blocks: Record<string, T> | T[]): Array<[string, T]> {
|
||||
if (Array.isArray(blocks)) {
|
||||
return blocks.map((block, index) => {
|
||||
const potentialId = (block as { id?: unknown }).id
|
||||
const inferredId = typeof potentialId === 'string' ? potentialId : `${index}`
|
||||
return [inferredId, block]
|
||||
})
|
||||
}
|
||||
return Object.entries(blocks)
|
||||
}
|
||||
|
||||
type ResolveStartOptions = {
|
||||
execution: StartExecutionKind
|
||||
isChildWorkflow?: boolean
|
||||
allowLegacyStarter?: boolean
|
||||
}
|
||||
|
||||
function supportsExecution(path: StartBlockPath, execution: StartExecutionKind): boolean {
|
||||
if (execution === 'external') {
|
||||
return path === StartBlockPath.EXTERNAL_TRIGGER
|
||||
}
|
||||
|
||||
if (path === StartBlockPath.UNIFIED || path === StartBlockPath.LEGACY_STARTER) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (execution === 'chat') {
|
||||
return path === StartBlockPath.SPLIT_CHAT
|
||||
}
|
||||
|
||||
if (execution === 'api') {
|
||||
return path === StartBlockPath.SPLIT_API || path === StartBlockPath.SPLIT_INPUT
|
||||
}
|
||||
|
||||
return (
|
||||
path === StartBlockPath.SPLIT_API ||
|
||||
path === StartBlockPath.SPLIT_INPUT ||
|
||||
path === StartBlockPath.SPLIT_MANUAL ||
|
||||
path === StartBlockPath.EXTERNAL_TRIGGER
|
||||
)
|
||||
}
|
||||
|
||||
export function resolveStartCandidates<T extends MinimalBlock>(
|
||||
blocks: Record<string, T> | T[],
|
||||
options: ResolveStartOptions
|
||||
): StartBlockCandidate<T>[] {
|
||||
const entries = toEntries(blocks)
|
||||
if (entries.length === 0) return []
|
||||
|
||||
const priorities = options.isChildWorkflow
|
||||
? CHILD_PRIORITIES
|
||||
: EXECUTION_PRIORITIES[options.execution]
|
||||
|
||||
const candidates: StartBlockCandidate<T>[] = []
|
||||
|
||||
for (const [blockId, block] of entries) {
|
||||
// Skip disabled blocks - they cannot be used as triggers
|
||||
if ('enabled' in block && block.enabled === false) {
|
||||
continue
|
||||
}
|
||||
|
||||
const path = classifyStartBlock(block)
|
||||
if (!path) continue
|
||||
|
||||
if (options.isChildWorkflow) {
|
||||
if (!CHILD_PRIORITIES.includes(path)) {
|
||||
continue
|
||||
}
|
||||
} else if (!supportsExecution(path, options.execution)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (path === StartBlockPath.LEGACY_STARTER && options.allowLegacyStarter === false) {
|
||||
continue
|
||||
}
|
||||
|
||||
candidates.push({ blockId, block, path })
|
||||
}
|
||||
|
||||
candidates.sort((a, b) => {
|
||||
const order = options.isChildWorkflow ? CHILD_PRIORITIES : priorities
|
||||
const aIdx = order.indexOf(a.path)
|
||||
const bIdx = order.indexOf(b.path)
|
||||
if (aIdx === -1 && bIdx === -1) return 0
|
||||
if (aIdx === -1) return 1
|
||||
if (bIdx === -1) return -1
|
||||
return aIdx - bIdx
|
||||
})
|
||||
|
||||
return candidates
|
||||
}
|
||||
|
||||
type SubBlockWithValue = { value?: unknown }
|
||||
|
||||
function readSubBlockValue(subBlocks: Record<string, unknown> | undefined, key: string): unknown {
|
||||
const raw = subBlocks?.[key]
|
||||
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
||||
return (raw as SubBlockWithValue).value
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function getLegacyStarterMode(block: {
|
||||
subBlocks?: Record<string, unknown>
|
||||
}): 'manual' | 'api' | 'chat' | null {
|
||||
const modeValue = readSubBlockValue(block.subBlocks, 'startWorkflow')
|
||||
if (modeValue === 'chat') return 'chat'
|
||||
if (modeValue === 'api' || modeValue === 'run') return 'api'
|
||||
if (modeValue === undefined || modeValue === 'manual') return 'manual'
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping from reference alias (used in inline refs like <api.*>, <chat.*>, etc.)
|
||||
* to concrete trigger block type identifiers used across the system.
|
||||
*/
|
||||
export const TRIGGER_REFERENCE_ALIAS_MAP = {
|
||||
start: TRIGGER_TYPES.START,
|
||||
api: TRIGGER_TYPES.API,
|
||||
chat: TRIGGER_TYPES.CHAT,
|
||||
manual: TRIGGER_TYPES.START,
|
||||
} as const
|
||||
|
||||
export type TriggerReferenceAlias = keyof typeof TRIGGER_REFERENCE_ALIAS_MAP
|
||||
|
||||
/**
|
||||
* Trigger classification and utilities
|
||||
*/
|
||||
export class TriggerUtils {
|
||||
/**
|
||||
* Check if a block is any kind of trigger
|
||||
*/
|
||||
static isTriggerBlock(block: { type: string; triggerMode?: boolean }): boolean {
|
||||
const blockConfig = getBlock(block.type)
|
||||
|
||||
return (
|
||||
// New trigger blocks (explicit category)
|
||||
blockConfig?.category === 'triggers' ||
|
||||
// Blocks with trigger mode enabled
|
||||
block.triggerMode === true ||
|
||||
// Legacy starter block
|
||||
block.type === TRIGGER_TYPES.STARTER
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a block is a specific trigger type
|
||||
*/
|
||||
static isTriggerType(block: { type: string }, triggerType: TriggerType): boolean {
|
||||
return block.type === triggerType
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a type string is any trigger type
|
||||
*/
|
||||
static isAnyTriggerType(type: string): boolean {
|
||||
return Object.values(TRIGGER_TYPES).includes(type as TriggerType)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a block is a chat-compatible trigger
|
||||
*/
|
||||
static isChatTrigger(block: { type: string; subBlocks?: any }): boolean {
|
||||
if (block.type === TRIGGER_TYPES.CHAT || block.type === TRIGGER_TYPES.START) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Legacy: starter block in chat mode
|
||||
if (block.type === TRIGGER_TYPES.STARTER) {
|
||||
return block.subBlocks?.startWorkflow?.value === 'chat'
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a block is a manual-compatible trigger
|
||||
*/
|
||||
static isManualTrigger(block: { type: string; subBlocks?: any }): boolean {
|
||||
if (
|
||||
block.type === TRIGGER_TYPES.INPUT ||
|
||||
block.type === TRIGGER_TYPES.MANUAL ||
|
||||
block.type === TRIGGER_TYPES.START
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Legacy: starter block in manual mode or without explicit mode (default to manual)
|
||||
if (block.type === TRIGGER_TYPES.STARTER) {
|
||||
// If startWorkflow is not set or is set to 'manual', treat as manual trigger
|
||||
const startWorkflowValue = block.subBlocks?.startWorkflow?.value
|
||||
return startWorkflowValue === 'manual' || startWorkflowValue === undefined
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a block is an API-compatible trigger
|
||||
* @param block - Block to check
|
||||
* @param isChildWorkflow - Whether this is being called from a child workflow context
|
||||
*/
|
||||
static isApiTrigger(block: { type: string; subBlocks?: any }, isChildWorkflow = false): boolean {
|
||||
if (isChildWorkflow) {
|
||||
// Child workflows (workflow-in-workflow) support legacy input trigger and new start block
|
||||
return block.type === TRIGGER_TYPES.INPUT || block.type === TRIGGER_TYPES.START
|
||||
}
|
||||
// Direct API calls work with api_trigger and the new start block
|
||||
if (block.type === TRIGGER_TYPES.API || block.type === TRIGGER_TYPES.START) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Legacy: starter block in API mode
|
||||
if (block.type === TRIGGER_TYPES.STARTER) {
|
||||
const mode = block.subBlocks?.startWorkflow?.value
|
||||
return mode === 'api' || mode === 'run'
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default name for a trigger type
|
||||
*/
|
||||
static getDefaultTriggerName(triggerType: string): string | null {
|
||||
// Use the block's actual name from the registry
|
||||
const block = getBlock(triggerType)
|
||||
if (block) {
|
||||
if (triggerType === TRIGGER_TYPES.GENERIC_WEBHOOK) {
|
||||
return 'Webhook'
|
||||
}
|
||||
return block.name
|
||||
}
|
||||
|
||||
// Fallback for legacy or unknown types
|
||||
switch (triggerType) {
|
||||
case TRIGGER_TYPES.CHAT:
|
||||
return 'Chat'
|
||||
case TRIGGER_TYPES.INPUT:
|
||||
return 'Input Trigger'
|
||||
case TRIGGER_TYPES.MANUAL:
|
||||
return 'Manual'
|
||||
case TRIGGER_TYPES.API:
|
||||
return 'API'
|
||||
case TRIGGER_TYPES.START:
|
||||
return 'Start'
|
||||
case TRIGGER_TYPES.WEBHOOK:
|
||||
return 'Webhook'
|
||||
case TRIGGER_TYPES.SCHEDULE:
|
||||
return 'Schedule'
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find trigger blocks of a specific type in a workflow
|
||||
*/
|
||||
static findTriggersByType<T extends { type: string; subBlocks?: any }>(
|
||||
blocks: T[] | Record<string, T>,
|
||||
triggerType: 'chat' | 'manual' | 'api',
|
||||
isChildWorkflow = false
|
||||
): T[] {
|
||||
const blockArray = Array.isArray(blocks) ? blocks : Object.values(blocks)
|
||||
|
||||
switch (triggerType) {
|
||||
case 'chat':
|
||||
return blockArray.filter((block) => TriggerUtils.isChatTrigger(block))
|
||||
case 'manual':
|
||||
return blockArray.filter((block) => TriggerUtils.isManualTrigger(block))
|
||||
case 'api':
|
||||
return blockArray.filter((block) => TriggerUtils.isApiTrigger(block, isChildWorkflow))
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the appropriate start block for a given execution context
|
||||
*/
|
||||
static findStartBlock<T extends { type: string; subBlocks?: any }>(
|
||||
blocks: Record<string, T>,
|
||||
executionType: 'chat' | 'manual' | 'api' | 'external',
|
||||
isChildWorkflow = false
|
||||
): (StartBlockCandidate<T> & { block: T }) | null {
|
||||
const candidates = resolveStartCandidates(blocks, {
|
||||
execution: executionType,
|
||||
isChildWorkflow,
|
||||
})
|
||||
|
||||
if (candidates.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [primary] = candidates
|
||||
return primary
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if multiple triggers of a restricted type exist
|
||||
*/
|
||||
static hasMultipleTriggers<T extends { type: string }>(
|
||||
blocks: T[] | Record<string, T>,
|
||||
triggerType: TriggerType
|
||||
): boolean {
|
||||
const blockArray = Array.isArray(blocks) ? blocks : Object.values(blocks)
|
||||
const count = blockArray.filter((block) => block.type === triggerType).length
|
||||
return count > 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a trigger type requires single instance constraint
|
||||
*/
|
||||
static requiresSingleInstance(triggerType: string): boolean {
|
||||
// Each trigger type can only have one instance of itself
|
||||
// Manual and Input Form can coexist
|
||||
// API, Chat triggers must be unique
|
||||
// Schedules and webhooks can have multiple instances
|
||||
return (
|
||||
triggerType === TRIGGER_TYPES.API ||
|
||||
triggerType === TRIGGER_TYPES.INPUT ||
|
||||
triggerType === TRIGGER_TYPES.MANUAL ||
|
||||
triggerType === TRIGGER_TYPES.CHAT ||
|
||||
triggerType === TRIGGER_TYPES.START
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a workflow has a legacy starter block
|
||||
*/
|
||||
static hasLegacyStarter<T extends { type: string }>(blocks: T[] | Record<string, T>): boolean {
|
||||
const blockArray = Array.isArray(blocks) ? blocks : Object.values(blocks)
|
||||
return blockArray.some((block) => block.type === TRIGGER_TYPES.STARTER)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if adding a trigger would violate single instance constraint
|
||||
*/
|
||||
static wouldViolateSingleInstance<T extends { type: string }>(
|
||||
blocks: T[] | Record<string, T>,
|
||||
triggerType: string
|
||||
): boolean {
|
||||
const blockArray = Array.isArray(blocks) ? blocks : Object.values(blocks)
|
||||
const hasLegacyStarter = TriggerUtils.hasLegacyStarter(blocks)
|
||||
|
||||
// Legacy starter block can't coexist with Chat, Input, Manual, or API triggers
|
||||
if (hasLegacyStarter) {
|
||||
if (
|
||||
triggerType === TRIGGER_TYPES.CHAT ||
|
||||
triggerType === TRIGGER_TYPES.INPUT ||
|
||||
triggerType === TRIGGER_TYPES.MANUAL ||
|
||||
triggerType === TRIGGER_TYPES.API ||
|
||||
triggerType === TRIGGER_TYPES.START
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (triggerType === TRIGGER_TYPES.STARTER) {
|
||||
const hasModernTriggers = blockArray.some(
|
||||
(block) =>
|
||||
block.type === TRIGGER_TYPES.CHAT ||
|
||||
block.type === TRIGGER_TYPES.INPUT ||
|
||||
block.type === TRIGGER_TYPES.MANUAL ||
|
||||
block.type === TRIGGER_TYPES.API ||
|
||||
block.type === TRIGGER_TYPES.START
|
||||
)
|
||||
if (hasModernTriggers) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Start trigger cannot coexist with other single-instance trigger types
|
||||
if (triggerType === TRIGGER_TYPES.START) {
|
||||
return blockArray.some((block) => START_CONFLICT_TYPES.includes(block.type as TriggerType))
|
||||
}
|
||||
|
||||
// Only one Input trigger allowed
|
||||
if (triggerType === TRIGGER_TYPES.INPUT) {
|
||||
return blockArray.some((block) => block.type === TRIGGER_TYPES.INPUT)
|
||||
}
|
||||
|
||||
// Only one Manual trigger allowed
|
||||
if (triggerType === TRIGGER_TYPES.MANUAL) {
|
||||
return blockArray.some((block) => block.type === TRIGGER_TYPES.MANUAL)
|
||||
}
|
||||
|
||||
// Only one API trigger allowed
|
||||
if (triggerType === TRIGGER_TYPES.API) {
|
||||
return blockArray.some((block) => block.type === TRIGGER_TYPES.API)
|
||||
}
|
||||
|
||||
// Chat trigger must be unique
|
||||
if (triggerType === TRIGGER_TYPES.CHAT) {
|
||||
return blockArray.some((block) => block.type === TRIGGER_TYPES.CHAT)
|
||||
}
|
||||
|
||||
// Centralized rule: only API, Input, Chat are single-instance
|
||||
if (!TriggerUtils.requiresSingleInstance(triggerType)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return blockArray.some((block) => block.type === triggerType)
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate whether adding a trigger of the given type is allowed and, if not, why.
|
||||
* Returns null if allowed; otherwise returns an object describing the violation.
|
||||
* This avoids duplicating UI logic across toolbar/drop handlers.
|
||||
*/
|
||||
static getTriggerAdditionIssue<T extends { type: string }>(
|
||||
blocks: T[] | Record<string, T>,
|
||||
triggerType: string
|
||||
): { issue: 'legacy' | 'duplicate'; triggerName: string } | null {
|
||||
if (!TriggerUtils.wouldViolateSingleInstance(blocks, triggerType)) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Legacy starter present + adding modern trigger → legacy incompatibility
|
||||
if (TriggerUtils.hasLegacyStarter(blocks) && TriggerUtils.isAnyTriggerType(triggerType)) {
|
||||
return { issue: 'legacy', triggerName: 'new trigger' }
|
||||
}
|
||||
|
||||
// Otherwise treat as duplicate of a single-instance trigger
|
||||
const triggerName = TriggerUtils.getDefaultTriggerName(triggerType) || 'trigger'
|
||||
return { issue: 'duplicate', triggerName }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get trigger validation message
|
||||
*/
|
||||
static getTriggerValidationMessage(
|
||||
triggerType: 'chat' | 'manual' | 'api',
|
||||
issue: 'missing' | 'multiple'
|
||||
): string {
|
||||
const triggerName = triggerType.charAt(0).toUpperCase() + triggerType.slice(1)
|
||||
|
||||
if (issue === 'missing') {
|
||||
return `${triggerName} execution requires a ${triggerName} Trigger block`
|
||||
}
|
||||
|
||||
return `Multiple ${triggerName} Trigger blocks found. Keep only one.`
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a block is inside a loop or parallel subflow
|
||||
* @param blockId - ID of the block to check
|
||||
* @param blocks - Record of all blocks in the workflow
|
||||
* @returns true if the block is inside a loop or parallel, false otherwise
|
||||
*/
|
||||
static isBlockInSubflow<T extends { id: string; data?: { parentId?: string } }>(
|
||||
blockId: string,
|
||||
blocks: T[] | Record<string, T>
|
||||
): boolean {
|
||||
const blockArray = Array.isArray(blocks) ? blocks : Object.values(blocks)
|
||||
const block = blockArray.find((b) => b.id === blockId)
|
||||
|
||||
if (!block || !block.data?.parentId) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if the parent is a loop or parallel block
|
||||
const parent = blockArray.find((b) => b.id === block.data?.parentId)
|
||||
if (!parent) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Type-safe check: parent must have a 'type' property
|
||||
const parentWithType = parent as T & { type?: string }
|
||||
return parentWithType.type === 'loop' || parentWithType.type === 'parallel'
|
||||
}
|
||||
|
||||
static isSingleInstanceBlockType(blockType: string): boolean {
|
||||
const blockConfig = getBlock(blockType)
|
||||
return blockConfig?.singleInstance === true
|
||||
}
|
||||
|
||||
static wouldViolateSingleInstanceBlock<T extends { type: string }>(
|
||||
blocks: T[] | Record<string, T>,
|
||||
blockType: string
|
||||
): boolean {
|
||||
if (!TriggerUtils.isSingleInstanceBlockType(blockType)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const blockArray = Array.isArray(blocks) ? blocks : Object.values(blocks)
|
||||
return blockArray.some((block) => block.type === blockType)
|
||||
}
|
||||
|
||||
static getSingleInstanceBlockIssue<T extends { type: string }>(
|
||||
blocks: T[] | Record<string, T>,
|
||||
blockType: string
|
||||
): { issue: 'duplicate'; blockName: string } | null {
|
||||
if (!TriggerUtils.wouldViolateSingleInstanceBlock(blocks, blockType)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const blockConfig = getBlock(blockType)
|
||||
const blockName = blockConfig?.name || blockType
|
||||
return { issue: 'duplicate', blockName }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user