@@ -9,14 +9,92 @@ import {
99 type ResolutionContext ,
1010 type Resolver ,
1111} from '@/executor/variables/resolvers/reference'
12- import type { SerializedWorkflow } from '@/serializer/types'
12+ import type { SerializedBlock , SerializedWorkflow } from '@/serializer/types'
13+
14+ /**
15+ * Check if a path exists in an output schema.
16+ * Handles nested objects, arrays, and various schema formats.
17+ * Numeric indices (array access) are skipped during validation.
18+ */
19+ function isPathInOutputSchema (
20+ outputs : Record < string , any > | undefined ,
21+ pathParts : string [ ]
22+ ) : boolean {
23+ if ( ! outputs || pathParts . length === 0 ) {
24+ return true // No schema or no path = allow (lenient)
25+ }
26+
27+ let current : any = outputs
28+ for ( let i = 0 ; i < pathParts . length ; i ++ ) {
29+ const part = pathParts [ i ]
30+
31+ // Skip numeric indices (array access like items.0.name)
32+ if ( / ^ \d + $ / . test ( part ) ) {
33+ continue
34+ }
35+
36+ if ( current === null || current === undefined ) {
37+ return false
38+ }
39+
40+ // Check if the key exists directly
41+ if ( part in current ) {
42+ current = current [ part ]
43+ continue
44+ }
45+
46+ // Check if current has 'properties' (object type with nested schema)
47+ if ( current . properties && part in current . properties ) {
48+ current = current . properties [ part ]
49+ continue
50+ }
51+
52+ // Check if current is an array type with items
53+ if ( current . type === 'array' && current . items ) {
54+ // Array items can have properties or be a nested schema
55+ if ( current . items . properties && part in current . items . properties ) {
56+ current = current . items . properties [ part ]
57+ continue
58+ }
59+ if ( part in current . items ) {
60+ current = current . items [ part ]
61+ continue
62+ }
63+ }
64+
65+ // Check if current has a 'type' field (it's a leaf with type definition)
66+ // but we're trying to go deeper - this means the path doesn't exist
67+ if ( 'type' in current && typeof current . type === 'string' ) {
68+ // It's a typed field, can't go deeper unless it has properties
69+ if ( ! current . properties && ! current . items ) {
70+ return false
71+ }
72+ }
73+
74+ // Path part not found in schema
75+ return false
76+ }
77+
78+ return true
79+ }
80+
81+ /**
82+ * Get available top-level field names from an output schema for error messages.
83+ */
84+ function getSchemaFieldNames ( outputs : Record < string , any > | undefined ) : string [ ] {
85+ if ( ! outputs ) return [ ]
86+ return Object . keys ( outputs )
87+ }
1388
1489export class BlockResolver implements Resolver {
1590 private nameToBlockId : Map < string , string >
91+ private blockById : Map < string , SerializedBlock >
1692
1793 constructor ( private workflow : SerializedWorkflow ) {
1894 this . nameToBlockId = new Map ( )
95+ this . blockById = new Map ( )
1996 for ( const block of workflow . blocks ) {
97+ this . blockById . set ( block . id , block )
2098 if ( block . metadata ?. name ) {
2199 this . nameToBlockId . set ( normalizeName ( block . metadata . name ) , block . id )
22100 }
@@ -47,7 +125,9 @@ export class BlockResolver implements Resolver {
47125 return undefined
48126 }
49127
128+ const block = this . blockById . get ( blockId )
50129 const output = this . getBlockOutput ( blockId , context )
130+
51131 if ( output === undefined ) {
52132 return undefined
53133 }
@@ -63,9 +143,6 @@ export class BlockResolver implements Resolver {
63143 return result
64144 }
65145
66- // If failed, check if we should try backwards compatibility fallback
67- const block = this . workflow . blocks . find ( ( b ) => b . id === blockId )
68-
69146 // Response block backwards compatibility:
70147 // Old: <responseBlock.response.data> -> New: <responseBlock.data>
71148 // Only apply fallback if:
@@ -108,6 +185,18 @@ export class BlockResolver implements Resolver {
108185 }
109186 }
110187
188+ // Path not found in data - check if it exists in the schema
189+ // If path is NOT in schema, it's likely a typo - throw an error
190+ // If path IS in schema but data is missing, it's an optional field - return undefined
191+ const schemaFields = getSchemaFieldNames ( block ?. outputs )
192+ if ( schemaFields . length > 0 && ! isPathInOutputSchema ( block ?. outputs , pathParts ) ) {
193+ throw new Error (
194+ `"${ pathParts . join ( '.' ) } " doesn't exist on block "${ blockName } ". ` +
195+ `Available fields: ${ schemaFields . join ( ', ' ) } `
196+ )
197+ }
198+
199+ // Path exists in schema but data is missing - return undefined (optional field)
111200 return undefined
112201 }
113202
0 commit comments