@oomol-lab/open-flow 0.1.0-beta.74 → 0.1.0-beta.76
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 +3 -1
- package/dist/browser/{ContentIcon-CUQHjQIw.js → ContentIcon-Cm1hbVzW.js} +4 -3
- package/dist/browser/{FlowCanvas-Dtf8_cQp.js → FlowCanvas-NzKtla_c.js} +62 -62
- package/dist/browser/{createResourceDialog-DI9pL-Yr.js → createResourceDialog-BgdedQqu.js} +3 -3
- package/dist/browser/{dialog-BYJGvItc.js → dialog-aSElFFw2.js} +322 -26
- package/dist/browser/{exportSvg-BOoy7-Db.js → exportSvg-Dcz4HTo_.js} +2 -2
- package/dist/browser/{flowChanges-RL-H4CjD.js → flowChanges-NZyS7HSp.js} +490 -438
- package/dist/browser/{flowWorkspace-BkDsETj0.js → flowWorkspace-Bl0SDehy.js} +499 -61
- package/dist/browser/preview.css +696 -825
- package/dist/browser/preview.js +1399 -717
- package/dist/browser/theme.css +114 -262
- package/dist/browser/{tooltip-wZJRB9oP.js → tooltip-3XMK9jkr.js} +1 -1
- package/dist/browser/{typeScriptSession-CSRwWmZm.js → typeScriptSession-C9sBGOKZ.js} +1 -1
- package/dist/browser/ui.css +354 -460
- package/dist/browser/ui.js +1 -1
- package/dist/browser/workbench.css +696 -825
- package/dist/browser/workbench.js +13 -7
- package/dist/browser/{workbenchSelect-BcWPWGzw.js → workbenchSelect-DBGP7Y4Z.js} +2 -2
- package/dist/command/index.js +41 -43
- package/dist/common/authoring.d.ts +5 -5
- package/dist/common/authoringExamples.d.ts +3 -2
- package/dist/common/authoringSchema.d.ts +70 -4
- package/dist/common/connector-proxy.d.ts +1 -1
- package/dist/common/control-api-conformance.js +250 -138
- package/dist/common/control-api.d.ts +3 -2
- package/dist/common/control-api.js +246 -134
- package/dist/common/control-requests.d.ts +3 -3
- package/dist/common/control-requests.js +353 -79
- package/dist/common/cron-trigger.d.ts +2 -1
- package/dist/common/decision.d.ts +4 -4
- package/dist/common/decision.js +5 -2
- package/dist/common/engine-contract.d.ts +6 -1
- package/dist/common/event-source-definition.d.ts +1 -1
- package/dist/common/event-source-view.d.ts +22 -0
- package/dist/common/event-source.d.ts +1 -1
- package/dist/common/event-source.js +24 -65
- package/dist/common/event-sources.d.ts +0 -21
- package/dist/common/flow-authoring.js +90 -19
- package/dist/common/flow-change-schema.d.ts +9 -1
- package/dist/common/flow-change.d.ts +7 -201
- package/dist/common/flow-change.js +96 -23
- package/dist/common/flow-encoding.js +89 -21
- package/dist/common/flow-graph.d.ts +2 -1
- package/dist/common/flow-input-value.d.ts +1 -1
- package/dist/common/flow-model.d.ts +121 -0
- package/dist/common/flow-notifications.d.ts +1 -1
- package/dist/common/flow-retry.d.ts +3 -5
- package/dist/common/flow-semantics.js +816 -586
- package/dist/common/flow-workflowReferences.d.ts +8 -0
- package/dist/common/flowInspection.d.ts +1 -1
- package/dist/common/http-hosts.d.ts +6 -0
- package/dist/common/http-request.d.ts +164 -0
- package/dist/common/http-request.js +5272 -0
- package/dist/common/integration-trigger.d.ts +2 -1
- package/dist/common/mcp.d.ts +3 -3
- package/dist/common/mcp.js +248 -136
- package/dist/common/openapi.d.ts +4 -4
- package/dist/common/openapi.js +4 -1
- package/dist/common/poll-trigger.d.ts +2 -1
- package/dist/common/run-events.js +11 -9
- package/dist/common/runtime-contract.d.ts +23 -1
- package/dist/common/runtime-contract.js +607 -72
- package/dist/common/runtime-nodejs-compat.d.ts +44 -0
- package/dist/common/runtime-nodejs-compat.js +5870 -0
- package/dist/common/scheduler-checkpoint.d.ts +1 -1
- package/dist/common/scheduler-node-execution.d.ts +1 -1
- package/dist/common/scheduler.d.ts +2 -2
- package/dist/common/scheduler.js +95 -25
- package/dist/common/trigger-catalog.d.ts +1 -7
- package/dist/common/trigger-config-options.d.ts +1 -1
- package/dist/common/trigger-config.d.ts +1 -1
- package/dist/common/trigger-contract.d.ts +2 -1
- package/dist/common/trigger-localization.d.ts +8 -2
- package/dist/common/trigger-model.d.ts +101 -0
- package/dist/common/webhook-response.d.ts +1 -3
- package/dist/common/webhook-trigger.d.ts +3 -1
- package/dist/common/webhook-trigger.js +12 -11
- package/dist/common/workflow.d.ts +3 -12
- package/dist/common/workflow.js +25 -23
- package/dist/index.d.ts +41 -14
- package/package.json +9 -1
- package/skills/open-flow/references/flow-authoring.md +1 -0
- package/skills/open-flow/references/flow-n8n-conversion.md +19 -15
- package/src/base/common/json.ts +1 -1
- package/src/connector/common/proxy.ts +1 -1
- package/src/control/common/api.ts +7 -5
- package/src/control/common/authoring.ts +14 -3
- package/src/control/common/authoringExamples.ts +8 -0
- package/src/control/common/authoringSchema.ts +38 -25
- package/src/control/common/authoringTools.ts +1 -1
- package/src/control/common/conformance.ts +1 -1
- package/src/control/common/connectorDecoders.ts +1 -1
- package/src/control/common/eventSources.ts +2 -29
- package/src/control/common/triggerCatalog.ts +1 -8
- package/src/decision/common/decision.ts +15 -7
- package/src/event-source/common/definition.ts +1 -1
- package/src/event-source/common/index.ts +1 -1
- package/src/event-source/common/view.ts +20 -0
- package/src/execution/common/checkpoint.ts +1 -1
- package/src/execution/common/events.ts +3 -0
- package/src/execution/common/node-execution.ts +5 -3
- package/src/execution/common/scheduler.ts +2 -1
- package/src/flow/common/change.ts +59 -192
- package/src/flow/common/changeSchema.ts +39 -5
- package/src/flow/common/connectionUsage.ts +1 -1
- package/src/flow/common/graph.ts +10 -4
- package/src/flow/common/inputValue.ts +1 -1
- package/src/flow/common/model.ts +130 -0
- package/src/flow/common/modules.ts +1 -1
- package/src/flow/common/portChanges.ts +1 -1
- package/src/flow/common/retry.ts +3 -5
- package/src/flow/common/semantics.ts +71 -7
- package/src/flow/common/webhookResponse.ts +4 -8
- package/src/flow/common/workflowReferences.ts +31 -0
- package/src/http/common/authBindings.ts +9 -0
- package/src/http/common/headers.ts +5 -0
- package/src/http/common/hosts.ts +13 -0
- package/src/http/common/httpRequest.ts +266 -0
- package/src/openapi/common/openapi.ts +9 -5
- package/src/{execution → runtime}/common/engineContract.ts +24 -15
- package/src/runtime/common/nodejsCompat.ts +275 -0
- package/src/runtime/common/nodejsGuest.generated.json +47 -0
- package/src/runtime/common/runtime.ts +1138 -0
- package/src/trigger/common/config.ts +1 -1
- package/src/trigger/common/configOptions.ts +1 -1
- package/src/trigger/common/contract.ts +2 -1
- package/src/trigger/common/cron.ts +2 -1
- package/src/trigger/common/integration.ts +2 -1
- package/src/trigger/common/model.ts +87 -0
- package/src/trigger/common/poll.ts +2 -1
- package/src/trigger/common/webhook.ts +7 -4
- package/src/trigger/providers/fieldDescriptions.ts +1 -1
- package/src/trigger/providers/localization.ts +9 -2
- package/src/trigger/providers/remote.ts +1 -1
- package/src/types/index.ts +44 -15
- package/src/workflow/common/workflow.ts +3 -44
- package/src/execution/common/runtime.ts +0 -563
- package/src/openapi/common/authBindings.ts +0 -12
- /package/src/{control → connector}/common/providerIconSprite.ts +0 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { JsonValue, Port, PortDefinition } from '../../types/index.ts'
|
|
2
|
+
|
|
3
|
+
export type { JsonValue, Port, PortDefinition } from '../../types/index.ts'
|
|
4
|
+
|
|
5
|
+
/** Primitives shared by every node family. Node unions and graph operations belong to change.ts. */
|
|
6
|
+
|
|
7
|
+
export interface RetryConfiguration {
|
|
8
|
+
readonly maxRetries: number
|
|
9
|
+
readonly intervalMs: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface InputPortDefinition extends PortDefinition {
|
|
13
|
+
readonly value?: JsonValue
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface InputPort extends InputPortDefinition {
|
|
17
|
+
readonly handle: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Group {
|
|
21
|
+
readonly collapsed?: boolean
|
|
22
|
+
readonly group: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function portsByHandle<Value extends Port>(ports: readonly (Value | Group)[]): Readonly<Record<string, Value>> {
|
|
26
|
+
return Object.fromEntries(ports.flatMap((port) => ('handle' in port ? [[port.handle, port]] : [])))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface NodeSource {
|
|
30
|
+
readonly system?: true
|
|
31
|
+
readonly field?: string
|
|
32
|
+
readonly kind: 'node'
|
|
33
|
+
readonly nodeId: string
|
|
34
|
+
readonly output: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface BindingSource {
|
|
38
|
+
readonly bindingId: string
|
|
39
|
+
readonly kind: 'binding'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type FixedInputValue = { readonly kind: 'unset' } | { readonly kind: 'value'; readonly value: JsonValue }
|
|
43
|
+
export type InputValues = Readonly<Record<string, FixedInputValue>>
|
|
44
|
+
|
|
45
|
+
export type InputMapping = FixedInputValue | { readonly kind: 'sources'; readonly sources: readonly (BindingSource | NodeSource)[] }
|
|
46
|
+
|
|
47
|
+
export interface GraphNodeBase {
|
|
48
|
+
readonly description?: string
|
|
49
|
+
readonly icon?: string
|
|
50
|
+
readonly inputs: Readonly<Record<string, InputMapping>>
|
|
51
|
+
readonly name?: string
|
|
52
|
+
readonly maxExecutions?: number
|
|
53
|
+
readonly timeoutMs?: number
|
|
54
|
+
readonly retry?: RetryConfiguration
|
|
55
|
+
readonly onError?: 'stop' | 'error'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type Source = BindingSource | NodeSource
|
|
59
|
+
|
|
60
|
+
export interface WorkflowCapability {
|
|
61
|
+
readonly kind: 'workflow'
|
|
62
|
+
readonly name: string
|
|
63
|
+
readonly flowId: string
|
|
64
|
+
readonly revisionId: string
|
|
65
|
+
readonly inputs: readonly Port[]
|
|
66
|
+
readonly outputs: readonly Port[] | null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Hosts that Code may call through `context.fetch`; requires Flow model 12 and Engine v9. */
|
|
70
|
+
export interface HttpCapability {
|
|
71
|
+
readonly kind: 'http'
|
|
72
|
+
readonly hosts: readonly string[]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type TaskCapability = ConnectorCapability | WorkflowCapability | HttpCapability
|
|
76
|
+
|
|
77
|
+
export interface ConnectorAccessCapability {
|
|
78
|
+
readonly actionHints?: readonly string[]
|
|
79
|
+
readonly connectionHints?: readonly {
|
|
80
|
+
readonly action: string
|
|
81
|
+
readonly connectionId: string
|
|
82
|
+
readonly alias?: string
|
|
83
|
+
}[]
|
|
84
|
+
readonly kind: 'connector'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface ConnectorActionCapability {
|
|
88
|
+
readonly action: string
|
|
89
|
+
readonly connections: readonly { readonly connectionId: string; readonly alias?: string }[]
|
|
90
|
+
readonly connectionId?: string
|
|
91
|
+
readonly kind: 'connector'
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type ConnectorPermissionCapability =
|
|
95
|
+
| { readonly kind: 'connector'; readonly mode: 'shared' }
|
|
96
|
+
| { readonly kind: 'connector'; readonly mode: 'independent'; readonly actions: readonly { readonly action: string; readonly connectionId?: string }[] }
|
|
97
|
+
|
|
98
|
+
export type ConnectorCapability = ConnectorAccessCapability | ConnectorActionCapability | ConnectorPermissionCapability
|
|
99
|
+
|
|
100
|
+
export interface TaskDefinitionBase {
|
|
101
|
+
readonly inputs: readonly (InputPort | Group)[]
|
|
102
|
+
readonly name: string
|
|
103
|
+
readonly outputs: readonly (Port | Group)[]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface InlineTaskDefinition extends TaskDefinitionBase {
|
|
107
|
+
readonly capabilities?: readonly TaskCapability[]
|
|
108
|
+
readonly moduleId: string
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The executor shape every managed Task shares; each node family narrows it with its own executor. */
|
|
112
|
+
export interface TaskExecutor {
|
|
113
|
+
readonly kind: string
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface ManagedTaskDefinitionOf<Executor extends TaskExecutor> extends TaskDefinitionBase {
|
|
117
|
+
readonly executor: Executor
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface WorkflowExecutor {
|
|
121
|
+
readonly kind: 'workflow'
|
|
122
|
+
readonly flowId: string
|
|
123
|
+
readonly revisionId: string
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface CodeModule {
|
|
127
|
+
readonly imports: readonly string[]
|
|
128
|
+
readonly name: string
|
|
129
|
+
readonly source: string
|
|
130
|
+
}
|
package/src/flow/common/retry.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import type { GraphNode } from './change.ts'
|
|
2
|
+
import type { RetryConfiguration } from './model.ts'
|
|
2
3
|
|
|
3
4
|
import { z } from 'zod'
|
|
4
5
|
|
|
5
|
-
export
|
|
6
|
-
readonly maxRetries: number
|
|
7
|
-
readonly intervalMs: number
|
|
8
|
-
}
|
|
6
|
+
export type { RetryConfiguration } from './model.ts'
|
|
9
7
|
|
|
10
|
-
export const retryNodeTypes = ['code', 'connector', 'openapi', 'llm', 'decision'] as const
|
|
8
|
+
export const retryNodeTypes = ['code', 'connector', 'openapi', 'http-request', 'llm', 'decision'] as const
|
|
11
9
|
export const nodeRetryLimits = { maxRetries: 5, minIntervalMs: 1000, maxIntervalMs: 60000, intervalStepMs: 1000 } as const
|
|
12
10
|
export const defaultNodeRetry: RetryConfiguration = { maxRetries: 2, intervalMs: 2000 }
|
|
13
11
|
export const retryConfigurationSchema = z.strictObject({
|
|
@@ -1,18 +1,20 @@
|
|
|
1
|
+
import { dequal } from 'dequal/lite'
|
|
1
2
|
import { decisionTaskIssues } from '../../decision/common/decision.ts'
|
|
3
|
+
import { httpRequestIssues, httpAuthHandles, httpRequestTask, httpRequestUrl } from '../../http/common/httpRequest.ts'
|
|
2
4
|
import { openApiIssues, authHandles } from '../../openapi/common/openapi.ts'
|
|
3
|
-
import { nodeWorkflowReferences } from '../../workflow/common/workflow.ts'
|
|
4
5
|
import { isTriggerNode } from './change.ts'
|
|
6
|
+
import { nodeWorkflowReferences } from './workflowReferences.ts'
|
|
5
7
|
export { renderPrompt } from './promptTemplate.ts'
|
|
6
8
|
export { connectionUsage, removeConnectionUsage } from './connectionUsage.ts'
|
|
7
9
|
import { matchesTriggerOutputs, normalizeTriggerOutputs } from '../../trigger/common/contract.ts'
|
|
8
10
|
import { nodeInputMappings } from './condition.ts'
|
|
9
11
|
import { connectionUsage } from './connectionUsage.ts'
|
|
10
12
|
export { matchesTriggerOutputs, normalizeTriggerOutputs, triggerOutputDefinitions, triggerOutputPorts } from '../../trigger/common/contract.ts'
|
|
11
|
-
import type { EngineContract } from '../../
|
|
12
|
-
import type { RuntimeProgram } from '../../
|
|
13
|
+
import type { EngineContract } from '../../runtime/common/engineContract.ts'
|
|
14
|
+
import type { RuntimeProgram } from '../../runtime/common/runtime.ts'
|
|
13
15
|
import type { ConnectorActionCapability, ConnectorCapability, Graph, InputMapping, JsonValue, Port, RevisionContent, SchemaMismatch } from './change.ts'
|
|
14
16
|
|
|
15
|
-
import { findEngineContract } from '../../
|
|
17
|
+
import { findEngineContract } from '../../runtime/common/engineContract.ts'
|
|
16
18
|
import { agentConfigIssues } from './agent.ts'
|
|
17
19
|
import { decodeTaskCapabilities } from './change.ts'
|
|
18
20
|
import { canonicalJsonBytes, canonicalModule, canonicalRevisionGraph, digestBytes } from './encoding.ts'
|
|
@@ -271,13 +273,57 @@ export async function validateFlow(revision: RevisionContent, engine: EngineCont
|
|
|
271
273
|
})
|
|
272
274
|
for (const message of openApiIssues(task))
|
|
273
275
|
checked.diagnostics.push({ code: 'openapi.config-invalid', column: 0, line: 1, message, path: `/document/graph/nodes/${nodeId}/task/executor` })
|
|
274
|
-
if (task.executor.kind == '
|
|
275
|
-
const
|
|
276
|
+
if (task.executor.kind == 'http-request') {
|
|
277
|
+
for (const issue of httpRequestIssues(task.executor))
|
|
278
|
+
checked.diagnostics.push({
|
|
279
|
+
code: 'http-request.config-invalid',
|
|
280
|
+
column: 0,
|
|
281
|
+
line: 1,
|
|
282
|
+
message: issue.message,
|
|
283
|
+
path: `/document/graph/nodes/${nodeId}/task/executor/${issue.field}`,
|
|
284
|
+
})
|
|
285
|
+
if (node.inputs.url?.kind == 'value') {
|
|
286
|
+
try {
|
|
287
|
+
httpRequestUrl(node.inputs.url.value)
|
|
288
|
+
} catch {
|
|
289
|
+
checked.diagnostics.push({
|
|
290
|
+
code: 'http-request.url-invalid',
|
|
291
|
+
column: 0,
|
|
292
|
+
line: 1,
|
|
293
|
+
message: 'Enter an HTTP(S) URL without embedded credentials.',
|
|
294
|
+
path: `/document/graph/nodes/${nodeId}/inputs/url`,
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (task.executor.bodyFormat != 'none' && (node.inputs.body == null || node.inputs.body.kind == 'unset'))
|
|
299
|
+
checked.diagnostics.push({
|
|
300
|
+
code: 'http-request.body-required',
|
|
301
|
+
column: 0,
|
|
302
|
+
line: 1,
|
|
303
|
+
message: 'Set or bind the request body; JSON null is an explicit value.',
|
|
304
|
+
path: `/document/graph/nodes/${nodeId}/inputs/body`,
|
|
305
|
+
})
|
|
306
|
+
const expected = httpRequestTask(task.executor)
|
|
307
|
+
|
|
308
|
+
if (
|
|
309
|
+
!dequal(httpPortContracts(task.inputs), httpPortContracts(expected.inputs)) ||
|
|
310
|
+
!dequal(httpPortContracts(task.outputs), httpPortContracts(expected.outputs))
|
|
311
|
+
)
|
|
312
|
+
checked.diagnostics.push({
|
|
313
|
+
code: 'http-request.ports-invalid',
|
|
314
|
+
column: 0,
|
|
315
|
+
line: 1,
|
|
316
|
+
message: 'HTTP Request ports must match its configuration.',
|
|
317
|
+
path: `/document/graph/nodes/${nodeId}/task`,
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
if (task.executor.kind == 'openapi' || task.executor.kind == 'http-request') {
|
|
321
|
+
const handles = task.executor.kind == 'openapi' ? authHandles(task.executor.auth) : httpAuthHandles(task.executor)
|
|
276
322
|
for (const handle of handles) {
|
|
277
323
|
const mapping = node.inputs[handle]
|
|
278
324
|
if (mapping?.kind != 'sources' || mapping.sources.length == 0)
|
|
279
325
|
checked.diagnostics.push({
|
|
280
|
-
code:
|
|
326
|
+
code: `${task.executor.kind}.auth-source`,
|
|
281
327
|
column: 0,
|
|
282
328
|
line: 1,
|
|
283
329
|
message: 'Authentication requires a deployment variable or upstream output.',
|
|
@@ -385,9 +431,27 @@ function runRevision(revision: RevisionContent, triggerId: string): RevisionCont
|
|
|
385
431
|
}
|
|
386
432
|
}
|
|
387
433
|
|
|
434
|
+
const httpPortContracts = (ports: import('./change.ts').ManagedTaskDefinition['inputs']) =>
|
|
435
|
+
ports
|
|
436
|
+
.filter((port) => 'handle' in port)
|
|
437
|
+
.map(({ handle, jsonSchema, nullable }) => ({ handle, jsonSchema, nullable }))
|
|
438
|
+
.toSorted((a, b) => a.handle.localeCompare(b.handle))
|
|
439
|
+
|
|
388
440
|
export async function prepareFlow(revision: RevisionContent, engineContract: string, triggerId?: string): Promise<PrepareFlowResult> {
|
|
389
441
|
const engine = findEngineContract(engineContract)
|
|
390
442
|
if (engine == null) return { kind: 'engine-unsupported' }
|
|
443
|
+
if (
|
|
444
|
+
!engine.httpRequests &&
|
|
445
|
+
Object.values(revision.document.graph.nodes).some((node) => node.kind == 'task' && 'executor' in node.task && node.task.executor.kind == 'http-request')
|
|
446
|
+
)
|
|
447
|
+
return { kind: 'engine-unsupported' }
|
|
448
|
+
if (
|
|
449
|
+
!engine.codeFetch &&
|
|
450
|
+
Object.values(revision.document.graph.nodes).some(
|
|
451
|
+
(node) => node.kind == 'task' && 'moduleId' in node.task && node.task.capabilities?.some((item) => item.kind == 'http'),
|
|
452
|
+
)
|
|
453
|
+
)
|
|
454
|
+
return { kind: 'engine-unsupported' }
|
|
391
455
|
if (!engine.workflowTools && Object.values(revision.document.graph.nodes).some((node) => nodeWorkflowReferences(node).some((ref) => ref.tool != null)))
|
|
392
456
|
return { kind: 'engine-unsupported' }
|
|
393
457
|
if (triggerId != null) {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type { InputPort, JsonValue, Port, PortDefinition } from './
|
|
1
|
+
import type { InputPort, JsonValue, Port, PortDefinition } from './model.ts'
|
|
2
|
+
|
|
3
|
+
import { httpHeaderNameIssue } from '../../http/common/headers.ts'
|
|
2
4
|
|
|
3
5
|
export const webhookResponseTimeout = {
|
|
4
6
|
defaultMs: 30_000,
|
|
@@ -30,12 +32,6 @@ export function workflowOutputsPort(outputs: readonly Port[]): PortDefinition {
|
|
|
30
32
|
} as const
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
/** Header names are case-insensitive; never silently combine duplicate business fields. */
|
|
34
|
-
export function webhookHeaderNameIssue(name: string, names: readonly string[]): 'invalid' | 'duplicate' | undefined {
|
|
35
|
-
if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name)) return 'invalid'
|
|
36
|
-
if (names.some((candidate) => candidate.toLowerCase() === name.toLowerCase())) return 'duplicate'
|
|
37
|
-
}
|
|
38
|
-
|
|
39
35
|
/** Fixed inputs belong to the node, not author-supplied definitions. */
|
|
40
36
|
export const webhookResponseInputs: readonly InputPort[] = [
|
|
41
37
|
{ handle: 'body', jsonSchema: {}, nullable: true, value: '' },
|
|
@@ -60,7 +56,7 @@ export function webhookHeaderValues(source: unknown): Readonly<Record<string, st
|
|
|
60
56
|
const headers: Record<string, string> = {}
|
|
61
57
|
const names: string[] = []
|
|
62
58
|
for (const [name, value] of Object.entries(source)) {
|
|
63
|
-
if (
|
|
59
|
+
if (httpHeaderNameIssue(name, names) != null) return
|
|
64
60
|
if (typeof value !== 'string' && typeof value !== 'boolean' && !(typeof value === 'number' && Number.isFinite(value))) return
|
|
65
61
|
const text = String(value)
|
|
66
62
|
if ([...text].some((character) => character.charCodeAt(0) > 255 || /[\0\r\n]/.test(character))) return
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { GraphNode } from './change.ts'
|
|
2
|
+
import type { WorkflowCapability } from './model.ts'
|
|
3
|
+
|
|
4
|
+
/** Workflow declarations share discovery across authoring, admission and resource indexing. */
|
|
5
|
+
export function nodeWorkflowReferences(node: GraphNode): readonly {
|
|
6
|
+
readonly target: Omit<WorkflowCapability, 'kind' | 'name'>
|
|
7
|
+
readonly path: string
|
|
8
|
+
readonly tool?: string
|
|
9
|
+
}[] {
|
|
10
|
+
if (node.kind != 'task') return []
|
|
11
|
+
if ('moduleId' in node.task)
|
|
12
|
+
return (node.task.capabilities ?? []).flatMap((item, index) =>
|
|
13
|
+
item.kind == 'workflow' ? [{ target: item, path: `task/capabilities/${index}`, tool: item.name }] : [],
|
|
14
|
+
)
|
|
15
|
+
if (node.task.executor.kind == 'workflow')
|
|
16
|
+
return [
|
|
17
|
+
{
|
|
18
|
+
target: {
|
|
19
|
+
...node.task.executor,
|
|
20
|
+
inputs: node.task.inputs.filter((port) => 'handle' in port),
|
|
21
|
+
outputs: node.task.outputs.filter((port) => 'handle' in port),
|
|
22
|
+
},
|
|
23
|
+
path: 'task/executor',
|
|
24
|
+
},
|
|
25
|
+
]
|
|
26
|
+
if (node.task.executor.kind == 'agent')
|
|
27
|
+
return node.task.executor.tools.flatMap((item, index) =>
|
|
28
|
+
item.kind == 'workflow' ? [{ target: item, path: `task/executor/tools/${index}`, tool: item.name }] : [],
|
|
29
|
+
)
|
|
30
|
+
return []
|
|
31
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { InputMapping } from '../../flow/common/model.ts'
|
|
2
|
+
|
|
3
|
+
/** Never allow credentials to enter a Revision as literal inputs of an OpenAPI or HTTP request node. */
|
|
4
|
+
export function assertHttpAuthBindings(inputs: Readonly<Record<string, InputMapping>>): void {
|
|
5
|
+
for (const [handle, mapping] of Object.entries(inputs)) {
|
|
6
|
+
if (handle.startsWith('auth.') && mapping.kind == 'value')
|
|
7
|
+
throw new TypeError('HTTP authentication accepts deployment variables and upstream outputs only.')
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Header names are case-insensitive; never silently combine duplicate business fields. */
|
|
2
|
+
export function httpHeaderNameIssue(name: string, names: readonly string[]): 'invalid' | 'duplicate' | undefined {
|
|
3
|
+
if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name)) return 'invalid'
|
|
4
|
+
if (names.some((candidate) => candidate.toLowerCase() === name.toLowerCase())) return 'duplicate'
|
|
5
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Hosts of a Code `http` Capability: lowercase DNS names or IPv4 addresses; `*.example.com` matches subdomains only. */
|
|
2
|
+
export const httpHostPattern = /^(?:\*\.)?(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/
|
|
3
|
+
export const maxHttpHosts = 20
|
|
4
|
+
|
|
5
|
+
/** Wildcards must name a registrable suffix, so `*.com` is rejected. */
|
|
6
|
+
export function isHttpHost(value: string): boolean {
|
|
7
|
+
return httpHostPattern.test(value) && (!value.startsWith('*.') || value.slice(2).includes('.'))
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function httpHostAllowed(hosts: readonly string[], hostname: string): boolean {
|
|
11
|
+
const host = hostname.toLowerCase()
|
|
12
|
+
return hosts.some((pattern) => (pattern.startsWith('*.') ? host.endsWith(pattern.slice(1)) : host == pattern))
|
|
13
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import type { InputMapping, InputPort, JsonValue, ManagedTaskDefinitionOf, TaskDefinitionBase, TaskExecutor } from '../../flow/common/model.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
|
+
export { httpHostAllowed, isHttpHost, maxHttpHosts } from './hosts.ts'
|
|
9
|
+
|
|
10
|
+
const parameterSchema = z.strictObject({ mode: z.enum(['fields', 'object']).default('fields'), names: z.array(z.string()).default([]) })
|
|
11
|
+
export const httpRequestConfigSchema = z.strictObject({
|
|
12
|
+
method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']).default('GET'),
|
|
13
|
+
query: parameterSchema.default({ mode: 'fields', names: [] }),
|
|
14
|
+
headers: parameterSchema.default({ mode: 'fields', names: [] }),
|
|
15
|
+
bodyFormat: z.enum(['none', 'json', 'text', 'form']).default('none'),
|
|
16
|
+
authentication: z
|
|
17
|
+
.union([
|
|
18
|
+
z.strictObject({ type: z.literal('none') }),
|
|
19
|
+
z.strictObject({ type: z.enum(['bearer', 'basic']) }),
|
|
20
|
+
z.strictObject({ type: z.literal('apiKey'), name: z.string(), in: z.enum(['header', 'query']) }),
|
|
21
|
+
])
|
|
22
|
+
.default({ type: 'none' }),
|
|
23
|
+
responseFormat: z.enum(['auto', 'json', 'text']).default('auto'),
|
|
24
|
+
statusPolicy: z.enum(['success', 'all']).default('success'),
|
|
25
|
+
})
|
|
26
|
+
export type HttpRequestConfig = z.infer<typeof httpRequestConfigSchema>
|
|
27
|
+
export interface HttpRequestExecutor extends HttpRequestConfig {
|
|
28
|
+
readonly kind: 'http-request'
|
|
29
|
+
}
|
|
30
|
+
export const httpRequestExecutorSchema = httpRequestConfigSchema.extend({ kind: z.literal('http-request') })
|
|
31
|
+
const scalarSchema = { type: ['string', 'number', 'boolean'] }
|
|
32
|
+
const queryValueSchema = { anyOf: [scalarSchema, { type: 'array', items: scalarSchema }] }
|
|
33
|
+
export const httpHeaderSchema = { type: 'object', additionalProperties: { type: 'string' } }
|
|
34
|
+
export const httpQuerySchema = { type: 'object', additionalProperties: queryValueSchema }
|
|
35
|
+
|
|
36
|
+
export function httpAuthHandles(config: HttpRequestConfig): string[] {
|
|
37
|
+
return config.authentication.type == 'none' ? [] : config.authentication.type == 'basic' ? ['auth.username', 'auth.password'] : ['auth.token']
|
|
38
|
+
}
|
|
39
|
+
// Preserve metadata on ports whose contracts did not change.
|
|
40
|
+
function retain(ports: readonly InputPort[], old: TaskDefinitionBase['inputs']): TaskDefinitionBase['inputs'] {
|
|
41
|
+
const remaining = new Map(ports.map((port) => [port.handle, port]))
|
|
42
|
+
return [
|
|
43
|
+
...old.flatMap((item): TaskDefinitionBase['inputs'] => {
|
|
44
|
+
if ('group' in item) return [item]
|
|
45
|
+
const next = remaining.get(item.handle)
|
|
46
|
+
if (next == null) return []
|
|
47
|
+
remaining.delete(item.handle)
|
|
48
|
+
return [dequal(item.jsonSchema, next.jsonSchema) && item.nullable == next.nullable ? item : next]
|
|
49
|
+
}),
|
|
50
|
+
...remaining.values(),
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
export function httpRequestTask(
|
|
54
|
+
config: HttpRequestConfig = httpRequestConfigSchema.parse({}),
|
|
55
|
+
name = 'HTTP Request',
|
|
56
|
+
previous?: TaskDefinitionBase,
|
|
57
|
+
): ManagedTaskDefinitionOf<HttpRequestExecutor> {
|
|
58
|
+
const inputs: InputPort[] = [{ handle: 'url', jsonSchema: { type: 'string' }, nullable: false }]
|
|
59
|
+
for (const group of ['query', 'headers'] as const) {
|
|
60
|
+
if (config[group].mode == 'object') inputs.push({ handle: group, jsonSchema: group == 'query' ? httpQuerySchema : httpHeaderSchema, nullable: true })
|
|
61
|
+
else
|
|
62
|
+
for (const parameterName of config[group].names.filter((value) => value.trim()))
|
|
63
|
+
inputs.push({
|
|
64
|
+
handle: `${group == 'query' ? 'query' : 'header'}.${parameterName}`,
|
|
65
|
+
jsonSchema: group == 'query' ? queryValueSchema : { type: 'string' },
|
|
66
|
+
nullable: true,
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
if (config.bodyFormat != 'none')
|
|
70
|
+
inputs.push({
|
|
71
|
+
handle: 'body',
|
|
72
|
+
jsonSchema: config.bodyFormat == 'json' ? {} : config.bodyFormat == 'text' ? { type: 'string' } : httpQuerySchema,
|
|
73
|
+
nullable: config.bodyFormat == 'json',
|
|
74
|
+
})
|
|
75
|
+
inputs.push(...httpAuthHandles(config).map((handle) => ({ handle, jsonSchema: { type: 'string' }, nullable: false })))
|
|
76
|
+
const outputs: InputPort[] = [
|
|
77
|
+
{ handle: 'body', jsonSchema: config.responseFormat == 'text' ? { type: 'string' } : {}, nullable: true },
|
|
78
|
+
{ handle: 'statusCode', jsonSchema: { type: 'integer' }, nullable: false },
|
|
79
|
+
{ handle: 'headers', jsonSchema: httpHeaderSchema, nullable: false },
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
name,
|
|
84
|
+
executor: { ...config, kind: 'http-request' },
|
|
85
|
+
inputs: retain(inputs, previous?.inputs ?? []),
|
|
86
|
+
outputs: retain(outputs, previous?.outputs ?? []),
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export interface HttpRequestIssue {
|
|
90
|
+
field: string
|
|
91
|
+
index?: number
|
|
92
|
+
code: 'bodyUnsupported' | 'parameterNamesInvalid' | 'headerNameInvalid' | 'keyNameRequired' | 'authConflict'
|
|
93
|
+
message: string
|
|
94
|
+
}
|
|
95
|
+
export function httpRequestIssues(config: HttpRequestConfig): HttpRequestIssue[] {
|
|
96
|
+
const issues: HttpRequestIssue[] = []
|
|
97
|
+
if (['GET', 'HEAD'].includes(config.method) && config.bodyFormat != 'none')
|
|
98
|
+
issues.push({ field: 'bodyFormat', code: 'bodyUnsupported', message: 'GET and HEAD do not support a request body.' })
|
|
99
|
+
for (const group of ['query', 'headers'] as const) {
|
|
100
|
+
if (config[group].mode != 'fields') continue
|
|
101
|
+
const seen = new Set<string>()
|
|
102
|
+
for (const [index, name] of config[group].names.entries()) {
|
|
103
|
+
const key = group == 'headers' ? name.toLowerCase() : name
|
|
104
|
+
if (!name.trim() || seen.has(key))
|
|
105
|
+
issues.push({ field: group, index, code: 'parameterNamesInvalid', message: 'Parameter names must be nonempty and unique.' })
|
|
106
|
+
seen.add(key)
|
|
107
|
+
if (group == 'headers' && httpHeaderNameIssue(name, []) === 'invalid')
|
|
108
|
+
issues.push({ field: group, index, code: 'headerNameInvalid', message: 'Invalid HTTP header name.' })
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const auth = config.authentication
|
|
112
|
+
if (auth.type != 'none') {
|
|
113
|
+
const group = auth.type == 'apiKey' && auth.in == 'query' ? 'query' : 'headers'
|
|
114
|
+
const name = auth.type == 'apiKey' ? auth.name : 'authorization'
|
|
115
|
+
if (!name.trim()) issues.push({ field: 'authentication', code: 'keyNameRequired', message: 'API Key requires a name.' })
|
|
116
|
+
if (group == 'headers' && name.trim() && httpHeaderNameIssue(name, []) === 'invalid')
|
|
117
|
+
issues.push({ field: 'authentication', code: 'headerNameInvalid', message: 'Invalid authentication header name.' })
|
|
118
|
+
if (config[group].mode == 'fields' && config[group].names.some((item) => (group == 'headers' ? item.toLowerCase() == name.toLowerCase() : item == name)))
|
|
119
|
+
issues.push({ field: 'authentication', code: 'authConflict', message: 'Authentication conflicts with a request parameter.' })
|
|
120
|
+
}
|
|
121
|
+
return issues
|
|
122
|
+
}
|
|
123
|
+
function entries(value: JsonValue | undefined): [string, JsonValue][] {
|
|
124
|
+
if (value == null) return []
|
|
125
|
+
if (typeof value != 'object' || Array.isArray(value)) throw new Error('Expected a parameter object.')
|
|
126
|
+
return Object.entries(value)
|
|
127
|
+
}
|
|
128
|
+
function scalar(value: JsonValue): string {
|
|
129
|
+
if (typeof value != 'string' && typeof value != 'number' && typeof value != 'boolean') throw new Error('Expected a scalar parameter.')
|
|
130
|
+
return String(value)
|
|
131
|
+
}
|
|
132
|
+
function append(query: URLSearchParams, name: string, value: JsonValue): void {
|
|
133
|
+
for (const item of Array.isArray(value) ? value : [value]) query.append(name, scalar(item))
|
|
134
|
+
}
|
|
135
|
+
export function httpRequestUrl(value: unknown): URL {
|
|
136
|
+
if (typeof value != 'string' || !value.trim()) throw new Error('A URL is required.')
|
|
137
|
+
const url = new URL(value)
|
|
138
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) throw new Error('An HTTP(S) URL without credentials is required.')
|
|
139
|
+
return url
|
|
140
|
+
}
|
|
141
|
+
export function buildHttpRequest(config: HttpRequestConfig, input: Readonly<Record<string, JsonValue>>): { url: string; init: RequestInit } {
|
|
142
|
+
if (httpRequestIssues(config).length) throw new Error('Invalid HTTP request configuration.')
|
|
143
|
+
const url = httpRequestUrl(input.url)
|
|
144
|
+
const parameters = (group: 'query' | 'headers') =>
|
|
145
|
+
config[group].mode == 'object'
|
|
146
|
+
? entries(input[group])
|
|
147
|
+
: config[group].names.flatMap((name): [string, JsonValue][] => {
|
|
148
|
+
const value = input[`${group == 'query' ? 'query' : 'header'}.${name}`]
|
|
149
|
+
return value == null ? [] : [[name, value]]
|
|
150
|
+
})
|
|
151
|
+
for (const [name, value] of parameters('query')) {
|
|
152
|
+
url.searchParams.delete(name)
|
|
153
|
+
append(url.searchParams, name, value)
|
|
154
|
+
}
|
|
155
|
+
const headers = new Headers()
|
|
156
|
+
for (const [name, value] of parameters('headers')) {
|
|
157
|
+
if (headers.has(name)) throw new Error('Duplicate HTTP header.')
|
|
158
|
+
if (typeof value != 'string') throw new Error('HTTP headers require text values.')
|
|
159
|
+
headers.set(name, value)
|
|
160
|
+
}
|
|
161
|
+
const auth = config.authentication
|
|
162
|
+
const credential = (part: string) => {
|
|
163
|
+
const value = input[`auth.${part}`]
|
|
164
|
+
if (typeof value != 'string' || !value) throw new Error('Authentication source is required.')
|
|
165
|
+
return value
|
|
166
|
+
}
|
|
167
|
+
if (auth.type != 'none') {
|
|
168
|
+
const name = auth.type == 'apiKey' ? auth.name : 'authorization'
|
|
169
|
+
if (auth.type == 'apiKey' && auth.in == 'query') {
|
|
170
|
+
if (url.searchParams.has(name)) throw new Error('Authentication conflicts with a query parameter.')
|
|
171
|
+
url.searchParams.set(name, credential('token'))
|
|
172
|
+
} else {
|
|
173
|
+
if (headers.has(name)) throw new Error('Authentication conflicts with a header.')
|
|
174
|
+
if (auth.type == 'basic') {
|
|
175
|
+
const username = credential('username')
|
|
176
|
+
if (username.includes(':')) throw new Error('Basic username cannot contain a colon.')
|
|
177
|
+
const bytes = new TextEncoder().encode(`${username}:${credential('password')}`)
|
|
178
|
+
headers.set(name, `Basic ${btoa(Array.from(bytes, (byte) => String.fromCharCode(byte)).join(''))}`)
|
|
179
|
+
} else headers.set(name, auth.type == 'bearer' ? `Bearer ${credential('token')}` : credential('token'))
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
let body: string | undefined
|
|
183
|
+
if (config.bodyFormat != 'none') {
|
|
184
|
+
if (!Object.hasOwn(input, 'body')) throw new Error('Request body is unset.')
|
|
185
|
+
let contentType: string
|
|
186
|
+
if (config.bodyFormat == 'json') {
|
|
187
|
+
body = JSON.stringify(input.body)
|
|
188
|
+
contentType = 'application/json'
|
|
189
|
+
} else if (config.bodyFormat == 'text') {
|
|
190
|
+
if (typeof input.body != 'string') throw new Error('Text body requires a string.')
|
|
191
|
+
body = input.body
|
|
192
|
+
contentType = 'text/plain'
|
|
193
|
+
} else {
|
|
194
|
+
if (input.body == null) throw new Error('Form body requires an object.')
|
|
195
|
+
const form = new URLSearchParams()
|
|
196
|
+
for (const [name, value] of entries(input.body)) append(form, name, value)
|
|
197
|
+
body = form.toString()
|
|
198
|
+
contentType = 'application/x-www-form-urlencoded'
|
|
199
|
+
}
|
|
200
|
+
const supplied = headers.get('content-type')
|
|
201
|
+
if (config.bodyFormat != 'text' && supplied != null && supplied.split(';')[0]!.trim().toLowerCase() != contentType)
|
|
202
|
+
throw new Error('Content-Type conflicts with body format.')
|
|
203
|
+
if (supplied == null) headers.set('content-type', contentType)
|
|
204
|
+
}
|
|
205
|
+
return { url: url.href, init: { method: config.method, headers, body, redirect: 'manual' } }
|
|
206
|
+
}
|
|
207
|
+
export function parseHttpResponse(config: HttpRequestConfig, status: number, headers: Headers, text: string): Record<string, JsonValue> {
|
|
208
|
+
if (config.statusPolicy == 'success' && (status < 200 || status >= 300)) throw new Error(`HTTP ${status}`)
|
|
209
|
+
let body: JsonValue = null
|
|
210
|
+
if (text && config.method != 'HEAD' && ![204, 205, 304].includes(status)) {
|
|
211
|
+
const media = (headers.get('content-type') ?? '').split(';')[0]!.trim().toLowerCase()
|
|
212
|
+
body =
|
|
213
|
+
config.responseFormat == 'json' || (config.responseFormat == 'auto' && /^application\/(json|.+\+json)$/.test(media))
|
|
214
|
+
? (JSON.parse(text) as JsonValue)
|
|
215
|
+
: text
|
|
216
|
+
}
|
|
217
|
+
return { body, statusCode: status, headers: Object.fromEntries([...headers].filter(([name]) => name != 'set-cookie')) }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Only a named parameter rename carries a value; changing source modes is not a rename. */
|
|
221
|
+
export function httpRequestInputs(
|
|
222
|
+
mappings: Readonly<Record<string, InputMapping>>,
|
|
223
|
+
previous: TaskDefinitionBase,
|
|
224
|
+
next: TaskDefinitionBase,
|
|
225
|
+
): Record<string, InputMapping> {
|
|
226
|
+
const candidate = renamedPort(previous.inputs, next.inputs)
|
|
227
|
+
const rename = candidate != null && ['query.', 'header.'].some((prefix) => candidate.every((name) => name.startsWith(prefix))) ? candidate : undefined
|
|
228
|
+
return remapInputMappings(mappings, next.inputs, rename)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** A Task node as HTTP helpers read it; inline Tasks have no executor. */
|
|
232
|
+
interface HttpTaskNode {
|
|
233
|
+
readonly inputs: Readonly<Record<string, InputMapping>>
|
|
234
|
+
readonly task: TaskDefinitionBase & { readonly executor?: TaskExecutor }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function httpRequestExecutor(node: HttpTaskNode): HttpRequestExecutor | undefined {
|
|
238
|
+
const executor = node.task.executor
|
|
239
|
+
return executor?.kind === 'http-request' ? (executor as HttpRequestExecutor) : undefined
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** The object editor assembles named inputs without changing unrelated request settings. */
|
|
243
|
+
export function withHttpParameterFields<Node extends HttpTaskNode>(node: Node, group: 'query' | 'headers', values: Record<string, JsonValue>): Node {
|
|
244
|
+
const config = httpRequestExecutor(node)
|
|
245
|
+
if (config == null) return node
|
|
246
|
+
const task = httpRequestTask({ ...config, [group]: { mode: 'fields', names: Object.keys(values) } }, node.task.name, node.task)
|
|
247
|
+
const inputs = httpRequestInputs(node.inputs, node.task, task)
|
|
248
|
+
const rename = renamedPort(node.task.inputs, task.inputs)
|
|
249
|
+
for (const [name, value] of Object.entries(values)) {
|
|
250
|
+
const handle = `${group === 'query' ? 'query' : 'header'}.${name}`
|
|
251
|
+
const existing = config[group].mode === 'fields' && (config[group].names.includes(name) || rename?.[1] === handle)
|
|
252
|
+
if (inputs[handle] == null && !existing) inputs[handle] = { kind: 'value', value }
|
|
253
|
+
}
|
|
254
|
+
// Only the HTTP Task and its input mappings change, so the node keeps its own type.
|
|
255
|
+
return { ...node, task, inputs } as Node
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function withHttpParameterSource<Node extends HttpTaskNode>(node: Node, group: 'query' | 'headers', mapping?: InputMapping): Node {
|
|
259
|
+
const config = httpRequestExecutor(node)
|
|
260
|
+
if (config == null) return node
|
|
261
|
+
const task = httpRequestTask({ ...config, [group]: { mode: 'object', names: [] } }, node.task.name, node.task)
|
|
262
|
+
const inputs = httpRequestInputs(node.inputs, node.task, task)
|
|
263
|
+
if (mapping == null) delete inputs[group]
|
|
264
|
+
else inputs[group] = mapping
|
|
265
|
+
return { ...node, task, inputs } as Node
|
|
266
|
+
}
|