@revenexx/integrations-node-sdk 0.12.0 → 0.12.1

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.
Files changed (2) hide show
  1. package/README.md +157 -15
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -42,38 +42,180 @@ ESM/CJS output plus `.d.ts` types.
42
42
 
43
43
  ### Authoring a node
44
44
 
45
- Implement `INode`: a static `description` plus an `execute` method. Single-input
46
- nodes read the conventional `'in'` port.
45
+ Implement `INode`: an instance `description` property (the metadata the registry
46
+ publishes) plus an `execute` method. A few conventions the engine relies on:
47
+
48
+ - **`slug` is namespaced** — `<namespace>:<slug>`, e.g. `revenexx:text-replace`.
49
+ It is the stable identity of the node across versions; not a dotted path.
50
+ - **Every output port carries a `name`.** `execute` returns an `outputs` map
51
+ keyed by that port `name`, and sets `branch` to the port that fired. The key,
52
+ the `branch` and the declared `name` must match — that triple is how the
53
+ engine routes the result to the next node.
54
+ - **Config and inputs arrive in the same map.** The engine resolves each
55
+ declared `config` field (keyed by its `key`) and the input ports (`'in'`, plus
56
+ any named fan-in ports) into the single `inputs: Record<string, unknown>`
57
+ handed to `execute`. So `inputs['in']` is the upstream payload and
58
+ `inputs['<config-key>']` is the resolved config value.
59
+ - **`ctx.signal` is always provided** — propagate it to every I/O call.
60
+
61
+ #### Transform node — single input, single output
62
+
63
+ A `transform` node with one `'in'` port and one named `out` port. Mirrors
64
+ `TextReplaceNode` from `integrations-nodes-core`:
47
65
 
48
66
  ```ts
49
- import type { INode, INodeContext } from '@revenexx/integrations-node-sdk';
67
+ import type { INode, INodeContext, INodeDescription, INodeResult } from '@revenexx/integrations-node-sdk';
50
68
  import { NodeError } from '@revenexx/integrations-node-sdk';
51
69
 
52
70
  export class UppercaseNode implements INode {
53
- description: INode['description'] = {
54
- slug: 'text.uppercase',
71
+ description = {
72
+ slug: 'revenexx:text-uppercase',
55
73
  version: '1.0.0',
56
74
  category: 'transform',
57
- name: 'Uppercase',
75
+ name: { en: 'Uppercase', de: 'Großschreiben' },
76
+ description: { en: 'Upper-cases a string read from the input.' },
77
+ icon: 'mdi:format-letter-case-upper',
58
78
  inputs: { in: { dataType: 'string', required: true } },
59
- outputs: [{ kind: 'default', dataType: 'string' }],
60
- };
79
+ outputs: [{ name: 'out', kind: 'default', dataType: 'string', label: { en: 'Result', de: 'Ergebnis' } }],
80
+ } satisfies INodeDescription;
61
81
 
62
- async execute(ctx: INodeContext, inputs: Record<string, unknown>) {
82
+ async execute(ctx: INodeContext, inputs: Record<string, unknown>): Promise<INodeResult> {
63
83
  ctx.signal.throwIfAborted();
64
84
  const value = inputs['in'];
65
85
  if (typeof value !== 'string') {
66
- throw new NodeError('expected a string on the `in` port');
86
+ // NodeError(code, message, meta?) — a stable code plus a human message.
87
+ throw new NodeError('INVALID_INPUT', 'expected a string on the `in` port');
67
88
  }
68
- return { outputs: { out: value.toUpperCase() } };
89
+ return { outputs: { out: value.toUpperCase() }, branch: 'out' };
69
90
  }
70
91
  }
71
92
  ```
72
93
 
73
- **Error contract:** `throw NodeError` for unexpected failures;
74
- `return { outputs, branch: '<error-port>' }` for expected, routable errors.
75
- Never mix both for the same condition. The engine always provides
76
- `ctx.signal` propagate it to all I/O.
94
+ #### Control node branch outputs
95
+
96
+ A `control` node declares `kind: 'branch'` ports and routes by returning the
97
+ matching `branch`. Condensed from `ConditionIfNode`:
98
+
99
+ ```ts
100
+ export class ConditionIfNode implements INode {
101
+ description = {
102
+ slug: 'revenexx:condition-if',
103
+ version: '1.0.0',
104
+ category: 'control',
105
+ name: { en: 'If condition', de: 'Wenn-Bedingung' },
106
+ icon: 'mdi:source-branch',
107
+ inputs: { in: { dataType: 'any', required: true } },
108
+ outputs: [
109
+ { name: 'true', kind: 'branch', dataType: 'any', label: { en: 'True', de: 'Wahr' } },
110
+ { name: 'false', kind: 'branch', dataType: 'any', label: { en: 'False', de: 'Falsch' } },
111
+ ],
112
+ config: [
113
+ { key: 'field', label: { en: 'Field (dot-path)' }, type: 'string', required: true },
114
+ { key: 'value', label: { en: 'Value' }, type: 'string', expressionAllowed: true },
115
+ ],
116
+ } satisfies INodeDescription;
117
+
118
+ async execute(ctx: INodeContext, inputs: Record<string, unknown>): Promise<INodeResult> {
119
+ const inputData = inputs['in'];
120
+ const field = inputs['field'];
121
+ // `field` is a dot-path selector — resolve it against the input payload,
122
+ // then compare the resolved value to the configured `value`.
123
+ const actual =
124
+ typeof field === 'string'
125
+ ? field
126
+ .split('.')
127
+ .reduce<unknown>(
128
+ (acc, key) => (acc == null ? undefined : (acc as Record<string, unknown>)[key]),
129
+ inputData,
130
+ )
131
+ : undefined;
132
+ const branch = actual === inputs['value'] ? 'true' : 'false';
133
+ // The fired port's name is both the outputs key and the branch.
134
+ return { outputs: { [branch]: inputData }, branch };
135
+ }
136
+ }
137
+ ```
138
+
139
+ #### Action node — error output & credentials
140
+
141
+ An `action` node declares a `kind: 'error'` port for expected, routable
142
+ failures and reads a credential instance via a `credentials-ref` config field.
143
+ Condensed from `HttpRequestNode`:
144
+
145
+ ```ts
146
+ export class HttpRequestNode implements INode {
147
+ description = {
148
+ slug: 'revenexx:http-request',
149
+ version: '1.0.0',
150
+ category: 'action',
151
+ name: { en: 'HTTP Request', de: 'HTTP-Anfrage' },
152
+ icon: 'mdi:web',
153
+ inputs: { in: { dataType: 'object' } },
154
+ outputs: [
155
+ {
156
+ name: 'response',
157
+ kind: 'default',
158
+ dataType: 'object',
159
+ label: { en: 'Response', de: 'Antwort' },
160
+ fields: { status: { dataType: 'number' }, body: { dataType: 'any' } },
161
+ },
162
+ {
163
+ name: 'error',
164
+ kind: 'error',
165
+ dataType: 'object',
166
+ label: { en: 'Error', de: 'Fehler' },
167
+ fields: { code: { dataType: 'string' }, message: { dataType: 'string' } },
168
+ },
169
+ ],
170
+ config: [
171
+ { key: 'url', label: 'URL', type: 'string', required: true, expressionAllowed: true },
172
+ {
173
+ key: 'credentials',
174
+ label: { en: 'Credentials', de: 'Anmeldedaten' },
175
+ type: 'credentials-ref',
176
+ credentialType: 'revenexx:http-bearer',
177
+ },
178
+ ],
179
+ } satisfies INodeDescription;
180
+
181
+ async execute(ctx: INodeContext, inputs: Record<string, unknown>): Promise<INodeResult> {
182
+ const url = inputs['url'];
183
+ if (typeof url !== 'string' || !url) {
184
+ throw new NodeError('MISSING_URL', 'URL config field is required');
185
+ }
186
+
187
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
188
+ const credentialsRef = inputs['credentials'];
189
+ if (typeof credentialsRef === 'string') {
190
+ const creds = await ctx.credentials.get(credentialsRef);
191
+ if (typeof creds['token'] === 'string') headers['Authorization'] = `Bearer ${creds['token']}`;
192
+ }
193
+
194
+ try {
195
+ const res = await fetch(url, { headers, signal: ctx.signal }); // always propagate ctx.signal
196
+ const body = await res.json();
197
+ if (!res.ok) {
198
+ return {
199
+ outputs: { error: { code: 'HTTP_ERROR', message: `${url} → ${res.status}` } },
200
+ branch: 'error',
201
+ };
202
+ }
203
+ return { outputs: { response: { status: res.status, body } }, branch: 'response' };
204
+ } catch (err) {
205
+ if (err instanceof NodeError) throw err;
206
+ // Don't swallow cancellations/timeouts — let aborts propagate so the
207
+ // workflow can cancel promptly instead of routing to the error port.
208
+ if (ctx.signal.aborted || (err instanceof Error && err.name === 'AbortError')) throw err;
209
+ return { outputs: { error: { code: 'REQUEST_FAILED', message: String(err) } }, branch: 'error' };
210
+ }
211
+ }
212
+ }
213
+ ```
214
+
215
+ **Error contract:** `throw NodeError(code, message, meta?)` for unexpected,
216
+ system-level failures; `return { outputs, branch: '<error-port>' }` for
217
+ expected, routable errors (a declared `kind: 'error'` port). Never mix both for
218
+ the same condition.
77
219
 
78
220
  ### Building a manifest
79
221
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revenexx/integrations-node-sdk",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "TypeScript interfaces and utilities for Revenexx integration nodes",
5
5
  "license": "MIT",
6
6
  "author": "revenexx GmbH",