@bonsae/nrg-runtime 0.31.0 → 0.32.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/{server/index.cjs → index.cjs} +1381 -1280
- package/package.json +4 -4
- package/resources/nrg.229bba2d.js +13 -0
- package/server/resources/nrg-client.js +0 -12
|
@@ -27,18 +27,15 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
27
|
));
|
|
28
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
29
|
|
|
30
|
-
// src/
|
|
31
|
-
var
|
|
32
|
-
__export(
|
|
33
|
-
CompletePortSchema: () => CompletePortSchema,
|
|
30
|
+
// src/sdk/lib/runtime.ts
|
|
31
|
+
var runtime_exports = {};
|
|
32
|
+
__export(runtime_exports, {
|
|
34
33
|
ConfigNode: () => ConfigNode,
|
|
35
|
-
ErrorPortSchema: () => ErrorPortSchema,
|
|
36
34
|
IONode: () => IONode,
|
|
35
|
+
Kind: () => import_typebox3.Kind,
|
|
37
36
|
Node: () => Node,
|
|
38
|
-
NodeSourceSchema: () => NodeSourceSchema,
|
|
39
37
|
NrgError: () => NrgError,
|
|
40
38
|
SchemaType: () => SchemaType,
|
|
41
|
-
StatusPortSchema: () => StatusPortSchema,
|
|
42
39
|
defineConfigNode: () => defineConfigNode,
|
|
43
40
|
defineIONode: () => defineIONode,
|
|
44
41
|
defineModule: () => defineModule,
|
|
@@ -46,9 +43,14 @@ __export(index_exports, {
|
|
|
46
43
|
registerType: () => registerType,
|
|
47
44
|
registerTypes: () => registerTypes
|
|
48
45
|
});
|
|
49
|
-
module.exports = __toCommonJS(
|
|
46
|
+
module.exports = __toCommonJS(runtime_exports);
|
|
50
47
|
|
|
51
|
-
// src/
|
|
48
|
+
// src/sdk/lib/server/nodes/symbols.ts
|
|
49
|
+
var NRG_WIRE_HANDLERS = Symbol.for("nrg.wireHandlers");
|
|
50
|
+
var NRG_NODE = Symbol.for("nrg.node");
|
|
51
|
+
var NRG_CONFIG_NODE = Symbol.for("nrg.configNode");
|
|
52
|
+
|
|
53
|
+
// src/sdk/lib/shared/errors.ts
|
|
52
54
|
var NrgError = class _NrgError extends Error {
|
|
53
55
|
constructor(message) {
|
|
54
56
|
super(message);
|
|
@@ -57,1426 +59,1525 @@ var NrgError = class _NrgError extends Error {
|
|
|
57
59
|
}
|
|
58
60
|
};
|
|
59
61
|
|
|
60
|
-
// src/
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
resolvers = {
|
|
68
|
-
// evaluateNodeProperty returns the node ID string for "node" type —
|
|
69
|
-
// resolve it to the actual node instance via RED.nodes.getNode,
|
|
70
|
-
// then surface the NRG wrapper if available.
|
|
71
|
-
node: (raw) => {
|
|
72
|
-
if (typeof raw === "string") {
|
|
73
|
-
const node = this.RED.nodes.getNode(raw);
|
|
74
|
-
return node?._node ?? node ?? raw;
|
|
75
|
-
}
|
|
76
|
-
return raw?._node ?? raw;
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
get type() {
|
|
80
|
-
return this.input.type;
|
|
81
|
-
}
|
|
82
|
-
get value() {
|
|
83
|
-
return this.input.value;
|
|
84
|
-
}
|
|
85
|
-
resolve(msg) {
|
|
86
|
-
return new Promise((resolve, reject) => {
|
|
87
|
-
this.RED.util.evaluateNodeProperty(
|
|
88
|
-
this.input.value,
|
|
89
|
-
this.input.type,
|
|
90
|
-
this.node,
|
|
91
|
-
msg,
|
|
92
|
-
(err, raw) => {
|
|
93
|
-
if (err) return reject(err);
|
|
94
|
-
const post = this.resolvers[this.input.type];
|
|
95
|
-
resolve(post ? post(raw) : raw);
|
|
96
|
-
}
|
|
62
|
+
// src/sdk/lib/server/module.ts
|
|
63
|
+
function defineModule(definition) {
|
|
64
|
+
for (const NodeClass of definition.nodes) {
|
|
65
|
+
if (!NodeClass[NRG_NODE]) {
|
|
66
|
+
const name = NodeClass?.name || String(NodeClass);
|
|
67
|
+
throw new NrgError(
|
|
68
|
+
`defineModule: "${name}" is not an nrg node class \u2014 extend IONode/ConfigNode or use defineIONode/defineConfigNode.`
|
|
97
69
|
);
|
|
98
|
-
}
|
|
70
|
+
}
|
|
99
71
|
}
|
|
100
|
-
|
|
72
|
+
return definition;
|
|
73
|
+
}
|
|
101
74
|
|
|
102
|
-
// src/
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
75
|
+
// src/sdk/lib/shared/validator.ts
|
|
76
|
+
var import_ajv = __toESM(require("ajv"), 1);
|
|
77
|
+
var import_ajv_formats = __toESM(require("ajv-formats"), 1);
|
|
78
|
+
var import_ajv_errors = __toESM(require("ajv-errors"), 1);
|
|
79
|
+
var Validator = class {
|
|
80
|
+
// Coercing instance (coerceTypes + useDefaults): mutates the validated object.
|
|
81
|
+
// Used for the config plane, where `this.config.<field>` must read as its typed
|
|
82
|
+
// value (the proxy returns whatever coercion left on the raw config).
|
|
83
|
+
ajv;
|
|
84
|
+
// Non-mutating instance (no coercion, no defaults): a pure predicate. Used for
|
|
85
|
+
// message/form validation so a validated msg/form is never silently rewritten.
|
|
86
|
+
pureAjv;
|
|
87
|
+
// `$id` → the schema object (and owner label) that first claimed it, recorded
|
|
88
|
+
// by reserveSchemaId() at node registration. AJV caches compiled validators by
|
|
89
|
+
// `$id`, so two *different* schemas sharing one `$id` is a silent bug — this
|
|
90
|
+
// registry makes the collision fail loudly.
|
|
91
|
+
registeredSchemaIds = /* @__PURE__ */ new Map();
|
|
92
|
+
constructor(options) {
|
|
93
|
+
const { customKeywords, customFormats, ...ajvOptions } = options || {};
|
|
94
|
+
this.ajv = this.buildAjv(ajvOptions, true, customKeywords, customFormats);
|
|
95
|
+
this.pureAjv = this.buildAjv(
|
|
96
|
+
ajvOptions,
|
|
97
|
+
false,
|
|
98
|
+
customKeywords,
|
|
99
|
+
customFormats
|
|
100
|
+
);
|
|
114
101
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const mapped = obj.map(
|
|
121
|
-
(item) => item && typeof item === "object" ? createProxy(item) : item
|
|
122
|
-
);
|
|
123
|
-
cache.set(obj, mapped);
|
|
124
|
-
return mapped;
|
|
125
|
-
}
|
|
126
|
-
const proxy = new Proxy(obj, {
|
|
127
|
-
get(target, prop) {
|
|
128
|
-
if (typeof prop === "symbol") return target[prop];
|
|
129
|
-
if (SKIP_PROPS.has(prop)) return target[prop];
|
|
130
|
-
const value = target[prop];
|
|
131
|
-
if (nodeRefProps.has(prop)) {
|
|
132
|
-
if (typeof value !== "string" || value.length === 0) return void 0;
|
|
133
|
-
return RED.nodes.getNode(value)?._node ?? value;
|
|
134
|
-
}
|
|
135
|
-
if (typedInputProps.has(prop) && value && typeof value === "object" && "type" in value && "value" in value) {
|
|
136
|
-
let ref = cache.get(value);
|
|
137
|
-
if (!ref) {
|
|
138
|
-
ref = new TypedInput(RED, node, value);
|
|
139
|
-
cache.set(value, ref);
|
|
140
|
-
}
|
|
141
|
-
return ref;
|
|
142
|
-
}
|
|
143
|
-
if (value && typeof value === "object") {
|
|
144
|
-
return createProxy(value);
|
|
145
|
-
}
|
|
146
|
-
return value;
|
|
102
|
+
buildAjv(ajvOptions, mutate, customKeywords, customFormats) {
|
|
103
|
+
const ajv = new import_ajv.default({
|
|
104
|
+
allErrors: true,
|
|
105
|
+
code: {
|
|
106
|
+
source: false
|
|
147
107
|
},
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
108
|
+
// coerceTypes + useDefaults both WRITE into the validated object; gate them
|
|
109
|
+
// on `mutate` so the pure instance leaves data untouched.
|
|
110
|
+
coerceTypes: mutate,
|
|
111
|
+
removeAdditional: false,
|
|
112
|
+
strict: false,
|
|
113
|
+
strictSchema: false,
|
|
114
|
+
useDefaults: mutate,
|
|
115
|
+
validateFormats: true,
|
|
116
|
+
// NOTE: typebox handles validation via typescript
|
|
117
|
+
// NOTE: if true, types that are not serializable JSON, like Function, would not work
|
|
118
|
+
validateSchema: false,
|
|
119
|
+
verbose: true,
|
|
120
|
+
...ajvOptions
|
|
153
121
|
});
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
// src/core/server/utils.ts
|
|
161
|
-
function getCredentialsFromSchema(schema) {
|
|
162
|
-
const result = {};
|
|
163
|
-
for (const [key, value] of Object.entries(schema.properties)) {
|
|
164
|
-
const property = value;
|
|
165
|
-
const isPassword = property.format === "password";
|
|
166
|
-
if (!isPassword) {
|
|
167
|
-
console.warn(
|
|
168
|
-
`[nrg] credential "${key}" has no format:"password" \u2014 it is stored as a visible (cleartext-in-editor) credential. Add { format: "password" } to mask it.`
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
result[key] = {
|
|
172
|
-
// NOTE: required is always false because it is controlled by the JSON Schema and AJV validation instead of using node-red client core
|
|
173
|
-
required: false,
|
|
174
|
-
type: isPassword ? "password" : "text",
|
|
175
|
-
value: property.default ?? void 0
|
|
176
|
-
};
|
|
122
|
+
(0, import_ajv_formats.default)(ajv);
|
|
123
|
+
(0, import_ajv_errors.default)(ajv);
|
|
124
|
+
this.addCustomKeywords(ajv, customKeywords || []);
|
|
125
|
+
this.addCustomFormats(ajv, customFormats || {});
|
|
126
|
+
return ajv;
|
|
177
127
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const settings = {};
|
|
198
|
-
for (const key of Object.keys(properties)) {
|
|
199
|
-
const settingKey = prefix + key.charAt(0).toUpperCase() + key.slice(1);
|
|
200
|
-
const value = RED.settings[settingKey];
|
|
201
|
-
if (value !== void 0) {
|
|
202
|
-
settings[key] = value;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
for (const [key, prop] of Object.entries(properties)) {
|
|
206
|
-
if (settings[key] === void 0) {
|
|
207
|
-
const defaultValue = prop.default ?? prop._default;
|
|
208
|
-
if (defaultValue !== void 0) {
|
|
209
|
-
settings[key] = defaultValue;
|
|
210
|
-
}
|
|
128
|
+
/**
|
|
129
|
+
* Add custom keywords to the given ajv instance
|
|
130
|
+
*/
|
|
131
|
+
addCustomKeywords(ajv, keywords) {
|
|
132
|
+
if (!keywords) return;
|
|
133
|
+
keywords.forEach((keyword) => {
|
|
134
|
+
ajv.addKeyword(keyword);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Add custom formats to the given ajv instance
|
|
139
|
+
*/
|
|
140
|
+
addCustomFormats(ajv, formats) {
|
|
141
|
+
if (!formats) return;
|
|
142
|
+
Object.entries(formats).forEach(([name, validator]) => {
|
|
143
|
+
if (validator instanceof RegExp) {
|
|
144
|
+
ajv.addFormat(name, validator);
|
|
145
|
+
} else {
|
|
146
|
+
ajv.addFormat(name, { validate: validator });
|
|
211
147
|
}
|
|
212
|
-
}
|
|
213
|
-
RED.validator.validate(settings, this.settingsSchema, {
|
|
214
|
-
cacheKey: this.settingsSchema.$id || `${this.type}:settings`,
|
|
215
|
-
throwOnError: true
|
|
216
148
|
});
|
|
217
|
-
cachedSettingsMap.set(this, settings);
|
|
218
|
-
RED.log.info("Settings are valid");
|
|
219
149
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
150
|
+
/**
|
|
151
|
+
* Compile (and cache) a validator for a schema, using AJV's own registry as the
|
|
152
|
+
* cache — keyed by the schema's `$id`. `defineSchema` requires a unique `$id`,
|
|
153
|
+
* so `getSchema($id)` returns the previously compiled validator and `compile`
|
|
154
|
+
* registers it on first use. We never write `$id` (no mutation of the caller's
|
|
155
|
+
* schema). A schema without `$id` (rare — an ad-hoc `SchemaType.Object`) simply
|
|
156
|
+
* compiles fresh each call.
|
|
157
|
+
*
|
|
158
|
+
* @param schema - JSON Schema to validate against
|
|
159
|
+
* @param mutate - `true` (default) uses the coercing instance; `false` uses the
|
|
160
|
+
* non-mutating (pure-predicate) instance. Each AJV keeps its own registry.
|
|
161
|
+
*/
|
|
162
|
+
createValidator(schema, mutate = true) {
|
|
163
|
+
const ajv = mutate ? this.ajv : this.pureAjv;
|
|
164
|
+
if (schema.$id) {
|
|
165
|
+
const cached = ajv.getSchema(schema.$id);
|
|
166
|
+
if (cached) return cached;
|
|
230
167
|
}
|
|
231
|
-
return
|
|
168
|
+
return ajv.compile(schema);
|
|
232
169
|
}
|
|
233
170
|
/**
|
|
234
|
-
*
|
|
235
|
-
*
|
|
171
|
+
* Reserve a schema's `$id` at registration so a collision fails loudly.
|
|
172
|
+
* {@link createValidator} caches compiled validators by `$id`, so if two
|
|
173
|
+
* *different* schemas share one `$id` the second would silently validate
|
|
174
|
+
* against the first. Reserving the same schema object again (a re-deploy, or
|
|
175
|
+
* one schema reused across roles) is idempotent. A schema without `$id` is a
|
|
176
|
+
* no-op here. Convention: `"<node-type>:<role>"`.
|
|
177
|
+
*
|
|
178
|
+
* @param schema - the schema being registered (its `$id` is the key)
|
|
179
|
+
* @param owner - human-readable origin (e.g. `"my-node.config"`) for the error
|
|
180
|
+
* @throws NrgError when `$id` is already held by a different schema object.
|
|
236
181
|
*/
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
if (
|
|
182
|
+
reserveSchemaId(schema, owner) {
|
|
183
|
+
const id = schema.$id;
|
|
184
|
+
if (!id) return;
|
|
185
|
+
const existing = this.registeredSchemaIds.get(id);
|
|
186
|
+
if (existing && existing.schema !== schema) {
|
|
240
187
|
throw new NrgError(
|
|
241
|
-
`
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
RED.nodes.registerType(
|
|
245
|
-
NodeClass.type,
|
|
246
|
-
function(config) {
|
|
247
|
-
RED.nodes.createNode(this, config);
|
|
248
|
-
const node = new NodeClass(RED, this, config, this.credentials);
|
|
249
|
-
Object.defineProperty(this, "_node", {
|
|
250
|
-
value: node,
|
|
251
|
-
writable: false,
|
|
252
|
-
configurable: false,
|
|
253
|
-
enumerable: false
|
|
254
|
-
});
|
|
255
|
-
const createdPromise = Promise.resolve(node.created?.()).catch(
|
|
256
|
-
(error) => {
|
|
257
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
258
|
-
this.error("Error during created hook: " + message);
|
|
259
|
-
throw error;
|
|
260
|
-
}
|
|
261
|
-
);
|
|
262
|
-
createdPromise.catch(() => {
|
|
263
|
-
this.status({ fill: "red", shape: "ring", text: "created() failed" });
|
|
264
|
-
});
|
|
265
|
-
node[WIRE_HANDLERS](this, createdPromise);
|
|
266
|
-
},
|
|
267
|
-
{
|
|
268
|
-
credentials: NodeClass.credentialsSchema ? getCredentialsFromSchema(NodeClass.credentialsSchema) : {},
|
|
269
|
-
settings: _Node.#buildSettings(this)
|
|
270
|
-
}
|
|
271
|
-
);
|
|
272
|
-
NodeClass.validateSettings(RED);
|
|
273
|
-
try {
|
|
274
|
-
await Promise.resolve(NodeClass.registered?.(RED));
|
|
275
|
-
} catch (error) {
|
|
276
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
277
|
-
RED.log.error(
|
|
278
|
-
`Error during registered hook for ${NodeClass.type}: ${message}`
|
|
188
|
+
`Duplicate schema $id "${id}": declared by both ${existing.owner} and ${owner}. $id is the AJV compile-cache key and must be unique \u2014 a collision makes one schema validate against another. Convention: "<node-type>:<role>".`
|
|
279
189
|
);
|
|
280
190
|
}
|
|
191
|
+
this.registeredSchemaIds.set(id, { schema, owner });
|
|
281
192
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (constructor.configSchema) {
|
|
293
|
-
this.log("Validating configs");
|
|
294
|
-
const configResult = this.RED.validator.validate(
|
|
295
|
-
config,
|
|
296
|
-
constructor.configSchema,
|
|
297
|
-
{
|
|
298
|
-
cacheKey: constructor.configSchema.$id || `${constructor.type}:configs-schema`,
|
|
299
|
-
throwOnError: false
|
|
300
|
-
}
|
|
301
|
-
);
|
|
302
|
-
if (!configResult.valid && configResult.errors?.length) {
|
|
303
|
-
this.warn(
|
|
304
|
-
`Config validation errors: ${configResult.errors.map((e) => `${e.instancePath} ${e.message}`).join("; ")}`
|
|
305
|
-
);
|
|
193
|
+
/**
|
|
194
|
+
* Validate data against a schema and return a structured result
|
|
195
|
+
*/
|
|
196
|
+
validate(data, schema, options) {
|
|
197
|
+
const validator = this.createValidator(schema, options?.mutate ?? true);
|
|
198
|
+
const valid = validator(data);
|
|
199
|
+
if (!valid) {
|
|
200
|
+
const errorMessage = this.formatErrors(validator.errors);
|
|
201
|
+
if (options?.throwOnError) {
|
|
202
|
+
throw new ValidationError(errorMessage, validator.errors || []);
|
|
306
203
|
}
|
|
204
|
+
return {
|
|
205
|
+
valid: false,
|
|
206
|
+
errors: validator.errors || void 0,
|
|
207
|
+
errorMessage
|
|
208
|
+
};
|
|
307
209
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
schema: constructor.configSchema
|
|
313
|
-
});
|
|
314
|
-
if (constructor.credentialsSchema && credentials) {
|
|
315
|
-
this.log("Validating credentials");
|
|
316
|
-
const credResult = this.RED.validator.validate(
|
|
317
|
-
credentials,
|
|
318
|
-
constructor.credentialsSchema,
|
|
319
|
-
{
|
|
320
|
-
cacheKey: constructor.credentialsSchema.$id || `${constructor.type}:credentials-schema`,
|
|
321
|
-
throwOnError: false
|
|
322
|
-
}
|
|
323
|
-
);
|
|
324
|
-
if (!credResult.valid && credResult.errors?.length) {
|
|
325
|
-
this.warn(
|
|
326
|
-
`Credentials validation errors: ${credResult.errors.map((e) => `${e.instancePath} ${e.message}`).join("; ")}`
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
[WIRE_HANDLERS](nodeRedNode, createdPromise) {
|
|
332
|
-
nodeRedNode.on(
|
|
333
|
-
"close",
|
|
334
|
-
async (removed, done) => {
|
|
335
|
-
try {
|
|
336
|
-
this.log("Calling closed");
|
|
337
|
-
await this.#closed(removed);
|
|
338
|
-
this.log("Node was closed");
|
|
339
|
-
done();
|
|
340
|
-
} catch (error) {
|
|
341
|
-
if (error instanceof Error) {
|
|
342
|
-
this.error("Error while closing node: " + error.message);
|
|
343
|
-
done(error);
|
|
344
|
-
} else {
|
|
345
|
-
this.error("Unknown error occurred while closing node");
|
|
346
|
-
done(new Error("Unknown error occurred while closing node"));
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
);
|
|
210
|
+
return {
|
|
211
|
+
valid: true,
|
|
212
|
+
data
|
|
213
|
+
};
|
|
351
214
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
this.intervals.forEach((i) => clearInterval(i));
|
|
359
|
-
this.timers.clear();
|
|
360
|
-
this.intervals.clear();
|
|
361
|
-
this.log("timers and intervals cleared");
|
|
215
|
+
/**
|
|
216
|
+
* Format errors into a human-readable string
|
|
217
|
+
*/
|
|
218
|
+
formatErrors(errors, options) {
|
|
219
|
+
if (!errors || errors.length === 0) {
|
|
220
|
+
return "No errors";
|
|
362
221
|
}
|
|
222
|
+
return this.ajv.errorsText(errors, {
|
|
223
|
+
separator: "; ",
|
|
224
|
+
dataVar: "data",
|
|
225
|
+
...options
|
|
226
|
+
});
|
|
363
227
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
228
|
+
/**
|
|
229
|
+
* Get detailed error information
|
|
230
|
+
*/
|
|
231
|
+
getDetailedErrors(errors) {
|
|
232
|
+
if (!errors || errors.length === 0) return [];
|
|
233
|
+
return errors.map((error) => ({
|
|
234
|
+
field: error.instancePath || "/",
|
|
235
|
+
message: error.message || "Validation failed",
|
|
236
|
+
keyword: error.keyword,
|
|
237
|
+
params: error.params,
|
|
238
|
+
schemaPath: error.schemaPath
|
|
239
|
+
}));
|
|
367
240
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
this.
|
|
374
|
-
return
|
|
241
|
+
/**
|
|
242
|
+
* Add a schema to both validator instances for cross-schema `$ref` reference.
|
|
243
|
+
*/
|
|
244
|
+
addSchema(schema, key) {
|
|
245
|
+
this.ajv.addSchema(schema, key);
|
|
246
|
+
this.pureAjv.addSchema(schema, key);
|
|
247
|
+
return this;
|
|
375
248
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
249
|
+
/**
|
|
250
|
+
* Remove a schema from both validator instances.
|
|
251
|
+
*/
|
|
252
|
+
removeSchema(key) {
|
|
253
|
+
this.ajv.removeSchema(key);
|
|
254
|
+
this.pureAjv.removeSchema(key);
|
|
255
|
+
return this;
|
|
380
256
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
257
|
+
};
|
|
258
|
+
var ValidationError = class _ValidationError extends Error {
|
|
259
|
+
constructor(message, errors) {
|
|
260
|
+
super(message);
|
|
261
|
+
this.errors = errors;
|
|
262
|
+
this.name = "ValidationError";
|
|
263
|
+
Object.setPrototypeOf(this, _ValidationError.prototype);
|
|
384
264
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// src/sdk/lib/server/validation.ts
|
|
268
|
+
function initValidator(RED) {
|
|
269
|
+
if (RED.validator) return;
|
|
270
|
+
const nrg = {
|
|
271
|
+
validator: new Validator({
|
|
272
|
+
customKeywords: [
|
|
273
|
+
{
|
|
274
|
+
keyword: "x-nrg-skip-validation",
|
|
275
|
+
schemaType: "boolean",
|
|
276
|
+
valid: true
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
keyword: "x-nrg-node-type",
|
|
280
|
+
type: "string",
|
|
281
|
+
validate: (schemaValue, dataValue) => {
|
|
282
|
+
if (!dataValue) return true;
|
|
283
|
+
const node = RED.nodes.getNode(dataValue);
|
|
284
|
+
return node?.type === schemaValue;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
],
|
|
288
|
+
customFormats: {
|
|
289
|
+
"node-id": /^[a-zA-Z0-9-_]+$/
|
|
290
|
+
}
|
|
291
|
+
})
|
|
292
|
+
};
|
|
293
|
+
Object.defineProperty(RED, "_nrg", {
|
|
294
|
+
value: nrg,
|
|
295
|
+
writable: false,
|
|
296
|
+
enumerable: false,
|
|
297
|
+
configurable: false
|
|
298
|
+
});
|
|
299
|
+
Object.defineProperty(RED, "validator", {
|
|
300
|
+
get: () => nrg.validator,
|
|
301
|
+
enumerable: false,
|
|
302
|
+
configurable: false
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/sdk/lib/server/api/assets.ts
|
|
307
|
+
var import_path = __toESM(require("path"), 1);
|
|
308
|
+
var import_fs = __toESM(require("fs"), 1);
|
|
309
|
+
var import_module = require("module");
|
|
310
|
+
var CLIENT_ASSET = true ? "nrg.229bba2d.js" : "nrg.js";
|
|
311
|
+
function serveFile(filePath) {
|
|
312
|
+
return (_req, res, next) => {
|
|
313
|
+
if (!import_fs.default.existsSync(filePath)) return next();
|
|
314
|
+
res.setHeader("Content-Type", "application/javascript");
|
|
315
|
+
import_fs.default.createReadStream(filePath).pipe(res);
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function initAssetsRoutes(router) {
|
|
319
|
+
const resourcesDir = import_path.default.resolve(__dirname, "./resources");
|
|
320
|
+
if (!import_fs.default.existsSync(resourcesDir)) return;
|
|
321
|
+
const _require = (0, import_module.createRequire)(import_path.default.join(__dirname, "package.json"));
|
|
322
|
+
const vueFile = process.env.NODE_ENV !== "production" ? _require.resolve("vue/dist/vue.esm-browser.js") : _require.resolve("vue/dist/vue.esm-browser.prod.js");
|
|
323
|
+
router.get(
|
|
324
|
+
`/nrg/assets/${CLIENT_ASSET}`,
|
|
325
|
+
serveFile(import_path.default.join(resourcesDir, CLIENT_ASSET))
|
|
326
|
+
);
|
|
327
|
+
router.get("/nrg/assets/vue.esm-browser.prod.js", serveFile(vueFile));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// src/sdk/lib/server/api/routes.ts
|
|
331
|
+
var _initialized = false;
|
|
332
|
+
function initRoutes(RED) {
|
|
333
|
+
if (_initialized) return;
|
|
334
|
+
_initialized = true;
|
|
335
|
+
initAssetsRoutes(RED.httpAdmin);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/sdk/lib/shared/schemas/factories.ts
|
|
339
|
+
var import_typebox2 = require("@sinclair/typebox");
|
|
340
|
+
|
|
341
|
+
// src/sdk/lib/shared/constants.ts
|
|
342
|
+
var TYPED_INPUT_TYPES = [
|
|
343
|
+
"msg",
|
|
344
|
+
"flow",
|
|
345
|
+
"global",
|
|
346
|
+
"str",
|
|
347
|
+
"num",
|
|
348
|
+
"bool",
|
|
349
|
+
"json",
|
|
350
|
+
"bin",
|
|
351
|
+
"re",
|
|
352
|
+
"jsonata",
|
|
353
|
+
"date",
|
|
354
|
+
"env",
|
|
355
|
+
"node",
|
|
356
|
+
"cred"
|
|
357
|
+
];
|
|
358
|
+
|
|
359
|
+
// src/sdk/lib/shared/schemas/base.ts
|
|
360
|
+
var import_typebox = require("@sinclair/typebox");
|
|
361
|
+
var TypedInputSchema = import_typebox.Type.Object(
|
|
362
|
+
{
|
|
363
|
+
value: import_typebox.Type.Union(
|
|
364
|
+
[
|
|
365
|
+
import_typebox.Type.String(),
|
|
366
|
+
import_typebox.Type.Number(),
|
|
367
|
+
import_typebox.Type.Boolean(),
|
|
368
|
+
import_typebox.Type.Null()
|
|
369
|
+
],
|
|
370
|
+
{
|
|
371
|
+
description: "The actual value entered or selected.",
|
|
372
|
+
default: ""
|
|
373
|
+
}
|
|
374
|
+
),
|
|
375
|
+
type: import_typebox.Type.Union(
|
|
376
|
+
TYPED_INPUT_TYPES.map((type) => import_typebox.Type.Literal(type)),
|
|
377
|
+
{
|
|
378
|
+
description: "The type of the value (string, number, message property, etc.)",
|
|
379
|
+
default: "str"
|
|
380
|
+
}
|
|
381
|
+
)
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
description: "Represents a Node-RED TypedInput value and its type.",
|
|
385
|
+
default: {
|
|
386
|
+
type: "str",
|
|
387
|
+
value: ""
|
|
388
|
+
}
|
|
388
389
|
}
|
|
389
|
-
|
|
390
|
-
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
// src/sdk/lib/shared/schemas/factories.ts
|
|
393
|
+
var JSON_TYPES = /* @__PURE__ */ new Set([
|
|
394
|
+
"null",
|
|
395
|
+
"boolean",
|
|
396
|
+
"object",
|
|
397
|
+
"array",
|
|
398
|
+
"number",
|
|
399
|
+
"integer",
|
|
400
|
+
"string"
|
|
401
|
+
]);
|
|
402
|
+
function isJSONType(value) {
|
|
403
|
+
return typeof value === "string" && JSON_TYPES.has(value);
|
|
404
|
+
}
|
|
405
|
+
function NodeRef(type, options) {
|
|
406
|
+
return {
|
|
407
|
+
...import_typebox2.Type.String({
|
|
408
|
+
description: options?.description || `Reference to ${type}`
|
|
409
|
+
}),
|
|
410
|
+
// `...options` first so user options (description, x-nrg-form, …) apply, but
|
|
411
|
+
// the framework keys below win — a caller can't clobber `format`/
|
|
412
|
+
// `x-nrg-node-type`/`Kind` and silently break NodeRef resolution.
|
|
413
|
+
...options,
|
|
414
|
+
format: "node-id",
|
|
415
|
+
"x-nrg-node-type": type,
|
|
416
|
+
[import_typebox2.Kind]: "NodeRef"
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
function TypedInput(options) {
|
|
420
|
+
const opts = options ?? {};
|
|
421
|
+
const restrictedTypes = opts["x-nrg-form"]?.typedInputTypes;
|
|
422
|
+
if (restrictedTypes && restrictedTypes.length === 0) {
|
|
423
|
+
throw new Error(
|
|
424
|
+
"SchemaType.TypedInput: typedInputTypes must list at least one type."
|
|
425
|
+
);
|
|
391
426
|
}
|
|
392
|
-
|
|
393
|
-
|
|
427
|
+
const explicitType = opts.default?.type;
|
|
428
|
+
if (restrictedTypes && explicitType !== void 0 && !restrictedTypes.includes(explicitType)) {
|
|
429
|
+
throw new Error(
|
|
430
|
+
`SchemaType.TypedInput: default type "${explicitType}" is not one of its typedInputTypes [${restrictedTypes.join(", ")}].`
|
|
431
|
+
);
|
|
394
432
|
}
|
|
395
|
-
|
|
396
|
-
|
|
433
|
+
const defaultType = explicitType ?? (restrictedTypes ? restrictedTypes.includes("str") ? "str" : restrictedTypes[0] : "str");
|
|
434
|
+
return {
|
|
435
|
+
...TypedInputSchema,
|
|
436
|
+
// `...options` first; the framework keys below win (see NodeRef).
|
|
437
|
+
...options,
|
|
438
|
+
default: {
|
|
439
|
+
...TypedInputSchema.default,
|
|
440
|
+
...opts.default,
|
|
441
|
+
type: defaultType
|
|
442
|
+
},
|
|
443
|
+
"x-nrg-typed-input": true,
|
|
444
|
+
[import_typebox2.Kind]: "TypedInput"
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
function NrgString(options) {
|
|
448
|
+
return import_typebox2.Type.String(options);
|
|
449
|
+
}
|
|
450
|
+
function OutputReturnProperties(options) {
|
|
451
|
+
return import_typebox2.Type.Record(
|
|
452
|
+
import_typebox2.Type.Number(),
|
|
453
|
+
import_typebox2.Type.String({ pattern: "^[A-Za-z_$][A-Za-z0-9_$]*$" }),
|
|
454
|
+
{
|
|
455
|
+
description: "Per-port return property, keyed by output port index. A missing entry falls back to `output`.",
|
|
456
|
+
default: {},
|
|
457
|
+
...options
|
|
458
|
+
}
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
function OutputContextModes(options) {
|
|
462
|
+
return import_typebox2.Type.Record(
|
|
463
|
+
import_typebox2.Type.Number(),
|
|
464
|
+
import_typebox2.Type.Union([
|
|
465
|
+
import_typebox2.Type.Literal("carry"),
|
|
466
|
+
import_typebox2.Type.Literal("trace"),
|
|
467
|
+
import_typebox2.Type.Literal("reset")
|
|
468
|
+
]),
|
|
469
|
+
{
|
|
470
|
+
description: "Per-port context mode, keyed by output port index. A missing entry falls back to `carry`.",
|
|
471
|
+
default: {},
|
|
472
|
+
...options
|
|
473
|
+
}
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
function NrgUnsafe(options) {
|
|
477
|
+
return import_typebox2.Type.Unsafe(options);
|
|
478
|
+
}
|
|
479
|
+
var NRG_SCHEMA_TYPES_FACTORIES = {
|
|
480
|
+
String: NrgString,
|
|
481
|
+
Unsafe: NrgUnsafe,
|
|
482
|
+
NodeRef,
|
|
483
|
+
TypedInput,
|
|
484
|
+
OutputReturnProperties,
|
|
485
|
+
OutputContextModes
|
|
486
|
+
};
|
|
487
|
+
var SchemaType = Object.assign(
|
|
488
|
+
{},
|
|
489
|
+
import_typebox2.Type,
|
|
490
|
+
NRG_SCHEMA_TYPES_FACTORIES
|
|
491
|
+
);
|
|
492
|
+
function markNonValidatable(schema) {
|
|
493
|
+
const type = schema.type;
|
|
494
|
+
const hasInvalidType = type !== void 0 && (Array.isArray(type) ? !type.every(isJSONType) : !isJSONType(type));
|
|
495
|
+
if (hasInvalidType) {
|
|
496
|
+
schema["x-nrg-skip-validation"] = true;
|
|
497
|
+
if (schema.default !== void 0) {
|
|
498
|
+
schema._default = schema.default;
|
|
499
|
+
delete schema.default;
|
|
500
|
+
}
|
|
501
|
+
delete schema.type;
|
|
397
502
|
}
|
|
398
|
-
|
|
399
|
-
|
|
503
|
+
if (schema.properties) {
|
|
504
|
+
for (const prop of Object.values(schema.properties)) {
|
|
505
|
+
markNonValidatable(prop);
|
|
506
|
+
}
|
|
400
507
|
}
|
|
401
|
-
|
|
402
|
-
|
|
508
|
+
if (schema.patternProperties) {
|
|
509
|
+
for (const prop of Object.values(schema.patternProperties)) {
|
|
510
|
+
markNonValidatable(prop);
|
|
511
|
+
}
|
|
403
512
|
}
|
|
404
|
-
|
|
405
|
-
|
|
513
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
|
|
514
|
+
markNonValidatable(schema.additionalProperties);
|
|
406
515
|
}
|
|
407
|
-
|
|
408
|
-
|
|
516
|
+
if (schema.items) {
|
|
517
|
+
markNonValidatable(schema.items);
|
|
409
518
|
}
|
|
410
|
-
|
|
411
|
-
|
|
519
|
+
if (schema.anyOf) {
|
|
520
|
+
schema.anyOf.forEach((s) => markNonValidatable(s));
|
|
412
521
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
return cachedSettingsMap.get(constructor) ?? {};
|
|
522
|
+
if (schema.allOf) {
|
|
523
|
+
schema.allOf.forEach((s) => markNonValidatable(s));
|
|
416
524
|
}
|
|
417
|
-
|
|
525
|
+
if (schema.oneOf) {
|
|
526
|
+
schema.oneOf.forEach((s) => markNonValidatable(s));
|
|
527
|
+
}
|
|
528
|
+
return schema;
|
|
529
|
+
}
|
|
530
|
+
function defineSchema(properties, options) {
|
|
531
|
+
const schema = SchemaType.Object(properties, options);
|
|
532
|
+
return markNonValidatable(schema);
|
|
533
|
+
}
|
|
418
534
|
|
|
419
|
-
// src/
|
|
420
|
-
var
|
|
535
|
+
// src/sdk/lib/shared/schemas/index.ts
|
|
536
|
+
var import_typebox3 = require("@sinclair/typebox");
|
|
421
537
|
|
|
422
|
-
// src/
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
store,
|
|
429
|
-
(error, value) => error ? reject(error) : resolve(value)
|
|
430
|
-
)
|
|
431
|
-
);
|
|
432
|
-
const set = (key, value) => new Promise(
|
|
433
|
-
(resolve, reject) => context.set(
|
|
434
|
-
key,
|
|
435
|
-
value,
|
|
436
|
-
store,
|
|
437
|
-
(error) => error ? reject(error) : resolve()
|
|
438
|
-
)
|
|
439
|
-
);
|
|
440
|
-
const keys = () => new Promise(
|
|
441
|
-
(resolve, reject) => context.keys(store, (error, k) => error ? reject(error) : resolve(k))
|
|
442
|
-
);
|
|
443
|
-
const nativeUpdate = context.update;
|
|
444
|
-
const nativeIncrement = context.increment;
|
|
445
|
-
const update = (key, fn) => {
|
|
446
|
-
if (nativeUpdate) {
|
|
447
|
-
return new Promise(
|
|
448
|
-
(resolve, reject) => nativeUpdate(
|
|
449
|
-
key,
|
|
450
|
-
fn,
|
|
451
|
-
store,
|
|
452
|
-
(error, value) => error ? reject(error) : resolve(value)
|
|
453
|
-
)
|
|
454
|
-
);
|
|
455
|
-
}
|
|
456
|
-
let chains = updateLocks.get(context);
|
|
457
|
-
if (!chains) updateLocks.set(context, chains = /* @__PURE__ */ new Map());
|
|
458
|
-
const lockKey = JSON.stringify([store ?? null, key]);
|
|
459
|
-
const task = async () => {
|
|
460
|
-
const next = await fn(await get(key));
|
|
461
|
-
await set(key, next);
|
|
462
|
-
return next;
|
|
463
|
-
};
|
|
464
|
-
const previous = chains.get(lockKey) ?? Promise.resolve();
|
|
465
|
-
const run = previous.then(task, task);
|
|
466
|
-
const settled = run.then(
|
|
467
|
-
() => void 0,
|
|
468
|
-
() => void 0
|
|
469
|
-
);
|
|
470
|
-
chains.set(lockKey, settled);
|
|
471
|
-
void settled.then(() => {
|
|
472
|
-
if (chains.get(lockKey) === settled) chains.delete(lockKey);
|
|
473
|
-
});
|
|
474
|
-
return run;
|
|
475
|
-
};
|
|
476
|
-
const increment = (key, by = 1) => {
|
|
477
|
-
if (nativeIncrement) {
|
|
478
|
-
return new Promise(
|
|
479
|
-
(resolve, reject) => nativeIncrement(
|
|
480
|
-
key,
|
|
481
|
-
by,
|
|
482
|
-
store,
|
|
483
|
-
(error, value) => error ? reject(error) : resolve(value)
|
|
484
|
-
)
|
|
485
|
-
);
|
|
486
|
-
}
|
|
487
|
-
return update(
|
|
488
|
-
key,
|
|
489
|
-
(current) => typeof current === "number" ? current + by : by
|
|
538
|
+
// src/sdk/lib/server/registration.ts
|
|
539
|
+
async function registerType(RED, NodeClass) {
|
|
540
|
+
RED.log.debug(`Registering Type: ${NodeClass.type}`);
|
|
541
|
+
if (!NodeClass[NRG_NODE]) {
|
|
542
|
+
throw new NrgError(
|
|
543
|
+
`${NodeClass.name} must extend IONode or ConfigNode classes`
|
|
490
544
|
);
|
|
491
|
-
};
|
|
492
|
-
return { get, set, keys, update, increment };
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// src/core/server/nodes/io-node.ts
|
|
496
|
-
var import_node_async_hooks = require("node:async_hooks");
|
|
497
|
-
function isSchemaLike(obj) {
|
|
498
|
-
return obj != null && typeof obj === "object" && import_typebox.Kind in obj;
|
|
499
|
-
}
|
|
500
|
-
var RETURN_PROPERTY_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
501
|
-
var INPUT_KEY = "input";
|
|
502
|
-
var IONode = class _IONode extends Node {
|
|
503
|
-
static align;
|
|
504
|
-
static color;
|
|
505
|
-
static inputSchema;
|
|
506
|
-
// outputsSchema accepts any schema shape: the raw sent value (per port) is
|
|
507
|
-
// validated, and results are frequently non-objects.
|
|
508
|
-
static outputsSchema;
|
|
509
|
-
static validateInput = false;
|
|
510
|
-
// A single boolean applies to every output port; a boolean[] sets the
|
|
511
|
-
// per-port default by base-output index (missing entries default to false).
|
|
512
|
-
static validateOutput = false;
|
|
513
|
-
static get inputs() {
|
|
514
|
-
return this.inputSchema ? 1 : 0;
|
|
515
545
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
if (!s) return 0;
|
|
519
|
-
if (Array.isArray(s)) return s.length;
|
|
520
|
-
if (isSchemaLike(s)) return 1;
|
|
521
|
-
const keys = Object.keys(s);
|
|
522
|
-
for (const key of keys) {
|
|
523
|
-
if (/^\d+$/.test(key)) {
|
|
524
|
-
throw new NrgError(
|
|
525
|
-
`outputsSchema record key "${key}" in ${this.type} looks numeric. Use descriptive string names (e.g. "success", "failure") to avoid JavaScript object key ordering issues.`
|
|
526
|
-
);
|
|
527
|
-
}
|
|
528
|
-
if (key === "error" || key === "complete" || key === "status") {
|
|
529
|
-
throw new NrgError(
|
|
530
|
-
`outputsSchema record key "${key}" in ${this.type} is reserved for built-in ports. Use a different name (e.g. "failed" instead of "error").`
|
|
531
|
-
);
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
return keys.length;
|
|
546
|
+
if (!NodeClass.type) {
|
|
547
|
+
throw new NrgError("type must be provided when registering the node");
|
|
535
548
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
549
|
+
await NodeClass.register(RED);
|
|
550
|
+
RED.log.debug(`Type registered: ${NodeClass.type}`);
|
|
551
|
+
}
|
|
552
|
+
function collectNodeSchemas(NodeClass) {
|
|
553
|
+
const found = [];
|
|
554
|
+
const add = (role, schema) => {
|
|
555
|
+
if (schema && typeof schema === "object") {
|
|
556
|
+
found.push({ role, schema });
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
add("config", NodeClass.configSchema);
|
|
560
|
+
add("credentials", NodeClass.credentialsSchema);
|
|
561
|
+
add("settings", NodeClass.settingsSchema);
|
|
562
|
+
add("input", NodeClass.inputSchema);
|
|
563
|
+
const outputs = NodeClass.outputsSchema;
|
|
564
|
+
if (Array.isArray(outputs)) {
|
|
565
|
+
outputs.forEach((schema, i) => add(`output[${i}]`, schema));
|
|
566
|
+
} else if (outputs && typeof outputs === "object" && !(import_typebox3.Kind in outputs)) {
|
|
567
|
+
for (const [port, schema] of Object.entries(outputs)) {
|
|
568
|
+
add(`output.${port}`, schema);
|
|
569
|
+
}
|
|
570
|
+
} else {
|
|
571
|
+
add("output", outputs);
|
|
547
572
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
*/
|
|
561
|
-
static #invocation = new import_node_async_hooks.AsyncLocalStorage();
|
|
562
|
-
context;
|
|
563
|
-
constructor(RED, node, config, credentials) {
|
|
564
|
-
super(RED, node, config, credentials);
|
|
565
|
-
const context = node.context();
|
|
566
|
-
const resolve = (scope, store) => {
|
|
567
|
-
const target = scope === "global" ? context.global : scope === "flow" ? context.flow : context;
|
|
568
|
-
return setupContext(target, store);
|
|
569
|
-
};
|
|
570
|
-
this.context = Object.assign(resolve, {
|
|
571
|
-
node: setupContext(context),
|
|
572
|
-
flow: setupContext(context.flow),
|
|
573
|
-
global: setupContext(context.global)
|
|
574
|
-
});
|
|
575
|
-
const outputReturnProperties = this.config.outputReturnProperties;
|
|
576
|
-
if (outputReturnProperties) {
|
|
577
|
-
for (const [port, key] of Object.entries(outputReturnProperties)) {
|
|
578
|
-
if (typeof key === "string" && key.trim() && !RETURN_PROPERTY_PATTERN.test(key.trim())) {
|
|
579
|
-
throw new NrgError(
|
|
580
|
-
`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)`
|
|
573
|
+
return found;
|
|
574
|
+
}
|
|
575
|
+
function checkSchemaIds(RED, nodes) {
|
|
576
|
+
for (const NodeClass of nodes) {
|
|
577
|
+
for (const { role, schema } of collectNodeSchemas(NodeClass)) {
|
|
578
|
+
const owner = `${NodeClass.type}.${role}`;
|
|
579
|
+
const id = schema.$id;
|
|
580
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
581
|
+
const props = schema.properties;
|
|
582
|
+
if (props && Object.keys(props).length > 0) {
|
|
583
|
+
RED.log.warn(
|
|
584
|
+
`[nrg] ${owner} schema has properties but no $id \u2014 it recompiles on every validation and cannot be $ref'd. Build it with defineSchema(props, { $id: "${NodeClass.type}:${role}" }).`
|
|
581
585
|
);
|
|
582
586
|
}
|
|
587
|
+
continue;
|
|
583
588
|
}
|
|
589
|
+
RED.validator.reserveSchemaId(schema, owner);
|
|
584
590
|
}
|
|
585
591
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
);
|
|
598
|
-
return;
|
|
599
|
-
}
|
|
600
|
-
const store = { inputMsg: msg, send };
|
|
601
|
-
try {
|
|
602
|
-
nodeRedNode.log("Calling input");
|
|
603
|
-
const result = await this.#input(msg, store);
|
|
604
|
-
if (this.config.completePort) {
|
|
605
|
-
this.#sendToPort("complete", {
|
|
606
|
-
...msg,
|
|
607
|
-
...result !== void 0 ? { output: result } : {},
|
|
608
|
-
complete: {
|
|
609
|
-
source: this.#nodeSource()
|
|
610
|
-
},
|
|
611
|
-
[INPUT_KEY]: msg
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
done();
|
|
615
|
-
nodeRedNode.log("Input processed");
|
|
616
|
-
} catch (error) {
|
|
617
|
-
const errorMsg = error instanceof Error ? error.message : "Unknown error during input handling";
|
|
618
|
-
if (this.config.errorPort && !store.errorEmitted) {
|
|
619
|
-
const errorData = error && typeof error === "object" ? { ...error } : {};
|
|
620
|
-
this.#sendToPort("error", {
|
|
621
|
-
...msg,
|
|
622
|
-
error: {
|
|
623
|
-
...errorData,
|
|
624
|
-
name: error?.name ?? "Error",
|
|
625
|
-
message: errorMsg,
|
|
626
|
-
source: this.#nodeSource()
|
|
627
|
-
},
|
|
628
|
-
[INPUT_KEY]: msg
|
|
629
|
-
});
|
|
630
|
-
}
|
|
631
|
-
if (error instanceof Error) {
|
|
632
|
-
nodeRedNode.error(
|
|
633
|
-
"Error while processing input: " + error.message,
|
|
634
|
-
msg
|
|
635
|
-
);
|
|
636
|
-
done(error);
|
|
637
|
-
} else {
|
|
638
|
-
nodeRedNode.error(
|
|
639
|
-
"Unknown error occurred during input handling",
|
|
640
|
-
msg
|
|
641
|
-
);
|
|
642
|
-
done(new Error(errorMsg));
|
|
643
|
-
}
|
|
592
|
+
}
|
|
593
|
+
function registerTypes(nodes) {
|
|
594
|
+
const fn = Object.assign(
|
|
595
|
+
async function(RED) {
|
|
596
|
+
initValidator(RED);
|
|
597
|
+
initRoutes(RED);
|
|
598
|
+
try {
|
|
599
|
+
checkSchemaIds(RED, nodes);
|
|
600
|
+
RED.log.info("Registering node types in series");
|
|
601
|
+
for (const NodeClass of nodes) {
|
|
602
|
+
await registerType(RED, NodeClass);
|
|
644
603
|
}
|
|
604
|
+
RED.log.info("All node types registered in series");
|
|
605
|
+
} catch (error) {
|
|
606
|
+
RED.log.error("Error registering node types:", error);
|
|
607
|
+
throw error;
|
|
645
608
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
609
|
+
},
|
|
610
|
+
{ nodes }
|
|
611
|
+
);
|
|
612
|
+
return fn;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// src/sdk/lib/server/typed-input.ts
|
|
616
|
+
var TypedInput2 = class {
|
|
617
|
+
constructor(RED, node, input) {
|
|
618
|
+
this.RED = RED;
|
|
619
|
+
this.node = node;
|
|
620
|
+
this.input = input;
|
|
650
621
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
}
|
|
660
|
-
|
|
622
|
+
resolvers = {
|
|
623
|
+
// evaluateNodeProperty returns the node ID string for "node" type —
|
|
624
|
+
// resolve it to the actual node instance via RED.nodes.getNode,
|
|
625
|
+
// then surface the NRG wrapper if available.
|
|
626
|
+
node: (raw) => {
|
|
627
|
+
if (typeof raw === "string") {
|
|
628
|
+
const node = this.RED.nodes.getNode(raw);
|
|
629
|
+
return node?._node ?? node ?? raw;
|
|
630
|
+
}
|
|
631
|
+
return raw?._node ?? raw;
|
|
661
632
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
);
|
|
633
|
+
};
|
|
634
|
+
get type() {
|
|
635
|
+
return this.input.type;
|
|
666
636
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
const values = multi ? msg.slice(0, this.baseOutputs) : [msg];
|
|
670
|
-
const out = values.map((m, port) => {
|
|
671
|
-
if (m == null) return m;
|
|
672
|
-
this.#validatePort(m, port);
|
|
673
|
-
return this.#wrapOutgoing(m, this.#resolveContextMode(port), port);
|
|
674
|
-
});
|
|
675
|
-
this.#deliver(out);
|
|
637
|
+
get value() {
|
|
638
|
+
return this.input.value;
|
|
676
639
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
640
|
+
resolve(msg) {
|
|
641
|
+
return new Promise((resolve, reject) => {
|
|
642
|
+
this.RED.util.evaluateNodeProperty(
|
|
643
|
+
this.input.value,
|
|
644
|
+
this.input.type,
|
|
645
|
+
this.node,
|
|
646
|
+
msg,
|
|
647
|
+
(err, raw) => {
|
|
648
|
+
if (err) return reject(err);
|
|
649
|
+
const post = this.resolvers[this.input.type];
|
|
650
|
+
resolve(post ? post(raw) : raw);
|
|
651
|
+
}
|
|
652
|
+
);
|
|
653
|
+
});
|
|
684
654
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
// src/sdk/lib/server/nodes/proxy.ts
|
|
658
|
+
function setupConfigProxy(opts) {
|
|
659
|
+
const { RED, node, config, schema } = opts;
|
|
660
|
+
const SKIP_PROPS = /* @__PURE__ */ new Set(["id", "_id", "_users"]);
|
|
661
|
+
const proxyCache = /* @__PURE__ */ new WeakMap();
|
|
662
|
+
const typedInputCache = /* @__PURE__ */ new WeakMap();
|
|
663
|
+
const createProxy = (obj, subSchema) => {
|
|
664
|
+
const cached = proxyCache.get(obj);
|
|
665
|
+
if (cached) return cached;
|
|
666
|
+
const schemaForProp = (target, prop) => Array.isArray(target) ? subSchema?.items : subSchema?.properties?.[prop];
|
|
667
|
+
const proxy = new Proxy(obj, {
|
|
668
|
+
get(target, prop) {
|
|
669
|
+
if (typeof prop === "symbol") return target[prop];
|
|
670
|
+
if (SKIP_PROPS.has(prop)) return target[prop];
|
|
671
|
+
const value = target[prop];
|
|
672
|
+
const fieldSchema = schemaForProp(target, prop);
|
|
673
|
+
if (fieldSchema?.["x-nrg-node-type"]) {
|
|
674
|
+
if (typeof value !== "string" || value.length === 0) return void 0;
|
|
675
|
+
const resolved = RED.nodes.getNode(value)?._node;
|
|
676
|
+
const ctor = resolved?.constructor;
|
|
677
|
+
if (ctor?.[NRG_NODE] && !resolved[NRG_CONFIG_NODE]) {
|
|
678
|
+
throw new NrgError(
|
|
679
|
+
`Config field "${String(prop)}" references "${value}", which is not an nrg config node.`
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
return resolved ?? value;
|
|
683
|
+
}
|
|
684
|
+
if (fieldSchema?.["x-nrg-typed-input"] && value && typeof value === "object" && "type" in value && "value" in value) {
|
|
685
|
+
let ref = typedInputCache.get(value);
|
|
686
|
+
if (!ref) {
|
|
687
|
+
ref = new TypedInput2(RED, node, value);
|
|
688
|
+
typedInputCache.set(value, ref);
|
|
689
|
+
}
|
|
690
|
+
return ref;
|
|
691
|
+
}
|
|
692
|
+
if (value && typeof value === "object") {
|
|
693
|
+
return createProxy(value, fieldSchema);
|
|
694
|
+
}
|
|
695
|
+
return value;
|
|
696
|
+
},
|
|
697
|
+
set(_target, prop) {
|
|
698
|
+
throw new NrgError(
|
|
699
|
+
`Cannot set property '${String(prop)}' on read-only node config`
|
|
700
|
+
);
|
|
701
|
+
},
|
|
702
|
+
deleteProperty(_target, prop) {
|
|
703
|
+
throw new NrgError(
|
|
704
|
+
`Cannot delete property '${String(prop)}' on read-only node config`
|
|
705
|
+
);
|
|
706
|
+
}
|
|
703
707
|
});
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
*/
|
|
722
|
-
#returnPropertyKey(port) {
|
|
723
|
-
const configured = this.config.outputReturnProperties?.[port];
|
|
724
|
-
if (typeof configured === "string" && configured.trim()) {
|
|
725
|
-
return configured.trim();
|
|
708
|
+
proxyCache.set(obj, proxy);
|
|
709
|
+
return proxy;
|
|
710
|
+
};
|
|
711
|
+
return createProxy(config, schema);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// src/sdk/lib/shared/schemas/utils.ts
|
|
715
|
+
function getCredentialsFromSchema(schema) {
|
|
716
|
+
if (!schema?.properties) return void 0;
|
|
717
|
+
const result = {};
|
|
718
|
+
for (const [key, value] of Object.entries(schema.properties)) {
|
|
719
|
+
const property = value;
|
|
720
|
+
const isPassword = property.format === "password";
|
|
721
|
+
if (!isPassword) {
|
|
722
|
+
console.warn(
|
|
723
|
+
`[nrg] credential "${key}" has no format:"password" \u2014 it is stored as a visible (cleartext-in-editor) credential. Add { format: "password" } to mask it.`
|
|
724
|
+
);
|
|
726
725
|
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
*/
|
|
734
|
-
#resolveContextMode(port) {
|
|
735
|
-
return this.config.outputContextModes?.[port] ?? "carry";
|
|
726
|
+
result[key] = {
|
|
727
|
+
// NOTE: required is always false because it is controlled by the JSON Schema and AJV validation instead of using node-red client core
|
|
728
|
+
required: false,
|
|
729
|
+
type: isPassword ? "password" : "text",
|
|
730
|
+
value: property.default ?? void 0
|
|
731
|
+
};
|
|
736
732
|
}
|
|
733
|
+
return result;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// src/sdk/lib/server/nodes/node.ts
|
|
737
|
+
var cachedSettingsMap = /* @__PURE__ */ new WeakMap();
|
|
738
|
+
var Node = class _Node {
|
|
737
739
|
/**
|
|
738
|
-
*
|
|
739
|
-
*
|
|
740
|
-
*
|
|
740
|
+
* Runtime NRG-node brand — inherited by every subclass (IONode/ConfigNode and
|
|
741
|
+
* factory-built classes) and checked by `defineModule` at runtime. See
|
|
742
|
+
* symbols.ts for why it's a `Symbol.for()` runtime key, not a compile-time brand.
|
|
741
743
|
*/
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
744
|
+
static [NRG_NODE] = true;
|
|
745
|
+
static type;
|
|
746
|
+
static category;
|
|
747
|
+
static configSchema;
|
|
748
|
+
static credentialsSchema;
|
|
749
|
+
static settingsSchema;
|
|
750
|
+
static validateSettings(RED) {
|
|
751
|
+
if (!this.settingsSchema) return;
|
|
752
|
+
RED.log.info("Validating settings");
|
|
753
|
+
const prefix = this.type.replace(/-./g, (x) => x[1].toUpperCase());
|
|
754
|
+
const properties = this.settingsSchema.properties;
|
|
755
|
+
const settings = {};
|
|
756
|
+
for (const key of Object.keys(properties)) {
|
|
757
|
+
const settingKey = prefix + key.charAt(0).toUpperCase() + key.slice(1);
|
|
758
|
+
const value = RED.settings[settingKey];
|
|
759
|
+
if (value !== void 0) {
|
|
760
|
+
settings[key] = value;
|
|
761
|
+
}
|
|
747
762
|
}
|
|
748
|
-
|
|
749
|
-
|
|
763
|
+
for (const [key, prop] of Object.entries(properties)) {
|
|
764
|
+
if (settings[key] === void 0) {
|
|
765
|
+
const defaultValue = prop.default ?? prop._default;
|
|
766
|
+
if (defaultValue !== void 0) {
|
|
767
|
+
settings[key] = defaultValue;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
750
770
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
771
|
+
RED.validator.validate(settings, this.settingsSchema, {
|
|
772
|
+
throwOnError: true
|
|
773
|
+
});
|
|
774
|
+
cachedSettingsMap.set(this, settings);
|
|
775
|
+
RED.log.info("Settings are valid");
|
|
756
776
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
777
|
+
static #buildSettings(NC) {
|
|
778
|
+
if (!NC.settingsSchema) return;
|
|
779
|
+
const settings = {};
|
|
780
|
+
const prefix = NC.type.replace(/-./g, (x) => x[1].toUpperCase());
|
|
781
|
+
for (const [key, prop] of Object.entries(NC.settingsSchema.properties)) {
|
|
782
|
+
const settingKey = prefix + key.charAt(0).toUpperCase() + key.slice(1);
|
|
783
|
+
settings[settingKey] = {
|
|
784
|
+
value: prop.default,
|
|
785
|
+
exportable: prop.exportable ?? false
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
return settings;
|
|
763
789
|
}
|
|
764
790
|
/**
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
* Numeric indices refer to the base output ports (0-based).
|
|
768
|
-
*
|
|
769
|
-
* Built-in ports (`"error"`, `"complete"`, `"status"`) are managed by the
|
|
770
|
-
* framework and cannot be sent to directly. Use `this.status()` for status,
|
|
771
|
-
* throw an error or call `this.error()` for the error port, and the complete
|
|
772
|
-
* port is sent automatically on successful input processing.
|
|
791
|
+
* Registers this node class with Node-RED. Handles instance creation,
|
|
792
|
+
* event handler wiring, settings validation, and the user's registered() hook.
|
|
773
793
|
*/
|
|
774
|
-
|
|
775
|
-
|
|
794
|
+
static async register(RED) {
|
|
795
|
+
const NodeClass = this;
|
|
796
|
+
if (NodeClass.color && !/^#[0-9A-Fa-f]{6}$/.test(NodeClass.color)) {
|
|
776
797
|
throw new NrgError(
|
|
777
|
-
`
|
|
798
|
+
`Invalid color "${NodeClass.color}" for ${NodeClass.type}: must be a 6-digit hex color like "#a6bbcf" (shorthand "#abc" is not accepted).`
|
|
778
799
|
);
|
|
779
800
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
801
|
+
RED.nodes.registerType(
|
|
802
|
+
NodeClass.type,
|
|
803
|
+
function(config) {
|
|
804
|
+
RED.nodes.createNode(this, config);
|
|
805
|
+
const node = new NodeClass(RED, this, config, this.credentials);
|
|
806
|
+
Object.defineProperty(this, "_node", {
|
|
807
|
+
value: node,
|
|
808
|
+
writable: false,
|
|
809
|
+
configurable: false,
|
|
810
|
+
enumerable: false
|
|
811
|
+
});
|
|
812
|
+
const createdPromise = Promise.resolve(node.created?.()).catch(
|
|
813
|
+
(error) => {
|
|
814
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
815
|
+
this.error("Error during created hook: " + message);
|
|
816
|
+
throw error;
|
|
817
|
+
}
|
|
818
|
+
);
|
|
819
|
+
createdPromise.catch(() => {
|
|
820
|
+
this.status({ fill: "red", shape: "ring", text: "created() failed" });
|
|
821
|
+
});
|
|
822
|
+
node[NRG_WIRE_HANDLERS](this, createdPromise);
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
credentials: NodeClass.credentialsSchema ? getCredentialsFromSchema(NodeClass.credentialsSchema) : {},
|
|
826
|
+
settings: _Node.#buildSettings(this)
|
|
827
|
+
}
|
|
785
828
|
);
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
} else {
|
|
795
|
-
portIndex = this.#getNamedPortIndex(port);
|
|
796
|
-
if (portIndex === null) return;
|
|
829
|
+
NodeClass.validateSettings(RED);
|
|
830
|
+
try {
|
|
831
|
+
await Promise.resolve(NodeClass.registered?.(RED));
|
|
832
|
+
} catch (error) {
|
|
833
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
834
|
+
RED.log.error(
|
|
835
|
+
`Error during registered hook for ${NodeClass.type}: ${message}`
|
|
836
|
+
);
|
|
797
837
|
}
|
|
798
|
-
const out = new Array(this.totalOutputs);
|
|
799
|
-
out[portIndex] = msg;
|
|
800
|
-
this.node.send(out);
|
|
801
838
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
839
|
+
RED;
|
|
840
|
+
node;
|
|
841
|
+
context;
|
|
842
|
+
config;
|
|
843
|
+
timers = /* @__PURE__ */ new Set();
|
|
844
|
+
intervals = /* @__PURE__ */ new Set();
|
|
845
|
+
constructor(RED, node, config, credentials) {
|
|
846
|
+
this.RED = RED;
|
|
847
|
+
this.node = node;
|
|
848
|
+
const constructor = this.constructor;
|
|
849
|
+
if (constructor.configSchema) {
|
|
850
|
+
this.log("Validating configs");
|
|
851
|
+
const configResult = this.RED.validator.validate(
|
|
852
|
+
config,
|
|
853
|
+
constructor.configSchema,
|
|
854
|
+
{
|
|
855
|
+
// Coercing (default): the proxy at (this).config wraps this same object,
|
|
856
|
+
// so `this.config.<field>` must read as its coerced/typed value.
|
|
857
|
+
throwOnError: false
|
|
858
|
+
}
|
|
859
|
+
);
|
|
860
|
+
if (!configResult.valid && configResult.errors?.length) {
|
|
861
|
+
this.warn(
|
|
862
|
+
`Config validation errors: ${configResult.errors.map((e) => `${e.instancePath} ${e.message}`).join("; ")}`
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
this.config = setupConfigProxy({
|
|
867
|
+
RED,
|
|
868
|
+
node,
|
|
869
|
+
config,
|
|
870
|
+
schema: constructor.configSchema
|
|
871
|
+
});
|
|
872
|
+
if (constructor.credentialsSchema && credentials) {
|
|
873
|
+
this.log("Validating credentials");
|
|
874
|
+
const credResult = this.RED.validator.validate(
|
|
875
|
+
credentials,
|
|
876
|
+
constructor.credentialsSchema,
|
|
877
|
+
{
|
|
878
|
+
throwOnError: false
|
|
879
|
+
}
|
|
880
|
+
);
|
|
881
|
+
if (!credResult.valid && credResult.errors?.length) {
|
|
882
|
+
this.warn(
|
|
883
|
+
`Credentials validation errors: ${credResult.errors.map((e) => `${e.instancePath} ${e.message}`).join("; ")}`
|
|
884
|
+
);
|
|
885
|
+
}
|
|
816
886
|
}
|
|
817
|
-
if (this.config.completePort) idx++;
|
|
818
|
-
return this.config.statusPort ? idx : null;
|
|
819
|
-
}
|
|
820
|
-
#nodeSource() {
|
|
821
|
-
return {
|
|
822
|
-
id: this.id,
|
|
823
|
-
type: this.constructor.type,
|
|
824
|
-
name: this.name
|
|
825
|
-
};
|
|
826
887
|
}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
888
|
+
[NRG_WIRE_HANDLERS](nodeRedNode, createdPromise) {
|
|
889
|
+
nodeRedNode.on(
|
|
890
|
+
"close",
|
|
891
|
+
async (removed, done) => {
|
|
892
|
+
try {
|
|
893
|
+
this.log("Calling closed");
|
|
894
|
+
await this.#closed(removed);
|
|
895
|
+
this.log("Node was closed");
|
|
896
|
+
done();
|
|
897
|
+
} catch (error) {
|
|
898
|
+
if (error instanceof Error) {
|
|
899
|
+
this.error("Error while closing node: " + error.message);
|
|
900
|
+
done(error);
|
|
901
|
+
} else {
|
|
902
|
+
this.error("Unknown error occurred while closing node");
|
|
903
|
+
done(new Error("Unknown error occurred while closing node"));
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
);
|
|
833
908
|
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
source: this.#nodeSource()
|
|
845
|
-
},
|
|
846
|
-
[INPUT_KEY]: msg
|
|
847
|
-
});
|
|
848
|
-
const store = _IONode.#invocation.getStore();
|
|
849
|
-
if (store) store.errorEmitted = true;
|
|
909
|
+
async #closed(removed) {
|
|
910
|
+
try {
|
|
911
|
+
await Promise.resolve(this.closed?.(removed));
|
|
912
|
+
} finally {
|
|
913
|
+
this.log("clearing timers and intervals");
|
|
914
|
+
this.timers.forEach((t) => clearTimeout(t));
|
|
915
|
+
this.intervals.forEach((i) => clearInterval(i));
|
|
916
|
+
this.timers.clear();
|
|
917
|
+
this.intervals.clear();
|
|
918
|
+
this.log("timers and intervals cleared");
|
|
850
919
|
}
|
|
851
920
|
}
|
|
852
|
-
|
|
853
|
-
this.
|
|
921
|
+
i18n(key, substitutions) {
|
|
922
|
+
const nodeType = this.constructor.type;
|
|
923
|
+
return this.RED._(`${nodeType}.${key}`, substitutions);
|
|
854
924
|
}
|
|
855
|
-
|
|
856
|
-
|
|
925
|
+
setTimeout(fn, ms) {
|
|
926
|
+
const timer = setTimeout(() => {
|
|
927
|
+
this.timers.delete(timer);
|
|
928
|
+
fn();
|
|
929
|
+
}, ms);
|
|
930
|
+
this.timers.add(timer);
|
|
931
|
+
return timer;
|
|
857
932
|
}
|
|
858
|
-
|
|
859
|
-
|
|
933
|
+
setInterval(fn, ms) {
|
|
934
|
+
const interval = setInterval(fn, ms);
|
|
935
|
+
this.intervals.add(interval);
|
|
936
|
+
return interval;
|
|
860
937
|
}
|
|
861
|
-
|
|
862
|
-
|
|
938
|
+
clearTimeout(timer) {
|
|
939
|
+
clearTimeout(timer);
|
|
940
|
+
this.timers.delete(timer);
|
|
863
941
|
}
|
|
864
|
-
|
|
865
|
-
|
|
942
|
+
clearInterval(interval) {
|
|
943
|
+
clearInterval(interval);
|
|
944
|
+
this.intervals.delete(interval);
|
|
866
945
|
}
|
|
867
|
-
|
|
868
|
-
|
|
946
|
+
on(event, callback) {
|
|
947
|
+
this.node.on(event, callback);
|
|
869
948
|
}
|
|
870
|
-
|
|
871
|
-
|
|
949
|
+
log(msg) {
|
|
950
|
+
this.node.log(msg);
|
|
872
951
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
// src/core/server/nodes/config-node.ts
|
|
876
|
-
var ConfigNode = class extends Node {
|
|
877
|
-
static category = "config";
|
|
878
|
-
context;
|
|
879
|
-
constructor(RED, node, config, credentials) {
|
|
880
|
-
super(RED, node, config, credentials);
|
|
881
|
-
const context = node.context();
|
|
882
|
-
const resolve = (scope, store) => {
|
|
883
|
-
const target = scope === "global" ? context.global : context;
|
|
884
|
-
return setupContext(target, store);
|
|
885
|
-
};
|
|
886
|
-
this.context = Object.assign(resolve, {
|
|
887
|
-
node: setupContext(context),
|
|
888
|
-
global: setupContext(context.global)
|
|
889
|
-
});
|
|
952
|
+
warn(message) {
|
|
953
|
+
this.node.warn(message);
|
|
890
954
|
}
|
|
891
|
-
|
|
892
|
-
|
|
955
|
+
error(message, msg) {
|
|
956
|
+
this.node.error(message, msg);
|
|
893
957
|
}
|
|
894
|
-
get
|
|
895
|
-
return this.
|
|
958
|
+
get id() {
|
|
959
|
+
return this.node.id;
|
|
896
960
|
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
961
|
+
get name() {
|
|
962
|
+
return this.node.name;
|
|
963
|
+
}
|
|
964
|
+
get z() {
|
|
965
|
+
return this.node.z;
|
|
901
966
|
}
|
|
902
967
|
get credentials() {
|
|
903
968
|
return this.node.credentials;
|
|
904
969
|
}
|
|
970
|
+
get settings() {
|
|
971
|
+
const constructor = this.constructor;
|
|
972
|
+
return cachedSettingsMap.get(constructor) ?? {};
|
|
973
|
+
}
|
|
905
974
|
};
|
|
906
975
|
|
|
907
|
-
// src/
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
976
|
+
// src/sdk/lib/server/nodes/context.ts
|
|
977
|
+
var updateLocks = /* @__PURE__ */ new WeakMap();
|
|
978
|
+
function setupContext(context, store) {
|
|
979
|
+
const get = (key) => new Promise(
|
|
980
|
+
(resolve, reject) => context.get(
|
|
981
|
+
key,
|
|
982
|
+
store,
|
|
983
|
+
(error, value) => error ? reject(error) : resolve(value)
|
|
984
|
+
)
|
|
985
|
+
);
|
|
986
|
+
const set = (key, value) => new Promise(
|
|
987
|
+
(resolve, reject) => context.set(
|
|
988
|
+
key,
|
|
989
|
+
value,
|
|
990
|
+
store,
|
|
991
|
+
(error) => error ? reject(error) : resolve()
|
|
992
|
+
)
|
|
993
|
+
);
|
|
994
|
+
const keys = () => new Promise(
|
|
995
|
+
(resolve, reject) => context.keys(store, (error, k) => error ? reject(error) : resolve(k))
|
|
996
|
+
);
|
|
997
|
+
const nativeUpdate = context.update;
|
|
998
|
+
const nativeIncrement = context.increment;
|
|
999
|
+
const update = (key, fn) => {
|
|
1000
|
+
if (nativeUpdate) {
|
|
1001
|
+
return new Promise(
|
|
1002
|
+
(resolve, reject) => nativeUpdate(
|
|
1003
|
+
key,
|
|
1004
|
+
fn,
|
|
1005
|
+
store,
|
|
1006
|
+
(error, value) => error ? reject(error) : resolve(value)
|
|
1007
|
+
)
|
|
1008
|
+
);
|
|
932
1009
|
}
|
|
1010
|
+
let chains = updateLocks.get(context);
|
|
1011
|
+
if (!chains) updateLocks.set(context, chains = /* @__PURE__ */ new Map());
|
|
1012
|
+
const lockKey = JSON.stringify([store ?? null, key]);
|
|
1013
|
+
const task = async () => {
|
|
1014
|
+
const next = await fn(await get(key));
|
|
1015
|
+
await set(key, next);
|
|
1016
|
+
return next;
|
|
1017
|
+
};
|
|
1018
|
+
const previous = chains.get(lockKey) ?? Promise.resolve();
|
|
1019
|
+
const run = previous.then(task, task);
|
|
1020
|
+
const settled = run.then(
|
|
1021
|
+
() => void 0,
|
|
1022
|
+
() => void 0
|
|
1023
|
+
);
|
|
1024
|
+
chains.set(lockKey, settled);
|
|
1025
|
+
void settled.then(() => {
|
|
1026
|
+
if (chains.get(lockKey) === settled) chains.delete(lockKey);
|
|
1027
|
+
});
|
|
1028
|
+
return run;
|
|
933
1029
|
};
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
const NodeClass = class extends ConfigNode {
|
|
945
|
-
static type = def.type;
|
|
946
|
-
static configSchema = def.configSchema;
|
|
947
|
-
static credentialsSchema = def.credentialsSchema;
|
|
948
|
-
static settingsSchema = def.settingsSchema;
|
|
949
|
-
static registered(RED) {
|
|
950
|
-
return def.registered?.(RED);
|
|
951
|
-
}
|
|
952
|
-
async created() {
|
|
953
|
-
if (def.created) return def.created.call(this);
|
|
954
|
-
}
|
|
955
|
-
async closed(removed) {
|
|
956
|
-
if (def.closed) return def.closed.call(this, removed);
|
|
1030
|
+
const increment = (key, by = 1) => {
|
|
1031
|
+
if (nativeIncrement) {
|
|
1032
|
+
return new Promise(
|
|
1033
|
+
(resolve, reject) => nativeIncrement(
|
|
1034
|
+
key,
|
|
1035
|
+
by,
|
|
1036
|
+
store,
|
|
1037
|
+
(error, value) => error ? reject(error) : resolve(value)
|
|
1038
|
+
)
|
|
1039
|
+
);
|
|
957
1040
|
}
|
|
1041
|
+
return update(
|
|
1042
|
+
key,
|
|
1043
|
+
(current) => typeof current === "number" ? current + by : by
|
|
1044
|
+
);
|
|
958
1045
|
};
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
return NodeClass;
|
|
1046
|
+
return { get, set, keys, update, increment };
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// src/sdk/lib/server/nodes/io-node.ts
|
|
1050
|
+
var import_node_async_hooks = require("node:async_hooks");
|
|
1051
|
+
function isSchemaLike(obj) {
|
|
1052
|
+
return obj != null && typeof obj === "object" && import_typebox3.Kind in obj;
|
|
967
1053
|
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
var
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
removeAdditional: false,
|
|
984
|
-
strict: false,
|
|
985
|
-
strictSchema: false,
|
|
986
|
-
useDefaults: true,
|
|
987
|
-
validateFormats: true,
|
|
988
|
-
// NOTE: typebox handles validation via typescript
|
|
989
|
-
// NOTE: if true, types that are not serializable JSON, like Function, would not work
|
|
990
|
-
validateSchema: false,
|
|
991
|
-
verbose: true,
|
|
992
|
-
...ajvOptions
|
|
993
|
-
});
|
|
994
|
-
(0, import_ajv_formats.default)(this.ajv);
|
|
995
|
-
(0, import_ajv_errors.default)(this.ajv);
|
|
996
|
-
this.addCustomKeywords(customKeywords || []);
|
|
997
|
-
this.addCustomFormats(customFormats || {});
|
|
1054
|
+
var RETURN_PROPERTY_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
1055
|
+
var INPUT_KEY = "input";
|
|
1056
|
+
var IONode = class _IONode extends Node {
|
|
1057
|
+
static align;
|
|
1058
|
+
static color;
|
|
1059
|
+
static inputSchema;
|
|
1060
|
+
// outputsSchema accepts any schema shape: the raw sent value (per port) is
|
|
1061
|
+
// validated, and results are frequently non-objects.
|
|
1062
|
+
static outputsSchema;
|
|
1063
|
+
static validateInput = false;
|
|
1064
|
+
// A single boolean applies to every output port; a boolean[] sets the
|
|
1065
|
+
// per-port default by base-output index (missing entries default to false).
|
|
1066
|
+
static validateOutput = false;
|
|
1067
|
+
static get inputs() {
|
|
1068
|
+
return this.inputSchema ? 1 : 0;
|
|
998
1069
|
}
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
if (
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1070
|
+
static get outputs() {
|
|
1071
|
+
const s = this.outputsSchema;
|
|
1072
|
+
if (!s) return 0;
|
|
1073
|
+
if (Array.isArray(s)) return s.length;
|
|
1074
|
+
if (isSchemaLike(s)) return 1;
|
|
1075
|
+
const keys = Object.keys(s);
|
|
1076
|
+
for (const key of keys) {
|
|
1077
|
+
if (/^\d+$/.test(key)) {
|
|
1078
|
+
throw new NrgError(
|
|
1079
|
+
`outputsSchema record key "${key}" in ${this.type} looks numeric. Use descriptive string names (e.g. "success", "failure") to avoid JavaScript object key ordering issues.`
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
if (key === "error" || key === "complete" || key === "status") {
|
|
1083
|
+
throw new NrgError(
|
|
1084
|
+
`outputsSchema record key "${key}" in ${this.type} is reserved for built-in ports. Use a different name (e.g. "failed" instead of "error").`
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
return keys.length;
|
|
1007
1089
|
}
|
|
1008
1090
|
/**
|
|
1009
|
-
*
|
|
1091
|
+
* The names of the base output ports when `outputsSchema` is a record of
|
|
1092
|
+
* named ports (`{ success, failure }`), in declaration order — otherwise
|
|
1093
|
+
* `undefined` (a single schema or a positional array). Resolved here, where
|
|
1094
|
+
* TypeBox's `Kind` symbol is intact, so the editor reads the names directly
|
|
1095
|
+
* instead of guessing them from a serialized (symbol-stripped) schema.
|
|
1010
1096
|
*/
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
this.ajv.addFormat(name, validator);
|
|
1016
|
-
} else {
|
|
1017
|
-
this.ajv.addFormat(name, { validate: validator });
|
|
1018
|
-
}
|
|
1019
|
-
});
|
|
1097
|
+
static get outputPortNames() {
|
|
1098
|
+
const s = this.outputsSchema;
|
|
1099
|
+
if (!s || Array.isArray(s) || isSchemaLike(s)) return void 0;
|
|
1100
|
+
return Object.keys(s);
|
|
1020
1101
|
}
|
|
1021
1102
|
/**
|
|
1022
|
-
*
|
|
1023
|
-
*
|
|
1024
|
-
*
|
|
1103
|
+
* Per-`input()`-invocation context, scoped via AsyncLocalStorage. One store
|
|
1104
|
+
* per class, shared by every instance: each `.run()` isolates one input()
|
|
1105
|
+
* call (and everything it `await`s, including detached `.then`/timer
|
|
1106
|
+
* continuations it schedules) into its own session, so concurrent inputs on a
|
|
1107
|
+
* node never clobber each other's context and a deferred send keeps the
|
|
1108
|
+
* context of the input that scheduled it. Real Node-RED never awaits an async
|
|
1109
|
+
* input handler before delivering the next message, so two inputs can be in
|
|
1110
|
+
* flight at once. `getStore()` is `undefined` only for a send made entirely
|
|
1111
|
+
* outside any `input()` (e.g. a timer set up in `created()`): it carries no
|
|
1112
|
+
* inherited context and delivers via `node.send`. Server-only — this is part
|
|
1113
|
+
* of the node runtime and is never imported in a browser.
|
|
1025
1114
|
*/
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1115
|
+
static #invocation = new import_node_async_hooks.AsyncLocalStorage();
|
|
1116
|
+
context;
|
|
1117
|
+
constructor(RED, node, config, credentials) {
|
|
1118
|
+
super(RED, node, config, credentials);
|
|
1119
|
+
const context = node.context();
|
|
1120
|
+
const resolve = (scope, store) => {
|
|
1121
|
+
const target = scope === "global" ? context.global : scope === "flow" ? context.flow : context;
|
|
1122
|
+
return setupContext(target, store);
|
|
1123
|
+
};
|
|
1124
|
+
this.context = Object.assign(resolve, {
|
|
1125
|
+
node: setupContext(context),
|
|
1126
|
+
flow: setupContext(context.flow),
|
|
1127
|
+
global: setupContext(context.global)
|
|
1128
|
+
});
|
|
1129
|
+
const outputReturnProperties = this.config.outputReturnProperties;
|
|
1130
|
+
if (outputReturnProperties) {
|
|
1131
|
+
for (const [port, key] of Object.entries(outputReturnProperties)) {
|
|
1132
|
+
if (typeof key === "string" && key.trim() && !RETURN_PROPERTY_PATTERN.test(key.trim())) {
|
|
1133
|
+
throw new NrgError(
|
|
1134
|
+
`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)`
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1033
1138
|
}
|
|
1034
|
-
const validator = this.ajv.compile(schema);
|
|
1035
|
-
return validator;
|
|
1036
1139
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1140
|
+
[NRG_WIRE_HANDLERS](nodeRedNode, createdPromise) {
|
|
1141
|
+
super[NRG_WIRE_HANDLERS](nodeRedNode, createdPromise);
|
|
1142
|
+
const NC = this.constructor;
|
|
1143
|
+
nodeRedNode.on(
|
|
1144
|
+
"input",
|
|
1145
|
+
async (msg, send, done) => {
|
|
1146
|
+
try {
|
|
1147
|
+
await createdPromise;
|
|
1148
|
+
} catch (initError) {
|
|
1149
|
+
done(
|
|
1150
|
+
initError instanceof Error ? initError : new Error(String(initError))
|
|
1151
|
+
);
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
const store = { inputMsg: msg, send };
|
|
1155
|
+
try {
|
|
1156
|
+
nodeRedNode.log("Calling input");
|
|
1157
|
+
const result = await this.#input(msg, store);
|
|
1158
|
+
if (this.config.completePort) {
|
|
1159
|
+
this.#sendToPort("complete", {
|
|
1160
|
+
...msg,
|
|
1161
|
+
...result !== void 0 ? { output: result } : {},
|
|
1162
|
+
complete: {
|
|
1163
|
+
source: this.#nodeSource()
|
|
1164
|
+
},
|
|
1165
|
+
[INPUT_KEY]: msg
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
done();
|
|
1169
|
+
nodeRedNode.log("Input processed");
|
|
1170
|
+
} catch (error) {
|
|
1171
|
+
const errorMsg = error instanceof Error ? error.message : "Unknown error during input handling";
|
|
1172
|
+
if (this.config.errorPort && !store.errorEmitted) {
|
|
1173
|
+
const errorData = error && typeof error === "object" ? { ...error } : {};
|
|
1174
|
+
this.#sendToPort("error", {
|
|
1175
|
+
...msg,
|
|
1176
|
+
error: {
|
|
1177
|
+
...errorData,
|
|
1178
|
+
name: error?.name ?? "Error",
|
|
1179
|
+
message: errorMsg,
|
|
1180
|
+
source: this.#nodeSource()
|
|
1181
|
+
},
|
|
1182
|
+
[INPUT_KEY]: msg
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
if (error instanceof Error) {
|
|
1186
|
+
nodeRedNode.error(
|
|
1187
|
+
"Error while processing input: " + error.message,
|
|
1188
|
+
msg
|
|
1189
|
+
);
|
|
1190
|
+
done(error);
|
|
1191
|
+
} else {
|
|
1192
|
+
nodeRedNode.error(
|
|
1193
|
+
"Unknown error occurred during input handling",
|
|
1194
|
+
msg
|
|
1195
|
+
);
|
|
1196
|
+
done(new Error(errorMsg));
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1047
1199
|
}
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
input(msg) {
|
|
1203
|
+
return void 0;
|
|
1204
|
+
}
|
|
1205
|
+
async #input(msg, store) {
|
|
1206
|
+
const NodeClass = this.constructor;
|
|
1207
|
+
const shouldValidateInput = this.config.validateInput ?? NodeClass.validateInput;
|
|
1208
|
+
if (shouldValidateInput && NodeClass.inputSchema) {
|
|
1209
|
+
this.log("Validating input");
|
|
1210
|
+
this.RED.validator.validate(msg, NodeClass.inputSchema, {
|
|
1211
|
+
// Pure predicate: never coerce or inject defaults into the live msg that
|
|
1212
|
+
// continues downstream.
|
|
1213
|
+
mutate: false,
|
|
1214
|
+
throwOnError: true
|
|
1215
|
+
});
|
|
1216
|
+
this.log("Input is valid");
|
|
1217
|
+
}
|
|
1218
|
+
return await _IONode.#invocation.run(
|
|
1219
|
+
store,
|
|
1220
|
+
() => Promise.resolve(this.input(msg))
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
send(msg) {
|
|
1224
|
+
const multi = this.baseOutputs > 1 && Array.isArray(msg);
|
|
1225
|
+
const values = multi ? msg.slice(0, this.baseOutputs) : [msg];
|
|
1226
|
+
const out = values.map((m, port) => {
|
|
1227
|
+
if (m == null) return m;
|
|
1228
|
+
this.#validatePort(m, port);
|
|
1229
|
+
return this.#wrapOutgoing(m, this.#resolveContextMode(port), port);
|
|
1230
|
+
});
|
|
1231
|
+
this.#deliver(out);
|
|
1232
|
+
}
|
|
1233
|
+
#deliver(out) {
|
|
1234
|
+
const send = _IONode.#invocation.getStore()?.send;
|
|
1235
|
+
if (send) {
|
|
1236
|
+
send(out);
|
|
1237
|
+
} else {
|
|
1238
|
+
this.node.send(out);
|
|
1053
1239
|
}
|
|
1054
|
-
return {
|
|
1055
|
-
valid: true,
|
|
1056
|
-
data
|
|
1057
|
-
};
|
|
1058
1240
|
}
|
|
1059
1241
|
/**
|
|
1060
|
-
*
|
|
1242
|
+
* Per-port output validation. A port validates when its flow-author flag
|
|
1243
|
+
* (`config.validateOutputs[port]`) — or the node's static `validateOutput`
|
|
1244
|
+
* fallback — is on and a schema exists for that port. The static fallback is
|
|
1245
|
+
* a single boolean (all ports) or a boolean[] (per-port, by base index).
|
|
1061
1246
|
*/
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1247
|
+
#validatePort(value, port) {
|
|
1248
|
+
const NodeClass = this.constructor;
|
|
1249
|
+
const staticFlag = NodeClass.validateOutput;
|
|
1250
|
+
const staticForPort = Array.isArray(staticFlag) ? staticFlag[port] ?? false : staticFlag;
|
|
1251
|
+
const configured = this.config.validateOutputs?.[port];
|
|
1252
|
+
if (!(configured ?? staticForPort)) return;
|
|
1253
|
+
const schema = this.#outputSchemaForPort(port);
|
|
1254
|
+
if (!schema) return;
|
|
1255
|
+
this.log("Validating output");
|
|
1256
|
+
this.RED.validator.validate(value, schema, {
|
|
1257
|
+
// Pure predicate: don't coerce or default the outgoing value.
|
|
1258
|
+
mutate: false,
|
|
1259
|
+
throwOnError: true
|
|
1070
1260
|
});
|
|
1261
|
+
this.log("Output is valid");
|
|
1262
|
+
}
|
|
1263
|
+
/** Resolves the output schema for a base-output port: array → `[port]`,
|
|
1264
|
+
* record → the port-th value, single schema → itself. */
|
|
1265
|
+
#outputSchemaForPort(port) {
|
|
1266
|
+
const raw = this.constructor.outputsSchema;
|
|
1267
|
+
if (!raw) return void 0;
|
|
1268
|
+
if (Array.isArray(raw)) return raw[port];
|
|
1269
|
+
if (isSchemaLike(raw)) return raw;
|
|
1270
|
+
return Object.values(raw)[port];
|
|
1071
1271
|
}
|
|
1072
1272
|
/**
|
|
1073
|
-
*
|
|
1273
|
+
* The return key for an output port — `"output"` unless a custom one is set
|
|
1274
|
+
* via `outputReturnProperties[port]` (author default and/or flow-author
|
|
1275
|
+
* override, only possible when the node declares `outputReturnProperties`).
|
|
1276
|
+
* `this.send(x)` always means "x is the value at this port's return key",
|
|
1277
|
+
* never "x is the whole outgoing message".
|
|
1074
1278
|
*/
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
params: error.params,
|
|
1082
|
-
schemaPath: error.schemaPath
|
|
1083
|
-
}));
|
|
1279
|
+
#returnPropertyKey(port) {
|
|
1280
|
+
const configured = this.config.outputReturnProperties?.[port];
|
|
1281
|
+
if (typeof configured === "string" && configured.trim()) {
|
|
1282
|
+
return configured.trim();
|
|
1283
|
+
}
|
|
1284
|
+
return "output";
|
|
1084
1285
|
}
|
|
1085
1286
|
/**
|
|
1086
|
-
*
|
|
1287
|
+
* Resolves the context mode for a base-output port from the flow author's
|
|
1288
|
+
* per-port config (`config.outputContextModes[port]`, written by the editor
|
|
1289
|
+
* when the node declares `outputContextModes`), falling back to `"carry"`.
|
|
1087
1290
|
*/
|
|
1088
|
-
|
|
1089
|
-
this.
|
|
1090
|
-
return this;
|
|
1291
|
+
#resolveContextMode(port) {
|
|
1292
|
+
return this.config.outputContextModes?.[port] ?? "carry";
|
|
1091
1293
|
}
|
|
1092
1294
|
/**
|
|
1093
|
-
*
|
|
1295
|
+
* Merges a sent value into the incoming message at the returnProperty key so
|
|
1296
|
+
* upstream message properties propagate. A fresh base is built per call so
|
|
1297
|
+
* multi-port sends never share an object.
|
|
1094
1298
|
*/
|
|
1095
|
-
|
|
1096
|
-
this
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
};
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
this.errors = errors;
|
|
1104
|
-
this.name = "ValidationError";
|
|
1105
|
-
Object.setPrototypeOf(this, _ValidationError.prototype);
|
|
1106
|
-
}
|
|
1107
|
-
};
|
|
1108
|
-
|
|
1109
|
-
// src/core/server/validation.ts
|
|
1110
|
-
function initValidator(RED) {
|
|
1111
|
-
if (RED.validator) return;
|
|
1112
|
-
const nrg = {
|
|
1113
|
-
validator: new Validator({
|
|
1114
|
-
customKeywords: [
|
|
1115
|
-
{
|
|
1116
|
-
keyword: "x-nrg-skip-validation",
|
|
1117
|
-
schemaType: "boolean",
|
|
1118
|
-
valid: true
|
|
1119
|
-
},
|
|
1120
|
-
{
|
|
1121
|
-
keyword: "x-nrg-node-type",
|
|
1122
|
-
type: "string",
|
|
1123
|
-
validate: (schemaValue, dataValue) => {
|
|
1124
|
-
if (!dataValue) return true;
|
|
1125
|
-
const node = RED.nodes.getNode(dataValue);
|
|
1126
|
-
return node?.type === schemaValue;
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
],
|
|
1130
|
-
customFormats: {
|
|
1131
|
-
"node-id": /^[a-zA-Z0-9-_]+$/
|
|
1132
|
-
}
|
|
1133
|
-
})
|
|
1134
|
-
};
|
|
1135
|
-
Object.defineProperty(RED, "_nrg", {
|
|
1136
|
-
value: nrg,
|
|
1137
|
-
writable: false,
|
|
1138
|
-
enumerable: false,
|
|
1139
|
-
configurable: false
|
|
1140
|
-
});
|
|
1141
|
-
Object.defineProperty(RED, "validator", {
|
|
1142
|
-
get: () => nrg.validator,
|
|
1143
|
-
enumerable: false,
|
|
1144
|
-
configurable: false
|
|
1145
|
-
});
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
// src/core/server/api/assets.ts
|
|
1149
|
-
var import_path = __toESM(require("path"), 1);
|
|
1150
|
-
var import_fs = __toESM(require("fs"), 1);
|
|
1151
|
-
var import_module = require("module");
|
|
1152
|
-
function serveFile(filePath) {
|
|
1153
|
-
return (_req, res, next) => {
|
|
1154
|
-
if (!import_fs.default.existsSync(filePath)) return next();
|
|
1155
|
-
res.setHeader("Content-Type", "application/javascript");
|
|
1156
|
-
import_fs.default.createReadStream(filePath).pipe(res);
|
|
1157
|
-
};
|
|
1158
|
-
}
|
|
1159
|
-
function initAssetsRoutes(router) {
|
|
1160
|
-
const resourcesDir = import_path.default.resolve(__dirname, "./resources");
|
|
1161
|
-
if (!import_fs.default.existsSync(resourcesDir)) return;
|
|
1162
|
-
const _require = (0, import_module.createRequire)(import_path.default.join(__dirname, "package.json"));
|
|
1163
|
-
const vueFile = process.env.NODE_ENV !== "production" ? _require.resolve("vue/dist/vue.esm-browser.js") : _require.resolve("vue/dist/vue.esm-browser.prod.js");
|
|
1164
|
-
router.get(
|
|
1165
|
-
"/nrg/assets/nrg-client.js",
|
|
1166
|
-
serveFile(import_path.default.join(resourcesDir, "nrg-client.js"))
|
|
1167
|
-
);
|
|
1168
|
-
router.get("/nrg/assets/vue.esm-browser.prod.js", serveFile(vueFile));
|
|
1169
|
-
router.get("/nrg/assets/vue.esm-browser.js", serveFile(vueFile));
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
// src/core/server/api/routes.ts
|
|
1173
|
-
var _initialized = false;
|
|
1174
|
-
function initRoutes(RED) {
|
|
1175
|
-
if (_initialized) return;
|
|
1176
|
-
_initialized = true;
|
|
1177
|
-
initAssetsRoutes(RED.httpAdmin);
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
// src/core/server/registration.ts
|
|
1181
|
-
async function registerType(RED, NodeClass) {
|
|
1182
|
-
RED.log.debug(`Registering Type: ${NodeClass.type}`);
|
|
1183
|
-
if (!(NodeClass.prototype instanceof Node)) {
|
|
1184
|
-
throw new NrgError(
|
|
1185
|
-
`${NodeClass.name} must extend IONode or ConfigNode classes`
|
|
1186
|
-
);
|
|
1187
|
-
}
|
|
1188
|
-
if (!NodeClass.type) {
|
|
1189
|
-
throw new NrgError("type must be provided when registering the node");
|
|
1190
|
-
}
|
|
1191
|
-
await NodeClass.register(RED);
|
|
1192
|
-
RED.log.debug(`Type registered: ${NodeClass.type}`);
|
|
1193
|
-
}
|
|
1194
|
-
function registerTypes(nodes) {
|
|
1195
|
-
const fn = Object.assign(
|
|
1196
|
-
async function(RED) {
|
|
1197
|
-
initValidator(RED);
|
|
1198
|
-
initRoutes(RED);
|
|
1199
|
-
try {
|
|
1200
|
-
RED.log.info("Registering node types in series");
|
|
1201
|
-
for (const NodeClass of nodes) {
|
|
1202
|
-
await registerType(RED, NodeClass);
|
|
1203
|
-
}
|
|
1204
|
-
RED.log.info("All node types registered in series");
|
|
1205
|
-
} catch (error) {
|
|
1206
|
-
RED.log.error("Error registering node types:", error);
|
|
1207
|
-
throw error;
|
|
1208
|
-
}
|
|
1209
|
-
},
|
|
1210
|
-
{ nodes }
|
|
1211
|
-
);
|
|
1212
|
-
return fn;
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
// src/core/shared/schemas/type.ts
|
|
1216
|
-
var import_typebox3 = require("@sinclair/typebox");
|
|
1217
|
-
|
|
1218
|
-
// src/core/shared/constants.ts
|
|
1219
|
-
var TYPED_INPUT_TYPES = [
|
|
1220
|
-
"msg",
|
|
1221
|
-
"flow",
|
|
1222
|
-
"global",
|
|
1223
|
-
"str",
|
|
1224
|
-
"num",
|
|
1225
|
-
"bool",
|
|
1226
|
-
"json",
|
|
1227
|
-
"bin",
|
|
1228
|
-
"re",
|
|
1229
|
-
"jsonata",
|
|
1230
|
-
"date",
|
|
1231
|
-
"env",
|
|
1232
|
-
"node",
|
|
1233
|
-
"cred"
|
|
1234
|
-
];
|
|
1235
|
-
|
|
1236
|
-
// src/core/shared/schemas/base.ts
|
|
1237
|
-
var import_typebox2 = require("@sinclair/typebox");
|
|
1238
|
-
var NodeConfigSchema = import_typebox2.Type.Object({
|
|
1239
|
-
id: import_typebox2.Type.String(),
|
|
1240
|
-
type: import_typebox2.Type.String(),
|
|
1241
|
-
name: import_typebox2.Type.String(),
|
|
1242
|
-
z: import_typebox2.Type.Optional(import_typebox2.Type.String())
|
|
1243
|
-
});
|
|
1244
|
-
var ConfigNodeConfigSchema = import_typebox2.Type.Object({
|
|
1245
|
-
...NodeConfigSchema.properties,
|
|
1246
|
-
_users: import_typebox2.Type.Array(import_typebox2.Type.String())
|
|
1247
|
-
});
|
|
1248
|
-
var IONodeConfigSchema = import_typebox2.Type.Object({
|
|
1249
|
-
...NodeConfigSchema.properties,
|
|
1250
|
-
wires: import_typebox2.Type.Array(import_typebox2.Type.Array(import_typebox2.Type.String(), { default: [] }), {
|
|
1251
|
-
default: [[]]
|
|
1252
|
-
}),
|
|
1253
|
-
x: import_typebox2.Type.Number(),
|
|
1254
|
-
y: import_typebox2.Type.Number(),
|
|
1255
|
-
g: import_typebox2.Type.Optional(import_typebox2.Type.String())
|
|
1256
|
-
});
|
|
1257
|
-
var TypedInputSchema = import_typebox2.Type.Object(
|
|
1258
|
-
{
|
|
1259
|
-
value: import_typebox2.Type.Union(
|
|
1260
|
-
[
|
|
1261
|
-
import_typebox2.Type.String(),
|
|
1262
|
-
import_typebox2.Type.Number(),
|
|
1263
|
-
import_typebox2.Type.Boolean(),
|
|
1264
|
-
import_typebox2.Type.Null()
|
|
1265
|
-
],
|
|
1266
|
-
{
|
|
1267
|
-
description: "The actual value entered or selected.",
|
|
1268
|
-
default: ""
|
|
1269
|
-
}
|
|
1270
|
-
),
|
|
1271
|
-
type: import_typebox2.Type.Union(
|
|
1272
|
-
TYPED_INPUT_TYPES.map((type) => import_typebox2.Type.Literal(type)),
|
|
1273
|
-
{
|
|
1274
|
-
description: "The type of the value (string, number, message property, etc.)",
|
|
1275
|
-
default: "str"
|
|
1276
|
-
}
|
|
1277
|
-
)
|
|
1278
|
-
},
|
|
1279
|
-
{
|
|
1280
|
-
description: "Represents a Node-RED TypedInput value and its type.",
|
|
1281
|
-
default: {
|
|
1282
|
-
type: "str",
|
|
1283
|
-
value: ""
|
|
1299
|
+
#wrapOutgoing(value, mode, port) {
|
|
1300
|
+
const key = this.#returnPropertyKey(port);
|
|
1301
|
+
const input = _IONode.#invocation.getStore()?.inputMsg ?? {};
|
|
1302
|
+
if (mode === "reset") {
|
|
1303
|
+
return { [key]: value };
|
|
1304
|
+
}
|
|
1305
|
+
if (mode === "trace") {
|
|
1306
|
+
return { ...input, [key]: value, [INPUT_KEY]: input };
|
|
1284
1307
|
}
|
|
1308
|
+
return { ...input, [key]: value };
|
|
1285
1309
|
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
type: import_typebox2.Type.String(),
|
|
1290
|
-
name: import_typebox2.Type.String()
|
|
1291
|
-
});
|
|
1292
|
-
var ErrorPortSchema = import_typebox2.Type.Object({
|
|
1293
|
-
error: import_typebox2.Type.Object({
|
|
1294
|
-
message: import_typebox2.Type.String(),
|
|
1295
|
-
source: NodeSourceSchema
|
|
1296
|
-
})
|
|
1297
|
-
});
|
|
1298
|
-
var CompletePortSchema = import_typebox2.Type.Object({
|
|
1299
|
-
complete: import_typebox2.Type.Object({
|
|
1300
|
-
source: NodeSourceSchema
|
|
1301
|
-
})
|
|
1302
|
-
});
|
|
1303
|
-
var StatusPortSchema = import_typebox2.Type.Object({
|
|
1304
|
-
status: import_typebox2.Type.Union([
|
|
1305
|
-
import_typebox2.Type.Object({
|
|
1306
|
-
fill: import_typebox2.Type.Optional(
|
|
1307
|
-
import_typebox2.Type.Union([
|
|
1308
|
-
import_typebox2.Type.Literal("red"),
|
|
1309
|
-
import_typebox2.Type.Literal("green"),
|
|
1310
|
-
import_typebox2.Type.Literal("yellow"),
|
|
1311
|
-
import_typebox2.Type.Literal("blue"),
|
|
1312
|
-
import_typebox2.Type.Literal("grey"),
|
|
1313
|
-
import_typebox2.Type.Literal("gray")
|
|
1314
|
-
])
|
|
1315
|
-
),
|
|
1316
|
-
shape: import_typebox2.Type.Optional(
|
|
1317
|
-
import_typebox2.Type.Union([import_typebox2.Type.Literal("ring"), import_typebox2.Type.Literal("dot")])
|
|
1318
|
-
),
|
|
1319
|
-
text: import_typebox2.Type.Optional(import_typebox2.Type.String())
|
|
1320
|
-
}),
|
|
1321
|
-
import_typebox2.Type.String()
|
|
1322
|
-
]),
|
|
1323
|
-
source: NodeSourceSchema
|
|
1324
|
-
});
|
|
1325
|
-
|
|
1326
|
-
// src/core/shared/schemas/type.ts
|
|
1327
|
-
var JSON_TYPES = /* @__PURE__ */ new Set([
|
|
1328
|
-
"null",
|
|
1329
|
-
"boolean",
|
|
1330
|
-
"object",
|
|
1331
|
-
"array",
|
|
1332
|
-
"number",
|
|
1333
|
-
"integer",
|
|
1334
|
-
"string"
|
|
1335
|
-
]);
|
|
1336
|
-
function isJSONType(value) {
|
|
1337
|
-
return typeof value === "string" && JSON_TYPES.has(value);
|
|
1338
|
-
}
|
|
1339
|
-
function NodeRef(type, options) {
|
|
1340
|
-
return {
|
|
1341
|
-
...import_typebox3.Type.String({
|
|
1342
|
-
description: options?.description || `Reference to ${type}`
|
|
1343
|
-
}),
|
|
1344
|
-
// `...options` first so user options (description, x-nrg-form, …) apply, but
|
|
1345
|
-
// the framework keys below win — a caller can't clobber `format`/
|
|
1346
|
-
// `x-nrg-node-type`/`Kind` and silently break NodeRef resolution.
|
|
1347
|
-
...options,
|
|
1348
|
-
format: "node-id",
|
|
1349
|
-
"x-nrg-node-type": type,
|
|
1350
|
-
[import_typebox3.Kind]: "NodeRef"
|
|
1351
|
-
};
|
|
1352
|
-
}
|
|
1353
|
-
function TypedInput2(options) {
|
|
1354
|
-
const opts = options ?? {};
|
|
1355
|
-
const restrictedTypes = opts["x-nrg-form"]?.typedInputTypes;
|
|
1356
|
-
if (restrictedTypes && restrictedTypes.length === 0) {
|
|
1357
|
-
throw new Error(
|
|
1358
|
-
"SchemaType.TypedInput: typedInputTypes must list at least one type."
|
|
1359
|
-
);
|
|
1310
|
+
// --- Built-in port management ---
|
|
1311
|
+
get baseOutputs() {
|
|
1312
|
+
return this.constructor.outputs ?? 0;
|
|
1360
1313
|
}
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1314
|
+
get totalOutputs() {
|
|
1315
|
+
let count = this.baseOutputs;
|
|
1316
|
+
if (this.config.errorPort) count++;
|
|
1317
|
+
if (this.config.completePort) count++;
|
|
1318
|
+
if (this.config.statusPort) count++;
|
|
1319
|
+
return count;
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Send a message to a specific output port by index or name.
|
|
1323
|
+
* Custom named ports are resolved from `outputsSchema` when it is a record.
|
|
1324
|
+
* Numeric indices refer to the base output ports (0-based).
|
|
1325
|
+
*
|
|
1326
|
+
* Built-in ports (`"error"`, `"complete"`, `"status"`) are managed by the
|
|
1327
|
+
* framework and cannot be sent to directly. Use `this.status()` for status,
|
|
1328
|
+
* throw an error or call `this.error()` for the error port, and the complete
|
|
1329
|
+
* port is sent automatically on successful input processing.
|
|
1330
|
+
*/
|
|
1331
|
+
sendToPort(port, msg) {
|
|
1332
|
+
if (port === "error" || port === "complete" || port === "status") {
|
|
1333
|
+
throw new NrgError(
|
|
1334
|
+
`sendToPort("${port}") is not allowed. Built-in ports are managed by the framework.`
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
const portIndex = typeof port === "number" ? port : this.#getNamedPortIndex(port);
|
|
1338
|
+
if (typeof port === "string" && portIndex === null) {
|
|
1339
|
+
const keys = this.#namedPortKeys();
|
|
1340
|
+
throw new NrgError(
|
|
1341
|
+
keys && keys.length ? `sendToPort("${port}") \u2014 unknown output port. Valid named ports: ${keys.map((n) => `"${n}"`).join(", ")}.` : `sendToPort("${port}") \u2014 this node has no named output ports. Make outputsSchema a record of named schemas, or send to a numeric port index.`
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
const mode = this.#resolveContextMode(portIndex ?? 0);
|
|
1345
|
+
this.#sendToPort(
|
|
1346
|
+
port,
|
|
1347
|
+
msg == null ? msg : this.#wrapOutgoing(msg, mode, portIndex ?? 0)
|
|
1365
1348
|
);
|
|
1366
1349
|
}
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
};
|
|
1380
|
-
}
|
|
1381
|
-
function NrgString(options) {
|
|
1382
|
-
return import_typebox3.Type.String(options);
|
|
1383
|
-
}
|
|
1384
|
-
function OutputReturnProperties(options) {
|
|
1385
|
-
return import_typebox3.Type.Record(
|
|
1386
|
-
import_typebox3.Type.Number(),
|
|
1387
|
-
import_typebox3.Type.String({ pattern: "^[A-Za-z_$][A-Za-z0-9_$]*$" }),
|
|
1388
|
-
{
|
|
1389
|
-
description: "Per-port return property, keyed by output port index. A missing entry falls back to `output`.",
|
|
1390
|
-
default: {},
|
|
1391
|
-
...options
|
|
1350
|
+
#sendToPort(port, msg) {
|
|
1351
|
+
let portIndex;
|
|
1352
|
+
if (typeof port === "number") {
|
|
1353
|
+
portIndex = port;
|
|
1354
|
+
} else if (port === "error" || port === "complete" || port === "status") {
|
|
1355
|
+
portIndex = this.#getBuiltinPortIndex(port);
|
|
1356
|
+
if (portIndex === null) return;
|
|
1357
|
+
} else {
|
|
1358
|
+
portIndex = this.#getNamedPortIndex(port);
|
|
1359
|
+
if (portIndex === null) {
|
|
1360
|
+
throw new NrgError(`Unknown output port "${port}".`);
|
|
1361
|
+
}
|
|
1392
1362
|
}
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1363
|
+
const out = new Array(this.totalOutputs);
|
|
1364
|
+
out[portIndex] = msg;
|
|
1365
|
+
this.node.send(out);
|
|
1366
|
+
}
|
|
1367
|
+
/** The declared named output ports (record `outputsSchema`), or null when the
|
|
1368
|
+
* node has none (no schema, a tuple schema, or a single schema). */
|
|
1369
|
+
#namedPortKeys() {
|
|
1370
|
+
const schema = this.constructor.outputsSchema;
|
|
1371
|
+
if (!schema || Array.isArray(schema) || isSchemaLike(schema)) return null;
|
|
1372
|
+
return Object.keys(schema);
|
|
1373
|
+
}
|
|
1374
|
+
#getNamedPortIndex(name) {
|
|
1375
|
+
const keys = this.#namedPortKeys();
|
|
1376
|
+
if (!keys) return null;
|
|
1377
|
+
const idx = keys.indexOf(name);
|
|
1378
|
+
return idx === -1 ? null : idx;
|
|
1379
|
+
}
|
|
1380
|
+
#getBuiltinPortIndex(name) {
|
|
1381
|
+
if (name === "error") {
|
|
1382
|
+
return this.config.errorPort ? this.baseOutputs : null;
|
|
1407
1383
|
}
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
}
|
|
1413
|
-
var NrgBuilders = {
|
|
1414
|
-
String: NrgString,
|
|
1415
|
-
Unsafe: NrgUnsafe,
|
|
1416
|
-
NodeRef,
|
|
1417
|
-
TypedInput: TypedInput2,
|
|
1418
|
-
OutputReturnProperties,
|
|
1419
|
-
OutputContextModes
|
|
1420
|
-
};
|
|
1421
|
-
var SchemaType = Object.assign({}, import_typebox3.Type, NrgBuilders);
|
|
1422
|
-
function markNonValidatable(schema) {
|
|
1423
|
-
const type = schema.type;
|
|
1424
|
-
const hasInvalidType = type !== void 0 && (Array.isArray(type) ? !type.every(isJSONType) : !isJSONType(type));
|
|
1425
|
-
if (hasInvalidType) {
|
|
1426
|
-
schema["x-nrg-skip-validation"] = true;
|
|
1427
|
-
if (schema.default !== void 0) {
|
|
1428
|
-
schema._default = schema.default;
|
|
1429
|
-
delete schema.default;
|
|
1384
|
+
let idx = this.baseOutputs;
|
|
1385
|
+
if (this.config.errorPort) idx++;
|
|
1386
|
+
if (name === "complete") {
|
|
1387
|
+
return this.config.completePort ? idx : null;
|
|
1430
1388
|
}
|
|
1431
|
-
|
|
1389
|
+
if (this.config.completePort) idx++;
|
|
1390
|
+
return this.config.statusPort ? idx : null;
|
|
1432
1391
|
}
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1392
|
+
#nodeSource() {
|
|
1393
|
+
return {
|
|
1394
|
+
id: this.id,
|
|
1395
|
+
type: this.constructor.type,
|
|
1396
|
+
name: this.name
|
|
1397
|
+
};
|
|
1437
1398
|
}
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1399
|
+
status(status) {
|
|
1400
|
+
this.node.status(status);
|
|
1401
|
+
this.#sendToPort("status", {
|
|
1402
|
+
status,
|
|
1403
|
+
source: this.#nodeSource()
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
error(message, msg) {
|
|
1407
|
+
super.error(message, msg);
|
|
1408
|
+
if (msg && this.config.errorPort) {
|
|
1409
|
+
this.#sendToPort("error", {
|
|
1410
|
+
...msg,
|
|
1411
|
+
error: {
|
|
1412
|
+
// `name` keeps the error-port payload Catch-node compatible and
|
|
1413
|
+
// consistent with the auto-emit from a thrown error.
|
|
1414
|
+
name: "Error",
|
|
1415
|
+
message,
|
|
1416
|
+
source: this.#nodeSource()
|
|
1417
|
+
},
|
|
1418
|
+
[INPUT_KEY]: msg
|
|
1419
|
+
});
|
|
1420
|
+
const store = _IONode.#invocation.getStore();
|
|
1421
|
+
if (store) store.errorEmitted = true;
|
|
1441
1422
|
}
|
|
1442
1423
|
}
|
|
1443
|
-
|
|
1444
|
-
|
|
1424
|
+
updateWires(wires) {
|
|
1425
|
+
this.node.updateWires(wires);
|
|
1445
1426
|
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1427
|
+
receive(msg) {
|
|
1428
|
+
this.node.receive(msg);
|
|
1448
1429
|
}
|
|
1449
|
-
|
|
1450
|
-
|
|
1430
|
+
get x() {
|
|
1431
|
+
return this.node.x;
|
|
1451
1432
|
}
|
|
1452
|
-
|
|
1453
|
-
|
|
1433
|
+
get y() {
|
|
1434
|
+
return this.node.y;
|
|
1454
1435
|
}
|
|
1455
|
-
|
|
1456
|
-
|
|
1436
|
+
get g() {
|
|
1437
|
+
return this.node.g;
|
|
1438
|
+
}
|
|
1439
|
+
get wires() {
|
|
1440
|
+
return this.node.wires;
|
|
1441
|
+
}
|
|
1442
|
+
get credentials() {
|
|
1443
|
+
return this.node.credentials;
|
|
1444
|
+
}
|
|
1445
|
+
};
|
|
1446
|
+
|
|
1447
|
+
// src/sdk/lib/server/nodes/config-node.ts
|
|
1448
|
+
var ConfigNode = class extends Node {
|
|
1449
|
+
static category = "config";
|
|
1450
|
+
// Runtime counterpart: a real property so runtime code (the config proxy's
|
|
1451
|
+
// NodeRef resolution) can verify a referenced node is a config node — the only
|
|
1452
|
+
// guard a JS author gets. See NRG_CONFIG_NODE in ./symbols.
|
|
1453
|
+
[NRG_CONFIG_NODE] = true;
|
|
1454
|
+
context;
|
|
1455
|
+
constructor(RED, node, config, credentials) {
|
|
1456
|
+
super(RED, node, config, credentials);
|
|
1457
|
+
const context = node.context();
|
|
1458
|
+
const resolve = (scope, store) => {
|
|
1459
|
+
const target = scope === "global" ? context.global : context;
|
|
1460
|
+
return setupContext(target, store);
|
|
1461
|
+
};
|
|
1462
|
+
this.context = Object.assign(resolve, {
|
|
1463
|
+
node: setupContext(context),
|
|
1464
|
+
global: setupContext(context.global)
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
get userIds() {
|
|
1468
|
+
return this.config._users;
|
|
1469
|
+
}
|
|
1470
|
+
get users() {
|
|
1471
|
+
return this.userIds.map((id) => this.RED.nodes.getNode(id)?._node).filter((node) => node != null);
|
|
1472
|
+
}
|
|
1473
|
+
getUser(index) {
|
|
1474
|
+
const id = this.userIds[index];
|
|
1475
|
+
if (!id) return void 0;
|
|
1476
|
+
return this.RED.nodes.getNode(id)?._node;
|
|
1477
|
+
}
|
|
1478
|
+
get credentials() {
|
|
1479
|
+
return this.node.credentials;
|
|
1480
|
+
}
|
|
1481
|
+
};
|
|
1482
|
+
|
|
1483
|
+
// src/sdk/lib/server/nodes/factories.ts
|
|
1484
|
+
function normalizeSchemas(schemas, outputsSchema) {
|
|
1485
|
+
for (const schema of schemas) {
|
|
1486
|
+
if (schema) markNonValidatable(schema);
|
|
1487
|
+
}
|
|
1488
|
+
if (!outputsSchema) return;
|
|
1489
|
+
if (Array.isArray(outputsSchema)) {
|
|
1490
|
+
for (const schema of outputsSchema) markNonValidatable(schema);
|
|
1491
|
+
} else if (import_typebox3.Kind in outputsSchema) {
|
|
1492
|
+
markNonValidatable(outputsSchema);
|
|
1493
|
+
} else {
|
|
1494
|
+
for (const schema of Object.values(outputsSchema)) {
|
|
1495
|
+
markNonValidatable(schema);
|
|
1496
|
+
}
|
|
1457
1497
|
}
|
|
1458
|
-
return schema;
|
|
1459
1498
|
}
|
|
1460
|
-
function
|
|
1461
|
-
const
|
|
1462
|
-
|
|
1499
|
+
function defineIONode(def) {
|
|
1500
|
+
const NodeClass = class extends IONode {
|
|
1501
|
+
static type = def.type;
|
|
1502
|
+
static category = def.category ?? "function";
|
|
1503
|
+
static color = def.color ?? "#a6bbcf";
|
|
1504
|
+
static align = def.align;
|
|
1505
|
+
static configSchema = def.configSchema;
|
|
1506
|
+
static credentialsSchema = def.credentialsSchema;
|
|
1507
|
+
static settingsSchema = def.settingsSchema;
|
|
1508
|
+
static inputSchema = def.inputSchema;
|
|
1509
|
+
static outputsSchema = def.outputsSchema;
|
|
1510
|
+
static validateInput = def.validateInput ?? false;
|
|
1511
|
+
static validateOutput = def.validateOutput ?? false;
|
|
1512
|
+
static registered(RED) {
|
|
1513
|
+
return def.registered?.(RED);
|
|
1514
|
+
}
|
|
1515
|
+
async input(msg) {
|
|
1516
|
+
if (def.input) return def.input.call(this, msg);
|
|
1517
|
+
}
|
|
1518
|
+
async created() {
|
|
1519
|
+
if (def.created) return def.created.call(this);
|
|
1520
|
+
}
|
|
1521
|
+
async closed(removed) {
|
|
1522
|
+
if (def.closed) return def.closed.call(this, removed);
|
|
1523
|
+
}
|
|
1524
|
+
};
|
|
1525
|
+
Object.defineProperty(NodeClass, "name", {
|
|
1526
|
+
value: def.type.replace(
|
|
1527
|
+
/(^|-)(\w)/g,
|
|
1528
|
+
(_, __, c) => c.toUpperCase()
|
|
1529
|
+
),
|
|
1530
|
+
configurable: true
|
|
1531
|
+
});
|
|
1532
|
+
normalizeSchemas(
|
|
1533
|
+
[
|
|
1534
|
+
NodeClass.configSchema,
|
|
1535
|
+
NodeClass.credentialsSchema,
|
|
1536
|
+
NodeClass.settingsSchema,
|
|
1537
|
+
NodeClass.inputSchema
|
|
1538
|
+
],
|
|
1539
|
+
NodeClass.outputsSchema
|
|
1540
|
+
);
|
|
1541
|
+
return NodeClass;
|
|
1463
1542
|
}
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1543
|
+
function defineConfigNode(def) {
|
|
1544
|
+
const NodeClass = class extends ConfigNode {
|
|
1545
|
+
static type = def.type;
|
|
1546
|
+
static configSchema = def.configSchema;
|
|
1547
|
+
static credentialsSchema = def.credentialsSchema;
|
|
1548
|
+
static settingsSchema = def.settingsSchema;
|
|
1549
|
+
static registered(RED) {
|
|
1550
|
+
return def.registered?.(RED);
|
|
1551
|
+
}
|
|
1552
|
+
async created() {
|
|
1553
|
+
if (def.created) return def.created.call(this);
|
|
1554
|
+
}
|
|
1555
|
+
async closed(removed) {
|
|
1556
|
+
if (def.closed) return def.closed.call(this, removed);
|
|
1557
|
+
}
|
|
1558
|
+
};
|
|
1559
|
+
Object.defineProperty(NodeClass, "name", {
|
|
1560
|
+
value: def.type.replace(
|
|
1561
|
+
/(^|-)(\w)/g,
|
|
1562
|
+
(_, __, c) => c.toUpperCase()
|
|
1563
|
+
),
|
|
1564
|
+
configurable: true
|
|
1565
|
+
});
|
|
1566
|
+
normalizeSchemas([
|
|
1567
|
+
NodeClass.configSchema,
|
|
1568
|
+
NodeClass.credentialsSchema,
|
|
1569
|
+
NodeClass.settingsSchema
|
|
1570
|
+
]);
|
|
1571
|
+
return NodeClass;
|
|
1468
1572
|
}
|
|
1469
1573
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1470
1574
|
0 && (module.exports = {
|
|
1471
|
-
CompletePortSchema,
|
|
1472
1575
|
ConfigNode,
|
|
1473
|
-
ErrorPortSchema,
|
|
1474
1576
|
IONode,
|
|
1577
|
+
Kind,
|
|
1475
1578
|
Node,
|
|
1476
|
-
NodeSourceSchema,
|
|
1477
1579
|
NrgError,
|
|
1478
1580
|
SchemaType,
|
|
1479
|
-
StatusPortSchema,
|
|
1480
1581
|
defineConfigNode,
|
|
1481
1582
|
defineIONode,
|
|
1482
1583
|
defineModule,
|