d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
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
285 lines
7.7 KiB
TypeScript
285 lines
7.7 KiB
TypeScript
import { HttpError } from '@/lib/core/utils/http-error'
|
|
import { USER_FILE_ACCESSIBLE_PROPERTIES } from '@/lib/workflows/types'
|
|
import { normalizeName } from '@/executor/constants'
|
|
import {
|
|
type AsyncPathNavigator,
|
|
navigatePath,
|
|
type ResolutionContext,
|
|
} from '@/executor/variables/resolvers/reference'
|
|
|
|
/**
|
|
* A single schema node encountered while walking an `OutputSchema`. Captures
|
|
* only the fields this module inspects — not a full schema type.
|
|
*/
|
|
interface SchemaNode {
|
|
type?: string
|
|
description?: string
|
|
properties?: unknown
|
|
items?: unknown
|
|
}
|
|
|
|
export type OutputSchema = Record<string, SchemaNode | unknown>
|
|
|
|
export interface BlockReferenceContext {
|
|
blockNameMapping: Record<string, string>
|
|
blockData: Record<string, unknown>
|
|
blockOutputSchemas?: Record<string, OutputSchema>
|
|
}
|
|
|
|
export interface BlockReferenceResult {
|
|
value: unknown
|
|
blockId: string
|
|
}
|
|
|
|
export class InvalidFieldError extends HttpError {
|
|
readonly statusCode = 400
|
|
|
|
constructor(
|
|
public readonly blockName: string,
|
|
public readonly fieldPath: string,
|
|
public readonly availableFields: string[]
|
|
) {
|
|
super(
|
|
`"${fieldPath}" doesn't exist on block "${blockName}". ` +
|
|
`Available fields: ${availableFields.length > 0 ? availableFields.join(', ') : 'none'}`
|
|
)
|
|
this.name = 'InvalidFieldError'
|
|
}
|
|
}
|
|
|
|
function asSchemaNode(value: unknown): SchemaNode | undefined {
|
|
if (typeof value !== 'object' || value === null) return undefined
|
|
return value as SchemaNode
|
|
}
|
|
|
|
function isFileType(value: unknown): boolean {
|
|
const node = asSchemaNode(value)
|
|
return node?.type === 'file' || node?.type === 'file[]'
|
|
}
|
|
|
|
function isArrayType(value: unknown): value is { type: 'array'; items?: unknown } {
|
|
return asSchemaNode(value)?.type === 'array'
|
|
}
|
|
|
|
function getArrayItems(schema: unknown): unknown {
|
|
return asSchemaNode(schema)?.items
|
|
}
|
|
|
|
function getProperties(schema: unknown): Record<string, unknown> | undefined {
|
|
const props = asSchemaNode(schema)?.properties
|
|
return typeof props === 'object' && props !== null
|
|
? (props as Record<string, unknown>)
|
|
: undefined
|
|
}
|
|
|
|
function lookupField(schema: unknown, fieldName: string): unknown | undefined {
|
|
if (typeof schema !== 'object' || schema === null) return undefined
|
|
const typed = schema as Record<string, unknown>
|
|
|
|
if (fieldName in typed) {
|
|
return typed[fieldName]
|
|
}
|
|
|
|
const props = getProperties(schema)
|
|
if (props && fieldName in props) {
|
|
return props[fieldName]
|
|
}
|
|
|
|
return undefined
|
|
}
|
|
|
|
function isOpaqueSchemaNode(value: unknown): boolean {
|
|
const node = asSchemaNode(value)
|
|
if (!node) return false
|
|
// A schema node whose nested shape isn't enumerated. Any path beneath it
|
|
// is accepted because there's no declared structure to validate against.
|
|
// `object` / `json` with declared `properties` are walked via lookupField.
|
|
if (node.type === 'any') return true
|
|
if ((node.type === 'json' || node.type === 'object') && node.properties === undefined) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
function isPathInSchema(schema: OutputSchema | undefined, pathParts: string[]): boolean {
|
|
if (!schema || pathParts.length === 0) {
|
|
return true
|
|
}
|
|
|
|
let current: unknown = schema
|
|
|
|
for (let i = 0; i < pathParts.length; i++) {
|
|
const part = pathParts[i]
|
|
|
|
if (current === null || current === undefined) {
|
|
return false
|
|
}
|
|
|
|
if (isOpaqueSchemaNode(current)) {
|
|
return true
|
|
}
|
|
|
|
if (/^\d+$/.test(part)) {
|
|
if (isFileType(current)) {
|
|
const nextPart = pathParts[i + 1]
|
|
return (
|
|
!nextPart ||
|
|
USER_FILE_ACCESSIBLE_PROPERTIES.includes(
|
|
nextPart as (typeof USER_FILE_ACCESSIBLE_PROPERTIES)[number]
|
|
)
|
|
)
|
|
}
|
|
if (isArrayType(current)) {
|
|
current = getArrayItems(current)
|
|
}
|
|
continue
|
|
}
|
|
|
|
const arrayMatch = part.match(/^([^[]+)\[(\d+)\]$/)
|
|
if (arrayMatch) {
|
|
const [, prop] = arrayMatch
|
|
const fieldDef = lookupField(current, prop)
|
|
if (!fieldDef) return false
|
|
|
|
if (isFileType(fieldDef)) {
|
|
const nextPart = pathParts[i + 1]
|
|
return (
|
|
!nextPart ||
|
|
USER_FILE_ACCESSIBLE_PROPERTIES.includes(
|
|
nextPart as (typeof USER_FILE_ACCESSIBLE_PROPERTIES)[number]
|
|
)
|
|
)
|
|
}
|
|
|
|
current = isArrayType(fieldDef) ? getArrayItems(fieldDef) : fieldDef
|
|
continue
|
|
}
|
|
|
|
if (
|
|
isFileType(current) &&
|
|
USER_FILE_ACCESSIBLE_PROPERTIES.includes(
|
|
part as (typeof USER_FILE_ACCESSIBLE_PROPERTIES)[number]
|
|
)
|
|
) {
|
|
return true
|
|
}
|
|
|
|
const fieldDef = lookupField(current, part)
|
|
if (fieldDef !== undefined) {
|
|
if (isFileType(fieldDef)) {
|
|
const nextPart = pathParts[i + 1]
|
|
if (!nextPart) return true
|
|
if (/^\d+$/.test(nextPart)) {
|
|
const afterIndex = pathParts[i + 2]
|
|
return (
|
|
!afterIndex ||
|
|
USER_FILE_ACCESSIBLE_PROPERTIES.includes(
|
|
afterIndex as (typeof USER_FILE_ACCESSIBLE_PROPERTIES)[number]
|
|
)
|
|
)
|
|
}
|
|
return USER_FILE_ACCESSIBLE_PROPERTIES.includes(
|
|
nextPart as (typeof USER_FILE_ACCESSIBLE_PROPERTIES)[number]
|
|
)
|
|
}
|
|
current = fieldDef
|
|
continue
|
|
}
|
|
|
|
if (isArrayType(current)) {
|
|
const items = getArrayItems(current)
|
|
const itemField = lookupField(items, part)
|
|
if (itemField !== undefined) {
|
|
current = itemField
|
|
continue
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
function getSchemaFieldNames(schema: OutputSchema | undefined): string[] {
|
|
if (!schema) return []
|
|
return Object.keys(schema)
|
|
}
|
|
|
|
export function resolveBlockReference(
|
|
blockName: string,
|
|
pathParts: string[],
|
|
context: BlockReferenceContext,
|
|
options: {
|
|
allowLargeValueRefs?: boolean
|
|
executionContext?: ResolutionContext['executionContext']
|
|
} = {}
|
|
): BlockReferenceResult | undefined {
|
|
const normalizedName = normalizeName(blockName)
|
|
const blockId = context.blockNameMapping[normalizedName]
|
|
|
|
if (!blockId) {
|
|
return undefined
|
|
}
|
|
|
|
const blockOutput = context.blockData[blockId]
|
|
|
|
// When the block has not produced any output (e.g. it lives on a branched
|
|
// path that wasn't taken), resolve the reference to undefined without
|
|
// validating against the declared schema. Callers map this to an empty
|
|
// value so that references to skipped blocks don't fail the workflow.
|
|
if (blockOutput === undefined) {
|
|
return { value: undefined, blockId }
|
|
}
|
|
|
|
if (pathParts.length === 0) {
|
|
return { value: blockOutput, blockId }
|
|
}
|
|
|
|
const value = navigatePath(blockOutput, pathParts, options)
|
|
|
|
const schema = context.blockOutputSchemas?.[blockId]
|
|
if (value === undefined && schema) {
|
|
if (!isPathInSchema(schema, pathParts)) {
|
|
throw new InvalidFieldError(blockName, pathParts.join('.'), getSchemaFieldNames(schema))
|
|
}
|
|
}
|
|
|
|
return { value, blockId }
|
|
}
|
|
|
|
export async function resolveBlockReferenceAsync(
|
|
blockName: string,
|
|
pathParts: string[],
|
|
context: BlockReferenceContext,
|
|
resolutionContext: ResolutionContext,
|
|
navigatePathAsync: AsyncPathNavigator
|
|
): Promise<BlockReferenceResult | undefined> {
|
|
const normalizedName = normalizeName(blockName)
|
|
const blockId = context.blockNameMapping[normalizedName]
|
|
|
|
if (!blockId) {
|
|
return undefined
|
|
}
|
|
|
|
const blockOutput = context.blockData[blockId]
|
|
if (blockOutput === undefined) {
|
|
return { value: undefined, blockId }
|
|
}
|
|
|
|
if (pathParts.length === 0) {
|
|
return { value: blockOutput, blockId }
|
|
}
|
|
|
|
const value = await navigatePathAsync(blockOutput, pathParts, resolutionContext)
|
|
|
|
const schema = context.blockOutputSchemas?.[blockId]
|
|
if (value === undefined && schema) {
|
|
if (!isPathInSchema(schema, pathParts)) {
|
|
throw new InvalidFieldError(blockName, pathParts.join('.'), getSchemaFieldNames(schema))
|
|
}
|
|
}
|
|
|
|
return { value, blockId }
|
|
}
|