@oomol-lab/open-flow 0.1.0-beta.74 → 0.1.0-beta.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/browser/{ContentIcon-CUQHjQIw.js → ContentIcon-FCpKiwjp.js} +2 -1
- package/dist/browser/{FlowCanvas-Dtf8_cQp.js → FlowCanvas-dzRBnlUT.js} +1 -1
- package/dist/browser/{createResourceDialog-DI9pL-Yr.js → createResourceDialog-BeYDdUl-.js} +2 -2
- package/dist/browser/{dialog-BYJGvItc.js → dialog-ZtpsFbl4.js} +284 -13
- package/dist/browser/{exportSvg-BOoy7-Db.js → exportSvg-DqXw1yf-.js} +2 -2
- package/dist/browser/{flowChanges-RL-H4CjD.js → flowChanges-b_5UobFj.js} +285 -44
- package/dist/browser/{flowWorkspace-BkDsETj0.js → flowWorkspace-ItojZbO4.js} +414 -53
- package/dist/browser/preview.css +181 -176
- package/dist/browser/preview.js +955 -182
- package/dist/browser/workbench.css +181 -176
- package/dist/browser/workbench.js +12 -6
- package/dist/browser/{workbenchSelect-BcWPWGzw.js → workbenchSelect-M441Lz0d.js} +1 -1
- package/dist/command/index.js +36 -36
- package/dist/common/authoring.d.ts +4 -4
- package/dist/common/authoringExamples.d.ts +3 -2
- package/dist/common/authoringSchema.d.ts +67 -4
- package/dist/common/control-api-conformance.js +58 -7
- package/dist/common/control-api.js +58 -7
- package/dist/common/control-requests.d.ts +3 -3
- package/dist/common/control-requests.js +234 -62
- package/dist/common/engine-contract.d.ts +1 -0
- package/dist/common/flow-authoring.js +56 -7
- package/dist/common/flow-change.d.ts +1 -1
- package/dist/common/flow-change.js +56 -7
- package/dist/common/flow-encoding.js +55 -7
- package/dist/common/flow-graph.d.ts +2 -1
- package/dist/common/flow-retry.d.ts +1 -1
- package/dist/common/flow-semantics.js +810 -577
- package/dist/common/http-request.d.ts +156 -0
- package/dist/common/http-request.js +5254 -0
- package/dist/common/mcp.d.ts +3 -3
- package/dist/common/mcp.js +58 -7
- package/dist/common/run-events.js +11 -9
- package/dist/common/runtime-contract.js +3 -0
- package/dist/common/scheduler.d.ts +1 -1
- package/dist/common/scheduler.js +70 -21
- package/dist/common/webhook-response.d.ts +0 -2
- package/dist/common/webhook-trigger.js +12 -11
- package/package.json +5 -1
- package/skills/open-flow/references/flow-authoring.md +1 -0
- package/skills/open-flow/references/flow-n8n-conversion.md +19 -15
- package/src/control/common/api.ts +1 -0
- package/src/control/common/authoring.ts +14 -3
- package/src/control/common/authoringExamples.ts +8 -0
- package/src/control/common/authoringSchema.ts +30 -24
- package/src/execution/common/engineContract.ts +4 -0
- package/src/execution/common/events.ts +3 -0
- package/src/execution/common/node-execution.ts +4 -2
- package/src/execution/common/scheduler.ts +1 -0
- package/src/flow/common/change.ts +3 -2
- package/src/flow/common/changeSchema.ts +12 -3
- package/src/flow/common/graph.ts +10 -4
- package/src/flow/common/retry.ts +1 -1
- package/src/flow/common/semantics.ts +60 -3
- package/src/flow/common/webhookResponse.ts +3 -7
- package/src/{openapi → http}/common/authBindings.ts +3 -3
- package/src/http/common/headers.ts +5 -0
- package/src/http/common/httpRequest.ts +251 -0
- package/src/trigger/common/webhook.ts +4 -3
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type { InputMapping, InputPort, JsonValue, ManagedTaskDefinition, TaskNode } from '../../flow/common/change.ts'
|
|
2
|
+
|
|
3
|
+
import { dequal } from 'dequal/lite'
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
import { remapInputMappings, renamedPort } from '../../flow/common/portChanges.ts'
|
|
6
|
+
import { httpHeaderNameIssue } from './headers.ts'
|
|
7
|
+
|
|
8
|
+
const parameterSchema = z.strictObject({ mode: z.enum(['fields', 'object']).default('fields'), names: z.array(z.string()).default([]) })
|
|
9
|
+
export const httpRequestConfigSchema = z.strictObject({
|
|
10
|
+
method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']).default('GET'),
|
|
11
|
+
query: parameterSchema.default({ mode: 'fields', names: [] }),
|
|
12
|
+
headers: parameterSchema.default({ mode: 'fields', names: [] }),
|
|
13
|
+
bodyFormat: z.enum(['none', 'json', 'text', 'form']).default('none'),
|
|
14
|
+
authentication: z
|
|
15
|
+
.union([
|
|
16
|
+
z.strictObject({ type: z.literal('none') }),
|
|
17
|
+
z.strictObject({ type: z.enum(['bearer', 'basic']) }),
|
|
18
|
+
z.strictObject({ type: z.literal('apiKey'), name: z.string(), in: z.enum(['header', 'query']) }),
|
|
19
|
+
])
|
|
20
|
+
.default({ type: 'none' }),
|
|
21
|
+
responseFormat: z.enum(['auto', 'json', 'text']).default('auto'),
|
|
22
|
+
statusPolicy: z.enum(['success', 'all']).default('success'),
|
|
23
|
+
})
|
|
24
|
+
export type HttpRequestConfig = z.infer<typeof httpRequestConfigSchema>
|
|
25
|
+
export interface HttpRequestExecutor extends HttpRequestConfig {
|
|
26
|
+
readonly kind: 'http-request'
|
|
27
|
+
}
|
|
28
|
+
export const httpRequestExecutorSchema = httpRequestConfigSchema.extend({ kind: z.literal('http-request') })
|
|
29
|
+
const scalarSchema = { type: ['string', 'number', 'boolean'] }
|
|
30
|
+
const queryValueSchema = { anyOf: [scalarSchema, { type: 'array', items: scalarSchema }] }
|
|
31
|
+
export const httpHeaderSchema = { type: 'object', additionalProperties: { type: 'string' } }
|
|
32
|
+
export const httpQuerySchema = { type: 'object', additionalProperties: queryValueSchema }
|
|
33
|
+
|
|
34
|
+
export function httpAuthHandles(config: HttpRequestConfig): string[] {
|
|
35
|
+
return config.authentication.type == 'none' ? [] : config.authentication.type == 'basic' ? ['auth.username', 'auth.password'] : ['auth.token']
|
|
36
|
+
}
|
|
37
|
+
// Preserve metadata on ports whose contracts did not change.
|
|
38
|
+
function retain(ports: readonly InputPort[], old: ManagedTaskDefinition['inputs']): ManagedTaskDefinition['inputs'] {
|
|
39
|
+
const remaining = new Map(ports.map((port) => [port.handle, port]))
|
|
40
|
+
return [
|
|
41
|
+
...old.flatMap((item): ManagedTaskDefinition['inputs'] => {
|
|
42
|
+
if ('group' in item) return [item]
|
|
43
|
+
const next = remaining.get(item.handle)
|
|
44
|
+
if (next == null) return []
|
|
45
|
+
remaining.delete(item.handle)
|
|
46
|
+
return [dequal(item.jsonSchema, next.jsonSchema) && item.nullable == next.nullable ? item : next]
|
|
47
|
+
}),
|
|
48
|
+
...remaining.values(),
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
export function httpRequestTask(
|
|
52
|
+
config: HttpRequestConfig = httpRequestConfigSchema.parse({}),
|
|
53
|
+
name = 'HTTP Request',
|
|
54
|
+
previous?: ManagedTaskDefinition,
|
|
55
|
+
): ManagedTaskDefinition {
|
|
56
|
+
const inputs: InputPort[] = [{ handle: 'url', jsonSchema: { type: 'string' }, nullable: false }]
|
|
57
|
+
for (const group of ['query', 'headers'] as const) {
|
|
58
|
+
if (config[group].mode == 'object') inputs.push({ handle: group, jsonSchema: group == 'query' ? httpQuerySchema : httpHeaderSchema, nullable: true })
|
|
59
|
+
else
|
|
60
|
+
for (const parameterName of config[group].names.filter((value) => value.trim()))
|
|
61
|
+
inputs.push({
|
|
62
|
+
handle: `${group == 'query' ? 'query' : 'header'}.${parameterName}`,
|
|
63
|
+
jsonSchema: group == 'query' ? queryValueSchema : { type: 'string' },
|
|
64
|
+
nullable: true,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
if (config.bodyFormat != 'none')
|
|
68
|
+
inputs.push({
|
|
69
|
+
handle: 'body',
|
|
70
|
+
jsonSchema: config.bodyFormat == 'json' ? {} : config.bodyFormat == 'text' ? { type: 'string' } : httpQuerySchema,
|
|
71
|
+
nullable: config.bodyFormat == 'json',
|
|
72
|
+
})
|
|
73
|
+
inputs.push(...httpAuthHandles(config).map((handle) => ({ handle, jsonSchema: { type: 'string' }, nullable: false })))
|
|
74
|
+
const outputs: InputPort[] = [
|
|
75
|
+
{ handle: 'body', jsonSchema: config.responseFormat == 'text' ? { type: 'string' } : {}, nullable: true },
|
|
76
|
+
{ handle: 'statusCode', jsonSchema: { type: 'integer' }, nullable: false },
|
|
77
|
+
{ handle: 'headers', jsonSchema: httpHeaderSchema, nullable: false },
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
name,
|
|
82
|
+
executor: { ...config, kind: 'http-request' },
|
|
83
|
+
inputs: retain(inputs, previous?.inputs ?? []),
|
|
84
|
+
outputs: retain(outputs, previous?.outputs ?? []),
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export interface HttpRequestIssue {
|
|
88
|
+
field: string
|
|
89
|
+
index?: number
|
|
90
|
+
code: 'bodyUnsupported' | 'parameterNamesInvalid' | 'headerNameInvalid' | 'keyNameRequired' | 'authConflict'
|
|
91
|
+
message: string
|
|
92
|
+
}
|
|
93
|
+
export function httpRequestIssues(config: HttpRequestConfig): HttpRequestIssue[] {
|
|
94
|
+
const issues: HttpRequestIssue[] = []
|
|
95
|
+
if (['GET', 'HEAD'].includes(config.method) && config.bodyFormat != 'none')
|
|
96
|
+
issues.push({ field: 'bodyFormat', code: 'bodyUnsupported', message: 'GET and HEAD do not support a request body.' })
|
|
97
|
+
for (const group of ['query', 'headers'] as const) {
|
|
98
|
+
if (config[group].mode != 'fields') continue
|
|
99
|
+
const seen = new Set<string>()
|
|
100
|
+
for (const [index, name] of config[group].names.entries()) {
|
|
101
|
+
const key = group == 'headers' ? name.toLowerCase() : name
|
|
102
|
+
if (!name.trim() || seen.has(key))
|
|
103
|
+
issues.push({ field: group, index, code: 'parameterNamesInvalid', message: 'Parameter names must be nonempty and unique.' })
|
|
104
|
+
seen.add(key)
|
|
105
|
+
if (group == 'headers' && httpHeaderNameIssue(name, []) === 'invalid')
|
|
106
|
+
issues.push({ field: group, index, code: 'headerNameInvalid', message: 'Invalid HTTP header name.' })
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const auth = config.authentication
|
|
110
|
+
if (auth.type != 'none') {
|
|
111
|
+
const group = auth.type == 'apiKey' && auth.in == 'query' ? 'query' : 'headers'
|
|
112
|
+
const name = auth.type == 'apiKey' ? auth.name : 'authorization'
|
|
113
|
+
if (!name.trim()) issues.push({ field: 'authentication', code: 'keyNameRequired', message: 'API Key requires a name.' })
|
|
114
|
+
if (group == 'headers' && name.trim() && httpHeaderNameIssue(name, []) === 'invalid')
|
|
115
|
+
issues.push({ field: 'authentication', code: 'headerNameInvalid', message: 'Invalid authentication header name.' })
|
|
116
|
+
if (config[group].mode == 'fields' && config[group].names.some((item) => (group == 'headers' ? item.toLowerCase() == name.toLowerCase() : item == name)))
|
|
117
|
+
issues.push({ field: 'authentication', code: 'authConflict', message: 'Authentication conflicts with a request parameter.' })
|
|
118
|
+
}
|
|
119
|
+
return issues
|
|
120
|
+
}
|
|
121
|
+
function entries(value: JsonValue | undefined): [string, JsonValue][] {
|
|
122
|
+
if (value == null) return []
|
|
123
|
+
if (typeof value != 'object' || Array.isArray(value)) throw new Error('Expected a parameter object.')
|
|
124
|
+
return Object.entries(value)
|
|
125
|
+
}
|
|
126
|
+
function scalar(value: JsonValue): string {
|
|
127
|
+
if (typeof value != 'string' && typeof value != 'number' && typeof value != 'boolean') throw new Error('Expected a scalar parameter.')
|
|
128
|
+
return String(value)
|
|
129
|
+
}
|
|
130
|
+
function append(query: URLSearchParams, name: string, value: JsonValue): void {
|
|
131
|
+
for (const item of Array.isArray(value) ? value : [value]) query.append(name, scalar(item))
|
|
132
|
+
}
|
|
133
|
+
export function httpRequestUrl(value: unknown): URL {
|
|
134
|
+
if (typeof value != 'string' || !value.trim()) throw new Error('A URL is required.')
|
|
135
|
+
const url = new URL(value)
|
|
136
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) throw new Error('An HTTP(S) URL without credentials is required.')
|
|
137
|
+
return url
|
|
138
|
+
}
|
|
139
|
+
export function buildHttpRequest(config: HttpRequestConfig, input: Readonly<Record<string, JsonValue>>): { url: string; init: RequestInit } {
|
|
140
|
+
if (httpRequestIssues(config).length) throw new Error('Invalid HTTP request configuration.')
|
|
141
|
+
const url = httpRequestUrl(input.url)
|
|
142
|
+
const parameters = (group: 'query' | 'headers') =>
|
|
143
|
+
config[group].mode == 'object'
|
|
144
|
+
? entries(input[group])
|
|
145
|
+
: config[group].names.flatMap((name): [string, JsonValue][] => {
|
|
146
|
+
const value = input[`${group == 'query' ? 'query' : 'header'}.${name}`]
|
|
147
|
+
return value == null ? [] : [[name, value]]
|
|
148
|
+
})
|
|
149
|
+
for (const [name, value] of parameters('query')) {
|
|
150
|
+
url.searchParams.delete(name)
|
|
151
|
+
append(url.searchParams, name, value)
|
|
152
|
+
}
|
|
153
|
+
const headers = new Headers()
|
|
154
|
+
for (const [name, value] of parameters('headers')) {
|
|
155
|
+
if (headers.has(name)) throw new Error('Duplicate HTTP header.')
|
|
156
|
+
if (typeof value != 'string') throw new Error('HTTP headers require text values.')
|
|
157
|
+
headers.set(name, value)
|
|
158
|
+
}
|
|
159
|
+
const auth = config.authentication
|
|
160
|
+
const credential = (part: string) => {
|
|
161
|
+
const value = input[`auth.${part}`]
|
|
162
|
+
if (typeof value != 'string' || !value) throw new Error('Authentication source is required.')
|
|
163
|
+
return value
|
|
164
|
+
}
|
|
165
|
+
if (auth.type != 'none') {
|
|
166
|
+
const name = auth.type == 'apiKey' ? auth.name : 'authorization'
|
|
167
|
+
if (auth.type == 'apiKey' && auth.in == 'query') {
|
|
168
|
+
if (url.searchParams.has(name)) throw new Error('Authentication conflicts with a query parameter.')
|
|
169
|
+
url.searchParams.set(name, credential('token'))
|
|
170
|
+
} else {
|
|
171
|
+
if (headers.has(name)) throw new Error('Authentication conflicts with a header.')
|
|
172
|
+
if (auth.type == 'basic') {
|
|
173
|
+
const username = credential('username')
|
|
174
|
+
if (username.includes(':')) throw new Error('Basic username cannot contain a colon.')
|
|
175
|
+
const bytes = new TextEncoder().encode(`${username}:${credential('password')}`)
|
|
176
|
+
headers.set(name, `Basic ${btoa(Array.from(bytes, (byte) => String.fromCharCode(byte)).join(''))}`)
|
|
177
|
+
} else headers.set(name, auth.type == 'bearer' ? `Bearer ${credential('token')}` : credential('token'))
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
let body: string | undefined
|
|
181
|
+
if (config.bodyFormat != 'none') {
|
|
182
|
+
if (!Object.hasOwn(input, 'body')) throw new Error('Request body is unset.')
|
|
183
|
+
let contentType: string
|
|
184
|
+
if (config.bodyFormat == 'json') {
|
|
185
|
+
body = JSON.stringify(input.body)
|
|
186
|
+
contentType = 'application/json'
|
|
187
|
+
} else if (config.bodyFormat == 'text') {
|
|
188
|
+
if (typeof input.body != 'string') throw new Error('Text body requires a string.')
|
|
189
|
+
body = input.body
|
|
190
|
+
contentType = 'text/plain'
|
|
191
|
+
} else {
|
|
192
|
+
if (input.body == null) throw new Error('Form body requires an object.')
|
|
193
|
+
const form = new URLSearchParams()
|
|
194
|
+
for (const [name, value] of entries(input.body)) append(form, name, value)
|
|
195
|
+
body = form.toString()
|
|
196
|
+
contentType = 'application/x-www-form-urlencoded'
|
|
197
|
+
}
|
|
198
|
+
const supplied = headers.get('content-type')
|
|
199
|
+
if (config.bodyFormat != 'text' && supplied != null && supplied.split(';')[0]!.trim().toLowerCase() != contentType)
|
|
200
|
+
throw new Error('Content-Type conflicts with body format.')
|
|
201
|
+
if (supplied == null) headers.set('content-type', contentType)
|
|
202
|
+
}
|
|
203
|
+
return { url: url.href, init: { method: config.method, headers, body, redirect: 'manual' } }
|
|
204
|
+
}
|
|
205
|
+
export function parseHttpResponse(config: HttpRequestConfig, status: number, headers: Headers, text: string): Record<string, JsonValue> {
|
|
206
|
+
if (config.statusPolicy == 'success' && (status < 200 || status >= 300)) throw new Error(`HTTP ${status}`)
|
|
207
|
+
let body: JsonValue = null
|
|
208
|
+
if (text && config.method != 'HEAD' && ![204, 205, 304].includes(status)) {
|
|
209
|
+
const media = (headers.get('content-type') ?? '').split(';')[0]!.trim().toLowerCase()
|
|
210
|
+
body =
|
|
211
|
+
config.responseFormat == 'json' || (config.responseFormat == 'auto' && /^application\/(json|.+\+json)$/.test(media))
|
|
212
|
+
? (JSON.parse(text) as JsonValue)
|
|
213
|
+
: text
|
|
214
|
+
}
|
|
215
|
+
return { body, statusCode: status, headers: Object.fromEntries([...headers].filter(([name]) => name != 'set-cookie')) }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Only a named parameter rename carries a value; changing source modes is not a rename. */
|
|
219
|
+
export function httpRequestInputs(
|
|
220
|
+
mappings: Readonly<Record<string, InputMapping>>,
|
|
221
|
+
previous: ManagedTaskDefinition,
|
|
222
|
+
next: ManagedTaskDefinition,
|
|
223
|
+
): Record<string, InputMapping> {
|
|
224
|
+
const candidate = renamedPort(previous.inputs, next.inputs)
|
|
225
|
+
const rename = candidate != null && ['query.', 'header.'].some((prefix) => candidate.every((name) => name.startsWith(prefix))) ? candidate : undefined
|
|
226
|
+
return remapInputMappings(mappings, next.inputs, rename)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** The object editor assembles named inputs without changing unrelated request settings. */
|
|
230
|
+
export function withHttpParameterFields(node: TaskNode, group: 'query' | 'headers', values: Record<string, JsonValue>): TaskNode {
|
|
231
|
+
if (!('executor' in node.task) || node.task.executor.kind !== 'http-request') return node
|
|
232
|
+
const config = node.task.executor
|
|
233
|
+
const task = httpRequestTask({ ...config, [group]: { mode: 'fields', names: Object.keys(values) } }, node.task.name, node.task)
|
|
234
|
+
const inputs = httpRequestInputs(node.inputs, node.task, task)
|
|
235
|
+
const rename = renamedPort(node.task.inputs, task.inputs)
|
|
236
|
+
for (const [name, value] of Object.entries(values)) {
|
|
237
|
+
const handle = `${group === 'query' ? 'query' : 'header'}.${name}`
|
|
238
|
+
const existing = config[group].mode === 'fields' && (config[group].names.includes(name) || rename?.[1] === handle)
|
|
239
|
+
if (inputs[handle] == null && !existing) inputs[handle] = { kind: 'value', value }
|
|
240
|
+
}
|
|
241
|
+
return { ...node, task, inputs }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function withHttpParameterSource(node: TaskNode, group: 'query' | 'headers', mapping?: InputMapping): TaskNode {
|
|
245
|
+
if (!('executor' in node.task) || node.task.executor.kind !== 'http-request') return node
|
|
246
|
+
const task = httpRequestTask({ ...node.task.executor, [group]: { mode: 'object', names: [] } }, node.task.name, node.task)
|
|
247
|
+
const inputs = httpRequestInputs(node.inputs, node.task, task)
|
|
248
|
+
if (mapping == null) delete inputs[group]
|
|
249
|
+
else inputs[group] = mapping
|
|
250
|
+
return { ...node, task, inputs }
|
|
251
|
+
}
|
|
@@ -2,7 +2,8 @@ import type { JsonValue, WebhookBodyField, WebhookMethod, WebhookOptions } from
|
|
|
2
2
|
|
|
3
3
|
import { dequal } from 'dequal/lite'
|
|
4
4
|
import { isJsonObject } from '../../base/common/json.ts'
|
|
5
|
-
import {
|
|
5
|
+
import { validWebhookStatus, webhookHeaderValues, encodeWebhookBody } from '../../flow/common/webhookResponse.ts'
|
|
6
|
+
import { httpHeaderNameIssue } from '../../http/common/headers.ts'
|
|
6
7
|
|
|
7
8
|
export { webhookSupportsBody } from './contract.ts'
|
|
8
9
|
export { webhookResponseAllowsBody, webhookResponseTimeout } from '../../flow/common/webhookResponse.ts'
|
|
@@ -34,7 +35,7 @@ export function webhookResponseHeaders(result: unknown, options: WebhookOptions)
|
|
|
34
35
|
const values = new Map<string, { name: string; value: unknown }>()
|
|
35
36
|
for (const [name, value] of Object.entries(source)) {
|
|
36
37
|
if (
|
|
37
|
-
|
|
38
|
+
httpHeaderNameIssue(
|
|
38
39
|
name,
|
|
39
40
|
[...values.values()].map((entry) => entry.name),
|
|
40
41
|
) != null
|
|
@@ -45,7 +46,7 @@ export function webhookResponseHeaders(result: unknown, options: WebhookOptions)
|
|
|
45
46
|
if (whole == null) {
|
|
46
47
|
const names: string[] = []
|
|
47
48
|
for (const [name, output] of Object.entries(options.responseHeaderOutputs ?? {})) {
|
|
48
|
-
if (
|
|
49
|
+
if (httpHeaderNameIssue(name, names) != null) return
|
|
49
50
|
names.push(name)
|
|
50
51
|
values.set(name.toLowerCase(), { name, value: workflowOutputValue(result, output) })
|
|
51
52
|
}
|