@bonsae/nrg 0.26.1 → 0.26.3
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/package.json
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { fileURLToPath as __nrgFileURLToPath } from "url";
|
|
2
|
+
import { dirname as __nrgDirname } from "path";
|
|
3
|
+
var __filename = __nrgFileURLToPath(import.meta.url);
|
|
4
|
+
var __dirname = __nrgDirname(__filename);
|
|
5
|
+
|
|
6
|
+
|
|
1
7
|
// src/test/server/integration/config.ts
|
|
2
8
|
import path from "path";
|
|
3
9
|
var defaultConfig = {
|
|
@@ -1,373 +1,89 @@
|
|
|
1
|
+
import { fileURLToPath as __nrgFileURLToPath } from "url";
|
|
2
|
+
import { dirname as __nrgDirname } from "path";
|
|
3
|
+
var __filename = __nrgFileURLToPath(import.meta.url);
|
|
4
|
+
var __dirname = __nrgDirname(__filename);
|
|
5
|
+
|
|
6
|
+
|
|
1
7
|
// src/test/server/integration/runtime.ts
|
|
2
8
|
import http from "http";
|
|
3
9
|
import os from "os";
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import { createRequire
|
|
7
|
-
|
|
8
|
-
// src/core/errors.ts
|
|
9
|
-
var NrgError = class _NrgError extends Error {
|
|
10
|
-
constructor(message) {
|
|
11
|
-
super(message);
|
|
12
|
-
this.name = "NrgError";
|
|
13
|
-
Object.setPrototypeOf(this, _NrgError.prototype);
|
|
14
|
-
}
|
|
15
|
-
};
|
|
10
|
+
import fs from "fs";
|
|
11
|
+
import path from "path";
|
|
12
|
+
import { createRequire } from "module";
|
|
13
|
+
import { registerTypes } from "@bonsae/nrg/server";
|
|
16
14
|
|
|
17
|
-
// src/
|
|
18
|
-
var
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
// src/test/server/integration/recorder.ts
|
|
16
|
+
var Recorder = class {
|
|
17
|
+
#sent = /* @__PURE__ */ new Map();
|
|
18
|
+
#received = /* @__PURE__ */ new Map();
|
|
19
|
+
#waiters = [];
|
|
20
|
+
recordSent(id, port, msg) {
|
|
21
|
+
this.#push("sent", id, { port, msg });
|
|
23
22
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// then surface the NRG wrapper if available.
|
|
28
|
-
node: (raw) => {
|
|
29
|
-
if (typeof raw === "string") {
|
|
30
|
-
const node = this.RED.nodes.getNode(raw);
|
|
31
|
-
return node?._node ?? node ?? raw;
|
|
32
|
-
}
|
|
33
|
-
return raw?._node ?? raw;
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
get type() {
|
|
37
|
-
return this.input.type;
|
|
23
|
+
recordReceived(id, msg) {
|
|
24
|
+
if (!id) return;
|
|
25
|
+
this.#push("received", id, { port: 0, msg });
|
|
38
26
|
}
|
|
39
|
-
|
|
40
|
-
|
|
27
|
+
/** Snapshot of all messages on a channel for a node (optionally one port). */
|
|
28
|
+
snapshot(channel, id, port) {
|
|
29
|
+
return this.#filter(channel, id, port).map((c) => c.msg);
|
|
41
30
|
}
|
|
42
|
-
|
|
31
|
+
/** Resolve the message at `index` on a channel, awaiting it if not yet seen. */
|
|
32
|
+
next(channel, id, port, index, timeoutMs) {
|
|
33
|
+
const existing = this.#filter(channel, id, port);
|
|
34
|
+
if (existing.length > index) return Promise.resolve(existing[index].msg);
|
|
43
35
|
return new Promise((resolve, reject) => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
resolve(post ? post(raw) : raw);
|
|
53
|
-
}
|
|
54
|
-
);
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
// src/core/server/nodes/proxy.ts
|
|
60
|
-
function setupConfigProxy(opts) {
|
|
61
|
-
const { RED, node, config, schema } = opts;
|
|
62
|
-
const SKIP_PROPS = /* @__PURE__ */ new Set(["id", "_id", "_users"]);
|
|
63
|
-
const nodeRefProps = /* @__PURE__ */ new Set();
|
|
64
|
-
const typedInputProps = /* @__PURE__ */ new Set();
|
|
65
|
-
if (schema?.properties) {
|
|
66
|
-
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
67
|
-
const s = propSchema;
|
|
68
|
-
if (s?.["x-nrg-node-type"]) nodeRefProps.add(key);
|
|
69
|
-
if (s?.["x-nrg-typed-input"]) typedInputProps.add(key);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
const cache = /* @__PURE__ */ new WeakMap();
|
|
73
|
-
const createProxy = (obj) => {
|
|
74
|
-
const cached = cache.get(obj);
|
|
75
|
-
if (cached) return cached;
|
|
76
|
-
if (Array.isArray(obj)) {
|
|
77
|
-
const mapped = obj.map(
|
|
78
|
-
(item) => item && typeof item === "object" ? createProxy(item) : item
|
|
79
|
-
);
|
|
80
|
-
cache.set(obj, mapped);
|
|
81
|
-
return mapped;
|
|
82
|
-
}
|
|
83
|
-
const proxy = new Proxy(obj, {
|
|
84
|
-
get(target, prop) {
|
|
85
|
-
if (typeof prop === "symbol") return target[prop];
|
|
86
|
-
if (SKIP_PROPS.has(prop)) return target[prop];
|
|
87
|
-
const value = target[prop];
|
|
88
|
-
if (typeof value === "string" && value.length > 0 && nodeRefProps.has(prop)) {
|
|
89
|
-
return RED.nodes.getNode(value)?._node ?? value;
|
|
90
|
-
}
|
|
91
|
-
if (typedInputProps.has(prop) && value && typeof value === "object" && "type" in value && "value" in value) {
|
|
92
|
-
let ref = cache.get(value);
|
|
93
|
-
if (!ref) {
|
|
94
|
-
ref = new TypedInput(RED, node, value);
|
|
95
|
-
cache.set(value, ref);
|
|
96
|
-
}
|
|
97
|
-
return ref;
|
|
98
|
-
}
|
|
99
|
-
if (value && typeof value === "object") {
|
|
100
|
-
return createProxy(value);
|
|
36
|
+
const waiter = {
|
|
37
|
+
channel,
|
|
38
|
+
id,
|
|
39
|
+
port,
|
|
40
|
+
index,
|
|
41
|
+
settle: (msg) => {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
resolve(msg);
|
|
101
44
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
45
|
+
};
|
|
46
|
+
const timer = setTimeout(() => {
|
|
47
|
+
this.#waiters = this.#waiters.filter((w) => w !== waiter);
|
|
48
|
+
const where = port === void 0 ? "" : ` on port ${port}`;
|
|
49
|
+
reject(
|
|
50
|
+
new Error(
|
|
51
|
+
`Timed out after ${timeoutMs}ms waiting for ${channel} message #${index} from node ${id}${where}`
|
|
52
|
+
)
|
|
107
53
|
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
cache.set(obj, proxy);
|
|
111
|
-
return proxy;
|
|
112
|
-
};
|
|
113
|
-
return createProxy(config);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// src/core/server/utils.ts
|
|
117
|
-
function getCredentialsFromSchema(schema) {
|
|
118
|
-
const result = {};
|
|
119
|
-
for (const [key, value] of Object.entries(schema.properties)) {
|
|
120
|
-
const property = value;
|
|
121
|
-
result[key] = {
|
|
122
|
-
// NOTE: required is always false because it is controlled by the JSON Schema and AJV validation instead of using node-red client core
|
|
123
|
-
required: false,
|
|
124
|
-
type: property.format === "password" ? "password" : "text",
|
|
125
|
-
value: property.default ?? void 0
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
return result;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// src/core/server/nodes/symbols.ts
|
|
132
|
-
var WIRE_HANDLERS = Symbol.for("nrg.wireHandlers");
|
|
133
|
-
|
|
134
|
-
// src/core/server/nodes/node.ts
|
|
135
|
-
var cachedSettingsMap = /* @__PURE__ */ new WeakMap();
|
|
136
|
-
var Node = class _Node {
|
|
137
|
-
static type;
|
|
138
|
-
static category;
|
|
139
|
-
static configSchema;
|
|
140
|
-
static credentialsSchema;
|
|
141
|
-
static settingsSchema;
|
|
142
|
-
static validateSettings(RED) {
|
|
143
|
-
if (!this.settingsSchema) return;
|
|
144
|
-
RED.log.info("Validating settings");
|
|
145
|
-
const prefix = this.type.replace(/-./g, (x) => x[1].toUpperCase());
|
|
146
|
-
const properties = this.settingsSchema.properties;
|
|
147
|
-
const settings = {};
|
|
148
|
-
for (const key of Object.keys(properties)) {
|
|
149
|
-
const settingKey = prefix + key.charAt(0).toUpperCase() + key.slice(1);
|
|
150
|
-
const value = RED.settings[settingKey];
|
|
151
|
-
if (value !== void 0) {
|
|
152
|
-
settings[key] = value;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
for (const [key, prop] of Object.entries(properties)) {
|
|
156
|
-
if (settings[key] === void 0) {
|
|
157
|
-
const defaultValue = prop.default ?? prop._default;
|
|
158
|
-
if (defaultValue !== void 0) {
|
|
159
|
-
settings[key] = defaultValue;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
RED.validator.validate(settings, this.settingsSchema, {
|
|
164
|
-
cacheKey: this.settingsSchema.$id || `${this.type}:settings`,
|
|
165
|
-
throwOnError: true
|
|
54
|
+
}, timeoutMs);
|
|
55
|
+
this.#waiters.push(waiter);
|
|
166
56
|
});
|
|
167
|
-
cachedSettingsMap.set(this, settings);
|
|
168
|
-
RED.log.info("Settings are valid");
|
|
169
57
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
for (const [key, prop] of Object.entries(NC.settingsSchema.properties)) {
|
|
175
|
-
const settingKey = prefix + key.charAt(0).toUpperCase() + key.slice(1);
|
|
176
|
-
settings[settingKey] = {
|
|
177
|
-
value: prop.default,
|
|
178
|
-
exportable: prop.exportable ?? false
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
return settings;
|
|
58
|
+
clear() {
|
|
59
|
+
this.#sent.clear();
|
|
60
|
+
this.#received.clear();
|
|
61
|
+
this.#waiters = [];
|
|
182
62
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
* event handler wiring, settings validation, and the user's registered() hook.
|
|
186
|
-
*/
|
|
187
|
-
static async register(RED) {
|
|
188
|
-
const NodeClass = this;
|
|
189
|
-
if (NodeClass.color && !/^#[0-9A-Fa-f]{6}$/.test(NodeClass.color)) {
|
|
190
|
-
throw new NrgError(
|
|
191
|
-
`Invalid color for ${NodeClass.type}: ${NodeClass.color} color must be in hex format`
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
RED.nodes.registerType(
|
|
195
|
-
NodeClass.type,
|
|
196
|
-
function(config) {
|
|
197
|
-
RED.nodes.createNode(this, config);
|
|
198
|
-
const node = new NodeClass(RED, this, config, this.credentials);
|
|
199
|
-
Object.defineProperty(this, "_node", {
|
|
200
|
-
value: node,
|
|
201
|
-
writable: false,
|
|
202
|
-
configurable: false,
|
|
203
|
-
enumerable: false
|
|
204
|
-
});
|
|
205
|
-
const createdPromise = Promise.resolve(node.created?.()).catch(
|
|
206
|
-
(error) => {
|
|
207
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
208
|
-
this.error("Error during created hook: " + message);
|
|
209
|
-
throw error;
|
|
210
|
-
}
|
|
211
|
-
);
|
|
212
|
-
createdPromise.catch(() => {
|
|
213
|
-
});
|
|
214
|
-
node[WIRE_HANDLERS](this, createdPromise);
|
|
215
|
-
},
|
|
216
|
-
{
|
|
217
|
-
credentials: NodeClass.credentialsSchema ? getCredentialsFromSchema(NodeClass.credentialsSchema) : {},
|
|
218
|
-
settings: _Node.#buildSettings(this)
|
|
219
|
-
}
|
|
220
|
-
);
|
|
221
|
-
NodeClass.validateSettings(RED);
|
|
222
|
-
try {
|
|
223
|
-
await Promise.resolve(NodeClass.registered?.(RED));
|
|
224
|
-
} catch (error) {
|
|
225
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
226
|
-
RED.log.error(
|
|
227
|
-
`Error during registered hook for ${NodeClass.type}: ${message}`
|
|
228
|
-
);
|
|
229
|
-
}
|
|
63
|
+
#map(channel) {
|
|
64
|
+
return channel === "sent" ? this.#sent : this.#received;
|
|
230
65
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
config;
|
|
235
|
-
timers = /* @__PURE__ */ new Set();
|
|
236
|
-
intervals = /* @__PURE__ */ new Set();
|
|
237
|
-
constructor(RED, node, config, credentials) {
|
|
238
|
-
this.RED = RED;
|
|
239
|
-
this.node = node;
|
|
240
|
-
const constructor = this.constructor;
|
|
241
|
-
if (constructor.configSchema) {
|
|
242
|
-
this.log("Validating configs");
|
|
243
|
-
const configResult = this.RED.validator.validate(
|
|
244
|
-
config,
|
|
245
|
-
constructor.configSchema,
|
|
246
|
-
{
|
|
247
|
-
cacheKey: constructor.configSchema.$id || `${constructor.type}:configs-schema`,
|
|
248
|
-
throwOnError: false
|
|
249
|
-
}
|
|
250
|
-
);
|
|
251
|
-
if (!configResult.valid && configResult.errors?.length) {
|
|
252
|
-
this.warn(
|
|
253
|
-
`Config validation errors: ${configResult.errors.map((e) => `${e.instancePath} ${e.message}`).join("; ")}`
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
this.config = setupConfigProxy({
|
|
258
|
-
RED,
|
|
259
|
-
node,
|
|
260
|
-
config,
|
|
261
|
-
schema: constructor.configSchema
|
|
262
|
-
});
|
|
263
|
-
if (constructor.credentialsSchema && credentials) {
|
|
264
|
-
this.log("Validating credentials");
|
|
265
|
-
const credResult = this.RED.validator.validate(
|
|
266
|
-
credentials,
|
|
267
|
-
constructor.credentialsSchema,
|
|
268
|
-
{
|
|
269
|
-
cacheKey: constructor.credentialsSchema.$id || `${constructor.type}:credentials-schema`,
|
|
270
|
-
throwOnError: false
|
|
271
|
-
}
|
|
272
|
-
);
|
|
273
|
-
if (!credResult.valid && credResult.errors?.length) {
|
|
274
|
-
this.warn(
|
|
275
|
-
`Credentials validation errors: ${credResult.errors.map((e) => `${e.instancePath} ${e.message}`).join("; ")}`
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
66
|
+
#filter(channel, id, port) {
|
|
67
|
+
const list = this.#map(channel).get(id) ?? [];
|
|
68
|
+
return port === void 0 ? list : list.filter((c) => c.port === port);
|
|
279
69
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
done(error);
|
|
293
|
-
} else {
|
|
294
|
-
this.error("Unknown error occurred while closing node");
|
|
295
|
-
done(new Error("Unknown error occurred while closing node"));
|
|
296
|
-
}
|
|
297
|
-
}
|
|
70
|
+
#push(channel, id, captured) {
|
|
71
|
+
const map = this.#map(channel);
|
|
72
|
+
const list = map.get(id) ?? [];
|
|
73
|
+
list.push(captured);
|
|
74
|
+
map.set(id, list);
|
|
75
|
+
for (let i = this.#waiters.length - 1; i >= 0; i--) {
|
|
76
|
+
const w = this.#waiters[i];
|
|
77
|
+
if (w.channel !== channel || w.id !== id) continue;
|
|
78
|
+
const filtered = this.#filter(channel, id, w.port);
|
|
79
|
+
if (filtered.length > w.index) {
|
|
80
|
+
this.#waiters.splice(i, 1);
|
|
81
|
+
w.settle(filtered[w.index].msg);
|
|
298
82
|
}
|
|
299
|
-
);
|
|
300
|
-
}
|
|
301
|
-
async #closed(removed) {
|
|
302
|
-
try {
|
|
303
|
-
await Promise.resolve(this.closed?.(removed));
|
|
304
|
-
} finally {
|
|
305
|
-
this.log("clearing timers and intervals");
|
|
306
|
-
this.timers.forEach((t) => clearTimeout(t));
|
|
307
|
-
this.intervals.forEach((i) => clearInterval(i));
|
|
308
|
-
this.timers.clear();
|
|
309
|
-
this.intervals.clear();
|
|
310
|
-
this.log("timers and intervals cleared");
|
|
311
83
|
}
|
|
312
84
|
}
|
|
313
|
-
i18n(key, substitutions) {
|
|
314
|
-
const nodeType = this.constructor.type;
|
|
315
|
-
return this.RED._(`${nodeType}.${key}`, substitutions);
|
|
316
|
-
}
|
|
317
|
-
setTimeout(fn, ms) {
|
|
318
|
-
const timer = setTimeout(() => {
|
|
319
|
-
this.timers.delete(timer);
|
|
320
|
-
fn();
|
|
321
|
-
}, ms);
|
|
322
|
-
this.timers.add(timer);
|
|
323
|
-
return timer;
|
|
324
|
-
}
|
|
325
|
-
setInterval(fn, ms) {
|
|
326
|
-
const interval = setInterval(fn, ms);
|
|
327
|
-
this.intervals.add(interval);
|
|
328
|
-
return interval;
|
|
329
|
-
}
|
|
330
|
-
clearTimeout(timer) {
|
|
331
|
-
clearTimeout(timer);
|
|
332
|
-
this.timers.delete(timer);
|
|
333
|
-
}
|
|
334
|
-
clearInterval(interval) {
|
|
335
|
-
clearInterval(interval);
|
|
336
|
-
this.intervals.delete(interval);
|
|
337
|
-
}
|
|
338
|
-
on(event, callback) {
|
|
339
|
-
this.node.on(event, callback);
|
|
340
|
-
}
|
|
341
|
-
log(msg) {
|
|
342
|
-
this.node.log(msg);
|
|
343
|
-
}
|
|
344
|
-
warn(message) {
|
|
345
|
-
this.node.warn(message);
|
|
346
|
-
}
|
|
347
|
-
error(message, msg) {
|
|
348
|
-
this.node.error(message, msg);
|
|
349
|
-
}
|
|
350
|
-
get id() {
|
|
351
|
-
return this.node.id;
|
|
352
|
-
}
|
|
353
|
-
get name() {
|
|
354
|
-
return this.node.name;
|
|
355
|
-
}
|
|
356
|
-
get z() {
|
|
357
|
-
return this.node.z;
|
|
358
|
-
}
|
|
359
|
-
get credentials() {
|
|
360
|
-
return this.node.credentials;
|
|
361
|
-
}
|
|
362
|
-
get settings() {
|
|
363
|
-
const constructor = this.constructor;
|
|
364
|
-
return cachedSettingsMap.get(constructor) ?? {};
|
|
365
|
-
}
|
|
366
85
|
};
|
|
367
86
|
|
|
368
|
-
// src/core/server/nodes/io-node.ts
|
|
369
|
-
import { Kind } from "@sinclair/typebox";
|
|
370
|
-
|
|
371
87
|
// src/core/server/nodes/context.ts
|
|
372
88
|
var updateLocks = /* @__PURE__ */ new WeakMap();
|
|
373
89
|
function setupContext(context, store) {
|
|
@@ -439,857 +155,6 @@ function setupContext(context, store) {
|
|
|
439
155
|
return { get, set, keys, update, increment };
|
|
440
156
|
}
|
|
441
157
|
|
|
442
|
-
// src/core/server/nodes/io-node.ts
|
|
443
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
444
|
-
function isSchemaLike(obj) {
|
|
445
|
-
return obj != null && typeof obj === "object" && Kind in obj;
|
|
446
|
-
}
|
|
447
|
-
var RETURN_PROPERTY_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
448
|
-
var INPUT_KEY = "input";
|
|
449
|
-
var IONode = class _IONode extends Node {
|
|
450
|
-
static align;
|
|
451
|
-
static color;
|
|
452
|
-
static inputSchema;
|
|
453
|
-
// outputsSchema accepts any schema shape: the raw sent value (per port) is
|
|
454
|
-
// validated, and results are frequently non-objects.
|
|
455
|
-
static outputsSchema;
|
|
456
|
-
static validateInput = false;
|
|
457
|
-
static validateOutput = false;
|
|
458
|
-
static get inputs() {
|
|
459
|
-
return this.inputSchema ? 1 : 0;
|
|
460
|
-
}
|
|
461
|
-
static get outputs() {
|
|
462
|
-
const s = this.outputsSchema;
|
|
463
|
-
if (!s) return 0;
|
|
464
|
-
if (Array.isArray(s)) return s.length;
|
|
465
|
-
if (isSchemaLike(s)) return 1;
|
|
466
|
-
const keys = Object.keys(s);
|
|
467
|
-
for (const key of keys) {
|
|
468
|
-
if (/^\d+$/.test(key)) {
|
|
469
|
-
throw new NrgError(
|
|
470
|
-
`outputsSchema record key "${key}" in ${this.type} looks numeric. Use descriptive string names (e.g. "success", "failure") to avoid JavaScript object key ordering issues.`
|
|
471
|
-
);
|
|
472
|
-
}
|
|
473
|
-
if (key === "error" || key === "complete" || key === "status") {
|
|
474
|
-
throw new NrgError(
|
|
475
|
-
`outputsSchema record key "${key}" in ${this.type} is reserved for built-in ports. Use a different name (e.g. "failed" instead of "error").`
|
|
476
|
-
);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
return keys.length;
|
|
480
|
-
}
|
|
481
|
-
/**
|
|
482
|
-
* The names of the base output ports when `outputsSchema` is a record of
|
|
483
|
-
* named ports (`{ success, failure }`), in declaration order — otherwise
|
|
484
|
-
* `undefined` (a single schema or a positional array). Resolved here, where
|
|
485
|
-
* TypeBox's `Kind` symbol is intact, so the editor reads the names directly
|
|
486
|
-
* instead of guessing them from a serialized (symbol-stripped) schema.
|
|
487
|
-
*/
|
|
488
|
-
static get outputPortNames() {
|
|
489
|
-
const s = this.outputsSchema;
|
|
490
|
-
if (!s || Array.isArray(s) || isSchemaLike(s)) return void 0;
|
|
491
|
-
return Object.keys(s);
|
|
492
|
-
}
|
|
493
|
-
/**
|
|
494
|
-
* Per-`input()`-invocation context, scoped via AsyncLocalStorage. One store
|
|
495
|
-
* per class, shared by every instance: each `.run()` isolates one input()
|
|
496
|
-
* call (and everything it `await`s, including detached `.then`/timer
|
|
497
|
-
* continuations it schedules) into its own session, so concurrent inputs on a
|
|
498
|
-
* node never clobber each other's context and a deferred send keeps the
|
|
499
|
-
* context of the input that scheduled it. Real Node-RED never awaits an async
|
|
500
|
-
* input handler before delivering the next message, so two inputs can be in
|
|
501
|
-
* flight at once. `getStore()` is `undefined` only for a send made entirely
|
|
502
|
-
* outside any `input()` (e.g. a timer set up in `created()`): it carries no
|
|
503
|
-
* inherited context and delivers via `node.send`. Server-only — this is part
|
|
504
|
-
* of the node runtime and is never imported in a browser.
|
|
505
|
-
*/
|
|
506
|
-
static #invocation = new AsyncLocalStorage();
|
|
507
|
-
context;
|
|
508
|
-
constructor(RED, node, config, credentials) {
|
|
509
|
-
super(RED, node, config, credentials);
|
|
510
|
-
const context = node.context();
|
|
511
|
-
const resolve = (scope, store) => {
|
|
512
|
-
const target = scope === "global" ? context.global : scope === "flow" ? context.flow : context;
|
|
513
|
-
return setupContext(target, store);
|
|
514
|
-
};
|
|
515
|
-
this.context = Object.assign(resolve, {
|
|
516
|
-
node: setupContext(context),
|
|
517
|
-
flow: setupContext(context.flow),
|
|
518
|
-
global: setupContext(context.global)
|
|
519
|
-
});
|
|
520
|
-
const outputReturnProperties = this.config.outputReturnProperties;
|
|
521
|
-
if (outputReturnProperties) {
|
|
522
|
-
for (const [port, key] of Object.entries(outputReturnProperties)) {
|
|
523
|
-
if (typeof key === "string" && key.trim() && !RETURN_PROPERTY_PATTERN.test(key.trim())) {
|
|
524
|
-
throw new NrgError(
|
|
525
|
-
`Invalid return property "${key}" for output port ${port} in ${this.constructor.type} \u2014 it must be a valid JavaScript identifier (letters, digits, _, $; not starting with a digit)`
|
|
526
|
-
);
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
[WIRE_HANDLERS](nodeRedNode, createdPromise) {
|
|
532
|
-
super[WIRE_HANDLERS](nodeRedNode, createdPromise);
|
|
533
|
-
const NC = this.constructor;
|
|
534
|
-
nodeRedNode.on(
|
|
535
|
-
"input",
|
|
536
|
-
async (msg, send, done) => {
|
|
537
|
-
try {
|
|
538
|
-
await createdPromise;
|
|
539
|
-
} catch {
|
|
540
|
-
done(new Error("Node failed to initialize"));
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
try {
|
|
544
|
-
nodeRedNode.log("Calling input");
|
|
545
|
-
const result = await this.#input(msg, send);
|
|
546
|
-
this.#sendToPort("complete", {
|
|
547
|
-
...msg,
|
|
548
|
-
...result !== void 0 ? { output: result } : {},
|
|
549
|
-
complete: {
|
|
550
|
-
source: this.#nodeSource()
|
|
551
|
-
},
|
|
552
|
-
[INPUT_KEY]: msg
|
|
553
|
-
});
|
|
554
|
-
done();
|
|
555
|
-
nodeRedNode.log("Input processed");
|
|
556
|
-
} catch (error) {
|
|
557
|
-
const errorMsg = error instanceof Error ? error.message : "Unknown error during input handling";
|
|
558
|
-
const errorData = error && typeof error === "object" ? { ...error } : {};
|
|
559
|
-
this.#sendToPort("error", {
|
|
560
|
-
...msg,
|
|
561
|
-
error: {
|
|
562
|
-
...errorData,
|
|
563
|
-
name: error?.name ?? "Error",
|
|
564
|
-
message: errorMsg,
|
|
565
|
-
source: this.#nodeSource()
|
|
566
|
-
},
|
|
567
|
-
[INPUT_KEY]: msg
|
|
568
|
-
});
|
|
569
|
-
if (error instanceof Error) {
|
|
570
|
-
nodeRedNode.error(
|
|
571
|
-
"Error while processing input: " + error.message,
|
|
572
|
-
msg
|
|
573
|
-
);
|
|
574
|
-
done(error);
|
|
575
|
-
} else {
|
|
576
|
-
nodeRedNode.error(
|
|
577
|
-
"Unknown error occurred during input handling",
|
|
578
|
-
msg
|
|
579
|
-
);
|
|
580
|
-
done(new Error(errorMsg));
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
);
|
|
585
|
-
}
|
|
586
|
-
input(msg) {
|
|
587
|
-
return void 0;
|
|
588
|
-
}
|
|
589
|
-
async #input(msg, send) {
|
|
590
|
-
const NodeClass = this.constructor;
|
|
591
|
-
const shouldValidateInput = this.config.validateInput ?? NodeClass.validateInput;
|
|
592
|
-
if (shouldValidateInput && NodeClass.inputSchema) {
|
|
593
|
-
this.log("Validating input");
|
|
594
|
-
this.RED.validator.validate(msg, NodeClass.inputSchema, {
|
|
595
|
-
cacheKey: NodeClass.inputSchema.$id || `${NodeClass.type}:input-schema`,
|
|
596
|
-
throwOnError: true
|
|
597
|
-
});
|
|
598
|
-
this.log("Input is valid");
|
|
599
|
-
}
|
|
600
|
-
return await _IONode.#invocation.run(
|
|
601
|
-
{ inputMsg: msg, send },
|
|
602
|
-
() => Promise.resolve(this.input(msg))
|
|
603
|
-
);
|
|
604
|
-
}
|
|
605
|
-
send(msg) {
|
|
606
|
-
const multi = this.baseOutputs > 1 && Array.isArray(msg);
|
|
607
|
-
const values = multi ? msg.slice(0, this.baseOutputs) : [msg];
|
|
608
|
-
const out = values.map((m, port) => {
|
|
609
|
-
if (m == null) return m;
|
|
610
|
-
this.#validatePort(m, port);
|
|
611
|
-
return this.#wrapOutgoing(m, this.#resolveContextMode(port), port);
|
|
612
|
-
});
|
|
613
|
-
this.#deliver(out);
|
|
614
|
-
}
|
|
615
|
-
#deliver(out) {
|
|
616
|
-
const send = _IONode.#invocation.getStore()?.send;
|
|
617
|
-
if (send) {
|
|
618
|
-
send(out);
|
|
619
|
-
} else {
|
|
620
|
-
this.node.send(out);
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
/**
|
|
624
|
-
* Per-port output validation. A port validates when its flow-author flag
|
|
625
|
-
* (`config.validateOutputs[port]`) — or the node's static `validateOutput`
|
|
626
|
-
* fallback — is on and a schema exists for that port.
|
|
627
|
-
*/
|
|
628
|
-
#validatePort(value, port) {
|
|
629
|
-
const NodeClass = this.constructor;
|
|
630
|
-
const configured = this.config.validateOutputs?.[port];
|
|
631
|
-
if (!(configured ?? NodeClass.validateOutput)) return;
|
|
632
|
-
const schema = this.#outputSchemaForPort(port);
|
|
633
|
-
if (!schema) return;
|
|
634
|
-
this.log("Validating output");
|
|
635
|
-
this.RED.validator.validate(value, schema, {
|
|
636
|
-
cacheKey: schema.$id || `${NodeClass.type}:output-schema:${port}`,
|
|
637
|
-
throwOnError: true
|
|
638
|
-
});
|
|
639
|
-
this.log("Output is valid");
|
|
640
|
-
}
|
|
641
|
-
/** Resolves the output schema for a base-output port: array → `[port]`,
|
|
642
|
-
* record → the port-th value, single schema → itself. */
|
|
643
|
-
#outputSchemaForPort(port) {
|
|
644
|
-
const raw = this.constructor.outputsSchema;
|
|
645
|
-
if (!raw) return void 0;
|
|
646
|
-
if (Array.isArray(raw)) return raw[port];
|
|
647
|
-
if (isSchemaLike(raw)) return raw;
|
|
648
|
-
return Object.values(raw)[port];
|
|
649
|
-
}
|
|
650
|
-
/**
|
|
651
|
-
* The return key for an output port — `"output"` unless a custom one is set
|
|
652
|
-
* via `outputReturnProperties[port]` (author default and/or flow-author
|
|
653
|
-
* override, only possible when the node declares `outputReturnProperties`).
|
|
654
|
-
* `this.send(x)` always means "x is the value at this port's return key",
|
|
655
|
-
* never "x is the whole outgoing message".
|
|
656
|
-
*/
|
|
657
|
-
#returnPropertyKey(port) {
|
|
658
|
-
const configured = this.config.outputReturnProperties?.[port];
|
|
659
|
-
if (typeof configured === "string" && configured.trim()) {
|
|
660
|
-
return configured.trim();
|
|
661
|
-
}
|
|
662
|
-
return "output";
|
|
663
|
-
}
|
|
664
|
-
/**
|
|
665
|
-
* Resolves the context mode for a base-output port from the flow author's
|
|
666
|
-
* per-port config (`config.outputContextModes[port]`, written by the editor
|
|
667
|
-
* when the node declares `outputContextModes`), falling back to `"carry"`.
|
|
668
|
-
*/
|
|
669
|
-
#resolveContextMode(port) {
|
|
670
|
-
return this.config.outputContextModes?.[port] ?? "carry";
|
|
671
|
-
}
|
|
672
|
-
/**
|
|
673
|
-
* Merges a sent value into the incoming message at the returnProperty key so
|
|
674
|
-
* upstream message properties propagate. A fresh base is built per call so
|
|
675
|
-
* multi-port sends never share an object.
|
|
676
|
-
*/
|
|
677
|
-
#wrapOutgoing(value, mode, port) {
|
|
678
|
-
const key = this.#returnPropertyKey(port);
|
|
679
|
-
const input = _IONode.#invocation.getStore()?.inputMsg ?? {};
|
|
680
|
-
if (mode === "reset") {
|
|
681
|
-
return { [key]: value };
|
|
682
|
-
}
|
|
683
|
-
if (mode === "trace") {
|
|
684
|
-
return { ...input, [key]: value, [INPUT_KEY]: input };
|
|
685
|
-
}
|
|
686
|
-
return { ...input, [key]: value };
|
|
687
|
-
}
|
|
688
|
-
// --- Built-in port management ---
|
|
689
|
-
get baseOutputs() {
|
|
690
|
-
return this.constructor.outputs ?? 0;
|
|
691
|
-
}
|
|
692
|
-
get totalOutputs() {
|
|
693
|
-
let count = this.baseOutputs;
|
|
694
|
-
if (this.config.errorPort) count++;
|
|
695
|
-
if (this.config.completePort) count++;
|
|
696
|
-
if (this.config.statusPort) count++;
|
|
697
|
-
return count;
|
|
698
|
-
}
|
|
699
|
-
/**
|
|
700
|
-
* Send a message to a specific output port by index or name.
|
|
701
|
-
* Custom named ports are resolved from `outputsSchema` when it is a record.
|
|
702
|
-
* Numeric indices refer to the base output ports (0-based).
|
|
703
|
-
*
|
|
704
|
-
* Built-in ports (`"error"`, `"complete"`, `"status"`) are managed by the
|
|
705
|
-
* framework and cannot be sent to directly. Use `this.status()` for status,
|
|
706
|
-
* throw an error or call `this.error()` for the error port, and the complete
|
|
707
|
-
* port is sent automatically on successful input processing.
|
|
708
|
-
*/
|
|
709
|
-
sendToPort(port, msg) {
|
|
710
|
-
if (port === "error" || port === "complete" || port === "status") {
|
|
711
|
-
throw new NrgError(
|
|
712
|
-
`sendToPort("${port}") is not allowed. Built-in ports are managed by the framework.`
|
|
713
|
-
);
|
|
714
|
-
}
|
|
715
|
-
const portIndex = typeof port === "number" ? port : this.#getNamedPortIndex(port);
|
|
716
|
-
const mode = this.#resolveContextMode(portIndex ?? 0);
|
|
717
|
-
this.#sendToPort(
|
|
718
|
-
port,
|
|
719
|
-
msg == null ? msg : this.#wrapOutgoing(msg, mode, portIndex ?? 0)
|
|
720
|
-
);
|
|
721
|
-
}
|
|
722
|
-
#sendToPort(port, msg) {
|
|
723
|
-
let portIndex;
|
|
724
|
-
if (typeof port === "number") {
|
|
725
|
-
portIndex = port;
|
|
726
|
-
} else if (port === "error" || port === "complete" || port === "status") {
|
|
727
|
-
portIndex = this.#getBuiltinPortIndex(port);
|
|
728
|
-
if (portIndex === null) return;
|
|
729
|
-
} else {
|
|
730
|
-
portIndex = this.#getNamedPortIndex(port);
|
|
731
|
-
if (portIndex === null) return;
|
|
732
|
-
}
|
|
733
|
-
const out = new Array(this.totalOutputs);
|
|
734
|
-
out[portIndex] = msg;
|
|
735
|
-
this.node.send(out);
|
|
736
|
-
}
|
|
737
|
-
#getNamedPortIndex(name) {
|
|
738
|
-
const schema = this.constructor.outputsSchema;
|
|
739
|
-
if (!schema || Array.isArray(schema) || isSchemaLike(schema)) return null;
|
|
740
|
-
const idx = Object.keys(schema).indexOf(name);
|
|
741
|
-
return idx === -1 ? null : idx;
|
|
742
|
-
}
|
|
743
|
-
#getBuiltinPortIndex(name) {
|
|
744
|
-
if (name === "error") {
|
|
745
|
-
return this.config.errorPort ? this.baseOutputs : null;
|
|
746
|
-
}
|
|
747
|
-
let idx = this.baseOutputs;
|
|
748
|
-
if (this.config.errorPort) idx++;
|
|
749
|
-
if (name === "complete") {
|
|
750
|
-
return this.config.completePort ? idx : null;
|
|
751
|
-
}
|
|
752
|
-
if (this.config.completePort) idx++;
|
|
753
|
-
return this.config.statusPort ? idx : null;
|
|
754
|
-
}
|
|
755
|
-
#nodeSource() {
|
|
756
|
-
return {
|
|
757
|
-
id: this.id,
|
|
758
|
-
type: this.constructor.type,
|
|
759
|
-
name: this.name
|
|
760
|
-
};
|
|
761
|
-
}
|
|
762
|
-
status(status) {
|
|
763
|
-
this.node.status(status);
|
|
764
|
-
this.#sendToPort("status", {
|
|
765
|
-
status,
|
|
766
|
-
source: this.#nodeSource()
|
|
767
|
-
});
|
|
768
|
-
}
|
|
769
|
-
error(message, msg) {
|
|
770
|
-
super.error(message, msg);
|
|
771
|
-
if (msg) {
|
|
772
|
-
this.#sendToPort("error", {
|
|
773
|
-
...msg,
|
|
774
|
-
error: {
|
|
775
|
-
message,
|
|
776
|
-
source: this.#nodeSource()
|
|
777
|
-
},
|
|
778
|
-
[INPUT_KEY]: msg
|
|
779
|
-
});
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
updateWires(wires) {
|
|
783
|
-
this.node.updateWires(wires);
|
|
784
|
-
}
|
|
785
|
-
receive(msg) {
|
|
786
|
-
this.node.receive(msg);
|
|
787
|
-
}
|
|
788
|
-
get x() {
|
|
789
|
-
return this.node.x;
|
|
790
|
-
}
|
|
791
|
-
get y() {
|
|
792
|
-
return this.node.y;
|
|
793
|
-
}
|
|
794
|
-
get g() {
|
|
795
|
-
return this.node.g;
|
|
796
|
-
}
|
|
797
|
-
get wires() {
|
|
798
|
-
return this.node.wires;
|
|
799
|
-
}
|
|
800
|
-
get credentials() {
|
|
801
|
-
return this.node.credentials;
|
|
802
|
-
}
|
|
803
|
-
};
|
|
804
|
-
|
|
805
|
-
// src/core/validator.ts
|
|
806
|
-
import Ajv from "ajv";
|
|
807
|
-
import addFormats from "ajv-formats";
|
|
808
|
-
import addErrors from "ajv-errors";
|
|
809
|
-
var Validator = class {
|
|
810
|
-
ajv;
|
|
811
|
-
constructor(options) {
|
|
812
|
-
const { customKeywords, customFormats, ...ajvOptions } = options || {};
|
|
813
|
-
this.ajv = new Ajv({
|
|
814
|
-
allErrors: true,
|
|
815
|
-
code: {
|
|
816
|
-
source: false
|
|
817
|
-
},
|
|
818
|
-
coerceTypes: true,
|
|
819
|
-
removeAdditional: false,
|
|
820
|
-
strict: false,
|
|
821
|
-
strictSchema: false,
|
|
822
|
-
useDefaults: true,
|
|
823
|
-
validateFormats: true,
|
|
824
|
-
// NOTE: typebox handles validation via typescript
|
|
825
|
-
// NOTE: if true, types that are not serializable JSON, like Function, would not work
|
|
826
|
-
validateSchema: false,
|
|
827
|
-
verbose: true,
|
|
828
|
-
...ajvOptions
|
|
829
|
-
});
|
|
830
|
-
addFormats(this.ajv);
|
|
831
|
-
addErrors(this.ajv);
|
|
832
|
-
this.addCustomKeywords(customKeywords || []);
|
|
833
|
-
this.addCustomFormats(customFormats || {});
|
|
834
|
-
}
|
|
835
|
-
/**
|
|
836
|
-
* Add custom keywords to the validator
|
|
837
|
-
*/
|
|
838
|
-
addCustomKeywords(keywords) {
|
|
839
|
-
if (!keywords) return;
|
|
840
|
-
keywords.forEach((keyword) => {
|
|
841
|
-
this.ajv.addKeyword(keyword);
|
|
842
|
-
});
|
|
843
|
-
}
|
|
844
|
-
/**
|
|
845
|
-
* Add custom formats to the validator
|
|
846
|
-
*/
|
|
847
|
-
addCustomFormats(formats) {
|
|
848
|
-
if (!formats) return;
|
|
849
|
-
Object.entries(formats).forEach(([name, validator]) => {
|
|
850
|
-
if (validator instanceof RegExp) {
|
|
851
|
-
this.ajv.addFormat(name, validator);
|
|
852
|
-
} else {
|
|
853
|
-
this.ajv.addFormat(name, { validate: validator });
|
|
854
|
-
}
|
|
855
|
-
});
|
|
856
|
-
}
|
|
857
|
-
/**
|
|
858
|
-
* Create a validator function with caching
|
|
859
|
-
* @param schema - JSON Schema to validate against
|
|
860
|
-
* @param cacheKey - Optional cache key for reusing validators
|
|
861
|
-
*/
|
|
862
|
-
createValidator(schema, cacheKey) {
|
|
863
|
-
if (cacheKey && !schema.$id) {
|
|
864
|
-
schema.$id = cacheKey;
|
|
865
|
-
}
|
|
866
|
-
if (schema.$id) {
|
|
867
|
-
const cached = this.ajv.getSchema(schema.$id);
|
|
868
|
-
if (cached) return cached;
|
|
869
|
-
}
|
|
870
|
-
const validator = this.ajv.compile(schema);
|
|
871
|
-
return validator;
|
|
872
|
-
}
|
|
873
|
-
/**
|
|
874
|
-
* Validate data against a schema and return a structured result
|
|
875
|
-
*/
|
|
876
|
-
validate(data, schema, options) {
|
|
877
|
-
const validator = this.createValidator(schema, options?.cacheKey);
|
|
878
|
-
const valid = validator(data);
|
|
879
|
-
if (!valid) {
|
|
880
|
-
const errorMessage = this.formatErrors(validator.errors);
|
|
881
|
-
if (options?.throwOnError) {
|
|
882
|
-
throw new ValidationError(errorMessage, validator.errors || []);
|
|
883
|
-
}
|
|
884
|
-
return {
|
|
885
|
-
valid: false,
|
|
886
|
-
errors: validator.errors || void 0,
|
|
887
|
-
errorMessage
|
|
888
|
-
};
|
|
889
|
-
}
|
|
890
|
-
return {
|
|
891
|
-
valid: true,
|
|
892
|
-
data
|
|
893
|
-
};
|
|
894
|
-
}
|
|
895
|
-
/**
|
|
896
|
-
* Format errors into a human-readable string
|
|
897
|
-
*/
|
|
898
|
-
formatErrors(errors, options) {
|
|
899
|
-
if (!errors || errors.length === 0) {
|
|
900
|
-
return "No errors";
|
|
901
|
-
}
|
|
902
|
-
return this.ajv.errorsText(errors, {
|
|
903
|
-
separator: "; ",
|
|
904
|
-
dataVar: "data",
|
|
905
|
-
...options
|
|
906
|
-
});
|
|
907
|
-
}
|
|
908
|
-
/**
|
|
909
|
-
* Get detailed error information
|
|
910
|
-
*/
|
|
911
|
-
getDetailedErrors(errors) {
|
|
912
|
-
if (!errors || errors.length === 0) return [];
|
|
913
|
-
return errors.map((error) => ({
|
|
914
|
-
field: error.instancePath || "/",
|
|
915
|
-
message: error.message || "Validation failed",
|
|
916
|
-
keyword: error.keyword,
|
|
917
|
-
params: error.params,
|
|
918
|
-
schemaPath: error.schemaPath
|
|
919
|
-
}));
|
|
920
|
-
}
|
|
921
|
-
/**
|
|
922
|
-
* Add a schema to the validator for reference
|
|
923
|
-
*/
|
|
924
|
-
addSchema(schema, key) {
|
|
925
|
-
this.ajv.addSchema(schema, key);
|
|
926
|
-
return this;
|
|
927
|
-
}
|
|
928
|
-
/**
|
|
929
|
-
* Remove a schema from the validator
|
|
930
|
-
*/
|
|
931
|
-
removeSchema(key) {
|
|
932
|
-
this.ajv.removeSchema(key);
|
|
933
|
-
return this;
|
|
934
|
-
}
|
|
935
|
-
};
|
|
936
|
-
var ValidationError = class _ValidationError extends Error {
|
|
937
|
-
constructor(message, errors) {
|
|
938
|
-
super(message);
|
|
939
|
-
this.errors = errors;
|
|
940
|
-
this.name = "ValidationError";
|
|
941
|
-
Object.setPrototypeOf(this, _ValidationError.prototype);
|
|
942
|
-
}
|
|
943
|
-
};
|
|
944
|
-
|
|
945
|
-
// src/core/server/validation.ts
|
|
946
|
-
function initValidator(RED) {
|
|
947
|
-
if (RED.validator) return;
|
|
948
|
-
const nrg = {
|
|
949
|
-
validator: new Validator({
|
|
950
|
-
customKeywords: [
|
|
951
|
-
{
|
|
952
|
-
keyword: "x-nrg-skip-validation",
|
|
953
|
-
schemaType: "boolean",
|
|
954
|
-
valid: true
|
|
955
|
-
},
|
|
956
|
-
{
|
|
957
|
-
keyword: "x-nrg-node-type",
|
|
958
|
-
type: "string",
|
|
959
|
-
validate: (schemaValue, dataValue) => {
|
|
960
|
-
if (!dataValue) return true;
|
|
961
|
-
const node = RED.nodes.getNode(dataValue);
|
|
962
|
-
return node?.type === schemaValue;
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
],
|
|
966
|
-
customFormats: {
|
|
967
|
-
"node-id": /^[a-zA-Z0-9-_]+$/,
|
|
968
|
-
"flow-id": /^[a-f0-9]{16}$/,
|
|
969
|
-
"topic-path": (data) => /^[a-zA-Z0-9/_-]+$/.test(data)
|
|
970
|
-
}
|
|
971
|
-
})
|
|
972
|
-
};
|
|
973
|
-
Object.defineProperty(RED, "_nrg", {
|
|
974
|
-
value: nrg,
|
|
975
|
-
writable: false,
|
|
976
|
-
enumerable: false,
|
|
977
|
-
configurable: false
|
|
978
|
-
});
|
|
979
|
-
Object.defineProperty(RED, "validator", {
|
|
980
|
-
get: () => nrg.validator,
|
|
981
|
-
enumerable: false,
|
|
982
|
-
configurable: false
|
|
983
|
-
});
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
// src/core/server/api/assets.ts
|
|
987
|
-
import path from "path";
|
|
988
|
-
import fs from "fs";
|
|
989
|
-
import { createRequire } from "module";
|
|
990
|
-
function serveFile(filePath) {
|
|
991
|
-
return (_req, res, next) => {
|
|
992
|
-
if (!fs.existsSync(filePath)) return next();
|
|
993
|
-
res.setHeader("Content-Type", "application/javascript");
|
|
994
|
-
fs.createReadStream(filePath).pipe(res);
|
|
995
|
-
};
|
|
996
|
-
}
|
|
997
|
-
function initAssetsRoutes(router) {
|
|
998
|
-
const resourcesDir = path.resolve(__dirname, "./resources");
|
|
999
|
-
if (!fs.existsSync(resourcesDir)) return;
|
|
1000
|
-
const _require = createRequire(path.join(__dirname, "package.json"));
|
|
1001
|
-
const vueFile = process.env.NODE_ENV !== "production" ? _require.resolve("vue/dist/vue.esm-browser.js") : _require.resolve("vue/dist/vue.esm-browser.prod.js");
|
|
1002
|
-
router.get(
|
|
1003
|
-
"/nrg/assets/nrg-client.js",
|
|
1004
|
-
serveFile(path.join(resourcesDir, "nrg-client.js"))
|
|
1005
|
-
);
|
|
1006
|
-
router.get("/nrg/assets/vue.esm-browser.prod.js", serveFile(vueFile));
|
|
1007
|
-
router.get("/nrg/assets/vue.esm-browser.js", serveFile(vueFile));
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
// src/core/server/api/routes.ts
|
|
1011
|
-
var _initialized = false;
|
|
1012
|
-
function initRoutes(RED) {
|
|
1013
|
-
if (_initialized) return;
|
|
1014
|
-
_initialized = true;
|
|
1015
|
-
initAssetsRoutes(RED.httpAdmin);
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
// src/core/server/registration.ts
|
|
1019
|
-
async function registerType(RED, NodeClass) {
|
|
1020
|
-
RED.log.debug(`Registering Type: ${NodeClass.type}`);
|
|
1021
|
-
if (!(NodeClass.prototype instanceof Node)) {
|
|
1022
|
-
throw new NrgError(
|
|
1023
|
-
`${NodeClass.name} must extend IONode or ConfigNode classes`
|
|
1024
|
-
);
|
|
1025
|
-
}
|
|
1026
|
-
if (!NodeClass.type) {
|
|
1027
|
-
throw new NrgError("type must be provided when registering the node");
|
|
1028
|
-
}
|
|
1029
|
-
await NodeClass.register(RED);
|
|
1030
|
-
RED.log.debug(`Type registered: ${NodeClass.type}`);
|
|
1031
|
-
}
|
|
1032
|
-
function registerTypes(nodes) {
|
|
1033
|
-
const fn = Object.assign(
|
|
1034
|
-
async function(RED) {
|
|
1035
|
-
initValidator(RED);
|
|
1036
|
-
initRoutes(RED);
|
|
1037
|
-
try {
|
|
1038
|
-
RED.log.info("Registering node types in series");
|
|
1039
|
-
for (const NodeClass of nodes) {
|
|
1040
|
-
await registerType(RED, NodeClass);
|
|
1041
|
-
}
|
|
1042
|
-
RED.log.info("All node types registered in series");
|
|
1043
|
-
} catch (error) {
|
|
1044
|
-
RED.log.error("Error registering node types:", error);
|
|
1045
|
-
throw error;
|
|
1046
|
-
}
|
|
1047
|
-
},
|
|
1048
|
-
{ nodes }
|
|
1049
|
-
);
|
|
1050
|
-
return fn;
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
// src/core/constants.ts
|
|
1054
|
-
var TYPED_INPUT_TYPES = [
|
|
1055
|
-
"msg",
|
|
1056
|
-
"flow",
|
|
1057
|
-
"global",
|
|
1058
|
-
"str",
|
|
1059
|
-
"num",
|
|
1060
|
-
"bool",
|
|
1061
|
-
"json",
|
|
1062
|
-
"bin",
|
|
1063
|
-
"re",
|
|
1064
|
-
"jsonata",
|
|
1065
|
-
"date",
|
|
1066
|
-
"env",
|
|
1067
|
-
"node",
|
|
1068
|
-
"cred"
|
|
1069
|
-
];
|
|
1070
|
-
|
|
1071
|
-
// src/core/server/schemas/type.ts
|
|
1072
|
-
import { Type as BaseType, Kind as Kind2 } from "@sinclair/typebox";
|
|
1073
|
-
import { isJSONType } from "ajv/dist/compile/rules.js";
|
|
1074
|
-
function NodeRef(nodeClass, options) {
|
|
1075
|
-
return {
|
|
1076
|
-
...BaseType.String({
|
|
1077
|
-
description: options?.description || `Reference to ${nodeClass.type}`,
|
|
1078
|
-
format: "node-id"
|
|
1079
|
-
}),
|
|
1080
|
-
"x-nrg-node-type": nodeClass.type,
|
|
1081
|
-
...options,
|
|
1082
|
-
[Kind2]: "NodeRef"
|
|
1083
|
-
};
|
|
1084
|
-
}
|
|
1085
|
-
function TypedInput2(options) {
|
|
1086
|
-
return {
|
|
1087
|
-
...TypedInputSchema,
|
|
1088
|
-
"x-nrg-typed-input": true,
|
|
1089
|
-
...options,
|
|
1090
|
-
[Kind2]: "TypedInput"
|
|
1091
|
-
};
|
|
1092
|
-
}
|
|
1093
|
-
function NrgString(options) {
|
|
1094
|
-
return BaseType.String(options);
|
|
1095
|
-
}
|
|
1096
|
-
function OutputReturnProperties(options) {
|
|
1097
|
-
return BaseType.Record(
|
|
1098
|
-
BaseType.Number(),
|
|
1099
|
-
BaseType.String({ pattern: "^[A-Za-z_$][A-Za-z0-9_$]*$" }),
|
|
1100
|
-
{
|
|
1101
|
-
description: "Per-port return property, keyed by output port index. A missing entry falls back to `output`.",
|
|
1102
|
-
default: {},
|
|
1103
|
-
...options
|
|
1104
|
-
}
|
|
1105
|
-
);
|
|
1106
|
-
}
|
|
1107
|
-
function OutputContextModes(options) {
|
|
1108
|
-
return BaseType.Record(
|
|
1109
|
-
BaseType.Number(),
|
|
1110
|
-
BaseType.Union([
|
|
1111
|
-
BaseType.Literal("carry"),
|
|
1112
|
-
BaseType.Literal("trace"),
|
|
1113
|
-
BaseType.Literal("reset")
|
|
1114
|
-
]),
|
|
1115
|
-
{
|
|
1116
|
-
description: "Per-port context mode, keyed by output port index. A missing entry falls back to `carry`.",
|
|
1117
|
-
default: {},
|
|
1118
|
-
...options
|
|
1119
|
-
}
|
|
1120
|
-
);
|
|
1121
|
-
}
|
|
1122
|
-
var SchemaType = Object.assign({}, BaseType, {
|
|
1123
|
-
String: NrgString,
|
|
1124
|
-
NodeRef,
|
|
1125
|
-
TypedInput: TypedInput2,
|
|
1126
|
-
OutputReturnProperties,
|
|
1127
|
-
OutputContextModes
|
|
1128
|
-
});
|
|
1129
|
-
|
|
1130
|
-
// src/core/server/schemas/base.ts
|
|
1131
|
-
var NodeConfigSchema = SchemaType.Object({
|
|
1132
|
-
id: SchemaType.String(),
|
|
1133
|
-
type: SchemaType.String(),
|
|
1134
|
-
name: SchemaType.String(),
|
|
1135
|
-
z: SchemaType.Optional(SchemaType.String())
|
|
1136
|
-
});
|
|
1137
|
-
var ConfigNodeConfigSchema = SchemaType.Object({
|
|
1138
|
-
...NodeConfigSchema.properties,
|
|
1139
|
-
_users: SchemaType.Array(SchemaType.String())
|
|
1140
|
-
});
|
|
1141
|
-
var IONodeConfigSchema = SchemaType.Object({
|
|
1142
|
-
...NodeConfigSchema.properties,
|
|
1143
|
-
wires: SchemaType.Array(
|
|
1144
|
-
SchemaType.Array(SchemaType.String(), { default: [] }),
|
|
1145
|
-
{
|
|
1146
|
-
default: [[]]
|
|
1147
|
-
}
|
|
1148
|
-
),
|
|
1149
|
-
x: SchemaType.Number(),
|
|
1150
|
-
y: SchemaType.Number(),
|
|
1151
|
-
g: SchemaType.Optional(SchemaType.String())
|
|
1152
|
-
});
|
|
1153
|
-
var TypedInputSchema = SchemaType.Object(
|
|
1154
|
-
{
|
|
1155
|
-
value: SchemaType.Union(
|
|
1156
|
-
[
|
|
1157
|
-
SchemaType.String(),
|
|
1158
|
-
SchemaType.Number(),
|
|
1159
|
-
SchemaType.Boolean(),
|
|
1160
|
-
SchemaType.Null()
|
|
1161
|
-
],
|
|
1162
|
-
{
|
|
1163
|
-
description: "The actual value entered or selected.",
|
|
1164
|
-
default: ""
|
|
1165
|
-
}
|
|
1166
|
-
),
|
|
1167
|
-
type: SchemaType.Union(
|
|
1168
|
-
TYPED_INPUT_TYPES.map((type) => SchemaType.Literal(type)),
|
|
1169
|
-
{
|
|
1170
|
-
description: "The type of the value (string, number, message property, etc.)",
|
|
1171
|
-
default: "str"
|
|
1172
|
-
}
|
|
1173
|
-
)
|
|
1174
|
-
},
|
|
1175
|
-
{
|
|
1176
|
-
description: "Represents a Node-RED TypedInput value and its type.",
|
|
1177
|
-
default: {
|
|
1178
|
-
type: "str",
|
|
1179
|
-
value: ""
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
);
|
|
1183
|
-
var NodeSourceSchema = SchemaType.Object({
|
|
1184
|
-
id: SchemaType.String(),
|
|
1185
|
-
type: SchemaType.String(),
|
|
1186
|
-
name: SchemaType.String()
|
|
1187
|
-
});
|
|
1188
|
-
var ErrorPortSchema = SchemaType.Object({
|
|
1189
|
-
error: SchemaType.Object({
|
|
1190
|
-
message: SchemaType.String(),
|
|
1191
|
-
source: NodeSourceSchema
|
|
1192
|
-
})
|
|
1193
|
-
});
|
|
1194
|
-
var CompletePortSchema = SchemaType.Object({
|
|
1195
|
-
complete: SchemaType.Object({
|
|
1196
|
-
source: NodeSourceSchema
|
|
1197
|
-
})
|
|
1198
|
-
});
|
|
1199
|
-
var StatusPortSchema = SchemaType.Object({
|
|
1200
|
-
status: SchemaType.Union([
|
|
1201
|
-
SchemaType.Object({
|
|
1202
|
-
fill: SchemaType.Optional(
|
|
1203
|
-
SchemaType.Union([
|
|
1204
|
-
SchemaType.Literal("red"),
|
|
1205
|
-
SchemaType.Literal("green")
|
|
1206
|
-
])
|
|
1207
|
-
),
|
|
1208
|
-
shape: SchemaType.Optional(
|
|
1209
|
-
SchemaType.Union([
|
|
1210
|
-
SchemaType.Literal("dot"),
|
|
1211
|
-
SchemaType.Literal("string")
|
|
1212
|
-
])
|
|
1213
|
-
),
|
|
1214
|
-
text: SchemaType.Optional(SchemaType.String())
|
|
1215
|
-
}),
|
|
1216
|
-
SchemaType.String()
|
|
1217
|
-
]),
|
|
1218
|
-
source: NodeSourceSchema
|
|
1219
|
-
});
|
|
1220
|
-
|
|
1221
|
-
// src/test/server/integration/recorder.ts
|
|
1222
|
-
var Recorder = class {
|
|
1223
|
-
#sent = /* @__PURE__ */ new Map();
|
|
1224
|
-
#received = /* @__PURE__ */ new Map();
|
|
1225
|
-
#waiters = [];
|
|
1226
|
-
recordSent(id, port, msg) {
|
|
1227
|
-
this.#push("sent", id, { port, msg });
|
|
1228
|
-
}
|
|
1229
|
-
recordReceived(id, msg) {
|
|
1230
|
-
if (!id) return;
|
|
1231
|
-
this.#push("received", id, { port: 0, msg });
|
|
1232
|
-
}
|
|
1233
|
-
/** Snapshot of all messages on a channel for a node (optionally one port). */
|
|
1234
|
-
snapshot(channel, id, port) {
|
|
1235
|
-
return this.#filter(channel, id, port).map((c) => c.msg);
|
|
1236
|
-
}
|
|
1237
|
-
/** Resolve the message at `index` on a channel, awaiting it if not yet seen. */
|
|
1238
|
-
next(channel, id, port, index, timeoutMs) {
|
|
1239
|
-
const existing = this.#filter(channel, id, port);
|
|
1240
|
-
if (existing.length > index) return Promise.resolve(existing[index].msg);
|
|
1241
|
-
return new Promise((resolve, reject) => {
|
|
1242
|
-
const waiter = {
|
|
1243
|
-
channel,
|
|
1244
|
-
id,
|
|
1245
|
-
port,
|
|
1246
|
-
index,
|
|
1247
|
-
settle: (msg) => {
|
|
1248
|
-
clearTimeout(timer);
|
|
1249
|
-
resolve(msg);
|
|
1250
|
-
}
|
|
1251
|
-
};
|
|
1252
|
-
const timer = setTimeout(() => {
|
|
1253
|
-
this.#waiters = this.#waiters.filter((w) => w !== waiter);
|
|
1254
|
-
const where = port === void 0 ? "" : ` on port ${port}`;
|
|
1255
|
-
reject(
|
|
1256
|
-
new Error(
|
|
1257
|
-
`Timed out after ${timeoutMs}ms waiting for ${channel} message #${index} from node ${id}${where}`
|
|
1258
|
-
)
|
|
1259
|
-
);
|
|
1260
|
-
}, timeoutMs);
|
|
1261
|
-
this.#waiters.push(waiter);
|
|
1262
|
-
});
|
|
1263
|
-
}
|
|
1264
|
-
clear() {
|
|
1265
|
-
this.#sent.clear();
|
|
1266
|
-
this.#received.clear();
|
|
1267
|
-
this.#waiters = [];
|
|
1268
|
-
}
|
|
1269
|
-
#map(channel) {
|
|
1270
|
-
return channel === "sent" ? this.#sent : this.#received;
|
|
1271
|
-
}
|
|
1272
|
-
#filter(channel, id, port) {
|
|
1273
|
-
const list = this.#map(channel).get(id) ?? [];
|
|
1274
|
-
return port === void 0 ? list : list.filter((c) => c.port === port);
|
|
1275
|
-
}
|
|
1276
|
-
#push(channel, id, captured) {
|
|
1277
|
-
const map = this.#map(channel);
|
|
1278
|
-
const list = map.get(id) ?? [];
|
|
1279
|
-
list.push(captured);
|
|
1280
|
-
map.set(id, list);
|
|
1281
|
-
for (let i = this.#waiters.length - 1; i >= 0; i--) {
|
|
1282
|
-
const w = this.#waiters[i];
|
|
1283
|
-
if (w.channel !== channel || w.id !== id) continue;
|
|
1284
|
-
const filtered = this.#filter(channel, id, w.port);
|
|
1285
|
-
if (filtered.length > w.index) {
|
|
1286
|
-
this.#waiters.splice(i, 1);
|
|
1287
|
-
w.settle(filtered[w.index].msg);
|
|
1288
|
-
}
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
};
|
|
1292
|
-
|
|
1293
158
|
// src/test/server/integration/flow.ts
|
|
1294
159
|
var seq = 0;
|
|
1295
160
|
function genId(prefix) {
|
|
@@ -1299,7 +164,7 @@ function genId(prefix) {
|
|
|
1299
164
|
function tick() {
|
|
1300
165
|
return new Promise((resolve) => setImmediate(resolve));
|
|
1301
166
|
}
|
|
1302
|
-
var
|
|
167
|
+
var NodeRef = class {
|
|
1303
168
|
id;
|
|
1304
169
|
type;
|
|
1305
170
|
isConfig;
|
|
@@ -1390,7 +255,7 @@ var Flow = class {
|
|
|
1390
255
|
/** Add any node — regular or config (detected via `category === "config"`). */
|
|
1391
256
|
addNode(Cls, config = {}, opts = {}) {
|
|
1392
257
|
const isConfig = Cls.category === "config";
|
|
1393
|
-
const ref = new
|
|
258
|
+
const ref = new NodeRef(this, Cls.type, isConfig, config, opts);
|
|
1394
259
|
this.#nodes.push(ref);
|
|
1395
260
|
return ref;
|
|
1396
261
|
}
|
|
@@ -1442,7 +307,7 @@ var Flow = class {
|
|
|
1442
307
|
#serializeConfig(config) {
|
|
1443
308
|
const out = {};
|
|
1444
309
|
for (const [key, value] of Object.entries(config)) {
|
|
1445
|
-
out[key] = value instanceof
|
|
310
|
+
out[key] = value instanceof NodeRef ? value.id : value;
|
|
1446
311
|
}
|
|
1447
312
|
return out;
|
|
1448
313
|
}
|
|
@@ -1481,7 +346,7 @@ var Flow = class {
|
|
|
1481
346
|
|
|
1482
347
|
// src/test/server/integration/runtime.ts
|
|
1483
348
|
function requireNodeRed() {
|
|
1484
|
-
const req =
|
|
349
|
+
const req = createRequire(path.join(process.cwd(), "package.json"));
|
|
1485
350
|
let entry;
|
|
1486
351
|
try {
|
|
1487
352
|
entry = req.resolve("node-red");
|
|
@@ -1499,7 +364,7 @@ function headlessSettings(userDir, overrides) {
|
|
|
1499
364
|
disableEditor: true,
|
|
1500
365
|
httpNodeRoot: false,
|
|
1501
366
|
userDir,
|
|
1502
|
-
flowFile:
|
|
367
|
+
flowFile: path.join(userDir, "flows.json"),
|
|
1503
368
|
credentialSecret: "nrg-integration-test",
|
|
1504
369
|
logging: { console: { level: "fatal", metrics: false, audit: false } },
|
|
1505
370
|
functionGlobalContext: {},
|
|
@@ -1508,7 +373,7 @@ function headlessSettings(userDir, overrides) {
|
|
|
1508
373
|
}
|
|
1509
374
|
async function startRuntime(options) {
|
|
1510
375
|
const RED = requireNodeRed();
|
|
1511
|
-
const userDir =
|
|
376
|
+
const userDir = fs.mkdtempSync(path.join(os.tmpdir(), "nrg-integration-"));
|
|
1512
377
|
const server = http.createServer();
|
|
1513
378
|
await new Promise(
|
|
1514
379
|
(resolve) => server.listen(0, "127.0.0.1", () => resolve())
|
|
@@ -1549,14 +414,14 @@ var Runtime = class {
|
|
|
1549
414
|
}
|
|
1550
415
|
await new Promise((resolve) => this.#server.close(() => resolve()));
|
|
1551
416
|
try {
|
|
1552
|
-
|
|
417
|
+
fs.rmSync(this.#userDir, { recursive: true, force: true });
|
|
1553
418
|
} catch {
|
|
1554
419
|
}
|
|
1555
420
|
}
|
|
1556
421
|
};
|
|
1557
422
|
export {
|
|
1558
423
|
Flow,
|
|
1559
|
-
|
|
424
|
+
NodeRef,
|
|
1560
425
|
Runtime,
|
|
1561
426
|
startRuntime
|
|
1562
427
|
};
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { fileURLToPath as __nrgFileURLToPath } from "url";
|
|
2
|
+
import { dirname as __nrgDirname } from "path";
|
|
3
|
+
var __filename = __nrgFileURLToPath(import.meta.url);
|
|
4
|
+
var __dirname = __nrgDirname(__filename);
|
|
5
|
+
|
|
6
|
+
|
|
1
7
|
// src/test/server/unit/config.ts
|
|
2
8
|
import path from "path";
|
|
3
9
|
var defaultConfig = {
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { fileURLToPath as __nrgFileURLToPath } from "url";
|
|
2
|
+
import { dirname as __nrgDirname } from "path";
|
|
3
|
+
var __filename = __nrgFileURLToPath(import.meta.url);
|
|
4
|
+
var __dirname = __nrgDirname(__filename);
|
|
5
|
+
|
|
6
|
+
|
|
1
7
|
// src/test/server/unit/index.ts
|
|
2
8
|
import { vi as vi2 } from "vitest";
|
|
3
9
|
|