@openapi-flow/n8n 0.0.0-stage → 0.2.0
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 +21 -0
- package/README.md +101 -2
- package/THIRD_PARTY_NOTICES.md +9 -0
- package/dist/legacy/workflow/common/plan-validation.d.ts +1 -0
- package/dist/legacy/workflow/common/plan-validation.js +11 -0
- package/dist/legacy/workflow/common/workflow-id.d.ts +1 -0
- package/dist/legacy/workflow/common/workflow-id.js +8 -0
- package/dist/legacy/workflow/inbound/build-inbound-workflow.d.ts +2 -0
- package/dist/legacy/workflow/inbound/build-inbound-workflow.js +60 -0
- package/dist/legacy/workflow/inbound/compile-inbound-workflow.d.ts +2 -0
- package/dist/legacy/workflow/inbound/compile-inbound-workflow.js +25 -0
- package/dist/legacy/workflow/inbound/inbound-response.d.ts +2 -0
- package/dist/legacy/workflow/inbound/inbound-response.js +51 -0
- package/dist/legacy/workflow/request/base-url.d.ts +1 -0
- package/dist/legacy/workflow/request/base-url.js +14 -0
- package/dist/legacy/workflow/request/build-request-workflow.d.ts +3 -0
- package/dist/legacy/workflow/request/build-request-workflow.js +47 -0
- package/dist/legacy/workflow/request/build-sequence-workflow.d.ts +3 -0
- package/dist/legacy/workflow/request/build-sequence-workflow.js +53 -0
- package/dist/legacy/workflow/request/compile-catalog-sequence.d.ts +2 -0
- package/dist/legacy/workflow/request/compile-catalog-sequence.js +122 -0
- package/dist/legacy/workflow/request/compile-sequence.d.ts +2 -0
- package/dist/legacy/workflow/request/compile-sequence.js +41 -0
- package/dist/legacy/workflow/request/compile-workflow.d.ts +4 -0
- package/dist/legacy/workflow/request/compile-workflow.js +93 -0
- package/dist/legacy/workflow/request/credential-binding.d.ts +5 -0
- package/dist/legacy/workflow/request/credential-binding.js +31 -0
- package/dist/legacy/workflow/request/effect-policy.d.ts +4 -0
- package/dist/legacy/workflow/request/effect-policy.js +16 -0
- package/dist/legacy/workflow/request/http-request-node.d.ts +2 -0
- package/dist/legacy/workflow/request/http-request-node.js +69 -0
- package/dist/legacy/workflow/request/prepare-sequence.d.ts +4 -0
- package/dist/legacy/workflow/request/prepare-sequence.js +103 -0
- package/dist/legacy/workflow/request/request-body.d.ts +4 -0
- package/dist/legacy/workflow/request/request-body.js +64 -0
- package/dist/legacy/workflow/request/request-headers.d.ts +4 -0
- package/dist/legacy/workflow/request/request-headers.js +30 -0
- package/dist/legacy/workflow/request/request-url.d.ts +4 -0
- package/dist/legacy/workflow/request/request-url.js +34 -0
- package/dist/legacy/workflow/request/response-assertion-code.d.ts +3 -0
- package/dist/legacy/workflow/request/response-assertion-code.js +28 -0
- package/dist/legacy/workflow/request/sequence-url.d.ts +4 -0
- package/dist/legacy/workflow/request/sequence-url.js +104 -0
- package/dist/legacy-api.d.ts +25 -0
- package/dist/legacy-api.js +20 -0
- package/dist/nodes/common/validation/compile-standalone-validator.d.ts +3 -0
- package/dist/nodes/common/validation/compile-standalone-validator.js +44 -0
- package/dist/nodes/gap/create-gap-node.d.ts +3 -0
- package/dist/nodes/gap/create-gap-node.js +35 -0
- package/dist/nodes/native/arrays/create-array-node-fragment.d.ts +4 -0
- package/dist/nodes/native/arrays/create-array-node-fragment.js +42 -0
- package/dist/nodes/native/arrays/create-array-reader-code.d.ts +3 -0
- package/dist/nodes/native/arrays/create-array-reader-code.js +12 -0
- package/dist/nodes/native/arrays/create-native-array-capability.d.ts +3 -0
- package/dist/nodes/native/arrays/create-native-array-capability.js +64 -0
- package/dist/nodes/native/common/create-native-fragment.d.ts +2 -0
- package/dist/nodes/native/common/create-native-fragment.js +10 -0
- package/dist/nodes/native/common/response-references.d.ts +3 -0
- package/dist/nodes/native/common/response-references.js +5 -0
- package/dist/nodes/native/create-exclusive-branch-merge-capability.d.ts +3 -0
- package/dist/nodes/native/create-exclusive-branch-merge-capability.js +33 -0
- package/dist/nodes/native/create-json-output-capability.d.ts +3 -0
- package/dist/nodes/native/create-json-output-capability.js +38 -0
- package/dist/nodes/native/create-native-capabilities.d.ts +2 -0
- package/dist/nodes/native/create-native-capabilities.js +14 -0
- package/dist/nodes/native/create-pass-through-capability.d.ts +3 -0
- package/dist/nodes/native/create-pass-through-capability.js +30 -0
- package/dist/nodes/native/create-response-array-capability.d.ts +3 -0
- package/dist/nodes/native/create-response-array-capability.js +73 -0
- package/dist/nodes/native/create-response-collection-capability.d.ts +3 -0
- package/dist/nodes/native/create-response-collection-capability.js +132 -0
- package/dist/nodes/native/if-capability.d.ts +2 -0
- package/dist/nodes/native/if-capability.js +76 -0
- package/dist/nodes/native/item-join/create-item-join-capability.d.ts +3 -0
- package/dist/nodes/native/item-join/create-item-join-capability.js +153 -0
- package/dist/nodes/native/item-join/create-item-join-reader-code.d.ts +2 -0
- package/dist/nodes/native/item-join/create-item-join-reader-code.js +18 -0
- package/dist/nodes/native/item-join/runtime/item-join-runtime.bundle.js +24 -0
- package/dist/nodes/native/item-join/runtime/join-api-items.d.ts +3 -0
- package/dist/nodes/native/item-join/runtime/join-api-items.js +37 -0
- package/dist/nodes/native/merge-append-capability.d.ts +2 -0
- package/dist/nodes/native/merge-append-capability.js +31 -0
- package/dist/nodes/native/response-assertion-capability.d.ts +2 -0
- package/dist/nodes/native/response-assertion-capability.js +53 -0
- package/dist/nodes/native/response-check-code.d.ts +3 -0
- package/dist/nodes/native/response-check-code.js +59 -0
- package/dist/nodes/native/response-checks/create-response-check-guard.d.ts +4 -0
- package/dist/nodes/native/response-checks/create-response-check-guard.js +22 -0
- package/dist/nodes/native/response-checks/create-response-check-reader.d.ts +3 -0
- package/dist/nodes/native/response-checks/create-response-check-reader.js +23 -0
- package/dist/nodes/native/stop-and-error-capability.d.ts +2 -0
- package/dist/nodes/native/stop-and-error-capability.js +29 -0
- package/dist/nodes/request/authentication/credential-field-name.d.ts +1 -0
- package/dist/nodes/request/authentication/credential-field-name.js +3 -0
- package/dist/nodes/request/authentication/resolve-http-request-authentication.d.ts +2 -0
- package/dist/nodes/request/authentication/resolve-http-request-authentication.js +42 -0
- package/dist/nodes/request/bindings/compile-native-output-validators.d.ts +3 -0
- package/dist/nodes/request/bindings/compile-native-output-validators.js +12 -0
- package/dist/nodes/request/bindings/compile-request-validator.d.ts +3 -0
- package/dist/nodes/request/bindings/compile-request-validator.js +13 -0
- package/dist/nodes/request/bindings/compile-source-response-validators.d.ts +2 -0
- package/dist/nodes/request/bindings/compile-source-response-validators.js +12 -0
- package/dist/nodes/request/bindings/create-bound-request-fragment.d.ts +3 -0
- package/dist/nodes/request/bindings/create-bound-request-fragment.js +77 -0
- package/dist/nodes/request/bindings/create-output-reader-code.d.ts +3 -0
- package/dist/nodes/request/bindings/create-output-reader-code.js +26 -0
- package/dist/nodes/request/bindings/resolve-output-binding-sources.d.ts +3 -0
- package/dist/nodes/request/bindings/resolve-output-binding-sources.js +36 -0
- package/dist/nodes/request/create-http-request-node.d.ts +2 -0
- package/dist/nodes/request/create-http-request-node.js +94 -0
- package/dist/nodes/request/deployment/create-request-deployment-notes.d.ts +2 -0
- package/dist/nodes/request/deployment/create-request-deployment-notes.js +3 -0
- package/dist/nodes/request/deployment/resolve-http-request-deployment.d.ts +2 -0
- package/dist/nodes/request/deployment/resolve-http-request-deployment.js +33 -0
- package/dist/nodes/request/response/create-http-response-decoder.d.ts +4 -0
- package/dist/nodes/request/response/create-http-response-decoder.js +56 -0
- package/dist/nodes/request/response/decode-http-response.d.ts +3 -0
- package/dist/nodes/request/response/decode-http-response.js +42 -0
- package/dist/nodes/request/response/http-response-media.d.ts +2 -0
- package/dist/nodes/request/response/http-response-media.js +32 -0
- package/dist/nodes/request/response/response-runtime.bundle.js +24 -0
- package/dist/nodes/request/runtime/materialize-http-request.d.ts +3 -0
- package/dist/nodes/request/runtime/materialize-http-request.js +17 -0
- package/dist/nodes/request/runtime/request-runtime.bundle.js +24 -0
- package/dist/nodes/request/serialization/create-serialized-http-request-node.d.ts +3 -0
- package/dist/nodes/request/serialization/create-serialized-http-request-node.js +17 -0
- package/dist/nodes/request/serialization/serialize-http-request.d.ts +5 -0
- package/dist/nodes/request/serialization/serialize-http-request.js +70 -0
- package/dist/public-api.d.ts +30 -0
- package/dist/public-api.js +17 -0
- package/dist/schemas/batch-execution-schema.d.ts +15 -0
- package/dist/schemas/batch-execution-schema.js +14 -0
- package/dist/schemas/create-native-comparison-planning-schema.d.ts +81 -0
- package/dist/schemas/create-native-comparison-planning-schema.js +36 -0
- package/dist/schemas/exclusive-branch-merge-schema.d.ts +4 -0
- package/dist/schemas/exclusive-branch-merge-schema.js +7 -0
- package/dist/schemas/item-join-schema.d.ts +13 -0
- package/dist/schemas/item-join-schema.js +26 -0
- package/dist/schemas/native-array-schema.d.ts +11 -0
- package/dist/schemas/native-array-schema.js +19 -0
- package/dist/schemas/native-capability-schemas.d.ts +100 -0
- package/dist/schemas/native-capability-schemas.js +62 -0
- package/dist/schemas/request-deployment-schema.d.ts +9 -0
- package/dist/schemas/request-deployment-schema.js +27 -0
- package/dist/schemas/response-array-schema.d.ts +11 -0
- package/dist/schemas/response-array-schema.js +19 -0
- package/dist/schemas/response-collection-schema.d.ts +11 -0
- package/dist/schemas/response-collection-schema.js +19 -0
- package/dist/types/array-iteration.d.ts +8 -0
- package/dist/types/array-iteration.js +1 -0
- package/dist/types/array-split.d.ts +13 -0
- package/dist/types/array-split.js +1 -0
- package/dist/types/authentication.d.ts +14 -0
- package/dist/types/authentication.js +1 -0
- package/dist/types/batch-execution.d.ts +23 -0
- package/dist/types/batch-execution.js +1 -0
- package/dist/types/http-response-decoding.d.ts +16 -0
- package/dist/types/http-response-decoding.js +1 -0
- package/dist/types/item-join.d.ts +40 -0
- package/dist/types/item-join.js +1 -0
- package/dist/types/legacy/catalog-workflow.d.ts +37 -0
- package/dist/types/legacy/catalog-workflow.js +1 -0
- package/dist/types/legacy/inbound-workflow.d.ts +27 -0
- package/dist/types/legacy/inbound-workflow.js +1 -0
- package/dist/types/legacy/request-node.d.ts +23 -0
- package/dist/types/legacy/request-node.js +1 -0
- package/dist/types/legacy/request-workflow.d.ts +43 -0
- package/dist/types/legacy/request-workflow.js +1 -0
- package/dist/types/legacy/sequence-workflow.d.ts +57 -0
- package/dist/types/legacy/sequence-workflow.js +1 -0
- package/dist/types/native-array.d.ts +8 -0
- package/dist/types/native-array.js +1 -0
- package/dist/types/native-capability.d.ts +37 -0
- package/dist/types/native-capability.js +1 -0
- package/dist/types/node-fragment.d.ts +51 -0
- package/dist/types/node-fragment.js +1 -0
- package/dist/types/output-binding-source.d.ts +22 -0
- package/dist/types/output-binding-source.js +1 -0
- package/dist/types/request-deployment.d.ts +17 -0
- package/dist/types/request-deployment.js +1 -0
- package/dist/types/request-materialization.d.ts +22 -0
- package/dist/types/request-materialization.js +1 -0
- package/dist/types/response-check-compilation.d.ts +11 -0
- package/dist/types/response-check-compilation.js +1 -0
- package/dist/types/response-check-guard.d.ts +6 -0
- package/dist/types/response-check-guard.js +1 -0
- package/dist/types/response-collection.d.ts +7 -0
- package/dist/types/response-collection.js +1 -0
- package/dist/types/reviewable-workflow.d.ts +27 -0
- package/dist/types/reviewable-workflow.js +1 -0
- package/dist/types/schema-compilation.d.ts +5 -0
- package/dist/types/schema-compilation.js +1 -0
- package/dist/types/serialized-http-request.d.ts +4 -0
- package/dist/types/serialized-http-request.js +1 -0
- package/dist/types/workflow-compilation.d.ts +36 -0
- package/dist/types/workflow-compilation.js +1 -0
- package/dist/utils/bundled-license-banner.d.ts +3 -0
- package/dist/utils/bundled-license-banner.js +32 -0
- package/dist/utils/javascript-json-literal.d.ts +2 -0
- package/dist/utils/javascript-json-literal.js +10 -0
- package/dist/utils/json-values-equal.cjs +24 -0
- package/dist/utils/json-values-equal.d.cts +1 -0
- package/dist/workflow/assemble-n8n-workflow.d.ts +2 -0
- package/dist/workflow/assemble-n8n-workflow.js +11 -0
- package/dist/workflow/batch/compile-batched-n8n-workflow.d.ts +3 -0
- package/dist/workflow/batch/compile-batched-n8n-workflow.js +18 -0
- package/dist/workflow/batch/create-batch-execution-scopes.d.ts +3 -0
- package/dist/workflow/batch/create-batch-execution-scopes.js +59 -0
- package/dist/workflow/batch/validate-batch-exclusive-rejoin.d.ts +3 -0
- package/dist/workflow/batch/validate-batch-exclusive-rejoin.js +49 -0
- package/dist/workflow/batch/validate-batch-item-flow.d.ts +3 -0
- package/dist/workflow/batch/validate-batch-item-flow.js +98 -0
- package/dist/workflow/build-n8n-workflow.d.ts +2 -0
- package/dist/workflow/build-n8n-workflow.js +143 -0
- package/dist/workflow/compile-native-workflow-nodes.d.ts +3 -0
- package/dist/workflow/compile-native-workflow-nodes.js +26 -0
- package/dist/workflow/compile-planned-n8n-workflow.d.ts +3 -0
- package/dist/workflow/compile-planned-n8n-workflow.js +17 -0
- package/dist/workflow/compile-reviewable-n8n-workflow.d.ts +3 -0
- package/dist/workflow/compile-reviewable-n8n-workflow.js +70 -0
- package/dist/workflow/compile-validated-workflow-fragments.d.ts +3 -0
- package/dist/workflow/compile-validated-workflow-fragments.js +20 -0
- package/dist/workflow/create-n8n-native-output-sources.d.ts +3 -0
- package/dist/workflow/create-n8n-native-output-sources.js +17 -0
- package/dist/workflow/review/create-gap-review-content.d.ts +2 -0
- package/dist/workflow/review/create-gap-review-content.js +28 -0
- package/dist/workflow/review/create-gap-review-region.d.ts +2 -0
- package/dist/workflow/review/create-gap-review-region.js +23 -0
- package/dist/workflow/review/escape-review-markdown.d.ts +1 -0
- package/dist/workflow/review/escape-review-markdown.js +7 -0
- package/dist/workflow/review/gap-review-region-layout.d.ts +4 -0
- package/dist/workflow/review/gap-review-region-layout.js +21 -0
- package/dist/workflow/validate-compiled-binding-sources.d.ts +4 -0
- package/dist/workflow/validate-compiled-binding-sources.js +40 -0
- package/dist/workflow/validate-node-fragments.d.ts +3 -0
- package/dist/workflow/validate-node-fragments.js +58 -0
- package/package.json +54 -4
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { node } from '@n8n/workflow-sdk';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { createResponseValueSchema } from '@openapi-flow/core';
|
|
4
|
+
import { createResponseCollectionParametersSchema } from '../../schemas/response-collection-schema.js';
|
|
5
|
+
import { createOutputReaderCode } from '../request/bindings/create-output-reader-code.js';
|
|
6
|
+
import { javascriptJsonLiteral } from '../../utils/javascript-json-literal.js';
|
|
7
|
+
export function createResponseCollectionCapability(input) {
|
|
8
|
+
const materials = new Map(input.materials.map((material) => [material.callId, material]));
|
|
9
|
+
if (!materials.size ||
|
|
10
|
+
materials.size !== input.materials.length ||
|
|
11
|
+
input.materials.some((material) => !material.callId.trim()))
|
|
12
|
+
throw new Error('response collection capability requires unique API materials');
|
|
13
|
+
const parametersSchema = createResponseCollectionParametersSchema([
|
|
14
|
+
...materials.keys(),
|
|
15
|
+
]);
|
|
16
|
+
function source(parameters) {
|
|
17
|
+
const material = materials.get(parameters.sourceNodeId);
|
|
18
|
+
if (!material)
|
|
19
|
+
throw new Error(`response collection: unknown sourceNodeId ${parameters.sourceNodeId}`);
|
|
20
|
+
createResponseValueSchema(material.operation, parameters.pointer);
|
|
21
|
+
return material;
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
name: 'collect-api-responses',
|
|
25
|
+
description: 'Collect unchanged item-linked API response values from one incoming stream into {items: values} using n8n Aggregate. Validates each full OAS response first. Preserves null, nested arrays, duplicates and received order. Not a branch join. Empty streams do not trigger this node or create an artificial result.',
|
|
26
|
+
parametersSchema,
|
|
27
|
+
inputPorts: () => ['main'],
|
|
28
|
+
outputPorts: () => ['main'],
|
|
29
|
+
responseReferences: (values) => {
|
|
30
|
+
const parameters = parametersSchema.parse(values);
|
|
31
|
+
source(parameters);
|
|
32
|
+
return [
|
|
33
|
+
{
|
|
34
|
+
source: 'response',
|
|
35
|
+
nodeId: parameters.sourceNodeId,
|
|
36
|
+
pointer: parameters.pointer,
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
},
|
|
40
|
+
waitsForAllInputs: false,
|
|
41
|
+
exclusiveOutputPorts: false,
|
|
42
|
+
outputSchema: (values) => {
|
|
43
|
+
const parameters = parametersSchema.parse(values);
|
|
44
|
+
return {
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: {
|
|
47
|
+
items: {
|
|
48
|
+
type: 'array',
|
|
49
|
+
items: createResponseValueSchema(source(parameters).operation, parameters.pointer),
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
required: ['items'],
|
|
53
|
+
additionalProperties: false,
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
compile: ({ planned, position, apiNodeNames }) => {
|
|
57
|
+
const parameters = parametersSchema.parse(planned.parameters);
|
|
58
|
+
const material = source(parameters);
|
|
59
|
+
if (!Object.hasOwn(apiNodeNames, material.callId) ||
|
|
60
|
+
!apiNodeNames[material.callId].trim())
|
|
61
|
+
throw new Error(`apiNodeNames is missing ${material.callId}`);
|
|
62
|
+
const runtime = readFileSync(new URL('../request/runtime/request-runtime.bundle.js', import.meta.url), 'utf8');
|
|
63
|
+
const read = createOutputReaderCode([
|
|
64
|
+
{
|
|
65
|
+
kind: 'api-response',
|
|
66
|
+
nodeId: material.callId,
|
|
67
|
+
nodeName: apiNodeNames[material.callId],
|
|
68
|
+
operation: material.operation,
|
|
69
|
+
},
|
|
70
|
+
], true);
|
|
71
|
+
const extract = node({
|
|
72
|
+
type: 'n8n-nodes-base.code',
|
|
73
|
+
version: 2,
|
|
74
|
+
config: {
|
|
75
|
+
id: `${planned.id}-read-values`,
|
|
76
|
+
name: `Read values ${planned.id}`,
|
|
77
|
+
position,
|
|
78
|
+
parameters: {
|
|
79
|
+
mode: 'runOnceForAllItems',
|
|
80
|
+
jsCode: `${runtime}\nconst parameters=${javascriptJsonLiteral(parameters)};\nreturn $input.all().map((_,inputIndex)=>{${read}\nconst value=OpenApiFlowRequestRuntime.pointerValue(responses[parameters.sourceNodeId],parameters.pointer);if(value===undefined)throw new Error('Missing response pointer '+parameters.sourceNodeId+parameters.pointer);return {json:{value},pairedItem:{item:inputIndex}};});`,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
const aggregate = node({
|
|
85
|
+
type: 'n8n-nodes-base.aggregate',
|
|
86
|
+
version: 1,
|
|
87
|
+
config: {
|
|
88
|
+
id: planned.id,
|
|
89
|
+
name: planned.id,
|
|
90
|
+
position: [position[0] + 220, position[1]],
|
|
91
|
+
parameters: {
|
|
92
|
+
aggregate: 'aggregateIndividualFields',
|
|
93
|
+
fieldsToAggregate: {
|
|
94
|
+
fieldToAggregate: [
|
|
95
|
+
{
|
|
96
|
+
fieldToAggregate: 'value',
|
|
97
|
+
renameField: true,
|
|
98
|
+
outputFieldName: 'items',
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
options: {
|
|
103
|
+
disableDotNotation: true,
|
|
104
|
+
mergeLists: false,
|
|
105
|
+
keepMissing: true,
|
|
106
|
+
includeBinaries: false,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
nodeId: planned.id,
|
|
113
|
+
bindingSources: [
|
|
114
|
+
{
|
|
115
|
+
kind: 'api-response',
|
|
116
|
+
nodeId: material.callId,
|
|
117
|
+
nodeName: apiNodeNames[material.callId],
|
|
118
|
+
operation: material.operation,
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
nodes: [extract, aggregate],
|
|
122
|
+
entry: extract,
|
|
123
|
+
exit: aggregate,
|
|
124
|
+
inputPorts: { main: 0 },
|
|
125
|
+
outputPorts: { main: 0 },
|
|
126
|
+
internalEdges: [
|
|
127
|
+
{ from: extract.id, output: 0, to: aggregate.id, input: 0 },
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { node } from '@n8n/workflow-sdk';
|
|
2
|
+
import { ifParametersSchema } from '../../schemas/native-capability-schemas.js';
|
|
3
|
+
import { createIfPlanningParametersSchema } from '../../schemas/create-native-comparison-planning-schema.js';
|
|
4
|
+
import { createNativeFragment } from './common/create-native-fragment.js';
|
|
5
|
+
import { responseReferences } from './common/response-references.js';
|
|
6
|
+
import { conditionExpression } from './response-check-code.js';
|
|
7
|
+
import { createResponseCheckReader } from './response-checks/create-response-check-reader.js';
|
|
8
|
+
import { createResponseCheckGuard } from './response-checks/create-response-check-guard.js';
|
|
9
|
+
export function createIfCapability(input = {}) {
|
|
10
|
+
return {
|
|
11
|
+
name: 'if',
|
|
12
|
+
description: 'n8n IF: main input; true and false OUTPUT PORTS are mutually exclusive. Multiple edges from the SAME port all execute (fan-out) and can be joined. Type-strict declarative conditions.',
|
|
13
|
+
parametersSchema: ifParametersSchema,
|
|
14
|
+
createPlanningParametersSchema: createIfPlanningParametersSchema,
|
|
15
|
+
inputPorts: () => ['main'],
|
|
16
|
+
outputPorts: () => ['true', 'false'],
|
|
17
|
+
responseReferences: (parameters) => responseReferences(ifParametersSchema.parse(parameters).conditions),
|
|
18
|
+
waitsForAllInputs: false,
|
|
19
|
+
exclusiveOutputPorts: true,
|
|
20
|
+
compile: ({ planned, apiNodeNames, apiResponseContracts, position }) => {
|
|
21
|
+
const parameters = ifParametersSchema.parse(planned.parameters);
|
|
22
|
+
const reader = createResponseCheckReader({
|
|
23
|
+
nodeId: planned.id,
|
|
24
|
+
checks: parameters.conditions,
|
|
25
|
+
apiNodeNames,
|
|
26
|
+
apiResponseContracts,
|
|
27
|
+
linked: input.itemMode === 'linked',
|
|
28
|
+
});
|
|
29
|
+
const fragment = createNativeFragment(node({
|
|
30
|
+
type: 'n8n-nodes-base.if',
|
|
31
|
+
version: 2.2,
|
|
32
|
+
config: {
|
|
33
|
+
id: planned.id,
|
|
34
|
+
name: planned.id,
|
|
35
|
+
position,
|
|
36
|
+
parameters: {
|
|
37
|
+
conditions: {
|
|
38
|
+
options: {
|
|
39
|
+
caseSensitive: true,
|
|
40
|
+
leftValue: '',
|
|
41
|
+
typeValidation: 'strict',
|
|
42
|
+
version: 2,
|
|
43
|
+
},
|
|
44
|
+
conditions: parameters.conditions.map((check, index) => ({
|
|
45
|
+
id: `${planned.id}-${index}`,
|
|
46
|
+
leftValue: conditionExpression(check, apiNodeNames, input.itemMode === 'linked'),
|
|
47
|
+
rightValue: true,
|
|
48
|
+
operator: { type: 'boolean', operation: 'equals' },
|
|
49
|
+
})),
|
|
50
|
+
combinator: parameters.combinator,
|
|
51
|
+
},
|
|
52
|
+
options: {},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
}), ['main'], ['true', 'false']);
|
|
56
|
+
const partition = { ...fragment, partitionsInputItems: true };
|
|
57
|
+
if (!reader.bindingSources.length)
|
|
58
|
+
return partition;
|
|
59
|
+
const guard = createResponseCheckGuard({
|
|
60
|
+
nodeId: planned.id,
|
|
61
|
+
readerCode: reader.code,
|
|
62
|
+
linked: input.itemMode === 'linked',
|
|
63
|
+
position,
|
|
64
|
+
});
|
|
65
|
+
return {
|
|
66
|
+
...partition,
|
|
67
|
+
entry: guard,
|
|
68
|
+
nodes: [guard, ...fragment.nodes],
|
|
69
|
+
internalEdges: [
|
|
70
|
+
{ from: guard.id, output: 0, to: fragment.entry.id, input: 0 },
|
|
71
|
+
],
|
|
72
|
+
bindingSources: reader.bindingSources,
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { N8nNativeCapability } from '../../../types/native-capability.js';
|
|
2
|
+
import type { CreateItemJoinCapabilityInput } from '../../../types/item-join.js';
|
|
3
|
+
export declare function createItemJoinCapability({ materials, scopes, }: CreateItemJoinCapabilityInput): N8nNativeCapability;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { node } from '@n8n/workflow-sdk';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { createResponseValueSchema } from '@openapi-flow/core';
|
|
4
|
+
import { createItemJoinParametersSchema } from '../../../schemas/item-join-schema.js';
|
|
5
|
+
import { javascriptJsonLiteral } from '../../../utils/javascript-json-literal.js';
|
|
6
|
+
import { createItemJoinReaderCode } from './create-item-join-reader-code.js';
|
|
7
|
+
export function createItemJoinCapability({ materials, scopes, }) {
|
|
8
|
+
const apis = new Map(materials.map((material) => [material.callId, material]));
|
|
9
|
+
const scopeById = new Map(scopes.map((scope) => [scope.nodeId, scope]));
|
|
10
|
+
if (apis.size !== materials.length ||
|
|
11
|
+
materials.some((material) => !material.callId.trim()))
|
|
12
|
+
throw new Error('item join materials require unique non-empty callIds');
|
|
13
|
+
if (scopeById.size !== scopes.length ||
|
|
14
|
+
scopes.some((scope) => !scope.nodeId.trim() || !scope.nodeName.trim()))
|
|
15
|
+
throw new Error('item join scopes require unique non-empty identities and compiled names');
|
|
16
|
+
const parametersSchema = createItemJoinParametersSchema([...scopeById.keys()], [...apis.keys()]);
|
|
17
|
+
const ports = (count) => Array.from({ length: count }, (_, i) => `input${i + 1}`);
|
|
18
|
+
return {
|
|
19
|
+
name: 'join-api-items',
|
|
20
|
+
description: 'Join API branches by their shared native scope item ancestry. Each participating item requires exactly one full OAS-valid response per API. Different orders and duplicate JSON values are safe. Missing/duplicate branch responses fail; no business-ID matching, index zip, Cartesian product or value conversion. Output {responses: {callId: unchangedBody}}.',
|
|
21
|
+
parametersSchema,
|
|
22
|
+
inputPorts: (values) => ports(parametersSchema.parse(values).sourceCallIds.length),
|
|
23
|
+
outputPorts: () => ['main'],
|
|
24
|
+
responseReferences: (values) => parametersSchema.parse(values).sourceCallIds.map((nodeId) => ({
|
|
25
|
+
source: 'response',
|
|
26
|
+
nodeId,
|
|
27
|
+
pointer: '',
|
|
28
|
+
})),
|
|
29
|
+
nodeOutputReferences: (values) => [
|
|
30
|
+
{
|
|
31
|
+
source: 'node-output',
|
|
32
|
+
nodeId: parametersSchema.parse(values).scopeNodeId,
|
|
33
|
+
pointer: '',
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
inputReferences: (values) => {
|
|
37
|
+
const parameters = parametersSchema.parse(values);
|
|
38
|
+
return Object.fromEntries(parameters.sourceCallIds.map((nodeId, i) => [
|
|
39
|
+
`input${i + 1}`,
|
|
40
|
+
[
|
|
41
|
+
{ source: 'response', nodeId, pointer: '' },
|
|
42
|
+
{
|
|
43
|
+
source: 'node-output',
|
|
44
|
+
nodeId: parameters.scopeNodeId,
|
|
45
|
+
pointer: '',
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
]));
|
|
49
|
+
},
|
|
50
|
+
waitsForAllInputs: true,
|
|
51
|
+
exclusiveOutputPorts: false,
|
|
52
|
+
outputSchema: (values) => {
|
|
53
|
+
const parameters = parametersSchema.parse(values);
|
|
54
|
+
return {
|
|
55
|
+
type: 'object',
|
|
56
|
+
required: ['responses'],
|
|
57
|
+
additionalProperties: false,
|
|
58
|
+
properties: {
|
|
59
|
+
responses: {
|
|
60
|
+
type: 'object',
|
|
61
|
+
additionalProperties: false,
|
|
62
|
+
required: parameters.sourceCallIds,
|
|
63
|
+
properties: Object.fromEntries(parameters.sourceCallIds.map((id) => [
|
|
64
|
+
id,
|
|
65
|
+
createResponseValueSchema(apis.get(id).operation, ''),
|
|
66
|
+
])),
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
compile: ({ planned, position, apiNodeNames }) => {
|
|
72
|
+
const parameters = parametersSchema.parse(planned.parameters);
|
|
73
|
+
const scope = scopeById.get(parameters.scopeNodeId);
|
|
74
|
+
const bindingSources = [
|
|
75
|
+
{ ...scope, kind: 'native-json' },
|
|
76
|
+
...parameters.sourceCallIds.map((id) => {
|
|
77
|
+
if (!Object.hasOwn(apiNodeNames, id) || !apiNodeNames[id].trim())
|
|
78
|
+
throw new Error(`item join apiNodeNames is missing ${id}`);
|
|
79
|
+
return {
|
|
80
|
+
kind: 'api-response',
|
|
81
|
+
nodeId: id,
|
|
82
|
+
nodeName: apiNodeNames[id],
|
|
83
|
+
operation: apis.get(id).operation,
|
|
84
|
+
};
|
|
85
|
+
}),
|
|
86
|
+
];
|
|
87
|
+
const readers = parameters.sourceCallIds.map((id, index) => node({
|
|
88
|
+
type: 'n8n-nodes-base.code',
|
|
89
|
+
version: 2,
|
|
90
|
+
config: {
|
|
91
|
+
id: `${planned.id}-read-${index}`,
|
|
92
|
+
name: `Read branch ${index + 1} ${planned.id}`,
|
|
93
|
+
position: [position[0], position[1] + index * 180],
|
|
94
|
+
parameters: {
|
|
95
|
+
mode: 'runOnceForAllItems',
|
|
96
|
+
jsCode: createItemJoinReaderCode({
|
|
97
|
+
scope,
|
|
98
|
+
sourceCallId: id,
|
|
99
|
+
sources: [bindingSources[0], bindingSources[index + 1]],
|
|
100
|
+
}),
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
}));
|
|
104
|
+
const merge = node({
|
|
105
|
+
type: 'n8n-nodes-base.merge',
|
|
106
|
+
version: 3.2,
|
|
107
|
+
config: {
|
|
108
|
+
id: `${planned.id}-wait`,
|
|
109
|
+
name: `Wait branches ${planned.id}`,
|
|
110
|
+
position: [position[0] + 220, position[1]],
|
|
111
|
+
parameters: { mode: 'append', numberInputs: readers.length },
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
const runtime = readFileSync(new URL('./runtime/item-join-runtime.bundle.js', import.meta.url), 'utf8');
|
|
115
|
+
const joined = node({
|
|
116
|
+
type: 'n8n-nodes-base.code',
|
|
117
|
+
version: 2,
|
|
118
|
+
config: {
|
|
119
|
+
id: planned.id,
|
|
120
|
+
name: planned.id,
|
|
121
|
+
position: [position[0] + 440, position[1]],
|
|
122
|
+
parameters: {
|
|
123
|
+
mode: 'runOnceForAllItems',
|
|
124
|
+
jsCode: `${runtime}\nreturn OpenApiFlowItemJoinRuntime.joinApiItems({items:$input.all(),sourceCallIds:${javascriptJsonLiteral(parameters.sourceCallIds)}});`,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
return {
|
|
129
|
+
nodeId: planned.id,
|
|
130
|
+
joinsInputItemsByAncestry: { scopeNodeId: parameters.scopeNodeId },
|
|
131
|
+
nodes: [...readers, merge, joined],
|
|
132
|
+
entry: readers[0],
|
|
133
|
+
exit: joined,
|
|
134
|
+
inputPorts: Object.fromEntries(ports(readers.length).map((port) => [port, 0])),
|
|
135
|
+
inputEndpoints: Object.fromEntries(readers.map((reader, i) => [
|
|
136
|
+
`input${i + 1}`,
|
|
137
|
+
{ nodeId: reader.id, input: 0 },
|
|
138
|
+
])),
|
|
139
|
+
outputPorts: { main: 0 },
|
|
140
|
+
bindingSources,
|
|
141
|
+
internalEdges: [
|
|
142
|
+
...readers.map((reader, index) => ({
|
|
143
|
+
from: reader.id,
|
|
144
|
+
output: 0,
|
|
145
|
+
to: merge.id,
|
|
146
|
+
input: index,
|
|
147
|
+
})),
|
|
148
|
+
{ from: merge.id, output: 0, to: joined.id, input: 0 },
|
|
149
|
+
],
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { javascriptJsonLiteral } from '../../../utils/javascript-json-literal.js';
|
|
3
|
+
import { createOutputReaderCode } from '../../request/bindings/create-output-reader-code.js';
|
|
4
|
+
export function createItemJoinReaderCode({ scope, sourceCallId, sources, }) {
|
|
5
|
+
const runtime = readFileSync(new URL('../../request/runtime/request-runtime.bundle.js', import.meta.url), 'utf8');
|
|
6
|
+
const read = createOutputReaderCode(sources, true);
|
|
7
|
+
return `${runtime}
|
|
8
|
+
const scopeName=${javascriptJsonLiteral(scope.nodeName)};
|
|
9
|
+
const sourceCallId=${javascriptJsonLiteral(sourceCallId)};
|
|
10
|
+
const scopeItems=$(scopeName).all();
|
|
11
|
+
return $input.all().map((_,inputIndex)=>{
|
|
12
|
+
${read}
|
|
13
|
+
const ancestor=$(scopeName).itemMatching(inputIndex);
|
|
14
|
+
const matches=scopeItems.flatMap((item,index)=>item===ancestor?[index]:[]);
|
|
15
|
+
if(matches.length!==1)throw new Error('Item join '+sourceCallId+': scope ancestry must identify exactly one item in the current scope execution');
|
|
16
|
+
return {json:{scopeIndex:matches[0],sourceCallId,response:responses[sourceCallId]},pairedItem:{item:inputIndex}};
|
|
17
|
+
});`;
|
|
18
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*! MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 carefreelife98
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
*/
|
|
23
|
+
var OpenApiFlowItemJoinRuntime;
|
|
24
|
+
(()=>{function r({items:p,sourceCallIds:i}){if(i.length<2||new Set(i).size!==i.length)throw new Error("item join requires unique sourceCallIds");let s=new Map;for(let[n,o]of p.entries()){let e=o.json;if(!e||!Number.isInteger(e.scopeIndex)||e.scopeIndex<0)throw new Error(`item join input ${n}: scopeIndex is missing or invalid`);if(!i.includes(e.sourceCallId))throw new Error(`item join input ${n}: unknown sourceCallId ${e.sourceCallId}`);if(!Object.hasOwn(e,"response")||e.response===void 0)throw new Error(`item join input ${n}: response is missing`);let t=s.get(e.scopeIndex);if(t||(t={values:new Map,inputIndexes:[]},s.set(e.scopeIndex,t)),t.values.has(e.sourceCallId))throw new Error(`item join scope item ${e.scopeIndex}: duplicate response ${e.sourceCallId}`);t.values.set(e.sourceCallId,e.response),t.inputIndexes.push(n)}for(let[n,o]of s)for(let e of i)if(!o.values.has(e))throw new Error(`item join scope item ${n}: missing response ${e}`);return[...s].sort(([n],[o])=>n-o).map(([,n])=>({json:{responses:Object.fromEntries(i.map(o=>[o,n.values.get(o)]))},pairedItem:n.inputIndexes.map(o=>({item:o}))}))}OpenApiFlowItemJoinRuntime={joinApiItems:r};})();
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ItemJoinOutputItem, JoinApiItemsInput } from '../../../../types/item-join.js';
|
|
2
|
+
/** Join only reader-attested ancestry, never JSON business fields or stream positions. */
|
|
3
|
+
export declare function joinApiItems({ items, sourceCallIds, }: JoinApiItemsInput): ItemJoinOutputItem[];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Join only reader-attested ancestry, never JSON business fields or stream positions. */
|
|
2
|
+
export function joinApiItems({ items, sourceCallIds, }) {
|
|
3
|
+
if (sourceCallIds.length < 2 ||
|
|
4
|
+
new Set(sourceCallIds).size !== sourceCallIds.length)
|
|
5
|
+
throw new Error('item join requires unique sourceCallIds');
|
|
6
|
+
const groups = new Map();
|
|
7
|
+
for (const [inputIndex, item] of items.entries()) {
|
|
8
|
+
const row = item.json;
|
|
9
|
+
if (!row || !Number.isInteger(row.scopeIndex) || row.scopeIndex < 0)
|
|
10
|
+
throw new Error(`item join input ${inputIndex}: scopeIndex is missing or invalid`);
|
|
11
|
+
if (!sourceCallIds.includes(row.sourceCallId))
|
|
12
|
+
throw new Error(`item join input ${inputIndex}: unknown sourceCallId ${row.sourceCallId}`);
|
|
13
|
+
if (!Object.hasOwn(row, 'response') || row.response === undefined)
|
|
14
|
+
throw new Error(`item join input ${inputIndex}: response is missing`);
|
|
15
|
+
let group = groups.get(row.scopeIndex);
|
|
16
|
+
if (!group) {
|
|
17
|
+
group = { values: new Map(), inputIndexes: [] };
|
|
18
|
+
groups.set(row.scopeIndex, group);
|
|
19
|
+
}
|
|
20
|
+
if (group.values.has(row.sourceCallId))
|
|
21
|
+
throw new Error(`item join scope item ${row.scopeIndex}: duplicate response ${row.sourceCallId}`);
|
|
22
|
+
group.values.set(row.sourceCallId, row.response);
|
|
23
|
+
group.inputIndexes.push(inputIndex);
|
|
24
|
+
}
|
|
25
|
+
for (const [scopeIndex, group] of groups)
|
|
26
|
+
for (const id of sourceCallIds)
|
|
27
|
+
if (!group.values.has(id))
|
|
28
|
+
throw new Error(`item join scope item ${scopeIndex}: missing response ${id}`);
|
|
29
|
+
return [...groups]
|
|
30
|
+
.sort(([left], [right]) => left - right)
|
|
31
|
+
.map(([, group]) => ({
|
|
32
|
+
json: {
|
|
33
|
+
responses: Object.fromEntries(sourceCallIds.map((id) => [id, group.values.get(id)])),
|
|
34
|
+
},
|
|
35
|
+
pairedItem: group.inputIndexes.map((item) => ({ item })),
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { node } from '@n8n/workflow-sdk';
|
|
2
|
+
import { mergeParametersSchema } from '../../schemas/native-capability-schemas.js';
|
|
3
|
+
import { createNativeFragment } from './common/create-native-fragment.js';
|
|
4
|
+
export function createMergeAppendCapability() {
|
|
5
|
+
return {
|
|
6
|
+
name: 'merge-append',
|
|
7
|
+
description: 'n8n Merge append: input1..inputN; main output. Waits for all independent incoming branches.',
|
|
8
|
+
parametersSchema: mergeParametersSchema,
|
|
9
|
+
inputPorts: (parameters) => Array.from({ length: mergeParametersSchema.parse(parameters).numberInputs }, (_, index) => `input${index + 1}`),
|
|
10
|
+
outputPorts: () => ['main'],
|
|
11
|
+
responseReferences: () => [],
|
|
12
|
+
waitsForAllInputs: true,
|
|
13
|
+
exclusiveOutputPorts: false,
|
|
14
|
+
compile: ({ planned, position }) => {
|
|
15
|
+
const parameters = mergeParametersSchema.parse(planned.parameters);
|
|
16
|
+
return createNativeFragment(node({
|
|
17
|
+
type: 'n8n-nodes-base.merge',
|
|
18
|
+
version: 3.2,
|
|
19
|
+
config: {
|
|
20
|
+
id: planned.id,
|
|
21
|
+
name: planned.id,
|
|
22
|
+
position,
|
|
23
|
+
parameters: {
|
|
24
|
+
mode: 'append',
|
|
25
|
+
numberInputs: parameters.numberInputs,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
}), Array.from({ length: parameters.numberInputs }, (_, index) => `input${index + 1}`), ['main']);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { node } from '@n8n/workflow-sdk';
|
|
2
|
+
import { assertionParametersSchema } from '../../schemas/native-capability-schemas.js';
|
|
3
|
+
import { createAssertionPlanningParametersSchema } from '../../schemas/create-native-comparison-planning-schema.js';
|
|
4
|
+
import { createNativeFragment } from './common/create-native-fragment.js';
|
|
5
|
+
import { responseReferences } from './common/response-references.js';
|
|
6
|
+
import { responseCheckCode } from './response-check-code.js';
|
|
7
|
+
import { createResponseCheckReader } from './response-checks/create-response-check-reader.js';
|
|
8
|
+
export function createResponseAssertionCapability(input = {}) {
|
|
9
|
+
return {
|
|
10
|
+
name: 'assert-responses',
|
|
11
|
+
description: 'n8n Code: library-generated JSON comparisons. main input/output. Returns pass/assertionCount on success and can be the final node; no separate success/end node is needed. Failed checks throw their message and fail the workflow, without a failure output port. Join branches before comparing their responses.',
|
|
12
|
+
parametersSchema: assertionParametersSchema,
|
|
13
|
+
createPlanningParametersSchema: createAssertionPlanningParametersSchema,
|
|
14
|
+
inputPorts: () => ['main'],
|
|
15
|
+
outputPorts: () => ['main'],
|
|
16
|
+
responseReferences: (parameters) => responseReferences(assertionParametersSchema.parse(parameters).checks),
|
|
17
|
+
waitsForAllInputs: false,
|
|
18
|
+
exclusiveOutputPorts: false,
|
|
19
|
+
compile: ({ planned, apiNodeNames, apiResponseContracts, position }) => {
|
|
20
|
+
const parameters = assertionParametersSchema.parse(planned.parameters);
|
|
21
|
+
const reader = createResponseCheckReader({
|
|
22
|
+
nodeId: planned.id,
|
|
23
|
+
checks: parameters.checks,
|
|
24
|
+
apiNodeNames,
|
|
25
|
+
apiResponseContracts,
|
|
26
|
+
linked: input.itemMode === 'linked',
|
|
27
|
+
});
|
|
28
|
+
const checkCode = `${responseCheckCode(reader.code)}\nconst checks = ${JSON.stringify(parameters.checks)};\nfor (const check of checks) if (!compare(check)) throw new Error(check.message);`;
|
|
29
|
+
const code = input.itemMode === 'linked'
|
|
30
|
+
? `return $input.all().map((_,inputIndex)=>{${checkCode}\nreturn {json:{pass:true,assertionCount:checks.length},pairedItem:{item:inputIndex}};});`
|
|
31
|
+
: `${checkCode}\nreturn [{json: {pass: true, assertionCount: checks.length}}];`;
|
|
32
|
+
const fragment = createNativeFragment(node({
|
|
33
|
+
type: 'n8n-nodes-base.code',
|
|
34
|
+
version: 2,
|
|
35
|
+
config: {
|
|
36
|
+
id: planned.id,
|
|
37
|
+
name: planned.id,
|
|
38
|
+
position,
|
|
39
|
+
parameters: {
|
|
40
|
+
mode: 'runOnceForAllItems',
|
|
41
|
+
language: 'javaScript',
|
|
42
|
+
jsCode: code,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
}), ['main'], ['main']);
|
|
46
|
+
return {
|
|
47
|
+
...fragment,
|
|
48
|
+
preservesInputItems: input.itemMode === 'linked',
|
|
49
|
+
bindingSources: reader.bindingSources,
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { javascriptJsonLiteral } from '../../utils/javascript-json-literal.js';
|
|
2
|
+
import { responseReferences } from './common/response-references.js';
|
|
3
|
+
/** Library-owned runtime, shared by IF expressions and assertion Code nodes. */
|
|
4
|
+
function responseCheckRuntime() {
|
|
5
|
+
return `
|
|
6
|
+
function read(operand) {
|
|
7
|
+
if (operand.source === 'literal') return operand.value;
|
|
8
|
+
if (!Object.hasOwn(responses, operand.nodeId) || responses[operand.nodeId] === undefined)
|
|
9
|
+
throw new Error('Missing response body: ' + operand.nodeId);
|
|
10
|
+
let value = responses[operand.nodeId];
|
|
11
|
+
if (operand.pointer === '') return value;
|
|
12
|
+
for (const encoded of operand.pointer.slice(1).split('/')) {
|
|
13
|
+
const key = encoded.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
14
|
+
if (value === null || typeof value !== 'object' || !Object.hasOwn(value, key))
|
|
15
|
+
throw new Error('Missing response pointer: ' + operand.nodeId + operand.pointer);
|
|
16
|
+
value = value[key];
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function equal(a, b) {
|
|
21
|
+
if (a === b) return true;
|
|
22
|
+
if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') return false;
|
|
23
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
24
|
+
const keys = Object.keys(a);
|
|
25
|
+
return keys.length === Object.keys(b).length && keys.every(key => Object.hasOwn(b, key) && equal(a[key], b[key]));
|
|
26
|
+
}
|
|
27
|
+
function compare(check) {
|
|
28
|
+
const left = read(check.left), right = read(check.right);
|
|
29
|
+
switch (check.operator) {
|
|
30
|
+
case 'equals': return equal(left, right);
|
|
31
|
+
case 'notEquals': return !equal(left, right);
|
|
32
|
+
case 'keysEqual':
|
|
33
|
+
if (left === null || right === null || typeof left !== 'object' || typeof right !== 'object' || Array.isArray(left) || Array.isArray(right))
|
|
34
|
+
throw new Error('keysEqual requires objects');
|
|
35
|
+
return equal(Object.keys(left).sort(), Object.keys(right).sort());
|
|
36
|
+
case 'greaterThan': case 'lessThan':
|
|
37
|
+
if (typeof left !== 'number' || typeof right !== 'number') throw new Error('Numeric comparison requires numbers');
|
|
38
|
+
return check.operator === 'greaterThan' ? left > right : left < right;
|
|
39
|
+
default: throw new Error('Unknown comparison operator');
|
|
40
|
+
}
|
|
41
|
+
}`;
|
|
42
|
+
}
|
|
43
|
+
export function responseCheckCode(responseReaderCode) {
|
|
44
|
+
return `${responseReaderCode}\n${responseCheckRuntime()}`;
|
|
45
|
+
}
|
|
46
|
+
export function conditionExpression(check, names, linked = false) {
|
|
47
|
+
const declaredNames = Object.fromEntries(responseReferences([check]).map((ref) => [ref.nodeId, names[ref.nodeId]]));
|
|
48
|
+
// The preceding Code guard validates full responses. Keep IF expressions small.
|
|
49
|
+
const reader = `const names=${javascriptJsonLiteral(declaredNames)};
|
|
50
|
+
const responses=Object.create(null);
|
|
51
|
+
for (const id of Object.keys(names)) {
|
|
52
|
+
${linked ? 'const envelope=$(names[id]).itemMatching(inputIndex);' : "const items=$(names[id]).all();if(items.length!==1)throw new Error('Response '+id+' requires an unambiguous single JSON item');const envelope=items[0];"}
|
|
53
|
+
if (!envelope || !envelope.json || !Object.hasOwn(envelope.json, 'body'))
|
|
54
|
+
throw new Error('Missing response body: '+id);
|
|
55
|
+
responses[id]=envelope.json.body;
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
return `={{ (() => { ${linked ? 'const inputIndex=$itemIndex;' : ''}${responseCheckCode(reader)}; return compare(${javascriptJsonLiteral(check)}); })() }}`;
|
|
59
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CreateResponseCheckGuardInput } from '../../../types/response-check-guard.js';
|
|
2
|
+
import type { N8nSdkNode } from '../../../types/node-fragment.js';
|
|
3
|
+
/** Validate in Code, not inside n8n's expression-delimited IF parameter. */
|
|
4
|
+
export declare function createResponseCheckGuard({ nodeId, readerCode, linked, position, }: CreateResponseCheckGuardInput): N8nSdkNode;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { node } from '@n8n/workflow-sdk';
|
|
2
|
+
/** Validate in Code, not inside n8n's expression-delimited IF parameter. */
|
|
3
|
+
export function createResponseCheckGuard({ nodeId, readerCode, linked, position, }) {
|
|
4
|
+
const forward = 'return {...item,pairedItem:{item:inputIndex}};';
|
|
5
|
+
const code = linked
|
|
6
|
+
? `return $input.all().map((item,inputIndex)=>{${readerCode}\n${forward}});`
|
|
7
|
+
: `${readerCode}\nreturn $input.all().map((item,inputIndex)=>{${forward}});`;
|
|
8
|
+
return node({
|
|
9
|
+
type: 'n8n-nodes-base.code',
|
|
10
|
+
version: 2,
|
|
11
|
+
config: {
|
|
12
|
+
id: `${nodeId}-response-contract`,
|
|
13
|
+
name: `Validate responses ${nodeId}`,
|
|
14
|
+
position: [position[0] - 220, position[1]],
|
|
15
|
+
parameters: {
|
|
16
|
+
mode: 'runOnceForAllItems',
|
|
17
|
+
language: 'javaScript',
|
|
18
|
+
jsCode: code,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
}
|