@oomol-lab/open-flow 0.1.0-beta.84 → 0.1.0-beta.86
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/dist/browser/{FlowCanvas-B2g7NceZ.js → FlowCanvas-CHIaMaBv.js} +112 -16
- package/dist/browser/{createResourceDialog-Bi4fUnbD.js → createResourceDialog-C0E5yPe6.js} +3 -3
- package/dist/browser/{dialog-BzoSxhXQ.js → dialog-b3hsdu8i.js} +291 -143
- package/dist/browser/{exportPng-DLdtJvg3.js → exportPng-DGHcejgy.js} +2 -2
- package/dist/browser/{flowChanges-C567g-Pc.js → flowChanges-BvSWezEb.js} +375 -83
- package/dist/browser/{flowWorkspace-DBk4A-Ld.js → flowWorkspace-BKnMRClO.js} +1619 -475
- package/dist/browser/fr-CNTSedYU.js +593 -0
- package/dist/browser/ja-CxQBHr2J.js +593 -0
- package/dist/browser/ko-D1Exiv1h.js +593 -0
- package/dist/browser/licenses.md +1 -1
- package/dist/browser/{markdownContent-DIuJThGK.js → markdownContent-BR9-1ikU.js} +1 -1
- package/dist/browser/preview.css +181 -181
- package/dist/browser/preview.js +2103 -636
- package/dist/browser/ru-BzLZv0B7.js +593 -0
- package/dist/browser/{tooltip-DGj5pudg.js → tooltip-DSBF3bRw.js} +55 -23
- package/dist/browser/workbench.css +181 -181
- package/dist/browser/workbench.js +86 -17
- package/dist/browser/{workbenchSelect-BIaEVTx6.js → workbenchSelect-Di1Bm_-d.js} +2 -2
- package/dist/browser/{wrapLayout-C4moMKgb.js → wrapLayout-B0AT8_-i.js} +1 -1
- package/dist/browser/zh-CN-Dg_xSGde.js +593 -0
- package/dist/browser/zh-TW-oxuLwsV0.js +593 -0
- package/dist/command/index.js +61 -57
- package/dist/command/licenses.md +28 -4
- package/dist/common/authoring.d.ts +5 -5
- package/dist/common/authoringExamples.d.ts +3 -2
- package/dist/common/authoringSchema.d.ts +41 -4
- package/dist/common/catalogCache.d.ts +46 -0
- package/dist/common/connector-action.js +13 -10
- package/dist/common/connector-proxy.d.ts +96 -0
- package/dist/common/connector-proxy.js +5209 -0
- package/dist/common/control-api-conformance.js +312 -46
- package/dist/common/control-api.d.ts +12 -1
- package/dist/common/control-api.js +303 -41
- package/dist/common/control-requests.d.ts +4 -3
- package/dist/common/control-requests.js +14111 -171
- package/dist/common/engine-contract.d.ts +3 -2
- package/dist/common/event-source.js +55 -23
- package/dist/common/flow-authoring-node.d.ts +2 -0
- package/dist/common/flow-authoring.js +6936 -6781
- package/dist/common/flow-change-schema.d.ts +1 -1
- package/dist/common/flow-change.d.ts +2 -2
- package/dist/common/flow-change.js +91 -36
- package/dist/common/flow-connectionUsage.d.ts +1 -1
- package/dist/common/flow-encoding.js +93 -38
- package/dist/common/flow-requestParameters.d.ts +34 -0
- package/dist/common/flow-semantics.js +9429 -9236
- package/dist/common/flow-state.js +55 -23
- package/dist/common/flow-workflowFile.d.ts +3 -1
- package/dist/common/http-request.d.ts +6 -11
- package/dist/common/http-request.js +192 -141
- package/dist/common/licenses.md +1 -1
- package/dist/common/mcp.d.ts +3 -3
- package/dist/common/mcp.js +303 -41
- package/dist/common/run-events.js +1 -0
- package/dist/common/runtime-contract.js +10 -4
- package/dist/common/runtime-nodejs-compat.js +55 -23
- package/dist/common/scheduler-checkpoint.d.ts +3 -3
- package/dist/common/scheduler-dependencies.d.ts +22 -28
- package/dist/common/scheduler-node-execution.d.ts +1 -0
- package/dist/common/scheduler.d.ts +8 -2
- package/dist/common/scheduler.js +274 -95
- package/dist/common/workflow.js +51 -19
- package/dist/common/workflowAuthoring.d.ts +411 -0
- package/dist/render/browser/licenses.md +1 -1
- package/dist/render/browser/runtime.js +289 -61
- package/dist/render/node/index.js +85 -30
- package/dist/render/node/licenses.md +1 -1
- package/package.json +1 -1
- package/skills/open-flow/references/flow-authoring.md +14 -13
- package/src/base/common/jsonSchema.ts +11 -0
- package/src/connector/common/actionSchema.ts +4 -11
- package/src/connector/common/proxy.ts +152 -0
- package/src/control/common/api.ts +99 -3
- package/src/control/common/authoring.ts +23 -9
- package/src/control/common/authoringExamples.ts +9 -0
- package/src/control/common/authoringSchema.ts +21 -12
- package/src/control/common/catalogCache.ts +145 -0
- package/src/control/common/conformance.ts +9 -3
- package/src/control/common/connectorQueries.ts +17 -2
- package/src/control/common/requests.ts +1 -0
- package/src/control/common/workflowAuthoring.ts +120 -0
- package/src/execution/common/checkpoint.ts +18 -12
- package/src/execution/common/dependencies.ts +132 -15
- package/src/execution/common/events.ts +3 -0
- package/src/execution/common/node-execution.ts +22 -15
- package/src/execution/common/scheduler.ts +20 -31
- package/src/flow/common/change.ts +2 -1
- package/src/flow/common/changeSchema.ts +11 -1
- package/src/flow/common/connectionUsage.ts +4 -2
- package/src/flow/common/errorHandling.ts +3 -1
- package/src/flow/common/nodeChanges.ts +35 -18
- package/src/flow/common/requestParameters.ts +120 -0
- package/src/flow/common/semantics.ts +51 -1
- package/src/flow/common/sourceField.ts +3 -2
- package/src/flow/common/workflowFile.ts +2 -2
- package/src/http/common/httpRequest.ts +28 -78
- package/src/runtime/common/engineContract.ts +6 -3
|
@@ -166,6 +166,7 @@ export interface ConditionNode extends GraphNodeBase {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
export type ManagedTaskExecutor =
|
|
169
|
+
| import('../../connector/common/proxy.ts').ConnectorProxyExecutor
|
|
169
170
|
| import('../../http/common/httpRequest.ts').HttpRequestExecutor
|
|
170
171
|
| import('../../state/common/state.ts').SkipDuplicatesExecutor
|
|
171
172
|
| WorkflowExecutor
|
|
@@ -398,7 +399,7 @@ export interface FlowDocument {
|
|
|
398
399
|
|
|
399
400
|
export interface RevisionContent {
|
|
400
401
|
readonly document: FlowDocument
|
|
401
|
-
readonly modelVersion: 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | typeof currentFlowModelVersion
|
|
402
|
+
readonly modelVersion: 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | typeof currentFlowModelVersion
|
|
402
403
|
readonly modules: Readonly<Record<string, CodeModule>>
|
|
403
404
|
}
|
|
404
405
|
|
|
@@ -2,6 +2,7 @@ import type { ChangeOperation, FlowDocument, JsonValue, RevisionContent } from '
|
|
|
2
2
|
|
|
3
3
|
import { dequal } from 'dequal/lite'
|
|
4
4
|
import { z } from 'zod'
|
|
5
|
+
import { connectorProxyExecutorSchema } from '../../connector/common/proxy.ts'
|
|
5
6
|
import { limitDecisionTask } from '../../decision/common/decision.ts'
|
|
6
7
|
import { assertHttpAuthBindings } from '../../http/common/authBindings.ts'
|
|
7
8
|
import { isHttpHost, maxHttpHosts } from '../../http/common/hosts.ts'
|
|
@@ -14,7 +15,7 @@ import { waitAutoContinueDraftSchema, waitAutoContinueSchema } from './wait.ts'
|
|
|
14
15
|
import { webhookMethods } from './webhookMethod.ts'
|
|
15
16
|
import { webhookResponseTimeout } from './webhookResponse.ts'
|
|
16
17
|
|
|
17
|
-
export const currentFlowModelVersion =
|
|
18
|
+
export const currentFlowModelVersion = 14
|
|
18
19
|
const text = z.string()
|
|
19
20
|
const actionConnection = text.describe(
|
|
20
21
|
'Account selection: omit while unconfigured; use an account ID to select it; use "" to explicitly defer configuration. Deferred Actions fail if called; deferred Agent tools are excluded from the callable tool set. Not supported by Triggers.',
|
|
@@ -131,6 +132,7 @@ const managedFields = z.object({
|
|
|
131
132
|
z.strictObject({ kind: z.literal('workflow'), flowId: text, revisionId: text }),
|
|
132
133
|
httpRequestExecutorSchema,
|
|
133
134
|
skipDuplicatesExecutorSchema,
|
|
135
|
+
connectorProxyExecutorSchema,
|
|
134
136
|
]),
|
|
135
137
|
})
|
|
136
138
|
|
|
@@ -321,6 +323,7 @@ const revision = z.object({
|
|
|
321
323
|
z.literal(10),
|
|
322
324
|
z.literal(11),
|
|
323
325
|
z.literal(12),
|
|
326
|
+
z.literal(13),
|
|
324
327
|
z.literal(currentFlowModelVersion),
|
|
325
328
|
]),
|
|
326
329
|
document,
|
|
@@ -726,6 +729,13 @@ export function changeOperationsSchema(kind?: string): JsonValue {
|
|
|
726
729
|
}
|
|
727
730
|
|
|
728
731
|
function checkModelVersion(content: RevisionContent): void {
|
|
732
|
+
if (
|
|
733
|
+
content.modelVersion < 14 &&
|
|
734
|
+
Object.values(content.document.graph.nodes).some(
|
|
735
|
+
(candidateNode) => candidateNode.kind === 'task' && 'executor' in candidateNode.task && candidateNode.task.executor.kind === 'connector-proxy',
|
|
736
|
+
)
|
|
737
|
+
)
|
|
738
|
+
throw new TypeError('Custom API Request requires Flow model v14.')
|
|
729
739
|
if (
|
|
730
740
|
content.modelVersion < 13 &&
|
|
731
741
|
Object.values(content.document.graph.nodes).some(
|
|
@@ -8,7 +8,7 @@ export interface ConnectionUsage {
|
|
|
8
8
|
readonly nodeId: string
|
|
9
9
|
readonly name: string
|
|
10
10
|
|
|
11
|
-
readonly kind: 'connector' | 'agent' | 'trigger' | 'notification' | 'code'
|
|
11
|
+
readonly kind: 'connector' | 'connector-proxy' | 'agent' | 'trigger' | 'notification' | 'code'
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export function connectionUsage(document: FlowDocument): readonly ConnectionUsage[] {
|
|
@@ -31,6 +31,8 @@ export function connectionUsage(document: FlowDocument): readonly ConnectionUsag
|
|
|
31
31
|
})
|
|
32
32
|
}
|
|
33
33
|
if (task?.executor.kind == 'connector') add('connector', task.executor.action, task.executor.connectionId)
|
|
34
|
+
if (task?.executor.kind == 'connector-proxy')
|
|
35
|
+
uses.push({ kind: 'connector-proxy', providerId: task.executor.providerId, connectionId: task.executor.connectionId, nodeId, name })
|
|
34
36
|
if (node.kind == 'task' && 'moduleId' in node.task) {
|
|
35
37
|
for (const capability of node.task.capabilities ?? []) {
|
|
36
38
|
if ('mode' in capability && capability.mode == 'independent') for (const action of capability.actions) add('code', action.action, action.connectionId)
|
|
@@ -74,7 +76,7 @@ export function clearNodeConnections(graph: FlowDocument['graph'], connectionId?
|
|
|
74
76
|
const { connectionId: _, ...rest } = value
|
|
75
77
|
return rest as T
|
|
76
78
|
}
|
|
77
|
-
if (executor.kind == 'connector') return [id, { ...node, task: { ...node.task, executor: clear(executor) } }]
|
|
79
|
+
if (executor.kind == 'connector' || executor.kind == 'connector-proxy') return [id, { ...node, task: { ...node.task, executor: clear(executor) } }]
|
|
78
80
|
if (executor.kind == 'agent')
|
|
79
81
|
return [
|
|
80
82
|
id,
|
|
@@ -26,7 +26,9 @@ export const nodeErrorSchema = {
|
|
|
26
26
|
} as const
|
|
27
27
|
|
|
28
28
|
export function supportsNodeErrorHandling(node: GraphNode): boolean {
|
|
29
|
-
return
|
|
29
|
+
return (
|
|
30
|
+
supportsNodeRetry(node) || (node.kind === 'task' && 'executor' in node.task && ['skip-duplicates', 'connector-proxy'].includes(node.task.executor.kind))
|
|
31
|
+
)
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export function nodeSystemOutputPorts(node: GraphNode): Readonly<Record<string, PortDefinition>> {
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
} from './change.ts'
|
|
23
23
|
|
|
24
24
|
import { dequal } from 'dequal/lite'
|
|
25
|
+
import { connectorProxyHasBody, connectorProxyTask } from '../../connector/common/proxy.ts'
|
|
25
26
|
import { decisionTask } from '../../decision/common/decision.ts'
|
|
26
27
|
import { isTriggerNode, applyFlowChanges, nextNodeName, normalizeNodeName } from './change.ts'
|
|
27
28
|
import { codeSharedPermissionsEnabled } from './codePermissions.ts'
|
|
@@ -401,13 +402,37 @@ export function setInputValues(
|
|
|
401
402
|
content: RevisionContent,
|
|
402
403
|
nodeId: string,
|
|
403
404
|
values: Readonly<Record<string, JsonValue | undefined>>,
|
|
405
|
+
): readonly ChangeOperation[] | undefined {
|
|
406
|
+
return setInputMappings(content, nodeId, Object.fromEntries(Object.entries(values).map(([handle, value]) => [handle, fixedInputValue(value)])))
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Input sources own whether a Provider request includes a body and its generated port. */
|
|
410
|
+
export function setInputMappings(
|
|
411
|
+
content: RevisionContent,
|
|
412
|
+
nodeId: string,
|
|
413
|
+
mappings: Readonly<Record<string, InputMapping | undefined>>,
|
|
404
414
|
): readonly ChangeOperation[] | undefined {
|
|
405
415
|
const node = content.document.graph?.nodes[nodeId]
|
|
406
416
|
if (node == null || isTriggerNode(node)) return
|
|
407
417
|
const operations: ChangeOperation[] = []
|
|
408
|
-
|
|
418
|
+
const task = node.kind === 'task' && 'executor' in node.task && node.task.executor.kind === 'connector-proxy' ? node.task : undefined
|
|
419
|
+
let bodyUndeclared = false
|
|
420
|
+
if (task?.executor.kind === 'connector-proxy' && Object.hasOwn(mappings, 'body')) {
|
|
421
|
+
const port = task.inputs.find((input): input is InputPort => 'handle' in input && input.handle === 'body')
|
|
422
|
+
const hasBody = connectorProxyHasBody(mappings.body, port?.value)
|
|
423
|
+
bodyUndeclared = !hasBody && task.executor.bodyFormat === 'none'
|
|
424
|
+
if (hasBody && task.executor.bodyFormat === 'none') {
|
|
425
|
+
operations.push({
|
|
426
|
+
kind: 'graph.node.task.set',
|
|
427
|
+
nodeId,
|
|
428
|
+
before: task,
|
|
429
|
+
value: connectorProxyTask({ ...task.executor, bodyFormat: 'json' }, task.name, task),
|
|
430
|
+
})
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
for (const [handle, value] of Object.entries(mappings)) {
|
|
409
434
|
const before = nodeInputMappings(node)[handle]
|
|
410
|
-
const next =
|
|
435
|
+
const next = handle === 'body' && bodyUndeclared ? undefined : value
|
|
411
436
|
if (dequal(before, next)) continue
|
|
412
437
|
operations.push({ before, handle, kind: 'graph.node.input.set', nodeId, value: next })
|
|
413
438
|
}
|
|
@@ -418,13 +443,7 @@ export function setInputValues(
|
|
|
418
443
|
export function resetInputValues(content: RevisionContent, nodeId: string, handles: readonly string[]): readonly ChangeOperation[] {
|
|
419
444
|
const node = content.document.graph?.nodes[nodeId]
|
|
420
445
|
if (node == null || isTriggerNode(node)) return []
|
|
421
|
-
|
|
422
|
-
return cleanVariableBindings(
|
|
423
|
-
content,
|
|
424
|
-
[...new Set(handles)].flatMap((handle): ChangeOperation[] =>
|
|
425
|
-
mappings[handle] == null ? [] : [{ kind: 'graph.node.input.set', nodeId, handle, before: mappings[handle], value: undefined }],
|
|
426
|
-
),
|
|
427
|
-
)
|
|
446
|
+
return setInputMappings(content, nodeId, Object.fromEntries(handles.map((handle) => [handle, undefined]))) ?? []
|
|
428
447
|
}
|
|
429
448
|
|
|
430
449
|
export function setInputSources(
|
|
@@ -435,9 +454,7 @@ export function setInputSources(
|
|
|
435
454
|
): readonly ChangeOperation[] {
|
|
436
455
|
const node = content.document.graph?.nodes[nodeId]
|
|
437
456
|
if (node == null || isTriggerNode(node)) return []
|
|
438
|
-
return
|
|
439
|
-
{ kind: 'graph.node.input.set', nodeId, handle, before: nodeInputMappings(node)[handle], value: { kind: 'sources', sources } },
|
|
440
|
-
])
|
|
457
|
+
return setInputMappings(content, nodeId, { [handle]: { kind: 'sources', sources } }) ?? []
|
|
441
458
|
}
|
|
442
459
|
|
|
443
460
|
export function setInputVariable(
|
|
@@ -459,10 +476,7 @@ export function setInputVariable(
|
|
|
459
476
|
return [{ before: current.target, bindingId: currentId, kind: 'binding.target.set', value: name }]
|
|
460
477
|
}
|
|
461
478
|
const next: InputMapping = { kind: 'sources', sources: [{ bindingId, kind: 'binding' }] }
|
|
462
|
-
return [
|
|
463
|
-
{ binding: { kind: 'variable', target: name }, bindingId, kind: 'binding.create' },
|
|
464
|
-
...cleanVariableBindings(content, [{ before: mapping, handle, kind: 'graph.node.input.set', nodeId, value: next }]),
|
|
465
|
-
]
|
|
479
|
+
return [{ binding: { kind: 'variable', target: name }, bindingId, kind: 'binding.create' }, ...(setInputMappings(content, nodeId, { [handle]: next }) ?? [])]
|
|
466
480
|
}
|
|
467
481
|
|
|
468
482
|
export function cleanVariableBindings(content: RevisionContent, operations: readonly ChangeOperation[]): readonly ChangeOperation[] {
|
|
@@ -495,9 +509,9 @@ function bindingReferences(document: RevisionContent['document']): Map<string, n
|
|
|
495
509
|
|
|
496
510
|
export function setConnectorConnection(content: RevisionContent, nodeId: string, connectionId: string | undefined): readonly ChangeOperation[] | undefined {
|
|
497
511
|
const node = content.document.graph.nodes[nodeId]
|
|
498
|
-
if (node?.kind != 'task' || !('executor' in node.task) || node.task.executor.kind != 'connector') return
|
|
512
|
+
if (node?.kind != 'task' || !('executor' in node.task) || (node.task.executor.kind != 'connector' && node.task.executor.kind != 'connector-proxy')) return
|
|
499
513
|
const task = node.task
|
|
500
|
-
if (task.executor.kind != 'connector') return
|
|
514
|
+
if (task.executor.kind != 'connector' && task.executor.kind != 'connector-proxy') return
|
|
501
515
|
if (task.executor.connectionId == connectionId) return []
|
|
502
516
|
const { connectionId: _, ...executor } = task.executor
|
|
503
517
|
return [
|
|
@@ -542,6 +556,9 @@ export function setServiceConnections(
|
|
|
542
556
|
if (capabilities != null) operations.push(...(setCodeActions(content, nodeId, capabilities) ?? []))
|
|
543
557
|
} else if (task.executor.kind == 'connector') {
|
|
544
558
|
if (assign(task.executor) !== task.executor) operations.push(...(setConnectorConnection(content, nodeId, connectionId) ?? []))
|
|
559
|
+
} else if (task.executor.kind === 'connector-proxy') {
|
|
560
|
+
if (task.executor.providerId === providerId && task.executor.connectionId === previousConnectionId)
|
|
561
|
+
operations.push(...(setConnectorConnection(content, nodeId, connectionId) ?? []))
|
|
545
562
|
} else if (task.executor.kind == 'agent') {
|
|
546
563
|
const executor = {
|
|
547
564
|
...task.executor,
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { InputMapping, InputPort, JsonValue, TaskDefinitionBase, TaskExecutor } from './model.ts'
|
|
2
|
+
|
|
3
|
+
import { dequal } from 'dequal/lite'
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
import { httpHeaderNameIssue } from '../../http/common/headers.ts'
|
|
6
|
+
import { remapInputMappings, renamedPort } from './portChanges.ts'
|
|
7
|
+
|
|
8
|
+
export const requestParameterSchema = z.strictObject({ mode: z.enum(['fields', 'object']).default('fields'), names: z.array(z.string()).default([]) })
|
|
9
|
+
export type RequestParameters = z.infer<typeof requestParameterSchema>
|
|
10
|
+
export interface RequestParameterConfig {
|
|
11
|
+
readonly query: RequestParameters
|
|
12
|
+
readonly headers: RequestParameters
|
|
13
|
+
}
|
|
14
|
+
export interface RequestTaskNode {
|
|
15
|
+
readonly inputs: Readonly<Record<string, InputMapping>>
|
|
16
|
+
readonly task: TaskDefinitionBase & { readonly executor?: TaskExecutor }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function requestParameterHandle(group: 'query' | 'headers', name: string): string {
|
|
20
|
+
return `${group === 'query' ? 'query' : 'header'}.${name}`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function requestParameterPorts(
|
|
24
|
+
config: RequestParameterConfig,
|
|
25
|
+
querySchema: JsonValue,
|
|
26
|
+
queryValueSchema: JsonValue,
|
|
27
|
+
headerSchema: JsonValue,
|
|
28
|
+
): InputPort[] {
|
|
29
|
+
return (['query', 'headers'] as const).flatMap((group): InputPort[] =>
|
|
30
|
+
config[group].mode === 'object'
|
|
31
|
+
? [{ handle: group, jsonSchema: group === 'query' ? querySchema : headerSchema, nullable: true }]
|
|
32
|
+
: config[group].names
|
|
33
|
+
.filter((name) => name.trim())
|
|
34
|
+
.map((name) => ({
|
|
35
|
+
handle: requestParameterHandle(group, name),
|
|
36
|
+
jsonSchema: group === 'query' ? queryValueSchema : { type: 'string' },
|
|
37
|
+
nullable: true,
|
|
38
|
+
})),
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function retainRequestPorts(ports: readonly InputPort[], old: TaskDefinitionBase['inputs'] = []): TaskDefinitionBase['inputs'] {
|
|
43
|
+
const remaining = new Map(ports.map((port) => [port.handle, port]))
|
|
44
|
+
return [
|
|
45
|
+
...old.flatMap((item): TaskDefinitionBase['inputs'] => {
|
|
46
|
+
if ('group' in item) return [item]
|
|
47
|
+
const next = remaining.get(item.handle)
|
|
48
|
+
if (next == null) return []
|
|
49
|
+
remaining.delete(item.handle)
|
|
50
|
+
return [dequal(item.jsonSchema, next.jsonSchema) && item.nullable == next.nullable ? item : next]
|
|
51
|
+
}),
|
|
52
|
+
...remaining.values(),
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface RequestParameterIssue {
|
|
57
|
+
readonly field: 'query' | 'headers'
|
|
58
|
+
readonly index: number
|
|
59
|
+
readonly code: 'parameterNamesInvalid' | 'headerNameInvalid'
|
|
60
|
+
readonly message: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function requestParameterIssues(config: RequestParameterConfig): RequestParameterIssue[] {
|
|
64
|
+
const issues: RequestParameterIssue[] = []
|
|
65
|
+
for (const group of ['query', 'headers'] as const) {
|
|
66
|
+
if (config[group].mode !== 'fields') continue
|
|
67
|
+
const seen = new Set<string>()
|
|
68
|
+
for (const [index, name] of config[group].names.entries()) {
|
|
69
|
+
const key = group === 'headers' ? name.toLowerCase() : name
|
|
70
|
+
if (!name.trim() || seen.has(key))
|
|
71
|
+
issues.push({ field: group, index, code: 'parameterNamesInvalid', message: 'Parameter names must be nonempty and unique.' })
|
|
72
|
+
seen.add(key)
|
|
73
|
+
if (group === 'headers' && httpHeaderNameIssue(name, []) === 'invalid')
|
|
74
|
+
issues.push({ field: group, index, code: 'headerNameInvalid', message: 'Invalid HTTP header name.' })
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return issues
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Only a named parameter rename carries a source; changing modes does not rename a port. */
|
|
81
|
+
export function requestInputMappings(
|
|
82
|
+
mappings: Readonly<Record<string, InputMapping>>,
|
|
83
|
+
previous: TaskDefinitionBase,
|
|
84
|
+
next: TaskDefinitionBase,
|
|
85
|
+
): Record<string, InputMapping> {
|
|
86
|
+
const candidate = renamedPort(previous.inputs, next.inputs)
|
|
87
|
+
const rename = candidate != null && ['query.', 'header.'].some((prefix) => candidate.every((name) => name.startsWith(prefix))) ? candidate : undefined
|
|
88
|
+
return remapInputMappings(mappings, next.inputs, rename)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function withRequestParameterFields<Node extends RequestTaskNode>(
|
|
92
|
+
node: Node,
|
|
93
|
+
group: 'query' | 'headers',
|
|
94
|
+
values: Record<string, JsonValue>,
|
|
95
|
+
previous: RequestParameters,
|
|
96
|
+
createTask: (parameters: RequestParameters) => Node['task'],
|
|
97
|
+
): Node {
|
|
98
|
+
const task = createTask({ mode: 'fields', names: Object.keys(values) })
|
|
99
|
+
const inputs = requestInputMappings(node.inputs, node.task, task)
|
|
100
|
+
const rename = renamedPort(node.task.inputs, task.inputs)
|
|
101
|
+
for (const [name, value] of Object.entries(values)) {
|
|
102
|
+
const handle = requestParameterHandle(group, name)
|
|
103
|
+
const existing = previous.mode === 'fields' && (previous.names.includes(name) || rename?.[1] === handle)
|
|
104
|
+
if (inputs[handle] == null && !existing) inputs[handle] = { kind: 'value', value }
|
|
105
|
+
}
|
|
106
|
+
return { ...node, task, inputs }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function withRequestParameterSource<Node extends RequestTaskNode>(
|
|
110
|
+
node: Node,
|
|
111
|
+
group: 'query' | 'headers',
|
|
112
|
+
mapping: InputMapping | undefined,
|
|
113
|
+
createTask: (parameters: RequestParameters) => Node['task'],
|
|
114
|
+
): Node {
|
|
115
|
+
const task = createTask({ mode: 'object', names: [] })
|
|
116
|
+
const inputs = requestInputMappings(node.inputs, node.task, task)
|
|
117
|
+
if (mapping == null) delete inputs[group]
|
|
118
|
+
else inputs[group] = mapping
|
|
119
|
+
return { ...node, task, inputs }
|
|
120
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { dequal } from 'dequal/lite'
|
|
2
2
|
import { connectorActionContracts } from '../../connector/common/actionContract.ts'
|
|
3
|
+
import { connectorProxyHasBody, connectorProxyIssues, connectorProxyTask } from '../../connector/common/proxy.ts'
|
|
3
4
|
import { decisionTaskIssues } from '../../decision/common/decision.ts'
|
|
4
5
|
import { httpRequestIssues, httpAuthHandles, httpRequestTask, httpRequestUrl } from '../../http/common/httpRequest.ts'
|
|
5
6
|
import { openApiIssues, authHandles } from '../../openapi/common/openapi.ts'
|
|
@@ -14,7 +15,17 @@ import { connectionUsage } from './connectionUsage.ts'
|
|
|
14
15
|
export { matchesTriggerOutputs, normalizeTriggerOutputs, triggerOutputDefinitions, triggerOutputPorts } from '../../trigger/common/contract.ts'
|
|
15
16
|
import type { EngineContract } from '../../runtime/common/engineContract.ts'
|
|
16
17
|
import type { RuntimeProgram } from '../../runtime/common/runtime.ts'
|
|
17
|
-
import type {
|
|
18
|
+
import type {
|
|
19
|
+
ConnectorActionCapability,
|
|
20
|
+
ConnectorCapability,
|
|
21
|
+
Graph,
|
|
22
|
+
InputMapping,
|
|
23
|
+
InputPort,
|
|
24
|
+
JsonValue,
|
|
25
|
+
Port,
|
|
26
|
+
RevisionContent,
|
|
27
|
+
SchemaMismatch,
|
|
28
|
+
} from './change.ts'
|
|
18
29
|
|
|
19
30
|
import { findEngineContract } from '../../runtime/common/engineContract.ts'
|
|
20
31
|
import { agentConfigIssues } from './agent.ts'
|
|
@@ -289,6 +300,38 @@ export async function validateFlow(revision: RevisionContent, engine: EngineCont
|
|
|
289
300
|
path: `/document/graph/nodes/${nodeId}/task`,
|
|
290
301
|
})
|
|
291
302
|
}
|
|
303
|
+
if (task.executor.kind === 'connector-proxy') {
|
|
304
|
+
const endpoint = node.inputs.endpoint
|
|
305
|
+
if (endpoint?.kind === 'value' && (typeof endpoint.value !== 'string' || !endpoint.value.trim()))
|
|
306
|
+
checked.diagnostics.push({
|
|
307
|
+
code: 'connector-proxy.endpoint-invalid',
|
|
308
|
+
column: 0,
|
|
309
|
+
line: 0,
|
|
310
|
+
message: 'A nonempty endpoint is required.',
|
|
311
|
+
path: `/document/graph/nodes/${nodeId}/inputs/endpoint`,
|
|
312
|
+
})
|
|
313
|
+
const bodyPort = task.inputs.find((port): port is InputPort => 'handle' in port && port.handle === 'body')
|
|
314
|
+
for (const issue of connectorProxyIssues(task.executor, connectorProxyHasBody(node.inputs.body, bodyPort?.value)))
|
|
315
|
+
checked.diagnostics.push({
|
|
316
|
+
code: 'connector-proxy.config-invalid',
|
|
317
|
+
column: 0,
|
|
318
|
+
line: 0,
|
|
319
|
+
message: issue.message,
|
|
320
|
+
path: issue.field === 'bodyFormat' ? `/document/graph/nodes/${nodeId}/inputs/body` : `/document/graph/nodes/${nodeId}/task/executor/${issue.field}`,
|
|
321
|
+
})
|
|
322
|
+
const expected = connectorProxyTask(task.executor)
|
|
323
|
+
if (
|
|
324
|
+
!dequal(httpPortContracts(task.inputs), httpPortContracts(expected.inputs)) ||
|
|
325
|
+
!dequal(httpPortContracts(task.outputs), httpPortContracts(expected.outputs))
|
|
326
|
+
)
|
|
327
|
+
checked.diagnostics.push({
|
|
328
|
+
code: 'connector-proxy.ports-invalid',
|
|
329
|
+
column: 0,
|
|
330
|
+
line: 0,
|
|
331
|
+
message: 'Provider API request ports do not match their configuration.',
|
|
332
|
+
path: `/document/graph/nodes/${nodeId}/task`,
|
|
333
|
+
})
|
|
334
|
+
}
|
|
292
335
|
if (task.executor.kind == 'http-request') {
|
|
293
336
|
for (const issue of httpRequestIssues(task.executor))
|
|
294
337
|
checked.diagnostics.push({
|
|
@@ -458,6 +501,13 @@ const httpPortContracts = (ports: import('./change.ts').ManagedTaskDefinition['i
|
|
|
458
501
|
export async function prepareFlow(revision: RevisionContent, engineContract: string, triggerId?: string): Promise<PrepareFlowResult> {
|
|
459
502
|
const engine = findEngineContract(engineContract)
|
|
460
503
|
if (engine == null) return { kind: 'engine-unsupported' }
|
|
504
|
+
if (
|
|
505
|
+
!engine.connectorProxy &&
|
|
506
|
+
Object.values(revision.document.graph.nodes).some(
|
|
507
|
+
(node) => node.kind === 'task' && 'executor' in node.task && node.task.executor.kind === 'connector-proxy',
|
|
508
|
+
)
|
|
509
|
+
)
|
|
510
|
+
return { kind: 'engine-unsupported' }
|
|
461
511
|
if (
|
|
462
512
|
!engine.flowState &&
|
|
463
513
|
Object.values(revision.document.graph.nodes).some(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { NodeSource, PortDefinition } from './change.ts'
|
|
1
|
+
import type { JsonValue, NodeSource, PortDefinition } from './change.ts'
|
|
2
2
|
|
|
3
|
+
import { propertySchemaWithDefinitions } from '../../base/common/jsonSchema.ts'
|
|
3
4
|
import { schemaObject } from './schema.ts'
|
|
4
5
|
|
|
5
6
|
/** Only explicitly declared, first-level object properties are selectable. */
|
|
@@ -17,7 +18,7 @@ export function sourceFields(output: PortDefinition): readonly { readonly field:
|
|
|
17
18
|
field,
|
|
18
19
|
port: {
|
|
19
20
|
...(typeof description == 'string' ? { description } : {}),
|
|
20
|
-
jsonSchema,
|
|
21
|
+
jsonSchema: propertySchemaWithDefinitions(schema, jsonSchema) as JsonValue,
|
|
21
22
|
nullable: missingParent || !required.includes(field),
|
|
22
23
|
},
|
|
23
24
|
}
|
|
@@ -86,8 +86,8 @@ export function cleanWorkflowFile(value: unknown): { readonly file: WorkflowFile
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
export function encodeWorkflowFile(file: WorkflowFile): string {
|
|
90
|
-
return `${JSON.stringify(cleanWorkflowFile(file).file, null, 2)}\n`
|
|
89
|
+
export function encodeWorkflowFile(file: WorkflowFile, options: { readonly clean?: boolean } = {}): string {
|
|
90
|
+
return `${JSON.stringify(options.clean === false ? decodeWorkflowFile(file) : cleanWorkflowFile(file).file, null, 2)}\n`
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
export function workflowFileName(name: string): string {
|
|
@@ -1,17 +1,24 @@
|
|
|
1
|
-
import type { InputMapping, InputPort, JsonValue, ManagedTaskDefinitionOf, TaskDefinitionBase
|
|
1
|
+
import type { InputMapping, InputPort, JsonValue, ManagedTaskDefinitionOf, TaskDefinitionBase } from '../../flow/common/model.ts'
|
|
2
|
+
import type { RequestTaskNode } from '../../flow/common/requestParameters.ts'
|
|
2
3
|
|
|
3
|
-
import { dequal } from 'dequal/lite'
|
|
4
4
|
import { z } from 'zod'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
requestParameterSchema,
|
|
7
|
+
requestParameterPorts,
|
|
8
|
+
requestParameterIssues,
|
|
9
|
+
requestInputMappings,
|
|
10
|
+
retainRequestPorts,
|
|
11
|
+
withRequestParameterFields,
|
|
12
|
+
withRequestParameterSource,
|
|
13
|
+
} from '../../flow/common/requestParameters.ts'
|
|
6
14
|
import { httpHeaderNameIssue } from './headers.ts'
|
|
7
15
|
|
|
8
16
|
export { httpHostAllowed, isHttpHost, maxHttpHosts } from './hosts.ts'
|
|
9
17
|
|
|
10
|
-
const parameterSchema = z.strictObject({ mode: z.enum(['fields', 'object']).default('fields'), names: z.array(z.string()).default([]) })
|
|
11
18
|
export const httpRequestConfigSchema = z.strictObject({
|
|
12
19
|
method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']).default('GET'),
|
|
13
|
-
query:
|
|
14
|
-
headers:
|
|
20
|
+
query: requestParameterSchema.default({ mode: 'fields', names: [] }),
|
|
21
|
+
headers: requestParameterSchema.default({ mode: 'fields', names: [] }),
|
|
15
22
|
bodyFormat: z.enum(['none', 'json', 'text', 'form']).default('none'),
|
|
16
23
|
authentication: z
|
|
17
24
|
.union([
|
|
@@ -36,36 +43,15 @@ export const httpQuerySchema = { type: 'object', additionalProperties: queryValu
|
|
|
36
43
|
export function httpAuthHandles(config: HttpRequestConfig): string[] {
|
|
37
44
|
return config.authentication.type == 'none' ? [] : config.authentication.type == 'basic' ? ['auth.username', 'auth.password'] : ['auth.token']
|
|
38
45
|
}
|
|
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
46
|
export function httpRequestTask(
|
|
54
47
|
config: HttpRequestConfig = httpRequestConfigSchema.parse({}),
|
|
55
48
|
name = 'HTTP Request',
|
|
56
49
|
previous?: TaskDefinitionBase,
|
|
57
50
|
): ManagedTaskDefinitionOf<HttpRequestExecutor> {
|
|
58
|
-
const inputs: InputPort[] = [
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
}
|
|
51
|
+
const inputs: InputPort[] = [
|
|
52
|
+
{ handle: 'url', jsonSchema: { type: 'string' }, nullable: false },
|
|
53
|
+
...requestParameterPorts(config, httpQuerySchema, queryValueSchema, httpHeaderSchema),
|
|
54
|
+
]
|
|
69
55
|
if (config.bodyFormat != 'none')
|
|
70
56
|
inputs.push({
|
|
71
57
|
handle: 'body',
|
|
@@ -82,8 +68,8 @@ export function httpRequestTask(
|
|
|
82
68
|
return {
|
|
83
69
|
name,
|
|
84
70
|
executor: { ...config, kind: 'http-request' },
|
|
85
|
-
inputs:
|
|
86
|
-
outputs:
|
|
71
|
+
inputs: retainRequestPorts(inputs, previous?.inputs),
|
|
72
|
+
outputs: retainRequestPorts(outputs, previous?.outputs),
|
|
87
73
|
}
|
|
88
74
|
}
|
|
89
75
|
export interface HttpRequestIssue {
|
|
@@ -96,18 +82,7 @@ export function httpRequestIssues(config: HttpRequestConfig): HttpRequestIssue[]
|
|
|
96
82
|
const issues: HttpRequestIssue[] = []
|
|
97
83
|
if (['GET', 'HEAD'].includes(config.method) && config.bodyFormat != 'none')
|
|
98
84
|
issues.push({ field: 'bodyFormat', code: 'bodyUnsupported', message: 'GET and HEAD do not support a request body.' })
|
|
99
|
-
|
|
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
|
-
}
|
|
85
|
+
issues.push(...requestParameterIssues(config))
|
|
111
86
|
const auth = config.authentication
|
|
112
87
|
if (auth.type != 'none') {
|
|
113
88
|
const group = auth.type == 'apiKey' && auth.in == 'query' ? 'query' : 'headers'
|
|
@@ -218,49 +193,24 @@ export function parseHttpResponse(config: HttpRequestConfig, status: number, hea
|
|
|
218
193
|
}
|
|
219
194
|
|
|
220
195
|
/** Only a named parameter rename carries a value; changing source modes is not a rename. */
|
|
221
|
-
export
|
|
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
|
-
}
|
|
196
|
+
export const httpRequestInputs = requestInputMappings
|
|
230
197
|
|
|
231
|
-
|
|
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 {
|
|
198
|
+
function httpRequestExecutor(node: RequestTaskNode): HttpRequestExecutor | undefined {
|
|
238
199
|
const executor = node.task.executor
|
|
239
200
|
return executor?.kind === 'http-request' ? (executor as HttpRequestExecutor) : undefined
|
|
240
201
|
}
|
|
241
202
|
|
|
242
203
|
/** The object editor assembles named inputs without changing unrelated request settings. */
|
|
243
|
-
export function withHttpParameterFields<Node extends
|
|
204
|
+
export function withHttpParameterFields<Node extends RequestTaskNode>(node: Node, group: 'query' | 'headers', values: Record<string, JsonValue>): Node {
|
|
244
205
|
const config = httpRequestExecutor(node)
|
|
245
206
|
if (config == null) return node
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
|
207
|
+
return withRequestParameterFields(node, group, values, config[group], (parameters) =>
|
|
208
|
+
httpRequestTask({ ...config, [group]: parameters }, node.task.name, node.task),
|
|
209
|
+
)
|
|
256
210
|
}
|
|
257
211
|
|
|
258
|
-
export function withHttpParameterSource<Node extends
|
|
212
|
+
export function withHttpParameterSource<Node extends RequestTaskNode>(node: Node, group: 'query' | 'headers', mapping?: InputMapping): Node {
|
|
259
213
|
const config = httpRequestExecutor(node)
|
|
260
214
|
if (config == null) return node
|
|
261
|
-
|
|
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
|
|
215
|
+
return withRequestParameterSource(node, group, mapping, (parameters) => httpRequestTask({ ...config, [group]: parameters }, node.task.name, node.task))
|
|
266
216
|
}
|
|
@@ -10,10 +10,11 @@ export interface EngineContract {
|
|
|
10
10
|
/** Code may call `context.fetch` for hosts declared by its `http` Capability. */
|
|
11
11
|
readonly codeFetch?: boolean
|
|
12
12
|
readonly flowState?: boolean
|
|
13
|
+
readonly connectorProxy?: boolean
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
/**
|
|
16
|
-
export const currentEngineContract = 'open-flow-engine/
|
|
16
|
+
/** v11 adds Provider API requests through Connector Proxy. */
|
|
17
|
+
export const currentEngineContract = 'open-flow-engine/v11'
|
|
17
18
|
/** The published v8 Node compatibility contract, retained for existing Publications and Runs. */
|
|
18
19
|
export const nodejsEngineContract = 'open-flow-engine/v8/nodejs-compat-v1'
|
|
19
20
|
|
|
@@ -65,10 +66,12 @@ const nodejsContract: EngineContract = {
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
const v9Contract: EngineContract = { ...nodejsContract, codeFetch: true, platformSource: platformSource('open-flow-engine/v9') }
|
|
68
|
-
const
|
|
69
|
+
const v10Contract: EngineContract = { ...v9Contract, flowState: true, platformSource: platformSource('open-flow-engine/v10') }
|
|
70
|
+
const currentContract: EngineContract = { ...v10Contract, connectorProxy: true, platformSource: platformSource(currentEngineContract) }
|
|
69
71
|
|
|
70
72
|
export function findEngineContract(contract: string): EngineContract | undefined {
|
|
71
73
|
if (contract == currentEngineContract) return currentContract
|
|
74
|
+
if (contract == 'open-flow-engine/v10') return v10Contract
|
|
72
75
|
if (contract == 'open-flow-engine/v9') return v9Contract
|
|
73
76
|
if (contract == 'open-flow-engine/v8') return v8Contract
|
|
74
77
|
if (contract == nodejsEngineContract) return nodejsContract
|