@oomol-lab/open-flow 0.1.0-beta.73 → 0.1.0-beta.74
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/LICENSE +36 -0
- package/NOTICE +7 -0
- package/README.md +40 -0
- package/dist/browser/{createResourceDialog-BWJkjiOO.js → createResourceDialog-DI9pL-Yr.js} +2 -2
- package/dist/browser/{dialog-CUy_fwx9.js → dialog-BYJGvItc.js} +54 -19
- package/dist/browser/{flowChanges-CgSnSqqP.js → flowChanges-RL-H4CjD.js} +306 -48
- package/dist/browser/{flowWorkspace-Bf7H-r0t.js → flowWorkspace-BkDsETj0.js} +20745 -20186
- package/dist/browser/preview.css +73 -3
- package/dist/browser/preview.js +21410 -20607
- package/dist/browser/{typeScriptSession-B0hC-C8y.js → typeScriptSession-CSRwWmZm.js} +1 -1
- package/dist/browser/ui.css +72 -2
- package/dist/browser/workbench.css +73 -3
- package/dist/browser/workbench.js +14 -76
- package/dist/browser/{workbenchSelect-C3e4J93X.js → workbenchSelect-BcWPWGzw.js} +1 -1
- package/dist/command/index.js +34 -34
- package/dist/common/authoring.d.ts +1 -1
- package/dist/common/authoringFields.d.ts +1 -1
- package/dist/common/authoringSchema.d.ts +28 -4
- package/dist/common/connector-proxy.d.ts +1 -1
- package/dist/common/control-api-conformance.d.ts +1 -1
- package/dist/common/control-api-conformance.js +61 -24
- package/dist/common/control-api.d.ts +4 -3
- package/dist/common/control-api.js +59 -22
- package/dist/common/control-requests.d.ts +2 -1
- package/dist/common/control-requests.js +204 -42
- package/dist/common/cron-trigger.d.ts +1 -1
- package/dist/common/decision.d.ts +1 -1
- package/dist/common/draftOperations.d.ts +1 -1
- package/dist/common/engine-contract.d.ts +3 -2
- package/dist/common/event-source-definition.d.ts +1 -1
- package/dist/common/flow-agent.d.ts +1 -1
- package/dist/{browser → common}/flow-authoring-node.d.ts +3 -3
- package/dist/{browser → common}/flow-authoring.js +75 -23
- package/dist/{browser → common}/flow-change-schema.d.ts +177 -6
- package/dist/{browser → common}/flow-change.d.ts +35 -15
- package/dist/{browser → common}/flow-change.js +77 -25
- package/dist/common/flow-connectionUsage.d.ts +1 -1
- package/dist/common/flow-encoding.d.ts +1 -1
- package/dist/common/flow-encoding.js +34 -16
- package/dist/common/flow-errorHandling.d.ts +1 -1
- package/dist/common/flow-graph.d.ts +1 -1
- package/dist/common/flow-modules.d.ts +1 -1
- package/dist/common/flow-promptTemplate.d.ts +1 -1
- package/dist/common/flow-retry.d.ts +1 -1
- package/dist/common/flow-semantics.d.ts +1 -1
- package/dist/common/flow-semantics.js +151 -32
- package/dist/common/flow-workflowFile.d.ts +1 -1
- package/dist/common/flowInspection.d.ts +3 -3
- package/dist/common/integration-trigger.d.ts +1 -1
- package/dist/common/licenses.md +118 -1
- package/dist/common/liveDetails.d.ts +1 -1
- package/dist/common/mcp.d.ts +2 -1
- package/dist/common/mcp.js +59 -22
- package/dist/common/openapi.d.ts +1 -1
- package/dist/common/poll-trigger.d.ts +1 -1
- package/dist/common/run-results.d.ts +8 -1
- package/dist/common/runtime-contract.d.ts +8 -2
- package/dist/common/runtime-contract.js +22 -5
- package/dist/common/scheduler-checkpoint.d.ts +1 -1
- package/dist/common/scheduler-dependencies.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 +31 -12
- package/dist/common/trigger-catalog.d.ts +1 -1
- 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 -2
- package/dist/common/trigger-localization.d.ts +1 -1
- package/dist/common/webhook-response.d.ts +1 -1
- package/dist/common/webhook-trigger.d.ts +1 -1
- package/dist/common/workflow.d.ts +17 -1
- package/dist/common/workflow.js +68 -22
- package/dist/index.d.ts +6 -3
- package/package.json +7 -6
- package/src/base/common/json.ts +90 -0
- package/src/canvas/common/commands.ts +15 -0
- package/src/connector/common/actionSchema.ts +168 -0
- package/src/connector/common/proxy.ts +44 -0
- package/src/control/common/api.ts +1505 -0
- package/src/control/common/authoring.ts +912 -0
- package/src/control/common/authoringCondition.ts +110 -0
- package/src/control/common/authoringDiagnosticFields.ts +136 -0
- package/src/control/common/authoringExamples.ts +192 -0
- package/src/control/common/authoringFields.ts +53 -0
- package/src/control/common/authoringLlm.ts +70 -0
- package/src/control/common/authoringOpenApi.ts +129 -0
- package/src/control/common/authoringSchema.ts +404 -0
- package/src/control/common/authoringTools.ts +123 -0
- package/src/control/common/authoringTriggers.ts +26 -0
- package/src/control/common/checkDecoders.ts +65 -0
- package/src/control/common/conformance.ts +1989 -0
- package/src/control/common/connectorDecoders.ts +370 -0
- package/src/control/common/connectorQueries.ts +61 -0
- package/src/control/common/decoding.ts +60 -0
- package/src/control/common/draftOperations.ts +61 -0
- package/src/control/common/errors.ts +114 -0
- package/src/control/common/eventSourceSchemas.ts +13 -0
- package/src/control/common/eventSources.ts +186 -0
- package/src/control/common/flowDecoders.ts +153 -0
- package/src/control/common/flowInspection.ts +181 -0
- package/src/control/common/flowNotifications.ts +37 -0
- package/src/control/common/liveDetails.ts +124 -0
- package/src/control/common/mcp.ts +241 -0
- package/src/control/common/mcpConformance.ts +142 -0
- package/src/control/common/providerAccess.ts +28 -0
- package/src/control/common/providerIconSprite.ts +42 -0
- package/src/control/common/publicationDecoders.ts +98 -0
- package/src/control/common/random.ts +16 -0
- package/src/control/common/requests.ts +112 -0
- package/src/control/common/resultQuery.ts +8 -0
- package/src/control/common/results.ts +176 -0
- package/src/control/common/revisionDecoders.ts +52 -0
- package/src/control/common/runDecoders.ts +240 -0
- package/src/control/common/triggerCatalog.ts +89 -0
- package/src/control/common/triggerDecoders.ts +175 -0
- package/src/decision/common/decision.ts +168 -0
- package/src/event-source/common/catalog.generated.json +65 -0
- package/src/event-source/common/definition.ts +66 -0
- package/src/event-source/common/index.ts +99 -0
- package/src/execution/common/checkpoint.ts +311 -0
- package/src/execution/common/dependencies.ts +440 -0
- package/src/execution/common/engineContract.ts +80 -0
- package/src/execution/common/events.ts +328 -0
- package/src/execution/common/node-execution.ts +467 -0
- package/src/execution/common/runLifecycle.ts +250 -0
- package/src/execution/common/runtime.ts +563 -0
- package/src/execution/common/scheduler.ts +696 -0
- package/src/execution/common/wait.ts +36 -0
- package/src/flow/common/agent.ts +305 -0
- package/src/flow/common/authoring.ts +30 -0
- package/src/flow/common/change.ts +1036 -0
- package/src/flow/common/changeSchema.ts +783 -0
- package/src/flow/common/codePermissions.ts +2 -0
- package/src/flow/common/condition.ts +265 -0
- package/src/flow/common/connectionUsage.ts +131 -0
- package/src/flow/common/edgeChanges.ts +35 -0
- package/src/flow/common/encoding.ts +437 -0
- package/src/flow/common/errorHandling.ts +34 -0
- package/src/flow/common/graph.ts +892 -0
- package/src/flow/common/inputValue.ts +16 -0
- package/src/flow/common/json.ts +8 -0
- package/src/flow/common/moduleChanges.ts +31 -0
- package/src/flow/common/modules.ts +292 -0
- package/src/flow/common/nodeChanges.ts +762 -0
- package/src/flow/common/portChanges.ts +36 -0
- package/src/flow/common/promptTemplate.ts +15 -0
- package/src/flow/common/retry.ts +20 -0
- package/src/flow/common/schema.ts +162 -0
- package/src/flow/common/schemaCompare.ts +176 -0
- package/src/flow/common/schemaComparer.ts +178 -0
- package/src/flow/common/schemaKeywords.ts +53 -0
- package/src/flow/common/semantics.ts +474 -0
- package/src/flow/common/sourceField.ts +35 -0
- package/src/flow/common/triggerScheduleSchema.ts +8 -0
- package/src/flow/common/wait.ts +30 -0
- package/src/flow/common/webhookMethod.ts +7 -0
- package/src/flow/common/webhookResponse.ts +75 -0
- package/src/flow/common/workflowFile.ts +97 -0
- package/src/json-schema-subset/checker/array.ts +142 -0
- package/src/json-schema-subset/checker/cache.ts +57 -0
- package/src/json-schema-subset/checker/checker.ts +230 -0
- package/src/json-schema-subset/checker/enumAndConst.ts +113 -0
- package/src/json-schema-subset/checker/equals.ts +93 -0
- package/src/json-schema-subset/checker/numeric/checker.ts +50 -0
- package/src/json-schema-subset/checker/numeric/float.ts +121 -0
- package/src/json-schema-subset/checker/numeric/index.ts +1 -0
- package/src/json-schema-subset/checker/numeric/integer.ts +119 -0
- package/src/json-schema-subset/checker/numeric/integerNormalizer.ts +72 -0
- package/src/json-schema-subset/checker/object.ts +354 -0
- package/src/json-schema-subset/checker/range.ts +91 -0
- package/src/json-schema-subset/checker/string.ts +60 -0
- package/src/json-schema-subset/checker/tools/index.ts +3 -0
- package/src/json-schema-subset/checker/tools/mark.ts +82 -0
- package/src/json-schema-subset/checker/tools/quantum.ts +106 -0
- package/src/json-schema-subset/checker/tools/range.ts +210 -0
- package/src/json-schema-subset/checker/validator.ts +23 -0
- package/src/json-schema-subset/compiler/array.ts +163 -0
- package/src/json-schema-subset/compiler/combination.ts +109 -0
- package/src/json-schema-subset/compiler/common.ts +99 -0
- package/src/json-schema-subset/compiler/compiler.ts +188 -0
- package/src/json-schema-subset/compiler/enumAndConst.ts +75 -0
- package/src/json-schema-subset/compiler/index.ts +11 -0
- package/src/json-schema-subset/compiler/object.ts +411 -0
- package/src/json-schema-subset/compiler/primitive.ts +144 -0
- package/src/json-schema-subset/compiler/utils.ts +24 -0
- package/src/json-schema-subset/compiler/wrapper.ts +192 -0
- package/src/json-schema-subset/error.ts +55 -0
- package/src/json-schema-subset/expression/calculator.ts +442 -0
- package/src/json-schema-subset/expression/combination.ts +49 -0
- package/src/json-schema-subset/expression/expression.ts +191 -0
- package/src/json-schema-subset/expression/index.ts +25 -0
- package/src/json-schema-subset/expression/result.ts +88 -0
- package/src/json-schema-subset/expression/types.ts +36 -0
- package/src/json-schema-subset/extends/extendsContext.ts +89 -0
- package/src/json-schema-subset/extends/index.ts +2 -0
- package/src/json-schema-subset/extends/types.ts +12 -0
- package/src/json-schema-subset/global.ts +60 -0
- package/src/json-schema-subset/index.ts +8 -0
- package/src/json-schema-subset/preprocessing.ts +74 -0
- package/src/json-schema-subset/types.ts +8 -0
- package/src/json-schema-subset/utils.ts +95 -0
- package/src/localization/common/languages.ts +118 -0
- package/src/localization/common/timeZone.ts +25 -0
- package/src/openapi/common/authBindings.ts +12 -0
- package/src/openapi/common/openapi.ts +459 -0
- package/src/trigger/common/config.ts +56 -0
- package/src/trigger/common/configOptions.ts +15 -0
- package/src/trigger/common/configSchema.ts +8 -0
- package/src/trigger/common/contract.ts +132 -0
- package/src/trigger/common/cron.ts +151 -0
- package/src/trigger/common/integration.ts +498 -0
- package/src/trigger/common/poll.ts +187 -0
- package/src/trigger/common/webhook.ts +412 -0
- package/src/trigger/providers/airtable/locales/fr.json +18 -0
- package/src/trigger/providers/airtable/locales/ja.json +18 -0
- package/src/trigger/providers/airtable/locales/ko.json +18 -0
- package/src/trigger/providers/airtable/locales/ru.json +18 -0
- package/src/trigger/providers/airtable/locales/zh-CN.json +18 -0
- package/src/trigger/providers/airtable/locales/zh-TW.json +18 -0
- package/src/trigger/providers/asana/locales/fr.json +15 -0
- package/src/trigger/providers/asana/locales/ja.json +15 -0
- package/src/trigger/providers/asana/locales/ko.json +15 -0
- package/src/trigger/providers/asana/locales/ru.json +15 -0
- package/src/trigger/providers/asana/locales/zh-CN.json +15 -0
- package/src/trigger/providers/asana/locales/zh-TW.json +15 -0
- package/src/trigger/providers/calendly/locales/fr.json +16 -0
- package/src/trigger/providers/calendly/locales/ja.json +16 -0
- package/src/trigger/providers/calendly/locales/ko.json +16 -0
- package/src/trigger/providers/calendly/locales/ru.json +16 -0
- package/src/trigger/providers/calendly/locales/zh-CN.json +16 -0
- package/src/trigger/providers/calendly/locales/zh-TW.json +16 -0
- package/src/trigger/providers/catalog.generated.json +4996 -0
- package/src/trigger/providers/clickup/locales/fr.json +16 -0
- package/src/trigger/providers/clickup/locales/ja.json +16 -0
- package/src/trigger/providers/clickup/locales/ko.json +16 -0
- package/src/trigger/providers/clickup/locales/ru.json +16 -0
- package/src/trigger/providers/clickup/locales/zh-CN.json +16 -0
- package/src/trigger/providers/clickup/locales/zh-TW.json +16 -0
- package/src/trigger/providers/definitions.ts +19 -0
- package/src/trigger/providers/feishu/locales/fr.json +20 -0
- package/src/trigger/providers/feishu/locales/ja.json +20 -0
- package/src/trigger/providers/feishu/locales/ko.json +20 -0
- package/src/trigger/providers/feishu/locales/ru.json +20 -0
- package/src/trigger/providers/feishu/locales/zh-CN.json +20 -0
- package/src/trigger/providers/feishu/locales/zh-TW.json +20 -0
- package/src/trigger/providers/fieldDescriptions.ts +392 -0
- package/src/trigger/providers/fieldLabels.ts +256 -0
- package/src/trigger/providers/fireflies/locales/fr.json +17 -0
- package/src/trigger/providers/fireflies/locales/ja.json +17 -0
- package/src/trigger/providers/fireflies/locales/ko.json +17 -0
- package/src/trigger/providers/fireflies/locales/ru.json +17 -0
- package/src/trigger/providers/fireflies/locales/zh-CN.json +17 -0
- package/src/trigger/providers/fireflies/locales/zh-TW.json +17 -0
- package/src/trigger/providers/github/locales/fr.json +30 -0
- package/src/trigger/providers/github/locales/ja.json +30 -0
- package/src/trigger/providers/github/locales/ko.json +30 -0
- package/src/trigger/providers/github/locales/ru.json +30 -0
- package/src/trigger/providers/github/locales/zh-CN.json +30 -0
- package/src/trigger/providers/github/locales/zh-TW.json +30 -0
- package/src/trigger/providers/gitlab/locales/fr.json +18 -0
- package/src/trigger/providers/gitlab/locales/ja.json +18 -0
- package/src/trigger/providers/gitlab/locales/ko.json +18 -0
- package/src/trigger/providers/gitlab/locales/ru.json +18 -0
- package/src/trigger/providers/gitlab/locales/zh-CN.json +18 -0
- package/src/trigger/providers/gitlab/locales/zh-TW.json +18 -0
- package/src/trigger/providers/gmail/locales/fr.json +18 -0
- package/src/trigger/providers/gmail/locales/ja.json +18 -0
- package/src/trigger/providers/gmail/locales/ko.json +18 -0
- package/src/trigger/providers/gmail/locales/ru.json +18 -0
- package/src/trigger/providers/gmail/locales/zh-CN.json +18 -0
- package/src/trigger/providers/gmail/locales/zh-TW.json +18 -0
- package/src/trigger/providers/google-calendar/locales/fr.json +15 -0
- package/src/trigger/providers/google-calendar/locales/ja.json +15 -0
- package/src/trigger/providers/google-calendar/locales/ko.json +15 -0
- package/src/trigger/providers/google-calendar/locales/ru.json +15 -0
- package/src/trigger/providers/google-calendar/locales/zh-CN.json +15 -0
- package/src/trigger/providers/google-calendar/locales/zh-TW.json +15 -0
- package/src/trigger/providers/google-drive/locales/fr.json +48 -0
- package/src/trigger/providers/google-drive/locales/ja.json +48 -0
- package/src/trigger/providers/google-drive/locales/ko.json +48 -0
- package/src/trigger/providers/google-drive/locales/ru.json +48 -0
- package/src/trigger/providers/google-drive/locales/zh-CN.json +48 -0
- package/src/trigger/providers/google-drive/locales/zh-TW.json +48 -0
- package/src/trigger/providers/google-sheets/locales/fr.json +19 -0
- package/src/trigger/providers/google-sheets/locales/ja.json +19 -0
- package/src/trigger/providers/google-sheets/locales/ko.json +19 -0
- package/src/trigger/providers/google-sheets/locales/ru.json +19 -0
- package/src/trigger/providers/google-sheets/locales/zh-CN.json +19 -0
- package/src/trigger/providers/google-sheets/locales/zh-TW.json +19 -0
- package/src/trigger/providers/intercom/locales/fr.json +17 -0
- package/src/trigger/providers/intercom/locales/ja.json +17 -0
- package/src/trigger/providers/intercom/locales/ko.json +17 -0
- package/src/trigger/providers/intercom/locales/ru.json +17 -0
- package/src/trigger/providers/intercom/locales/zh-CN.json +17 -0
- package/src/trigger/providers/intercom/locales/zh-TW.json +17 -0
- package/src/trigger/providers/jira/locales/fr.json +17 -0
- package/src/trigger/providers/jira/locales/ja.json +17 -0
- package/src/trigger/providers/jira/locales/ko.json +17 -0
- package/src/trigger/providers/jira/locales/ru.json +17 -0
- package/src/trigger/providers/jira/locales/zh-CN.json +17 -0
- package/src/trigger/providers/jira/locales/zh-TW.json +17 -0
- package/src/trigger/providers/linear/locales/fr.json +13 -0
- package/src/trigger/providers/linear/locales/ja.json +13 -0
- package/src/trigger/providers/linear/locales/ko.json +13 -0
- package/src/trigger/providers/linear/locales/ru.json +13 -0
- package/src/trigger/providers/linear/locales/zh-CN.json +13 -0
- package/src/trigger/providers/linear/locales/zh-TW.json +13 -0
- package/src/trigger/providers/locales/fr.ts +71 -0
- package/src/trigger/providers/locales/ja.ts +71 -0
- package/src/trigger/providers/locales/ko.ts +71 -0
- package/src/trigger/providers/locales/ru.ts +71 -0
- package/src/trigger/providers/locales/zh-CN.ts +71 -0
- package/src/trigger/providers/locales/zh-TW.ts +71 -0
- package/src/trigger/providers/localization.ts +59 -0
- package/src/trigger/providers/notion/locales/fr.json +16 -0
- package/src/trigger/providers/notion/locales/ja.json +16 -0
- package/src/trigger/providers/notion/locales/ko.json +16 -0
- package/src/trigger/providers/notion/locales/ru.json +16 -0
- package/src/trigger/providers/notion/locales/zh-CN.json +16 -0
- package/src/trigger/providers/notion/locales/zh-TW.json +16 -0
- package/src/trigger/providers/one-drive/locales/fr.json +16 -0
- package/src/trigger/providers/one-drive/locales/ja.json +16 -0
- package/src/trigger/providers/one-drive/locales/ko.json +16 -0
- package/src/trigger/providers/one-drive/locales/ru.json +16 -0
- package/src/trigger/providers/one-drive/locales/zh-CN.json +16 -0
- package/src/trigger/providers/one-drive/locales/zh-TW.json +16 -0
- package/src/trigger/providers/outlook/locales/fr.json +18 -0
- package/src/trigger/providers/outlook/locales/ja.json +18 -0
- package/src/trigger/providers/outlook/locales/ko.json +18 -0
- package/src/trigger/providers/outlook/locales/ru.json +18 -0
- package/src/trigger/providers/outlook/locales/zh-CN.json +18 -0
- package/src/trigger/providers/outlook/locales/zh-TW.json +18 -0
- package/src/trigger/providers/pandadoc/locales/fr.json +12 -0
- package/src/trigger/providers/pandadoc/locales/ja.json +12 -0
- package/src/trigger/providers/pandadoc/locales/ko.json +12 -0
- package/src/trigger/providers/pandadoc/locales/ru.json +12 -0
- package/src/trigger/providers/pandadoc/locales/zh-CN.json +12 -0
- package/src/trigger/providers/pandadoc/locales/zh-TW.json +12 -0
- package/src/trigger/providers/pipedrive/locales/fr.json +15 -0
- package/src/trigger/providers/pipedrive/locales/ja.json +15 -0
- package/src/trigger/providers/pipedrive/locales/ko.json +15 -0
- package/src/trigger/providers/pipedrive/locales/ru.json +15 -0
- package/src/trigger/providers/pipedrive/locales/zh-CN.json +15 -0
- package/src/trigger/providers/pipedrive/locales/zh-TW.json +15 -0
- package/src/trigger/providers/remote.ts +163 -0
- package/src/trigger/providers/resend/locales/fr.json +16 -0
- package/src/trigger/providers/resend/locales/ja.json +16 -0
- package/src/trigger/providers/resend/locales/ko.json +16 -0
- package/src/trigger/providers/resend/locales/ru.json +16 -0
- package/src/trigger/providers/resend/locales/zh-CN.json +16 -0
- package/src/trigger/providers/resend/locales/zh-TW.json +16 -0
- package/src/trigger/providers/sentry/locales/fr.json +19 -0
- package/src/trigger/providers/sentry/locales/ja.json +19 -0
- package/src/trigger/providers/sentry/locales/ko.json +19 -0
- package/src/trigger/providers/sentry/locales/ru.json +19 -0
- package/src/trigger/providers/sentry/locales/zh-CN.json +19 -0
- package/src/trigger/providers/sentry/locales/zh-TW.json +19 -0
- package/src/trigger/providers/shopify/locales/fr.json +18 -0
- package/src/trigger/providers/shopify/locales/ja.json +18 -0
- package/src/trigger/providers/shopify/locales/ko.json +18 -0
- package/src/trigger/providers/shopify/locales/ru.json +18 -0
- package/src/trigger/providers/shopify/locales/zh-CN.json +18 -0
- package/src/trigger/providers/shopify/locales/zh-TW.json +18 -0
- package/src/trigger/providers/slack/locales/fr.json +18 -0
- package/src/trigger/providers/slack/locales/ja.json +18 -0
- package/src/trigger/providers/slack/locales/ko.json +18 -0
- package/src/trigger/providers/slack/locales/ru.json +18 -0
- package/src/trigger/providers/slack/locales/zh-CN.json +18 -0
- package/src/trigger/providers/slack/locales/zh-TW.json +18 -0
- package/src/trigger/providers/slackbot/locales/fr.json +23 -0
- package/src/trigger/providers/slackbot/locales/ja.json +23 -0
- package/src/trigger/providers/slackbot/locales/ko.json +23 -0
- package/src/trigger/providers/slackbot/locales/ru.json +23 -0
- package/src/trigger/providers/slackbot/locales/zh-CN.json +23 -0
- package/src/trigger/providers/slackbot/locales/zh-TW.json +23 -0
- package/src/trigger/providers/source-events.ts +25 -0
- package/src/trigger/providers/stripe/locales/fr.json +17 -0
- package/src/trigger/providers/stripe/locales/ja.json +17 -0
- package/src/trigger/providers/stripe/locales/ko.json +17 -0
- package/src/trigger/providers/stripe/locales/ru.json +17 -0
- package/src/trigger/providers/stripe/locales/zh-CN.json +17 -0
- package/src/trigger/providers/stripe/locales/zh-TW.json +17 -0
- package/src/trigger/providers/tally/locales/fr.json +12 -0
- package/src/trigger/providers/tally/locales/ja.json +12 -0
- package/src/trigger/providers/tally/locales/ko.json +12 -0
- package/src/trigger/providers/tally/locales/ru.json +12 -0
- package/src/trigger/providers/tally/locales/zh-CN.json +12 -0
- package/src/trigger/providers/tally/locales/zh-TW.json +12 -0
- package/src/trigger/providers/telegram/locales/fr.json +17 -0
- package/src/trigger/providers/telegram/locales/ja.json +17 -0
- package/src/trigger/providers/telegram/locales/ko.json +17 -0
- package/src/trigger/providers/telegram/locales/ru.json +17 -0
- package/src/trigger/providers/telegram/locales/zh-CN.json +17 -0
- package/src/trigger/providers/telegram/locales/zh-TW.json +17 -0
- package/src/trigger/providers/typeform/locales/fr.json +12 -0
- package/src/trigger/providers/typeform/locales/ja.json +12 -0
- package/src/trigger/providers/typeform/locales/ko.json +12 -0
- package/src/trigger/providers/typeform/locales/ru.json +12 -0
- package/src/trigger/providers/typeform/locales/zh-CN.json +12 -0
- package/src/trigger/providers/typeform/locales/zh-TW.json +12 -0
- package/src/trigger/providers/vercel/locales/fr.json +18 -0
- package/src/trigger/providers/vercel/locales/ja.json +18 -0
- package/src/trigger/providers/vercel/locales/ko.json +18 -0
- package/src/trigger/providers/vercel/locales/ru.json +18 -0
- package/src/trigger/providers/vercel/locales/zh-CN.json +18 -0
- package/src/trigger/providers/vercel/locales/zh-TW.json +18 -0
- package/src/trigger/providers/woocommerce/locales/fr.json +19 -0
- package/src/trigger/providers/woocommerce/locales/ja.json +19 -0
- package/src/trigger/providers/woocommerce/locales/ko.json +19 -0
- package/src/trigger/providers/woocommerce/locales/ru.json +19 -0
- package/src/trigger/providers/woocommerce/locales/zh-CN.json +19 -0
- package/src/trigger/providers/woocommerce/locales/zh-TW.json +19 -0
- package/src/trigger/providers/zendesk/locales/fr.json +15 -0
- package/src/trigger/providers/zendesk/locales/ja.json +15 -0
- package/src/trigger/providers/zendesk/locales/ko.json +15 -0
- package/src/trigger/providers/zendesk/locales/ru.json +15 -0
- package/src/trigger/providers/zendesk/locales/zh-CN.json +15 -0
- package/src/trigger/providers/zendesk/locales/zh-TW.json +15 -0
- package/src/trigger/providers/zoom/locales/fr.json +17 -0
- package/src/trigger/providers/zoom/locales/ja.json +17 -0
- package/src/trigger/providers/zoom/locales/ko.json +17 -0
- package/src/trigger/providers/zoom/locales/ru.json +17 -0
- package/src/trigger/providers/zoom/locales/zh-CN.json +17 -0
- package/src/trigger/providers/zoom/locales/zh-TW.json +17 -0
- package/src/types/index.ts +112 -0
- package/src/workflow/common/workflow.ts +273 -0
- package/dist/browser/lib-DrKav3HG.js +0 -11403
- /package/dist/{browser → common}/flow-authoring-edge.d.ts +0 -0
- /package/dist/{browser → common}/flow-authoring-module.d.ts +0 -0
- /package/dist/{browser → common}/flow-authoring.d.ts +0 -0
- /package/dist/{browser → common}/flow-input-value.d.ts +0 -0
- /package/dist/{browser → common}/webhook-method.d.ts +0 -0
|
@@ -0,0 +1,892 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BindingSource,
|
|
3
|
+
FlowDocument,
|
|
4
|
+
Graph,
|
|
5
|
+
GraphNode,
|
|
6
|
+
InputMapping,
|
|
7
|
+
InputPortDefinition,
|
|
8
|
+
JsonValue,
|
|
9
|
+
NodeSource,
|
|
10
|
+
PortDefinition,
|
|
11
|
+
RevisionContent,
|
|
12
|
+
SchemaMismatch,
|
|
13
|
+
TriggerNode,
|
|
14
|
+
} from './change.ts'
|
|
15
|
+
import type { Diagnostic } from './semantics.ts'
|
|
16
|
+
|
|
17
|
+
import { missingTriggerConfig, resolveTriggerConfig } from '../../trigger/common/config.ts'
|
|
18
|
+
import { triggerOutputDefinitions, triggerOutputPorts, webhookSupportsBody } from '../../trigger/common/contract.ts'
|
|
19
|
+
import { isTriggerNode, portsByHandle, validVariableName } from './change.ts'
|
|
20
|
+
import { conditionInputPorts, nodeInputMappings, otherwiseOutput, unaryOperator, comparisonIssue, valueType } from './condition.ts'
|
|
21
|
+
import { nodeSystemOutputPorts, supportsNodeErrorHandling } from './errorHandling.ts'
|
|
22
|
+
import { supportsNodeRetry, retryConfigurationSchema } from './retry.ts'
|
|
23
|
+
import { schemaObject, matchesSchema, comparePorts, portsAssignable, variableInputCompatible, hasRetiredRef } from './schema.ts'
|
|
24
|
+
import { sourceFields, sourcePort, sourceOutputLabel } from './sourceField.ts'
|
|
25
|
+
import { waitAutoContinueSchema } from './wait.ts'
|
|
26
|
+
import {
|
|
27
|
+
webhookResponseInputs,
|
|
28
|
+
validWebhookStatus,
|
|
29
|
+
webhookHeaderValues,
|
|
30
|
+
webhookHeaderNameIssue,
|
|
31
|
+
webhookHeaderValuePort,
|
|
32
|
+
webhookHeadersPort,
|
|
33
|
+
webhookResponseAllowsBody,
|
|
34
|
+
webhookStatusCodePort,
|
|
35
|
+
workflowOutputsPort,
|
|
36
|
+
} from './webhookResponse.ts'
|
|
37
|
+
|
|
38
|
+
export function isResolutionNode(node: GraphNode | undefined): node is Extract<GraphNode, { readonly kind: 'approval' | 'wait' }> {
|
|
39
|
+
return node?.kind == 'approval' || node?.kind == 'wait'
|
|
40
|
+
}
|
|
41
|
+
function graphDiagnostic(
|
|
42
|
+
code: string,
|
|
43
|
+
message: string,
|
|
44
|
+
path: string,
|
|
45
|
+
values?: Readonly<Record<string, string | number>>,
|
|
46
|
+
mismatch?: SchemaMismatch,
|
|
47
|
+
): Diagnostic {
|
|
48
|
+
return { code, column: 0, line: 1, message, mismatch, path, ...(values == null ? {} : { values }) }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Execution reachability is independent of data bindings and selected runtime branches. */
|
|
52
|
+
export function executionReachableNodes(graph: Graph, source: string): ReadonlySet<string> {
|
|
53
|
+
const reachable = new Set([source])
|
|
54
|
+
for (const id of reachable) for (const edge of graph.edges) if (edge.source === id) reachable.add(edge.target)
|
|
55
|
+
return reachable
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function webhookResponseDiagnostics(graph: Graph, triggerId: string, path = `/document/graph/nodes/${triggerId}`): readonly Diagnostic[] {
|
|
59
|
+
const trigger = graph.nodes[triggerId]
|
|
60
|
+
if (trigger?.kind != 'webhook') return []
|
|
61
|
+
const options = trigger.options ?? {}
|
|
62
|
+
const response = options.responseBody
|
|
63
|
+
const statusOutput = options.responseStatusCodeOutput
|
|
64
|
+
const statusPath = `${path}/options/responseStatusCodeOutput`
|
|
65
|
+
const diagnostics: Diagnostic[] = []
|
|
66
|
+
const headersSource = options.responseHeadersSource
|
|
67
|
+
const headerOutputs = options.responseHeaderOutputs
|
|
68
|
+
const headersPath = `${path}/options/${headersSource == null ? 'responseHeaderOutputs' : 'responseHeadersSource'}`
|
|
69
|
+
if (response?.kind !== 'workflow-outputs') {
|
|
70
|
+
if (statusOutput != null)
|
|
71
|
+
diagnostics.push(
|
|
72
|
+
graphDiagnostic('trigger.webhook-response-status-source', 'Choose workflow outputs as the response source to use a status output.', statusPath),
|
|
73
|
+
)
|
|
74
|
+
if (headersSource != null || headerOutputs != null)
|
|
75
|
+
diagnostics.push(
|
|
76
|
+
graphDiagnostic('trigger.webhook-response-headers-source', 'Choose workflow outputs as the response source to use dynamic headers.', headersPath),
|
|
77
|
+
)
|
|
78
|
+
if (response?.kind === 'webhook-response') {
|
|
79
|
+
const location = `${path}/options/responseBody/nodeId`
|
|
80
|
+
const { nodeId } = response
|
|
81
|
+
if (nodeId == null || graph.nodes[nodeId]?.kind !== 'webhook-response')
|
|
82
|
+
diagnostics.push(graphDiagnostic('trigger.webhook-response-node-missing', 'Select a Webhook Response node.', location))
|
|
83
|
+
else if (!executionReachableNodes(graph, triggerId).has(nodeId))
|
|
84
|
+
diagnostics.push(graphDiagnostic('trigger.webhook-response-node-unreachable', 'Connect this Webhook to the selected Webhook Response node.', location))
|
|
85
|
+
}
|
|
86
|
+
return diagnostics
|
|
87
|
+
}
|
|
88
|
+
if (headersSource == null) {
|
|
89
|
+
for (const [headers, location] of [
|
|
90
|
+
[options.responseHeaders, `${path}/options/responseHeaders`],
|
|
91
|
+
[headerOutputs, headersPath],
|
|
92
|
+
] as const) {
|
|
93
|
+
const names: string[] = []
|
|
94
|
+
for (const name of Object.keys(headers ?? {})) {
|
|
95
|
+
if (webhookHeaderNameIssue(name, names) != null)
|
|
96
|
+
diagnostics.push(graphDiagnostic('trigger.webhook-response-header-name', 'Use valid, unique HTTP header names.', location))
|
|
97
|
+
names.push(name)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const responsePath = `${path}/options/responseBody`
|
|
102
|
+
const outputs = Object.entries(graph.nodes).filter(([, node]) => node.kind == 'outputs')
|
|
103
|
+
if (outputs.length == 0) {
|
|
104
|
+
diagnostics.push(graphDiagnostic('trigger.webhook-response-outputs-missing', 'Add a workflow Outputs node for the HTTP response.', responsePath))
|
|
105
|
+
} else if (outputs.length == 1) {
|
|
106
|
+
const output = outputs[0]!
|
|
107
|
+
const reachable = executionReachableNodes(graph, triggerId)
|
|
108
|
+
if (!reachable.has(output[0]))
|
|
109
|
+
diagnostics.push(graphDiagnostic('trigger.webhook-response-unreachable', 'Connect this Webhook to the workflow Outputs node.', responsePath))
|
|
110
|
+
const field = response.output
|
|
111
|
+
if (field != null && output[1].kind == 'outputs' && !output[1].outputDefs.some((port) => port.handle == field))
|
|
112
|
+
diagnostics.push(
|
|
113
|
+
graphDiagnostic('trigger.webhook-response-output-missing', `Workflow output "${field}" does not exist.`, `${responsePath}/output`, { output: field }),
|
|
114
|
+
)
|
|
115
|
+
if (statusOutput != null && output[1].kind == 'outputs') {
|
|
116
|
+
const port = output[1].outputDefs.find((candidate) => candidate.handle == statusOutput)
|
|
117
|
+
if (port == null)
|
|
118
|
+
diagnostics.push(
|
|
119
|
+
graphDiagnostic('trigger.webhook-response-status-output-missing', `Workflow status output "${statusOutput}" does not exist.`, statusPath, {
|
|
120
|
+
output: statusOutput,
|
|
121
|
+
}),
|
|
122
|
+
)
|
|
123
|
+
else if (!portsAssignable(port, webhookStatusCodePort))
|
|
124
|
+
diagnostics.push(
|
|
125
|
+
graphDiagnostic('trigger.webhook-response-status-output-type', 'Choose a numeric workflow output for the response status code.', statusPath),
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
if (output[1].kind == 'outputs') {
|
|
129
|
+
const fields = output[1].outputDefs
|
|
130
|
+
const checkHeaderSource = (headerField: string | undefined, target: PortDefinition, location: string) => {
|
|
131
|
+
const port = headerField == null ? workflowOutputsPort(fields) : fields.find((candidate) => candidate.handle == headerField)
|
|
132
|
+
if (port == null)
|
|
133
|
+
diagnostics.push(
|
|
134
|
+
graphDiagnostic('trigger.webhook-response-header-output-missing', `Workflow output "${headerField}" does not exist.`, location, {
|
|
135
|
+
output: headerField!,
|
|
136
|
+
}),
|
|
137
|
+
)
|
|
138
|
+
else if (!portsAssignable(port, target))
|
|
139
|
+
diagnostics.push(
|
|
140
|
+
graphDiagnostic('trigger.webhook-response-header-output-type', 'Choose a compatible workflow output for the response headers.', location),
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
if (headersSource != null) checkHeaderSource(headersSource.output, webhookHeadersPort, headersPath)
|
|
144
|
+
else
|
|
145
|
+
for (const [name, headerField] of Object.entries(headerOutputs ?? {}))
|
|
146
|
+
checkHeaderSource(headerField, webhookHeaderValuePort, `${headersPath}/${name.replaceAll('~', '~0').replaceAll('/', '~1')}`)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (statusOutput == null && !webhookResponseAllowsBody(options.responseStatusCode ?? 200))
|
|
150
|
+
diagnostics.push(
|
|
151
|
+
graphDiagnostic('trigger.webhook-response-status-invalid', 'Choose a response status code that permits a body.', `${path}/options/responseStatusCode`),
|
|
152
|
+
)
|
|
153
|
+
return diagnostics
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function validateTrigger(triggerId: string, trigger: TriggerNode, path: string, diagnostics: Diagnostic[]): void {
|
|
157
|
+
if (trigger.kind == 'start') return
|
|
158
|
+
const outputs = triggerOutputDefinitions(trigger)
|
|
159
|
+
if (outputs.some((port) => hasRetiredRef(port.jsonSchema)))
|
|
160
|
+
diagnostics.push(graphDiagnostic('graph.schema-unsupported', 'Runtime Ref schemas are not supported.', path))
|
|
161
|
+
const outputHandles = outputs.map((port) => port.handle)
|
|
162
|
+
if (new Set(outputHandles).size !== outputHandles.length)
|
|
163
|
+
diagnostics.push(graphDiagnostic('graph.port-duplicate', 'Trigger output handles must be unique.', `${path}/definition/outputs`))
|
|
164
|
+
if (trigger.kind == 'webhook') {
|
|
165
|
+
if (!webhookSupportsBody(trigger.method) && trigger.bodyFields.length > 0) {
|
|
166
|
+
diagnostics.push(graphDiagnostic('trigger.webhook-body-unsupported', 'GET Webhook triggers cannot declare request body fields.', `${path}/bodyFields`))
|
|
167
|
+
}
|
|
168
|
+
const handles = new Set<string>()
|
|
169
|
+
for (const [index, input] of trigger.bodyFields.entries()) {
|
|
170
|
+
if (handles.has(input.handle)) {
|
|
171
|
+
diagnostics.push(
|
|
172
|
+
graphDiagnostic(
|
|
173
|
+
'trigger.input-duplicate',
|
|
174
|
+
`Webhook Trigger input "${input.handle}" is declared more than once.`,
|
|
175
|
+
`${path}/bodyFields/${index}/handle`,
|
|
176
|
+
{ input: input.handle },
|
|
177
|
+
),
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
handles.add(input.handle)
|
|
181
|
+
}
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
if (trigger.kind == 'error' || trigger.kind == 'cron' || trigger.kind == 'manual') return
|
|
185
|
+
if (trigger.connectionId == null)
|
|
186
|
+
diagnostics.push(graphDiagnostic('trigger.connection-missing', 'Select a connection account for this Trigger.', `${path}/connectionId`))
|
|
187
|
+
const missingConfig = missingTriggerConfig(trigger.definition.configInputs, trigger.config)
|
|
188
|
+
if (missingConfig.length > 0) {
|
|
189
|
+
diagnostics.push({
|
|
190
|
+
...graphDiagnostic('trigger.config-incomplete', `Complete the required Trigger config fields: ${missingConfig.join(', ')}.`, `${path}/config`, {
|
|
191
|
+
fields: missingConfig.join(', '),
|
|
192
|
+
}),
|
|
193
|
+
fields: missingConfig,
|
|
194
|
+
})
|
|
195
|
+
} else {
|
|
196
|
+
try {
|
|
197
|
+
resolveTriggerConfig(trigger.definition.configInputs, trigger.config)
|
|
198
|
+
} catch {
|
|
199
|
+
diagnostics.push(
|
|
200
|
+
graphDiagnostic('trigger.config-invalid', `Trigger "${triggerId}" config does not match its fixed definition.`, `${path}/config`, { triggerId }),
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function nodeInputPorts(node: GraphNode): Readonly<Record<string, InputPortDefinition>> {
|
|
207
|
+
switch (node.kind) {
|
|
208
|
+
case 'webhook-response':
|
|
209
|
+
return portsByHandle(webhookResponseInputs)
|
|
210
|
+
case 'outputs':
|
|
211
|
+
return portsByHandle(node.outputDefs)
|
|
212
|
+
case 'condition':
|
|
213
|
+
return conditionInputPorts(node)
|
|
214
|
+
case 'value':
|
|
215
|
+
return {}
|
|
216
|
+
case 'task':
|
|
217
|
+
return portsByHandle([...node.task.inputs, ...(node.additionalInputs ?? [])])
|
|
218
|
+
case 'approval':
|
|
219
|
+
case 'wait':
|
|
220
|
+
return portsByHandle(node.inputDefinitions)
|
|
221
|
+
case 'cron':
|
|
222
|
+
case 'integration':
|
|
223
|
+
case 'poll':
|
|
224
|
+
case 'error':
|
|
225
|
+
case 'manual':
|
|
226
|
+
case 'start':
|
|
227
|
+
case 'webhook':
|
|
228
|
+
return {}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function resolutionActions(node: Extract<GraphNode, { readonly kind: 'approval' | 'wait' }>): readonly ['continue'] | readonly ['approve', 'reject'] {
|
|
233
|
+
return node.kind == 'wait' ? ['continue'] : ['approve', 'reject']
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const objectPort = (properties: Record<string, JsonValue>): PortDefinition => ({
|
|
237
|
+
nullable: false,
|
|
238
|
+
jsonSchema: { type: 'object', properties, required: Object.keys(properties), additionalProperties: false },
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
export function resolutionOutputPorts(node: Extract<GraphNode, { readonly kind: 'approval' | 'wait' }>): Readonly<Record<string, PortDefinition>> {
|
|
242
|
+
const actions = resolutionActions(node)
|
|
243
|
+
const inputs = {
|
|
244
|
+
type: 'object',
|
|
245
|
+
properties: Object.fromEntries(node.inputDefinitions.map((port) => [port.handle, {}])),
|
|
246
|
+
required: node.inputDefinitions.map((port) => port.handle),
|
|
247
|
+
additionalProperties: false,
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
pending: objectPort({
|
|
252
|
+
inputs,
|
|
253
|
+
prompt: { type: 'string' },
|
|
254
|
+
pageUrl: { type: 'string' },
|
|
255
|
+
message: { type: 'string', minLength: 1, pattern: '\\S' },
|
|
256
|
+
...Object.fromEntries(actions.map((action) => [`${action}Url`, { type: 'string' }])),
|
|
257
|
+
expiresAt: { type: 'string' },
|
|
258
|
+
...(node.kind == 'wait' && node.autoContinue != null ? { autoContinueAt: { type: 'string' } } : {}),
|
|
259
|
+
}),
|
|
260
|
+
...Object.fromEntries(
|
|
261
|
+
actions.map((action) => [
|
|
262
|
+
action,
|
|
263
|
+
objectPort({
|
|
264
|
+
inputs,
|
|
265
|
+
action: { const: action },
|
|
266
|
+
resolvedAt: { type: 'string' },
|
|
267
|
+
comment: { type: ['string', 'null'] },
|
|
268
|
+
}),
|
|
269
|
+
]),
|
|
270
|
+
),
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function nodeOutputPorts(node: GraphNode): Readonly<Record<string, PortDefinition>> {
|
|
275
|
+
switch (node.kind) {
|
|
276
|
+
case 'webhook-response':
|
|
277
|
+
case 'outputs':
|
|
278
|
+
case 'condition':
|
|
279
|
+
return {}
|
|
280
|
+
case 'value':
|
|
281
|
+
return portsByHandle(node.values)
|
|
282
|
+
case 'task':
|
|
283
|
+
return portsByHandle(node.task.outputs)
|
|
284
|
+
case 'approval':
|
|
285
|
+
case 'wait':
|
|
286
|
+
return resolutionOutputPorts(node)
|
|
287
|
+
case 'cron':
|
|
288
|
+
case 'integration':
|
|
289
|
+
case 'poll':
|
|
290
|
+
case 'error':
|
|
291
|
+
case 'manual':
|
|
292
|
+
case 'start':
|
|
293
|
+
case 'webhook':
|
|
294
|
+
return triggerOutputPorts(node)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function nodeSourceOutputPorts(node: GraphNode, system?: true): Readonly<Record<string, PortDefinition>> {
|
|
299
|
+
if (system) return nodeSystemOutputPorts(node)
|
|
300
|
+
const ports = nodeOutputPorts(node)
|
|
301
|
+
return node.onError == 'error' ? Object.fromEntries(Object.entries(ports).map(([key, port]) => [key, { ...port, nullable: true }])) : ports
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function nodeOutputDescription(graph: Graph, nodeId: string, output: string): string | undefined {
|
|
305
|
+
const node = graph.nodes[nodeId]
|
|
306
|
+
return node == null ? undefined : nodeOutputPorts(node)[output]?.description
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function checkSource(
|
|
310
|
+
source: BindingSource | NodeSource,
|
|
311
|
+
graph: Graph,
|
|
312
|
+
document: FlowDocument,
|
|
313
|
+
targetInput: InputPortDefinition | undefined,
|
|
314
|
+
path: string,
|
|
315
|
+
diagnostics: Diagnostic[],
|
|
316
|
+
): PortDefinition | undefined {
|
|
317
|
+
switch (source.kind) {
|
|
318
|
+
case 'binding':
|
|
319
|
+
if (document.bindings[source.bindingId] == null) {
|
|
320
|
+
diagnostics.push(graphDiagnostic('graph.binding-missing', `Binding "${source.bindingId}" does not exist.`, path, { bindingId: source.bindingId }))
|
|
321
|
+
} else if (document.bindings[source.bindingId].kind != 'variable') {
|
|
322
|
+
diagnostics.push(graphDiagnostic('graph.binding-invalid', `Binding "${source.bindingId}" must be a variable.`, path, { bindingId: source.bindingId }))
|
|
323
|
+
} else if (targetInput != null && !variableInputCompatible(targetInput.jsonSchema)) {
|
|
324
|
+
diagnostics.push(
|
|
325
|
+
graphDiagnostic('graph.variable-incompatible', `Variable binding "${source.bindingId}" is not compatible with this input.`, path, {
|
|
326
|
+
bindingId: source.bindingId,
|
|
327
|
+
}),
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
return
|
|
331
|
+
case 'node': {
|
|
332
|
+
const upstream = graph.nodes[source.nodeId]
|
|
333
|
+
if (upstream == null) {
|
|
334
|
+
diagnostics.push(
|
|
335
|
+
graphDiagnostic('graph.source-missing', `Upstream node "${source.nodeId}" does not exist.`, path, {
|
|
336
|
+
nodeId: source.nodeId,
|
|
337
|
+
variant: 'node',
|
|
338
|
+
}),
|
|
339
|
+
)
|
|
340
|
+
return
|
|
341
|
+
}
|
|
342
|
+
const output = nodeSourceOutputPorts(upstream, source.system)[source.output]
|
|
343
|
+
if (output == null) {
|
|
344
|
+
diagnostics.push(
|
|
345
|
+
graphDiagnostic('graph.source-missing', `Upstream node "${source.nodeId}" does not expose output "${source.output}".`, path, {
|
|
346
|
+
nodeId: source.nodeId,
|
|
347
|
+
output: source.output,
|
|
348
|
+
variant: 'output',
|
|
349
|
+
}),
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
if (output == null) return
|
|
353
|
+
const selected = sourcePort(output, source.field)
|
|
354
|
+
if (selected == null) {
|
|
355
|
+
diagnostics.push(
|
|
356
|
+
graphDiagnostic('graph.source-missing', `Output "${source.output}" does not declare field ${JSON.stringify(source.field)}.`, path, {
|
|
357
|
+
nodeId: source.nodeId,
|
|
358
|
+
output: source.output,
|
|
359
|
+
field: source.field!,
|
|
360
|
+
variant: 'field',
|
|
361
|
+
}),
|
|
362
|
+
)
|
|
363
|
+
return
|
|
364
|
+
}
|
|
365
|
+
if (targetInput != null) {
|
|
366
|
+
const comparison = comparePorts(selected, targetInput)
|
|
367
|
+
if (comparison.kind != 'compatible') {
|
|
368
|
+
diagnostics.push(
|
|
369
|
+
graphDiagnostic(
|
|
370
|
+
'graph.node-output-incompatible',
|
|
371
|
+
`Upstream node "${source.nodeId}" output ${JSON.stringify(sourceOutputLabel(source))} is not compatible with this input.`,
|
|
372
|
+
path,
|
|
373
|
+
{ nodeId: source.nodeId, output: source.output, ...(source.field === undefined ? {} : { field: source.field }) },
|
|
374
|
+
comparison.kind == 'incompatible' ? comparison.mismatch : undefined,
|
|
375
|
+
),
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return selected
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function graphOrder(graph: Graph): readonly string[] {
|
|
385
|
+
const pending = new Map(Object.keys(graph.nodes).map((id) => [id, new Set<string>()]))
|
|
386
|
+
const children = new Map<string, Set<string>>()
|
|
387
|
+
for (const edge of graph.edges) {
|
|
388
|
+
if (!pending.has(edge.source) || !pending.has(edge.target)) continue
|
|
389
|
+
pending.get(edge.target)?.add(edge.source)
|
|
390
|
+
const targets = children.get(edge.source) ?? new Set<string>()
|
|
391
|
+
targets.add(edge.target)
|
|
392
|
+
children.set(edge.source, targets)
|
|
393
|
+
}
|
|
394
|
+
const ready = [...pending]
|
|
395
|
+
.filter(([, parents]) => parents.size == 0)
|
|
396
|
+
.map(([id]) => id)
|
|
397
|
+
.toSorted()
|
|
398
|
+
for (let index = 0; index < ready.length; index++) {
|
|
399
|
+
const id = ready[index]!
|
|
400
|
+
pending.delete(id)
|
|
401
|
+
for (const child of children.get(id) ?? []) {
|
|
402
|
+
const parents = pending.get(child)
|
|
403
|
+
parents?.delete(id)
|
|
404
|
+
if (parents?.size == 0) ready.push(child)
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return [...ready, ...[...pending.keys()].toSorted()]
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const pathsByGraph = new WeakMap<
|
|
411
|
+
Graph,
|
|
412
|
+
{
|
|
413
|
+
readonly paths: Map<string, readonly Route[]>
|
|
414
|
+
readonly ancestors: Map<string, Set<string>>
|
|
415
|
+
readonly resolutionOutputs: Map<string, Set<string>>
|
|
416
|
+
}
|
|
417
|
+
>()
|
|
418
|
+
|
|
419
|
+
// Each route fixes branch choices by node ID; $trigger identifies the selected trigger.
|
|
420
|
+
// An absent key leaves that choice unrestricted, so {} covers every execution path.
|
|
421
|
+
type Route = Readonly<Record<string, string>>
|
|
422
|
+
|
|
423
|
+
function routeCovers(route: Route, target: Route): boolean {
|
|
424
|
+
return Object.entries(route).every(([key, value]) => target[key] == value)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function routesCompatible(left: Route, right: Route): boolean {
|
|
428
|
+
return Object.entries(left).every(([key, value]) => right[key] == null || right[key] == value)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function resolutionOutputKey(nodeId: string, output: string): string {
|
|
432
|
+
return JSON.stringify([nodeId, output])
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function graphPaths(graph: Graph) {
|
|
436
|
+
const cached = pathsByGraph.get(graph)
|
|
437
|
+
if (cached != null) return cached
|
|
438
|
+
const paths = new Map<string, readonly Route[]>()
|
|
439
|
+
const ancestors = new Map<string, Set<string>>()
|
|
440
|
+
const resolutionOutputs = new Map<string, Set<string>>()
|
|
441
|
+
const incoming = new Map<string, Graph['edges'][number][]>()
|
|
442
|
+
for (const edge of graph.edges) {
|
|
443
|
+
const edges = incoming.get(edge.target) ?? []
|
|
444
|
+
edges.push(edge)
|
|
445
|
+
incoming.set(edge.target, edges)
|
|
446
|
+
}
|
|
447
|
+
const order = graphOrder(graph)
|
|
448
|
+
let changed = true
|
|
449
|
+
while (changed) {
|
|
450
|
+
changed = false
|
|
451
|
+
for (const id of order) {
|
|
452
|
+
const node = graph.nodes[id]!
|
|
453
|
+
const edges = incoming.get(id) ?? []
|
|
454
|
+
const parents = new Set<string>()
|
|
455
|
+
const resolutions = new Set<string>()
|
|
456
|
+
const routes: Route[] = []
|
|
457
|
+
for (const edge of edges) {
|
|
458
|
+
parents.add(edge.source)
|
|
459
|
+
for (const output of resolutionOutputs.get(edge.source) ?? []) resolutions.add(output)
|
|
460
|
+
if (isResolutionNode(graph.nodes[edge.source]) && edge.sourceHandle != null) resolutions.add(resolutionOutputKey(edge.source, edge.sourceHandle))
|
|
461
|
+
for (const parent of ancestors.get(edge.source) ?? []) parents.add(parent)
|
|
462
|
+
for (const route of paths.get(edge.source) ?? []) {
|
|
463
|
+
const next =
|
|
464
|
+
edge.sourceHandle == null || (isResolutionNode(graph.nodes[edge.source]) && edge.sourceHandle == 'pending')
|
|
465
|
+
? route
|
|
466
|
+
: {
|
|
467
|
+
...route,
|
|
468
|
+
[edge.source]:
|
|
469
|
+
graph.nodes[edge.source]?.kind == 'condition' &&
|
|
470
|
+
(graph.nodes[edge.source] as import('./change.ts').ConditionNode).matchMode == 'all' &&
|
|
471
|
+
edge.sourceHandle != otherwiseOutput
|
|
472
|
+
? '$matched'
|
|
473
|
+
: edge.sourceHandle,
|
|
474
|
+
}
|
|
475
|
+
if (routes.some((known) => routeCovers(known, next))) continue
|
|
476
|
+
for (let index = routes.length - 1; index >= 0; index--) {
|
|
477
|
+
if (routeCovers(next, routes[index]!)) routes.splice(index, 1)
|
|
478
|
+
}
|
|
479
|
+
routes.push(next)
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
const nextRoutes = edges.length == 0 ? (isTriggerNode(node) ? [{ $trigger: id }] : [{}]) : routes
|
|
483
|
+
const previous = paths.get(id) ?? []
|
|
484
|
+
if (
|
|
485
|
+
parents.size != (ancestors.get(id)?.size ?? 0) ||
|
|
486
|
+
resolutions.size != (resolutionOutputs.get(id)?.size ?? 0) ||
|
|
487
|
+
nextRoutes.length != previous.length ||
|
|
488
|
+
nextRoutes.some((route) => !previous.some((known) => routeCovers(known, route) && routeCovers(route, known)))
|
|
489
|
+
)
|
|
490
|
+
changed = true
|
|
491
|
+
ancestors.set(id, parents)
|
|
492
|
+
resolutionOutputs.set(id, resolutions)
|
|
493
|
+
paths.set(id, nextRoutes)
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
const analysis = { ancestors, paths, resolutionOutputs }
|
|
497
|
+
pathsByGraph.set(graph, analysis)
|
|
498
|
+
return analysis
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function sourcePaths(graph: Graph, paths: ReturnType<typeof graphPaths>['paths'], source: BindingSource | NodeSource) {
|
|
502
|
+
if (source.kind != 'node') return [{}]
|
|
503
|
+
const node = graph.nodes[source.nodeId]
|
|
504
|
+
const routes = paths.get(source.nodeId) ?? []
|
|
505
|
+
return node?.kind == 'condition' || (isResolutionNode(node) && source.output != 'pending')
|
|
506
|
+
? routes.map((route) => ({ ...route, [source.nodeId]: source.output }))
|
|
507
|
+
: routes
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function mappingAvailable(graph: Graph, target: string | undefined, mapping: InputMapping, analysis: ReturnType<typeof graphPaths>): boolean {
|
|
511
|
+
if (mapping.kind != 'sources') return true
|
|
512
|
+
const { ancestors, paths } = analysis
|
|
513
|
+
if (target != null && mapping.sources.some((source) => source.kind == 'node' && !ancestors.get(target)?.has(source.nodeId))) return false
|
|
514
|
+
if (
|
|
515
|
+
target != null &&
|
|
516
|
+
mapping.sources.some(
|
|
517
|
+
(source) =>
|
|
518
|
+
source.kind == 'node' &&
|
|
519
|
+
isResolutionNode(graph.nodes[source.nodeId]) &&
|
|
520
|
+
!analysis.resolutionOutputs.get(target)?.has(resolutionOutputKey(source.nodeId, source.output)),
|
|
521
|
+
)
|
|
522
|
+
)
|
|
523
|
+
return false
|
|
524
|
+
const sources = mapping.sources.map((source) => sourcePaths(graph, paths, source))
|
|
525
|
+
const targetPaths = target == null ? [{}] : (paths.get(target) ?? [])
|
|
526
|
+
// Sources may be absent, but must never overlap on the same execution path.
|
|
527
|
+
for (const [index, routes] of sources.entries()) {
|
|
528
|
+
for (const other of sources.slice(index + 1)) {
|
|
529
|
+
for (const left of routes) {
|
|
530
|
+
for (const targetRoute of targetPaths) {
|
|
531
|
+
if (!routesCompatible(left, targetRoute)) continue
|
|
532
|
+
const path = { ...targetRoute, ...left }
|
|
533
|
+
if (other.some((right) => routesCompatible(right, path))) return false
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return true
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export type InputSourceCheck =
|
|
542
|
+
| { readonly kind: 'available' }
|
|
543
|
+
| { readonly kind: 'source-missing' }
|
|
544
|
+
| { readonly kind: 'output-missing' }
|
|
545
|
+
| { readonly kind: 'field-missing' }
|
|
546
|
+
| { readonly kind: 'not-ready' }
|
|
547
|
+
| { readonly kind: 'schema'; readonly mismatch: SchemaMismatch }
|
|
548
|
+
| { readonly kind: 'schema-error' }
|
|
549
|
+
|
|
550
|
+
export type InputSourceCandidateCheck = Extract<InputSourceCheck, { readonly kind: 'available' | 'schema' | 'schema-error' }>
|
|
551
|
+
|
|
552
|
+
interface InputSourceFieldCandidate {
|
|
553
|
+
readonly system?: true
|
|
554
|
+
readonly description?: string
|
|
555
|
+
readonly output: string
|
|
556
|
+
readonly field: string
|
|
557
|
+
readonly check: InputSourceCandidateCheck
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export interface InputSourceCandidate {
|
|
561
|
+
readonly system?: true
|
|
562
|
+
readonly description?: string
|
|
563
|
+
readonly field?: string
|
|
564
|
+
readonly fields?: readonly InputSourceFieldCandidate[]
|
|
565
|
+
readonly output: string
|
|
566
|
+
readonly check: InputSourceCandidateCheck
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export interface InputSourcesCheck {
|
|
570
|
+
readonly conflict: boolean
|
|
571
|
+
readonly sources: readonly InputSourceCheck[]
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function sourceCompatibility(source: PortDefinition, input: PortDefinition): InputSourceCandidateCheck {
|
|
575
|
+
const result = comparePorts(source, input)
|
|
576
|
+
if (result.kind == 'compatible') return { kind: 'available' }
|
|
577
|
+
return result.kind == 'incompatible' ? { kind: 'schema', mismatch: result.mismatch } : { kind: 'schema-error' }
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/** Check one saved binding without enumerating candidate ports. */
|
|
581
|
+
export function checkInputSource(
|
|
582
|
+
graph: Graph,
|
|
583
|
+
target: string,
|
|
584
|
+
handle: string,
|
|
585
|
+
source: Pick<NodeSource, 'nodeId' | 'output' | 'field' | 'system'>,
|
|
586
|
+
): InputSourceCheck {
|
|
587
|
+
const node = graph.nodes[source.nodeId]
|
|
588
|
+
const targetNode = graph.nodes[target]
|
|
589
|
+
if (node == null) return { kind: 'source-missing' }
|
|
590
|
+
if (targetNode == null) return { kind: 'not-ready' }
|
|
591
|
+
const output = nodeSourceOutputPorts(node, source.system)[source.output]
|
|
592
|
+
const input = nodeInputPorts(targetNode)[handle]
|
|
593
|
+
if (output == null) return { kind: 'output-missing' }
|
|
594
|
+
if (input == null) return { kind: 'not-ready' }
|
|
595
|
+
const analysis = graphPaths(graph)
|
|
596
|
+
if (analysis.ancestors.get(target)?.has(source.nodeId) !== true) return { kind: 'not-ready' }
|
|
597
|
+
if (isResolutionNode(node) && !analysis.resolutionOutputs.get(target)?.has(resolutionOutputKey(source.nodeId, source.output))) return { kind: 'not-ready' }
|
|
598
|
+
const selected = sourcePort(output, source.field)
|
|
599
|
+
if (selected == null) return { kind: 'field-missing' }
|
|
600
|
+
return sourceCompatibility(selected, input)
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
export function checkInputSources(
|
|
604
|
+
graph: Graph,
|
|
605
|
+
target: string,
|
|
606
|
+
handle: string,
|
|
607
|
+
sources: readonly Pick<NodeSource, 'nodeId' | 'output' | 'field' | 'system'>[],
|
|
608
|
+
): InputSourcesCheck {
|
|
609
|
+
const checks = sources.map((source) => checkInputSource(graph, target, handle, source))
|
|
610
|
+
const conflict =
|
|
611
|
+
checks.length > 1 &&
|
|
612
|
+
checks.every((check) => check.kind == 'available') &&
|
|
613
|
+
!mappingAvailable(graph, target, { kind: 'sources', sources: sources.map((source) => ({ kind: 'node', ...source })) }, graphPaths(graph))
|
|
614
|
+
return { conflict, sources: checks }
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** Enumerate structurally available outputs while retaining their compatibility with one input. */
|
|
618
|
+
export function inputSourceCandidates(graph: Graph, target: string, handle: string): Readonly<Record<string, readonly InputSourceCandidate[]>> {
|
|
619
|
+
const targetNode = graph.nodes[target]
|
|
620
|
+
const input = targetNode == null ? undefined : nodeInputPorts(targetNode)[handle]
|
|
621
|
+
if (input == null) return {}
|
|
622
|
+
const analysis = graphPaths(graph)
|
|
623
|
+
return Object.fromEntries(
|
|
624
|
+
[...(analysis.ancestors.get(target) ?? [])].flatMap((nodeId) => {
|
|
625
|
+
const node = graph.nodes[nodeId]
|
|
626
|
+
if (node == null) return []
|
|
627
|
+
const ports = [
|
|
628
|
+
...Object.entries(nodeSourceOutputPorts(node)).map(([output, definition]) => ({ output, definition, system: undefined })),
|
|
629
|
+
...Object.entries(nodeSystemOutputPorts(node)).map(([output, definition]) => ({ output, definition, system: true as const })),
|
|
630
|
+
]
|
|
631
|
+
const outputs = ports.flatMap(({ output, definition, system }) => {
|
|
632
|
+
if (!mappingAvailable(graph, target, { kind: 'sources', sources: [{ kind: 'node', nodeId, output, ...(system ? { system } : {}) }] }, analysis))
|
|
633
|
+
return []
|
|
634
|
+
const check = sourceCompatibility(definition, input)
|
|
635
|
+
const fields = sourceFields(definition).map(({ field, port }) =>
|
|
636
|
+
Object.assign(
|
|
637
|
+
{ output, field, ...(system ? { system } : {}), check: sourceCompatibility(port, input) },
|
|
638
|
+
port.description == null ? {} : { description: port.description },
|
|
639
|
+
),
|
|
640
|
+
)
|
|
641
|
+
return [
|
|
642
|
+
{
|
|
643
|
+
...(system ? { system } : {}),
|
|
644
|
+
...(definition.description == null ? {} : { description: definition.description }),
|
|
645
|
+
output,
|
|
646
|
+
check,
|
|
647
|
+
...(fields.length > 0 ? { fields } : {}),
|
|
648
|
+
},
|
|
649
|
+
]
|
|
650
|
+
})
|
|
651
|
+
return outputs.length == 0 ? [] : [[nodeId, outputs]]
|
|
652
|
+
}),
|
|
653
|
+
)
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
export function availableOutputs(graph: Graph, target: string, handle: string | undefined): Readonly<Record<string, readonly string[]>> {
|
|
657
|
+
const analysis = graphPaths(graph)
|
|
658
|
+
return Object.fromEntries(
|
|
659
|
+
[...(analysis.ancestors.get(target) ?? [])].flatMap((id) => {
|
|
660
|
+
const node = graph.nodes[id]
|
|
661
|
+
if (node == null) return []
|
|
662
|
+
const outputs = Object.keys(nodeOutputPorts(node)).filter(
|
|
663
|
+
(output) =>
|
|
664
|
+
mappingAvailable(graph, target, { kind: 'sources', sources: [{ kind: 'node', nodeId: id, output }] }, analysis) &&
|
|
665
|
+
(handle == null || portsAssignable(nodeOutputPorts(node)[output]!, nodeInputPorts(graph.nodes[target]!)[handle]!)),
|
|
666
|
+
)
|
|
667
|
+
return outputs.length == 0 ? [] : [[id, outputs]]
|
|
668
|
+
}),
|
|
669
|
+
)
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function validateResolution(nodeId: string, node: Extract<GraphNode, { readonly kind: 'approval' | 'wait' }>, path: string, diagnostics: Diagnostic[]): void {
|
|
673
|
+
const fields = new Set(['description', 'icon', 'inputDefinitions', 'inputs', 'kind', 'maxExecutions', 'name', 'prompt'])
|
|
674
|
+
if (node.kind == 'wait') {
|
|
675
|
+
fields.add('autoContinue')
|
|
676
|
+
if (node.autoContinue != null && !waitAutoContinueSchema.safeParse(node.autoContinue).success)
|
|
677
|
+
diagnostics.push(
|
|
678
|
+
graphDiagnostic('wait.auto-continue-invalid', 'Enter a positive integer duration or a timestamp with an explicit timezone.', `${path}/autoContinue`),
|
|
679
|
+
)
|
|
680
|
+
}
|
|
681
|
+
const label = node.kind == 'wait' ? 'Wait' : 'Approval'
|
|
682
|
+
const unsupported = Object.keys(node).filter((field) => !fields.has(field))
|
|
683
|
+
if (unsupported.length > 0) {
|
|
684
|
+
diagnostics.push(
|
|
685
|
+
graphDiagnostic(`${node.kind}.field-unsupported`, `${label} node "${nodeId}" contains unsupported fields: ${unsupported.join(', ')}.`, path, {
|
|
686
|
+
fields: unsupported.join(', '),
|
|
687
|
+
nodeId,
|
|
688
|
+
}),
|
|
689
|
+
)
|
|
690
|
+
}
|
|
691
|
+
const names = node.inputDefinitions.map((port) => port.handle)
|
|
692
|
+
if (new Set(names).size != names.length)
|
|
693
|
+
diagnostics.push(graphDiagnostic(`${node.kind}.input-invalid`, `${label} input names must be unique.`, `${path}/inputDefinitions`))
|
|
694
|
+
if (typeof node.prompt != 'string' || node.prompt.trim().length == 0 || [...node.prompt].length > 1_000) {
|
|
695
|
+
diagnostics.push(graphDiagnostic(`${node.kind}.prompt-invalid`, `${label} prompt must contain between 1 and 1,000 Unicode code points.`, `${path}/prompt`))
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function validateGraph(graph: Graph, document: FlowDocument, path: string, diagnostics: Diagnostic[]): void {
|
|
700
|
+
const analysis = graphPaths(graph)
|
|
701
|
+
const errorTriggers = Object.entries(graph.nodes).filter(([, node]) => node.kind == 'error')
|
|
702
|
+
if (errorTriggers.length > 1)
|
|
703
|
+
for (const [nodeId] of errorTriggers)
|
|
704
|
+
diagnostics.push(graphDiagnostic('graph.error-trigger-duplicate', 'A graph can contain only one Flow Error node.', `${path}/nodes/${nodeId}`))
|
|
705
|
+
const outputNodes = Object.entries(graph.nodes).filter(([, node]) => node.kind == 'outputs')
|
|
706
|
+
for (const [nodeId, node] of Object.entries(graph.nodes)) {
|
|
707
|
+
if (node.kind == 'webhook') diagnostics.push(...webhookResponseDiagnostics(graph, nodeId, `${path}/nodes/${nodeId}`))
|
|
708
|
+
if (node.kind === 'webhook-response') {
|
|
709
|
+
const status = node.inputs.statusCode
|
|
710
|
+
if (status?.kind === 'value' && !validWebhookStatus(status.value))
|
|
711
|
+
diagnostics.push(
|
|
712
|
+
graphDiagnostic(
|
|
713
|
+
'webhook.response-status-invalid',
|
|
714
|
+
'Use an integer status from 200 to 599, excluding 204, 205 and 304.',
|
|
715
|
+
`${path}/nodes/${nodeId}/inputs/statusCode`,
|
|
716
|
+
),
|
|
717
|
+
)
|
|
718
|
+
const headers = node.inputs.headers
|
|
719
|
+
if (
|
|
720
|
+
headers?.kind === 'value' &&
|
|
721
|
+
(webhookHeaderValues(headers.value) == null || Object.values(headers.value as Record<string, unknown>).some((value) => typeof value !== 'string'))
|
|
722
|
+
)
|
|
723
|
+
diagnostics.push(
|
|
724
|
+
graphDiagnostic(
|
|
725
|
+
'webhook.response-headers-invalid',
|
|
726
|
+
'Use valid, unique HTTP header names and single-line text values.',
|
|
727
|
+
`${path}/nodes/${nodeId}/inputs/headers`,
|
|
728
|
+
),
|
|
729
|
+
)
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
if (outputNodes.length > 1)
|
|
733
|
+
for (const [nodeId] of outputNodes)
|
|
734
|
+
diagnostics.push(graphDiagnostic('graph.outputs-duplicate', 'A graph can contain only one Outputs node.', `${path}/nodes/${nodeId}`))
|
|
735
|
+
const startTriggers = Object.entries(graph.nodes).filter(([, node]) => node.kind == 'manual' || node.kind == 'start')
|
|
736
|
+
if (startTriggers.length > 1) {
|
|
737
|
+
for (const [nodeId] of startTriggers) {
|
|
738
|
+
diagnostics.push(graphDiagnostic('graph.start-trigger-duplicate', 'A graph can contain only one Start Trigger.', `${path}/nodes/${nodeId}`))
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
const seen = new Set<string>()
|
|
742
|
+
for (const [index, edge] of graph.edges.entries()) {
|
|
743
|
+
const edgePath = `${path}/edges/${index}`
|
|
744
|
+
const source = graph.nodes[edge.source]
|
|
745
|
+
const target = graph.nodes[edge.target]
|
|
746
|
+
if (source == null || source.kind == 'outputs' || target == null || isTriggerNode(target)) {
|
|
747
|
+
diagnostics.push(graphDiagnostic('graph.edge-invalid', 'Execution edges require an existing source and an executable target.', edgePath))
|
|
748
|
+
} else if (source.kind == 'condition' || isResolutionNode(source)) {
|
|
749
|
+
const exits = isResolutionNode(source) ? Object.keys(resolutionOutputPorts(source)) : [...source.cases.map((item) => item.output), otherwiseOutput]
|
|
750
|
+
if (edge.sourceHandle == null || !exits.some((exit) => exit == edge.sourceHandle)) {
|
|
751
|
+
diagnostics.push(graphDiagnostic('graph.edge-invalid', 'Choose a declared execution branch.', edgePath))
|
|
752
|
+
}
|
|
753
|
+
} else if (edge.sourceHandle != null) {
|
|
754
|
+
diagnostics.push(graphDiagnostic('graph.edge-invalid', 'Ordinary execution edges do not select a data output.', edgePath))
|
|
755
|
+
}
|
|
756
|
+
const key = JSON.stringify([edge.source, edge.sourceHandle, edge.target])
|
|
757
|
+
if (seen.has(key)) diagnostics.push(graphDiagnostic('graph.edge-duplicate', 'Execution edge is duplicated.', edgePath))
|
|
758
|
+
seen.add(key)
|
|
759
|
+
}
|
|
760
|
+
for (const [nodeId, node] of Object.entries(graph.nodes)) {
|
|
761
|
+
const nodePath = `${path}/nodes/${nodeId}`
|
|
762
|
+
if (!isTriggerNode(node) && node.maxExecutions != null && (!Number.isSafeInteger(node.maxExecutions) || node.maxExecutions < 1)) {
|
|
763
|
+
diagnostics.push(graphDiagnostic('node.max-executions-invalid', 'Maximum executions must be a positive safe integer.', `${nodePath}/maxExecutions`))
|
|
764
|
+
}
|
|
765
|
+
if (node.onError != null && (!supportsNodeErrorHandling(node) || !['stop', 'error'].includes(node.onError)))
|
|
766
|
+
diagnostics.push(graphDiagnostic('node.error-handling-invalid', 'This node does not support this error handling setting.', `${nodePath}/onError`))
|
|
767
|
+
if ('retry' in node && node.retry != null) {
|
|
768
|
+
if (!supportsNodeRetry(node)) diagnostics.push(graphDiagnostic('node.retry-unsupported', 'This node does not support retries.', `${nodePath}/retry`))
|
|
769
|
+
if (!retryConfigurationSchema.safeParse(node.retry).success)
|
|
770
|
+
diagnostics.push(graphDiagnostic('node.retry-invalid', 'Retries must be 0–5 with an interval of 1–60 whole seconds.', `${nodePath}/retry`))
|
|
771
|
+
}
|
|
772
|
+
if (isTriggerNode(node)) {
|
|
773
|
+
validateTrigger(nodeId, node, nodePath, diagnostics)
|
|
774
|
+
continue
|
|
775
|
+
}
|
|
776
|
+
for (const [handle, mapping] of Object.entries(nodeInputMappings(node))) {
|
|
777
|
+
if (!mappingAvailable(graph, nodeId, mapping, analysis))
|
|
778
|
+
diagnostics.push(
|
|
779
|
+
graphDiagnostic(
|
|
780
|
+
'graph.source-unavailable',
|
|
781
|
+
'Input sources must reference execution ancestors, respect pending Wait decisions, and never provide multiple values on the same path.',
|
|
782
|
+
node.kind == 'outputs' ? `${nodePath}/outputMappings/${handle.replaceAll('~', '~0').replaceAll('/', '~1')}` : `${nodePath}/inputs/${handle}`,
|
|
783
|
+
),
|
|
784
|
+
)
|
|
785
|
+
}
|
|
786
|
+
const inputPorts = nodeInputPorts(node)
|
|
787
|
+
if (node.kind == 'task') {
|
|
788
|
+
const ports = [...node.task.inputs, ...(node.additionalInputs ?? [])]
|
|
789
|
+
const handles = new Set<string>()
|
|
790
|
+
for (const port of ports) {
|
|
791
|
+
if (!('handle' in port)) continue
|
|
792
|
+
if (handles.has(port.handle)) {
|
|
793
|
+
diagnostics.push(
|
|
794
|
+
graphDiagnostic('graph.input-duplicate', `Node "${nodeId}" declares input "${port.handle}" more than once.`, nodePath, {
|
|
795
|
+
handle: port.handle,
|
|
796
|
+
nodeId,
|
|
797
|
+
}),
|
|
798
|
+
)
|
|
799
|
+
}
|
|
800
|
+
handles.add(port.handle)
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
const ports = [...Object.values(inputPorts), ...Object.values(nodeOutputPorts(node))]
|
|
804
|
+
if (ports.some((port) => hasRetiredRef(port.jsonSchema))) {
|
|
805
|
+
diagnostics.push(graphDiagnostic('graph.schema-unsupported', 'Runtime Ref schemas are not supported.', nodePath))
|
|
806
|
+
}
|
|
807
|
+
const inputs = new Set(Object.keys(inputPorts))
|
|
808
|
+
for (const [handle, mapping] of Object.entries(nodeInputMappings(node))) {
|
|
809
|
+
const mappingPath =
|
|
810
|
+
node.kind == 'outputs' ? `${nodePath}/outputMappings/${handle.replaceAll('~', '~0').replaceAll('/', '~1')}` : `${nodePath}/inputs/${handle}`
|
|
811
|
+
if (!inputs.has(handle)) {
|
|
812
|
+
diagnostics.push(graphDiagnostic('graph.input-missing', `Node "${nodeId}" does not expose input "${handle}".`, mappingPath, { handle, nodeId }))
|
|
813
|
+
}
|
|
814
|
+
if (mapping.kind == 'sources') {
|
|
815
|
+
const variableSources = mapping.sources.filter((source) => source.kind == 'binding' && document.bindings[source.bindingId]?.kind == 'variable')
|
|
816
|
+
if (variableSources.length > 0 && (variableSources.length != 1 || mapping.sources.length != 1)) {
|
|
817
|
+
diagnostics.push(graphDiagnostic('graph.variable-source-mixed', 'A variable must be the only source for an input.', mappingPath))
|
|
818
|
+
}
|
|
819
|
+
for (const source of mapping.sources) checkSource(source, graph, document, inputPorts[handle], mappingPath, diagnostics)
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
if (isResolutionNode(node)) validateResolution(nodeId, node, nodePath, diagnostics)
|
|
823
|
+
if (node.kind != 'condition') continue
|
|
824
|
+
if (Object.keys(node.inputs).length > 0)
|
|
825
|
+
diagnostics.push(graphDiagnostic('condition.invalid', 'Condition cannot declare node inputs.', nodePath, { variant: 'inputs' }))
|
|
826
|
+
const outputs = new Set<string>([otherwiseOutput])
|
|
827
|
+
for (const [index, condition] of node.cases.entries()) {
|
|
828
|
+
const casePath = `${nodePath}/cases/${index}`
|
|
829
|
+
if (condition.output.trim().length == 0 || condition.output == '__proto__' || outputs.has(condition.output))
|
|
830
|
+
diagnostics.push(
|
|
831
|
+
graphDiagnostic('condition.invalid', 'Case output must be nonempty, unique, and not reserved.', `${casePath}/output`, { variant: 'output' }),
|
|
832
|
+
)
|
|
833
|
+
outputs.add(condition.output)
|
|
834
|
+
if (condition.groups.length == 0)
|
|
835
|
+
diagnostics.push(graphDiagnostic('condition.invalid', 'Case requires at least one group.', casePath, { variant: 'case' }))
|
|
836
|
+
for (const [g, group] of condition.groups.entries()) {
|
|
837
|
+
const groupPath = `${casePath}/groups/${g}`
|
|
838
|
+
if (group.expressions.length == 0)
|
|
839
|
+
diagnostics.push(graphDiagnostic('condition.invalid', 'Group requires at least one expression.', groupPath, { variant: 'group' }))
|
|
840
|
+
for (const [e, expression] of group.expressions.entries()) {
|
|
841
|
+
const expressionPath = `${groupPath}/expressions/${e}`
|
|
842
|
+
const type = (operand: typeof expression.left | undefined): string | undefined => {
|
|
843
|
+
if (operand == null || (operand.kind == 'value' && operand.value === undefined)) {
|
|
844
|
+
diagnostics.push(graphDiagnostic('condition.invalid', 'Condition operand is incomplete.', expressionPath, { variant: 'operand' }))
|
|
845
|
+
return
|
|
846
|
+
}
|
|
847
|
+
if (operand.kind == 'value') {
|
|
848
|
+
if (operand.jsonSchema != null && !matchesSchema(operand.value!, operand.jsonSchema))
|
|
849
|
+
diagnostics.push(graphDiagnostic('condition.invalid', 'Operand does not match its declared schema.', expressionPath, { variant: 'schema' }))
|
|
850
|
+
return valueType(operand.value!)
|
|
851
|
+
}
|
|
852
|
+
if (operand.source.kind === 'binding') return 'string'
|
|
853
|
+
const port = checkSource(operand.source, graph, document, undefined, expressionPath, [])
|
|
854
|
+
const raw = schemaObject(port?.jsonSchema ?? {})?.type
|
|
855
|
+
return typeof raw == 'string' ? raw : undefined
|
|
856
|
+
}
|
|
857
|
+
const left = type(expression.left)
|
|
858
|
+
const right = unaryOperator(expression.operator) ? undefined : type(expression.right)
|
|
859
|
+
if (unaryOperator(expression.operator) && expression.right != null)
|
|
860
|
+
diagnostics.push(graphDiagnostic('condition.invalid', 'Unary operators cannot have a right operand.', expressionPath, { variant: 'unary' }))
|
|
861
|
+
const issue = comparisonIssue(expression.operator, left, right)
|
|
862
|
+
if (issue != null) diagnostics.push(graphDiagnostic('condition.invalid', issue.message, expressionPath, { variant: 'type' }))
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
export function validateFlowGraph(revision: RevisionContent): readonly Diagnostic[] {
|
|
870
|
+
const diagnostics: Diagnostic[] = []
|
|
871
|
+
for (const [id, node] of Object.entries(revision.document.graph.nodes)) {
|
|
872
|
+
if (node.kind != 'start' && node.kind != 'outputs') continue
|
|
873
|
+
const field = node.kind == 'start' ? 'inputDefs' : 'outputDefs'
|
|
874
|
+
const ports = node.kind == 'start' ? node.inputDefs : node.outputDefs
|
|
875
|
+
const path = `/document/graph/nodes/${id.replaceAll('~', '~0').replaceAll('/', '~1')}/${field}`
|
|
876
|
+
if (new Set(ports.map((port) => port.handle)).size != ports.length)
|
|
877
|
+
diagnostics.push(graphDiagnostic('graph.port-duplicate', 'Workflow interface names must be unique.', path))
|
|
878
|
+
if (ports.some((port) => hasRetiredRef(port.jsonSchema)))
|
|
879
|
+
diagnostics.push(graphDiagnostic('graph.schema-unsupported', 'Runtime Ref schemas are not supported.', path))
|
|
880
|
+
}
|
|
881
|
+
for (const [bindingId, binding] of Object.entries(revision.document.bindings)) {
|
|
882
|
+
if (binding.kind == 'variable' && !validVariableName(binding.target)) {
|
|
883
|
+
diagnostics.push(
|
|
884
|
+
graphDiagnostic('binding.variable-invalid', `Variable binding "${bindingId}" has an invalid target.`, `/document/bindings/${bindingId}/target`, {
|
|
885
|
+
bindingId,
|
|
886
|
+
}),
|
|
887
|
+
)
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
validateGraph(revision.document.graph, revision.document, '/document/graph', diagnostics)
|
|
891
|
+
return diagnostics
|
|
892
|
+
}
|