@bonsae/nrg 0.13.1 → 0.15.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 CHANGED
@@ -30,203 +30,25 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/core/server/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ CompletePortSchema: () => CompletePortSchema,
33
34
  ConfigNode: () => ConfigNode,
35
+ ErrorPortSchema: () => ErrorPortSchema,
34
36
  IONode: () => IONode,
35
37
  Node: () => Node,
36
38
  NrgError: () => NrgError,
37
39
  SchemaType: () => SchemaType,
40
+ StatusPortSchema: () => StatusPortSchema,
38
41
  defineConfigNode: () => defineConfigNode,
39
42
  defineIONode: () => defineIONode,
40
43
  defineModule: () => defineModule,
41
44
  defineSchema: () => defineSchema,
42
- initValidator: () => initValidator,
43
45
  registerType: () => registerType,
44
46
  registerTypes: () => registerTypes
45
47
  });
46
48
  module.exports = __toCommonJS(index_exports);
47
49
 
48
- // src/core/server/utils.ts
49
- function getCredentialsFromSchema(schema) {
50
- const result = {};
51
- for (const [key, value] of Object.entries(schema.properties)) {
52
- const property = value;
53
- result[key] = {
54
- // NOTE: required is always false because it is controlled by the JSON Schema and AJV validation instead of using node-red client core
55
- required: false,
56
- type: property.format === "password" ? "password" : "text",
57
- value: property.default ?? void 0
58
- };
59
- }
60
- return result;
61
- }
62
-
63
- // src/core/validator.ts
64
- var import_ajv = __toESM(require("ajv"), 1);
65
- var import_ajv_formats = __toESM(require("ajv-formats"), 1);
66
- var import_ajv_errors = __toESM(require("ajv-errors"), 1);
67
- var Validator = class {
68
- ajv;
69
- constructor(options) {
70
- const { customKeywords, customFormats, ...ajvOptions } = options || {};
71
- this.ajv = new import_ajv.default({
72
- allErrors: true,
73
- code: {
74
- source: false
75
- },
76
- coerceTypes: true,
77
- removeAdditional: false,
78
- strict: false,
79
- strictSchema: false,
80
- useDefaults: true,
81
- validateFormats: true,
82
- // NOTE: typebox handles validation via typescript
83
- // NOTE: if true, types that are not serializable JSON, like Function, would not work
84
- validateSchema: false,
85
- verbose: true,
86
- ...ajvOptions
87
- });
88
- (0, import_ajv_formats.default)(this.ajv);
89
- (0, import_ajv_errors.default)(this.ajv);
90
- this.addCustomKeywords(customKeywords || []);
91
- this.addCustomFormats(customFormats || {});
92
- }
93
- /**
94
- * Add custom keywords to the validator
95
- */
96
- addCustomKeywords(keywords) {
97
- if (!keywords) return;
98
- keywords.forEach((keyword) => {
99
- this.ajv.addKeyword(keyword);
100
- });
101
- }
102
- /**
103
- * Add custom formats to the validator
104
- */
105
- addCustomFormats(formats) {
106
- if (!formats) return;
107
- Object.entries(formats).forEach(([name, validator2]) => {
108
- if (validator2 instanceof RegExp) {
109
- this.ajv.addFormat(name, validator2);
110
- } else {
111
- this.ajv.addFormat(name, { validate: validator2 });
112
- }
113
- });
114
- }
115
- /**
116
- * Create a validator function with caching
117
- * @param schema - JSON Schema to validate against
118
- * @param cacheKey - Optional cache key for reusing validators
119
- */
120
- createValidator(schema, cacheKey) {
121
- if (cacheKey && !schema.$id) {
122
- schema.$id = cacheKey;
123
- }
124
- if (schema.$id) {
125
- const cached = this.ajv.getSchema(schema.$id);
126
- if (cached) return cached;
127
- }
128
- const validator2 = this.ajv.compile(schema);
129
- return validator2;
130
- }
131
- /**
132
- * Validate data against a schema and return a structured result
133
- */
134
- validate(data, schema, options) {
135
- const validator2 = this.createValidator(schema, options?.cacheKey);
136
- const valid = validator2(data);
137
- if (!valid) {
138
- const errorMessage = this.formatErrors(validator2.errors);
139
- if (options?.throwOnError) {
140
- throw new ValidationError(errorMessage, validator2.errors || []);
141
- }
142
- return {
143
- valid: false,
144
- errors: validator2.errors || void 0,
145
- errorMessage
146
- };
147
- }
148
- return {
149
- valid: true,
150
- data
151
- };
152
- }
153
- /**
154
- * Format errors into a human-readable string
155
- */
156
- formatErrors(errors, options) {
157
- if (!errors || errors.length === 0) {
158
- return "No errors";
159
- }
160
- return this.ajv.errorsText(errors, {
161
- separator: "; ",
162
- dataVar: "data",
163
- ...options
164
- });
165
- }
166
- /**
167
- * Get detailed error information
168
- */
169
- getDetailedErrors(errors) {
170
- if (!errors || errors.length === 0) return [];
171
- return errors.map((error) => ({
172
- field: error.instancePath || "/",
173
- message: error.message || "Validation failed",
174
- keyword: error.keyword,
175
- params: error.params,
176
- schemaPath: error.schemaPath
177
- }));
178
- }
179
- /**
180
- * Add a schema to the validator for reference
181
- */
182
- addSchema(schema, key) {
183
- this.ajv.addSchema(schema, key);
184
- return this;
185
- }
186
- /**
187
- * Remove a schema from the validator
188
- */
189
- removeSchema(key) {
190
- this.ajv.removeSchema(key);
191
- return this;
192
- }
193
- };
194
- var ValidationError = class _ValidationError extends Error {
195
- constructor(message, errors) {
196
- super(message);
197
- this.errors = errors;
198
- this.name = "ValidationError";
199
- Object.setPrototypeOf(this, _ValidationError.prototype);
200
- }
201
- };
202
-
203
- // src/core/server/validation.ts
204
- var validator = void 0;
205
- function initValidator(RED) {
206
- validator = new Validator({
207
- customKeywords: [
208
- {
209
- keyword: "x-nrg-skip-validation",
210
- schemaType: "boolean",
211
- valid: true
212
- },
213
- {
214
- keyword: "x-nrg-node-type",
215
- type: "string",
216
- validate: (schemaValue, dataValue) => {
217
- if (!dataValue) return true;
218
- const node = RED.nodes.getNode(dataValue);
219
- return node?.type === schemaValue;
220
- }
221
- }
222
- ],
223
- customFormats: {
224
- "node-id": /^[a-zA-Z0-9-_]+$/,
225
- "flow-id": /^[a-f0-9]{16}$/,
226
- "topic-path": (data) => /^[a-zA-Z0-9/_-]+$/.test(data)
227
- }
228
- });
229
- }
50
+ // src/core/server/nodes/utils.ts
51
+ var import_typebox = require("@sinclair/typebox");
230
52
 
231
53
  // src/core/errors.ts
232
54
  var NrgError = class _NrgError extends Error {
@@ -280,6 +102,9 @@ var TypedInput = class {
280
102
  };
281
103
 
282
104
  // src/core/server/nodes/utils.ts
105
+ function isSchemaLike(obj) {
106
+ return obj != null && typeof obj === "object" && import_typebox.Kind in obj;
107
+ }
283
108
  function setupContext(context, store) {
284
109
  return {
285
110
  get: (key) => new Promise(
@@ -358,29 +183,32 @@ function setupConfigProxy(opts) {
358
183
  return createProxy(config);
359
184
  }
360
185
 
186
+ // src/core/server/utils.ts
187
+ function getCredentialsFromSchema(schema) {
188
+ const result = {};
189
+ for (const [key, value] of Object.entries(schema.properties)) {
190
+ const property = value;
191
+ result[key] = {
192
+ // NOTE: required is always false because it is controlled by the JSON Schema and AJV validation instead of using node-red client core
193
+ required: false,
194
+ type: property.format === "password" ? "password" : "text",
195
+ value: property.default ?? void 0
196
+ };
197
+ }
198
+ return result;
199
+ }
200
+
201
+ // src/core/server/nodes/symbols.ts
202
+ var WIRE_HANDLERS = Symbol("wireHandlers");
203
+
361
204
  // src/core/server/nodes/node.ts
362
- var Node = class {
205
+ var cachedSettingsMap = /* @__PURE__ */ new WeakMap();
206
+ var Node = class _Node {
363
207
  static type;
364
208
  static category;
365
209
  static configSchema;
366
210
  static credentialsSchema;
367
211
  static settingsSchema;
368
- static _cachedSettings = null;
369
- /** @internal */
370
- static _settings() {
371
- if (!this.settingsSchema) return;
372
- const settings = {};
373
- const prefix = this.type.replace(/-./g, (x) => x[1].toUpperCase());
374
- for (const [key, prop] of Object.entries(this.settingsSchema.properties)) {
375
- const settingKey = prefix + key.charAt(0).toUpperCase() + key.slice(1);
376
- settings[settingKey] = {
377
- value: prop.default,
378
- exportable: prop.exportable ?? false
379
- };
380
- }
381
- return settings;
382
- }
383
- // NOTE:
384
212
  static validateSettings(RED) {
385
213
  if (!this.settingsSchema) return;
386
214
  RED.log.info("Validating settings");
@@ -402,13 +230,65 @@ var Node = class {
402
230
  }
403
231
  }
404
232
  }
405
- validator.validate(settings, this.settingsSchema, {
233
+ RED.validator.validate(settings, this.settingsSchema, {
406
234
  cacheKey: this.settingsSchema.$id || `${this.type}:settings`,
407
235
  throwOnError: true
408
236
  });
409
- this._cachedSettings = settings;
237
+ cachedSettingsMap.set(this, settings);
410
238
  RED.log.info("Settings are valid");
411
239
  }
240
+ static #buildSettings(NC) {
241
+ if (!NC.settingsSchema) return;
242
+ const settings = {};
243
+ const prefix = NC.type.replace(/-./g, (x) => x[1].toUpperCase());
244
+ for (const [key, prop] of Object.entries(NC.settingsSchema.properties)) {
245
+ const settingKey = prefix + key.charAt(0).toUpperCase() + key.slice(1);
246
+ settings[settingKey] = {
247
+ value: prop.default,
248
+ exportable: prop.exportable ?? false
249
+ };
250
+ }
251
+ return settings;
252
+ }
253
+ /**
254
+ * Registers this node class with Node-RED. Handles instance creation,
255
+ * event handler wiring, settings validation, and the user's registered() hook.
256
+ */
257
+ static async register(RED) {
258
+ const NodeClass = this;
259
+ if (NodeClass.color && !/^#[0-9A-Fa-f]{6}$/.test(NodeClass.color)) {
260
+ throw new NrgError(
261
+ `Invalid color for ${NodeClass.type}: ${NodeClass.color} color must be in hex format`
262
+ );
263
+ }
264
+ RED.nodes.registerType(
265
+ NodeClass.type,
266
+ function(config) {
267
+ RED.nodes.createNode(this, config);
268
+ const node = new NodeClass(RED, this, config, this.credentials);
269
+ Object.defineProperty(this, "_node", {
270
+ value: node,
271
+ writable: false,
272
+ configurable: false,
273
+ enumerable: false
274
+ });
275
+ const createdPromise = Promise.resolve(node.created?.()).catch(
276
+ (error) => {
277
+ const message = error instanceof Error ? error.message : String(error);
278
+ this.error("Error during created hook: " + message);
279
+ throw error;
280
+ }
281
+ );
282
+ node[WIRE_HANDLERS](this, createdPromise);
283
+ },
284
+ {
285
+ credentials: NodeClass.credentialsSchema ? getCredentialsFromSchema(NodeClass.credentialsSchema) : {},
286
+ settings: _Node.#buildSettings(this)
287
+ }
288
+ );
289
+ NodeClass.validateSettings(RED);
290
+ await Promise.resolve(NodeClass.registered?.(RED));
291
+ }
412
292
  RED;
413
293
  node;
414
294
  context;
@@ -421,7 +301,7 @@ var Node = class {
421
301
  const constructor = this.constructor;
422
302
  if (constructor.configSchema) {
423
303
  this.log("Validating configs");
424
- const configResult = validator.validate(
304
+ const configResult = this.RED.validator.validate(
425
305
  config,
426
306
  constructor.configSchema,
427
307
  {
@@ -443,7 +323,7 @@ var Node = class {
443
323
  });
444
324
  if (constructor.credentialsSchema && credentials) {
445
325
  this.log("Validating credentials");
446
- const credResult = validator.validate(
326
+ const credResult = this.RED.validator.validate(
447
327
  credentials,
448
328
  constructor.credentialsSchema,
449
329
  {
@@ -458,6 +338,39 @@ var Node = class {
458
338
  }
459
339
  }
460
340
  }
341
+ [WIRE_HANDLERS](nodeRedNode, createdPromise) {
342
+ nodeRedNode.on(
343
+ "close",
344
+ async (removed, done) => {
345
+ try {
346
+ this.log("Calling closed");
347
+ await this.#closed(removed);
348
+ this.log("Node was closed");
349
+ done();
350
+ } catch (error) {
351
+ if (error instanceof Error) {
352
+ this.error("Error while closing node: " + error.message);
353
+ done(error);
354
+ } else {
355
+ this.error("Unknown error occurred while closing node");
356
+ done(new Error("Unknown error occurred while closing node"));
357
+ }
358
+ }
359
+ }
360
+ );
361
+ }
362
+ async #closed(removed) {
363
+ try {
364
+ await Promise.resolve(this.closed?.(removed));
365
+ } finally {
366
+ this.log("clearing timers and intervals");
367
+ this.timers.forEach((t) => clearTimeout(t));
368
+ this.intervals.forEach((i) => clearInterval(i));
369
+ this.timers.clear();
370
+ this.intervals.clear();
371
+ this.log("timers and intervals cleared");
372
+ }
373
+ }
461
374
  i18n(key, substitutions) {
462
375
  const nodeType = this.constructor.type;
463
376
  return this.RED._(`${nodeType}.${key}`, substitutions);
@@ -483,20 +396,6 @@ var Node = class {
483
396
  clearInterval(interval);
484
397
  this.intervals.delete(interval);
485
398
  }
486
- // NOTE: used by the registered function. Had to be a different one to avoid calling the parent's closed again
487
- /** @internal */
488
- async _closed(removed) {
489
- try {
490
- await Promise.resolve(this.closed?.(removed));
491
- } finally {
492
- this.log("clearing timers and intervals");
493
- this.timers.forEach((t) => clearTimeout(t));
494
- this.intervals.forEach((i) => clearInterval(i));
495
- this.timers.clear();
496
- this.intervals.clear();
497
- this.log("timers and intervals cleared");
498
- }
499
- }
500
399
  on(event, callback) {
501
400
  this.node.on(event, callback);
502
401
  }
@@ -523,7 +422,7 @@ var Node = class {
523
422
  }
524
423
  get settings() {
525
424
  const constructor = this.constructor;
526
- return constructor._cachedSettings ?? {};
425
+ return cachedSettingsMap.get(constructor) ?? {};
527
426
  }
528
427
  };
529
428
 
@@ -541,16 +440,20 @@ var IONode = class extends Node {
541
440
  static get outputs() {
542
441
  const s = this.outputsSchema;
543
442
  if (!s) return 0;
544
- return Array.isArray(s) ? s.length : 1;
443
+ if (Array.isArray(s)) return s.length;
444
+ if (isSchemaLike(s)) return 1;
445
+ const keys = Object.keys(s);
446
+ for (const key of keys) {
447
+ if (/^\d+$/.test(key)) {
448
+ throw new NrgError(
449
+ `outputsSchema record key "${key}" in ${this.type} looks numeric. Use descriptive string names (e.g. "success", "failure") to avoid JavaScript object key ordering issues.`
450
+ );
451
+ }
452
+ }
453
+ return keys.length;
545
454
  }
546
- _send;
455
+ #send;
547
456
  context;
548
- // NOTE: used by the registered function. Had to be a different one to avoid calling the parent's input again
549
- /** @internal */
550
- static _registered(RED) {
551
- this.validateSettings(RED);
552
- return this.registered?.(RED);
553
- }
554
457
  constructor(RED, node, config, credentials) {
555
458
  super(RED, node, config, credentials);
556
459
  const context = node.context();
@@ -563,26 +466,73 @@ var IONode = class extends Node {
563
466
  fn.global = setupContext(context.global);
564
467
  this.context = fn;
565
468
  }
469
+ [WIRE_HANDLERS](nodeRedNode, createdPromise) {
470
+ super[WIRE_HANDLERS](nodeRedNode, createdPromise);
471
+ const NC = this.constructor;
472
+ nodeRedNode.on(
473
+ "input",
474
+ async (msg, send, done) => {
475
+ try {
476
+ await createdPromise;
477
+ } catch {
478
+ done(new Error("Node failed to initialize"));
479
+ return;
480
+ }
481
+ try {
482
+ nodeRedNode.log("Calling input");
483
+ await Promise.resolve(this.#input(msg, send));
484
+ this.#sendToPort("complete", {
485
+ ...msg,
486
+ complete: {
487
+ source: this.#nodeSource()
488
+ }
489
+ });
490
+ done();
491
+ nodeRedNode.log("Input processed");
492
+ } catch (error) {
493
+ const errorMsg = error instanceof Error ? error.message : "Unknown error during input handling";
494
+ this.#sendToPort("error", {
495
+ ...msg,
496
+ error: {
497
+ message: errorMsg,
498
+ source: this.#nodeSource()
499
+ }
500
+ });
501
+ if (error instanceof Error) {
502
+ nodeRedNode.error(
503
+ "Error while processing input: " + error.message,
504
+ msg
505
+ );
506
+ done(error);
507
+ } else {
508
+ nodeRedNode.error(
509
+ "Unknown error occurred during input handling",
510
+ msg
511
+ );
512
+ done(new Error(errorMsg));
513
+ }
514
+ }
515
+ }
516
+ );
517
+ }
566
518
  input(msg) {
567
519
  }
568
- // NOTE: used by the registered function. Had to be a different one to avoid calling the parent's input again
569
- /** @internal */
570
- async _input(msg, send) {
520
+ async #input(msg, send) {
571
521
  const NodeClass = this.constructor;
572
522
  const shouldValidateInput = this.config.validateInput ?? NodeClass.validateInput;
573
523
  if (shouldValidateInput && NodeClass.inputSchema) {
574
524
  this.log("Validating input");
575
- validator.validate(msg, NodeClass.inputSchema, {
525
+ this.RED.validator.validate(msg, NodeClass.inputSchema, {
576
526
  cacheKey: NodeClass.inputSchema.$id || `${NodeClass.type}:input-schema`,
577
527
  throwOnError: true
578
528
  });
579
529
  this.log("Input is valid");
580
530
  }
581
- this._send = send;
531
+ this.#send = send;
582
532
  try {
583
533
  await Promise.resolve(this.input(msg));
584
534
  } finally {
585
- this._send = void 0;
535
+ this.#send = void 0;
586
536
  }
587
537
  }
588
538
  send(msg) {
@@ -590,78 +540,108 @@ var IONode = class extends Node {
590
540
  const shouldValidateOutput = this.config.validateOutput ?? NodeClass.validateOutput;
591
541
  if (shouldValidateOutput && NodeClass.outputsSchema) {
592
542
  this.log("Validating output");
593
- const schemas = NodeClass.outputsSchema;
594
- if (Array.isArray(schemas)) {
543
+ const rawSchema = NodeClass.outputsSchema;
544
+ if (Array.isArray(rawSchema)) {
595
545
  const msgs = msg;
596
- for (let i = 0; i < schemas.length; i++) {
546
+ for (let i = 0; i < rawSchema.length; i++) {
597
547
  if (msgs[i] == null) continue;
598
- validator.validate(msgs[i], schemas[i], {
599
- cacheKey: schemas[i].$id || `${NodeClass.type}:output-schema:${i}`,
548
+ this.RED.validator.validate(msgs[i], rawSchema[i], {
549
+ cacheKey: rawSchema[i].$id || `${NodeClass.type}:output-schema:${i}`,
600
550
  throwOnError: true
601
551
  });
602
552
  }
603
- } else if (Array.isArray(msg)) {
604
- for (let i = 0; i < msg.length; i++) {
605
- if (msg[i] == null) continue;
606
- validator.validate(msg[i], schemas, {
607
- cacheKey: schemas.$id || `${NodeClass.type}:output-schema`,
553
+ } else if (isSchemaLike(rawSchema)) {
554
+ if (Array.isArray(msg)) {
555
+ const msgs = msg;
556
+ for (let i = 0; i < msgs.length; i++) {
557
+ if (msgs[i] == null) continue;
558
+ this.RED.validator.validate(msgs[i], rawSchema, {
559
+ cacheKey: rawSchema.$id || `${NodeClass.type}:output-schema`,
560
+ throwOnError: true
561
+ });
562
+ }
563
+ } else {
564
+ this.RED.validator.validate(msg, rawSchema, {
565
+ cacheKey: rawSchema.$id || `${NodeClass.type}:output-schema`,
608
566
  throwOnError: true
609
567
  });
610
568
  }
611
569
  } else {
612
- validator.validate(msg, schemas, {
613
- cacheKey: schemas.$id || `${NodeClass.type}:output-schema`,
614
- throwOnError: true
615
- });
570
+ const schemaArray = Object.values(rawSchema);
571
+ const msgs = msg;
572
+ for (let i = 0; i < schemaArray.length; i++) {
573
+ if (msgs[i] == null) continue;
574
+ this.RED.validator.validate(msgs[i], schemaArray[i], {
575
+ cacheKey: schemaArray[i].$id || `${NodeClass.type}:output-schema:${i}`,
576
+ throwOnError: true
577
+ });
578
+ }
616
579
  }
617
580
  this.log("Output is valid");
618
581
  }
619
- if (this._send) {
620
- this._send(msg);
582
+ if (this.#send) {
583
+ this.#send(msg);
621
584
  } else {
622
585
  this.node.send(msg);
623
586
  }
624
587
  }
625
- // --- Emit port management ---
626
- /** @internal */
627
- get _baseOutputs() {
588
+ // --- Built-in port management ---
589
+ get baseOutputs() {
628
590
  return this.constructor.outputs ?? 0;
629
591
  }
630
- /** @internal */
631
- get _totalOutputs() {
632
- let count = this._baseOutputs;
633
- if (this.config.emitError) count++;
634
- if (this.config.emitComplete) count++;
635
- if (this.config.emitStatus) count++;
592
+ get totalOutputs() {
593
+ const config = this.config;
594
+ let count = this.baseOutputs;
595
+ if (config.errorPort) count++;
596
+ if (config.completePort) count++;
597
+ if (config.statusPort) count++;
636
598
  return count;
637
599
  }
638
- /** @internal */
639
- _sendToPort(portIndex, msg) {
640
- const out = Array(this._totalOutputs).fill(null);
600
+ /**
601
+ * Send a message to a specific output port by index or name.
602
+ * Built-in ports: `"error"`, `"complete"`, `"status"` — resolved automatically
603
+ * based on the node's built-in port configuration.
604
+ * Custom named ports are resolved from `outputsSchema` when it is a record.
605
+ * Numeric indices refer to the base output ports (0-based).
606
+ */
607
+ sendToPort(port, msg) {
608
+ this.#sendToPort(port, msg);
609
+ }
610
+ #sendToPort(port, msg) {
611
+ let portIndex;
612
+ if (typeof port === "number") {
613
+ portIndex = port;
614
+ } else if (port === "error" || port === "complete" || port === "status") {
615
+ portIndex = this.#getBuiltinPortIndex(port);
616
+ if (portIndex === null) return;
617
+ } else {
618
+ portIndex = this.#getNamedPortIndex(port);
619
+ if (portIndex === null) return;
620
+ }
621
+ const out = Array(this.totalOutputs).fill(null);
641
622
  out[portIndex] = msg;
642
623
  this.node.send(out);
643
624
  }
644
- /** @internal */
645
- _getErrorPortIndex() {
646
- if (!this.config.emitError) return null;
647
- return this._baseOutputs;
648
- }
649
- /** @internal */
650
- _getCompletePortIndex() {
651
- if (!this.config.emitComplete) return null;
652
- let idx = this._baseOutputs;
653
- if (this.config.emitError) idx++;
654
- return idx;
655
- }
656
- /** @internal */
657
- _getStatusPortIndex() {
658
- if (!this.config.emitStatus) return null;
659
- let idx = this._baseOutputs;
660
- if (this.config.emitError) idx++;
661
- if (this.config.emitComplete) idx++;
662
- return idx;
663
- }
664
- _nodeSource() {
625
+ #getNamedPortIndex(name) {
626
+ const schema = this.constructor.outputsSchema;
627
+ if (!schema || Array.isArray(schema) || isSchemaLike(schema)) return null;
628
+ const idx = Object.keys(schema).indexOf(name);
629
+ return idx === -1 ? null : idx;
630
+ }
631
+ #getBuiltinPortIndex(name) {
632
+ const config = this.config;
633
+ if (name === "error") {
634
+ return config.errorPort ? this.baseOutputs : null;
635
+ }
636
+ let idx = this.baseOutputs;
637
+ if (config.errorPort) idx++;
638
+ if (name === "complete") {
639
+ return config.completePort ? idx : null;
640
+ }
641
+ if (config.completePort) idx++;
642
+ return config.statusPort ? idx : null;
643
+ }
644
+ #nodeSource() {
665
645
  return {
666
646
  id: this.id,
667
647
  type: this.constructor.type,
@@ -670,23 +650,19 @@ var IONode = class extends Node {
670
650
  }
671
651
  status(status) {
672
652
  this.node.status(status);
673
- const portIdx = this._getStatusPortIndex();
674
- if (portIdx !== null) {
675
- this._sendToPort(portIdx, {
676
- status,
677
- source: this._nodeSource()
678
- });
679
- }
653
+ this.#sendToPort("status", {
654
+ status,
655
+ source: this.#nodeSource()
656
+ });
680
657
  }
681
658
  error(message, msg) {
682
659
  super.error(message, msg);
683
- const portIdx = this._getErrorPortIndex();
684
- if (portIdx !== null && msg) {
685
- this._sendToPort(portIdx, {
660
+ if (msg) {
661
+ this.#sendToPort("error", {
686
662
  ...msg,
687
663
  error: {
688
664
  message,
689
- source: this._nodeSource()
665
+ source: this.#nodeSource()
690
666
  }
691
667
  });
692
668
  }
@@ -718,12 +694,6 @@ var IONode = class extends Node {
718
694
  var ConfigNode = class extends Node {
719
695
  static category = "config";
720
696
  context;
721
- // NOTE: used by the registered function. Had to be a different one to avoid calling the parent's input again
722
- /** @internal */
723
- static _registered(RED) {
724
- this.validateSettings(RED);
725
- return this.registered?.(RED);
726
- }
727
697
  constructor(RED, node, config, credentials) {
728
698
  super(RED, node, config, credentials);
729
699
  const context = node.context();
@@ -765,8 +735,7 @@ function defineIONode(def) {
765
735
  static outputsSchema = def.outputsSchema;
766
736
  static validateInput = def.validateInput ?? false;
767
737
  static validateOutput = def.validateOutput ?? false;
768
- static _registered(RED) {
769
- this.validateSettings(RED);
738
+ static registered(RED) {
770
739
  return def.registered?.(RED);
771
740
  }
772
741
  async input(msg) {
@@ -794,8 +763,7 @@ function defineConfigNode(def) {
794
763
  static configSchema = def.configSchema;
795
764
  static credentialsSchema = def.credentialsSchema;
796
765
  static settingsSchema = def.settingsSchema;
797
- static _registered(RED) {
798
- this.validateSettings(RED);
766
+ static registered(RED) {
799
767
  return def.registered?.(RED);
800
768
  }
801
769
  async created() {
@@ -815,29 +783,208 @@ function defineConfigNode(def) {
815
783
  return NodeClass;
816
784
  }
817
785
 
818
- // src/core/server/api/nrg-assets.ts
786
+ // src/core/validator.ts
787
+ var import_ajv = __toESM(require("ajv"), 1);
788
+ var import_ajv_formats = __toESM(require("ajv-formats"), 1);
789
+ var import_ajv_errors = __toESM(require("ajv-errors"), 1);
790
+ var Validator = class {
791
+ ajv;
792
+ constructor(options) {
793
+ const { customKeywords, customFormats, ...ajvOptions } = options || {};
794
+ this.ajv = new import_ajv.default({
795
+ allErrors: true,
796
+ code: {
797
+ source: false
798
+ },
799
+ coerceTypes: true,
800
+ removeAdditional: false,
801
+ strict: false,
802
+ strictSchema: false,
803
+ useDefaults: true,
804
+ validateFormats: true,
805
+ // NOTE: typebox handles validation via typescript
806
+ // NOTE: if true, types that are not serializable JSON, like Function, would not work
807
+ validateSchema: false,
808
+ verbose: true,
809
+ ...ajvOptions
810
+ });
811
+ (0, import_ajv_formats.default)(this.ajv);
812
+ (0, import_ajv_errors.default)(this.ajv);
813
+ this.addCustomKeywords(customKeywords || []);
814
+ this.addCustomFormats(customFormats || {});
815
+ }
816
+ /**
817
+ * Add custom keywords to the validator
818
+ */
819
+ addCustomKeywords(keywords) {
820
+ if (!keywords) return;
821
+ keywords.forEach((keyword) => {
822
+ this.ajv.addKeyword(keyword);
823
+ });
824
+ }
825
+ /**
826
+ * Add custom formats to the validator
827
+ */
828
+ addCustomFormats(formats) {
829
+ if (!formats) return;
830
+ Object.entries(formats).forEach(([name, validator]) => {
831
+ if (validator instanceof RegExp) {
832
+ this.ajv.addFormat(name, validator);
833
+ } else {
834
+ this.ajv.addFormat(name, { validate: validator });
835
+ }
836
+ });
837
+ }
838
+ /**
839
+ * Create a validator function with caching
840
+ * @param schema - JSON Schema to validate against
841
+ * @param cacheKey - Optional cache key for reusing validators
842
+ */
843
+ createValidator(schema, cacheKey) {
844
+ if (cacheKey && !schema.$id) {
845
+ schema.$id = cacheKey;
846
+ }
847
+ if (schema.$id) {
848
+ const cached = this.ajv.getSchema(schema.$id);
849
+ if (cached) return cached;
850
+ }
851
+ const validator = this.ajv.compile(schema);
852
+ return validator;
853
+ }
854
+ /**
855
+ * Validate data against a schema and return a structured result
856
+ */
857
+ validate(data, schema, options) {
858
+ const validator = this.createValidator(schema, options?.cacheKey);
859
+ const valid = validator(data);
860
+ if (!valid) {
861
+ const errorMessage = this.formatErrors(validator.errors);
862
+ if (options?.throwOnError) {
863
+ throw new ValidationError(errorMessage, validator.errors || []);
864
+ }
865
+ return {
866
+ valid: false,
867
+ errors: validator.errors || void 0,
868
+ errorMessage
869
+ };
870
+ }
871
+ return {
872
+ valid: true,
873
+ data
874
+ };
875
+ }
876
+ /**
877
+ * Format errors into a human-readable string
878
+ */
879
+ formatErrors(errors, options) {
880
+ if (!errors || errors.length === 0) {
881
+ return "No errors";
882
+ }
883
+ return this.ajv.errorsText(errors, {
884
+ separator: "; ",
885
+ dataVar: "data",
886
+ ...options
887
+ });
888
+ }
889
+ /**
890
+ * Get detailed error information
891
+ */
892
+ getDetailedErrors(errors) {
893
+ if (!errors || errors.length === 0) return [];
894
+ return errors.map((error) => ({
895
+ field: error.instancePath || "/",
896
+ message: error.message || "Validation failed",
897
+ keyword: error.keyword,
898
+ params: error.params,
899
+ schemaPath: error.schemaPath
900
+ }));
901
+ }
902
+ /**
903
+ * Add a schema to the validator for reference
904
+ */
905
+ addSchema(schema, key) {
906
+ this.ajv.addSchema(schema, key);
907
+ return this;
908
+ }
909
+ /**
910
+ * Remove a schema from the validator
911
+ */
912
+ removeSchema(key) {
913
+ this.ajv.removeSchema(key);
914
+ return this;
915
+ }
916
+ };
917
+ var ValidationError = class _ValidationError extends Error {
918
+ constructor(message, errors) {
919
+ super(message);
920
+ this.errors = errors;
921
+ this.name = "ValidationError";
922
+ Object.setPrototypeOf(this, _ValidationError.prototype);
923
+ }
924
+ };
925
+
926
+ // src/core/server/validation.ts
927
+ function initValidator(RED) {
928
+ const nrg = {
929
+ validator: new Validator({
930
+ customKeywords: [
931
+ {
932
+ keyword: "x-nrg-skip-validation",
933
+ schemaType: "boolean",
934
+ valid: true
935
+ },
936
+ {
937
+ keyword: "x-nrg-node-type",
938
+ type: "string",
939
+ validate: (schemaValue, dataValue) => {
940
+ if (!dataValue) return true;
941
+ const node = RED.nodes.getNode(dataValue);
942
+ return node?.type === schemaValue;
943
+ }
944
+ }
945
+ ],
946
+ customFormats: {
947
+ "node-id": /^[a-zA-Z0-9-_]+$/,
948
+ "flow-id": /^[a-f0-9]{16}$/,
949
+ "topic-path": (data) => /^[a-zA-Z0-9/_-]+$/.test(data)
950
+ }
951
+ })
952
+ };
953
+ Object.defineProperty(RED, "_nrg", {
954
+ value: nrg,
955
+ writable: false,
956
+ enumerable: false,
957
+ configurable: false
958
+ });
959
+ Object.defineProperty(RED, "validator", {
960
+ get: () => nrg.validator,
961
+ enumerable: false,
962
+ configurable: false
963
+ });
964
+ }
965
+
966
+ // src/core/server/api/assets.ts
819
967
  var import_path = __toESM(require("path"), 1);
820
968
  var import_fs = __toESM(require("fs"), 1);
821
- var RESOURCES_DIR = import_path.default.resolve(__dirname, "./resources");
822
- var ALLOWED_FILES = /* @__PURE__ */ new Set([
823
- "nrg-client.js",
824
- "vue.esm-browser.prod.js",
825
- "vue.esm-browser.js"
826
- ]);
827
- var handleNrgAsset = (req, res, next) => {
828
- let fileName = req.params[0];
829
- if (fileName === "vue.esm-browser.prod.js" && process.env.NODE_ENV !== "production") {
830
- fileName = "vue.esm-browser.js";
831
- }
832
- if (!ALLOWED_FILES.has(fileName)) return next();
833
- const filePath = import_path.default.join(RESOURCES_DIR, fileName);
834
- if (!import_fs.default.existsSync(filePath)) return next();
835
- res.setHeader("Content-Type", "application/javascript");
836
- import_fs.default.createReadStream(filePath).pipe(res);
837
- };
838
- function initNrgAssetsRoute(router) {
839
- if (!import_fs.default.existsSync(RESOURCES_DIR)) return;
840
- router.get("/nrg/assets/*", handleNrgAsset);
969
+ var import_module = require("module");
970
+ function serveFile(filePath) {
971
+ return (_req, res, next) => {
972
+ if (!import_fs.default.existsSync(filePath)) return next();
973
+ res.setHeader("Content-Type", "application/javascript");
974
+ import_fs.default.createReadStream(filePath).pipe(res);
975
+ };
976
+ }
977
+ function initAssetsRoutes(router) {
978
+ const resourcesDir = import_path.default.resolve(__dirname, "./resources");
979
+ if (!import_fs.default.existsSync(resourcesDir)) return;
980
+ const _require = (0, import_module.createRequire)(import_path.default.join(__dirname, "package.json"));
981
+ const vueFile = process.env.NODE_ENV !== "production" ? _require.resolve("vue/dist/vue.esm-browser.js") : _require.resolve("vue/dist/vue.esm-browser.prod.js");
982
+ router.get(
983
+ "/nrg/assets/nrg-client.js",
984
+ serveFile(import_path.default.join(resourcesDir, "nrg-client.js"))
985
+ );
986
+ router.get("/nrg/assets/vue.esm-browser.prod.js", serveFile(vueFile));
987
+ router.get("/nrg/assets/vue.esm-browser.js", serveFile(vueFile));
841
988
  }
842
989
 
843
990
  // src/core/server/api/routes.ts
@@ -845,7 +992,42 @@ var _initialized = false;
845
992
  function initRoutes(RED) {
846
993
  if (_initialized) return;
847
994
  _initialized = true;
848
- initNrgAssetsRoute(RED.httpAdmin);
995
+ initAssetsRoutes(RED.httpAdmin);
996
+ }
997
+
998
+ // src/core/server/registration.ts
999
+ async function registerType(RED, NodeClass) {
1000
+ RED.log.debug(`Registering Type: ${NodeClass.type}`);
1001
+ if (!(NodeClass.prototype instanceof Node)) {
1002
+ throw new NrgError(
1003
+ `${NodeClass.name} must extend IONode or ConfigNode classes`
1004
+ );
1005
+ }
1006
+ if (!NodeClass.type) {
1007
+ throw new NrgError("type must be provided when registering the node");
1008
+ }
1009
+ await NodeClass.register(RED);
1010
+ RED.log.debug(`Type registered: ${NodeClass.type}`);
1011
+ }
1012
+ function registerTypes(nodes) {
1013
+ const fn = Object.assign(
1014
+ async function(RED) {
1015
+ initValidator(RED);
1016
+ initRoutes(RED);
1017
+ try {
1018
+ RED.log.info("Registering node types in series");
1019
+ for (const NodeClass of nodes) {
1020
+ await registerType(RED, NodeClass);
1021
+ }
1022
+ RED.log.info("All node types registered in series");
1023
+ } catch (error) {
1024
+ RED.log.error("Error registering node types:", error);
1025
+ throw error;
1026
+ }
1027
+ },
1028
+ { nodes }
1029
+ );
1030
+ return fn;
849
1031
  }
850
1032
 
851
1033
  // src/core/constants.ts
@@ -867,17 +1049,17 @@ var TYPED_INPUT_TYPES = [
867
1049
  ];
868
1050
 
869
1051
  // src/core/server/schemas/type.ts
870
- var import_typebox = require("@sinclair/typebox");
1052
+ var import_typebox2 = require("@sinclair/typebox");
871
1053
  var import_rules = require("ajv/dist/compile/rules");
872
1054
  function NodeRef(nodeClass, options) {
873
1055
  return {
874
- ...import_typebox.Type.String({
1056
+ ...import_typebox2.Type.String({
875
1057
  description: options?.description || `Reference to ${nodeClass.type}`,
876
1058
  format: "node-id"
877
1059
  }),
878
1060
  "x-nrg-node-type": nodeClass.type,
879
1061
  ...options,
880
- [import_typebox.Kind]: "NodeRef"
1062
+ [import_typebox2.Kind]: "NodeRef"
881
1063
  };
882
1064
  }
883
1065
  function TypedInput2(options) {
@@ -885,10 +1067,10 @@ function TypedInput2(options) {
885
1067
  ...TypedInputSchema,
886
1068
  "x-nrg-typed-input": true,
887
1069
  ...options,
888
- [import_typebox.Kind]: "TypedInput"
1070
+ [import_typebox2.Kind]: "TypedInput"
889
1071
  };
890
1072
  }
891
- var SchemaType = Object.assign({}, import_typebox.Type, {
1073
+ var SchemaType = Object.assign({}, import_typebox2.Type, {
892
1074
  NodeRef,
893
1075
  TypedInput: TypedInput2
894
1076
  });
@@ -980,145 +1162,59 @@ var TypedInputSchema = SchemaType.Object(
980
1162
  }
981
1163
  }
982
1164
  );
1165
+ var NodeSourceSchema = SchemaType.Object({
1166
+ id: SchemaType.String(),
1167
+ type: SchemaType.String(),
1168
+ name: SchemaType.String()
1169
+ });
1170
+ var ErrorPortSchema = SchemaType.Object({
1171
+ error: SchemaType.Object({
1172
+ message: SchemaType.String(),
1173
+ source: NodeSourceSchema
1174
+ })
1175
+ });
1176
+ var CompletePortSchema = SchemaType.Object({
1177
+ complete: SchemaType.Object({
1178
+ source: NodeSourceSchema
1179
+ })
1180
+ });
1181
+ var StatusPortSchema = SchemaType.Object({
1182
+ status: SchemaType.Object({
1183
+ fill: SchemaType.Optional(
1184
+ SchemaType.Union([
1185
+ SchemaType.Literal("red"),
1186
+ SchemaType.Literal("green")
1187
+ ])
1188
+ ),
1189
+ shape: SchemaType.Optional(
1190
+ SchemaType.Union([
1191
+ SchemaType.Literal("dot"),
1192
+ SchemaType.Literal("string")
1193
+ ])
1194
+ ),
1195
+ text: SchemaType.Optional(SchemaType.String())
1196
+ }),
1197
+ source: NodeSourceSchema
1198
+ });
983
1199
 
984
1200
  // src/core/server/index.ts
985
- async function registerType(RED, NodeClass) {
986
- const NC = NodeClass;
987
- RED.log.debug(`Registering Type: ${NC.type}`);
988
- if (!(NC.prototype instanceof Node)) {
989
- throw new NrgError(`${NC.name} must extend IONode or ConfigNode classes`);
990
- }
991
- if (!NC.type) {
992
- throw new NrgError("type must be provided when registering the node");
993
- }
994
- if (NC.color && !/^#[0-9A-Fa-f]{6}$/.test(NC.color)) {
995
- throw new NrgError(
996
- `Invalid color for ${NodeClass.type}: ${NC.color} color must be in hex format`
997
- );
998
- }
999
- RED.nodes.registerType(
1000
- NC.type,
1001
- function(config) {
1002
- RED.nodes.createNode(this, config);
1003
- const node = new NC(RED, this, config, this.credentials);
1004
- Object.defineProperty(this, "_node", {
1005
- value: node,
1006
- writable: false,
1007
- configurable: false,
1008
- enumerable: false
1009
- });
1010
- const createdPromise = Promise.resolve(node.created?.()).catch(
1011
- (error) => {
1012
- this.error("Error during created hook: " + error.message);
1013
- throw error;
1014
- }
1015
- );
1016
- this.on(
1017
- "input",
1018
- async (msg, send, done) => {
1019
- try {
1020
- await createdPromise;
1021
- } catch {
1022
- done(new Error("Node failed to initialize"));
1023
- return;
1024
- }
1025
- try {
1026
- this.log("Calling input");
1027
- await Promise.resolve(node._input(msg, send));
1028
- const completeIdx = node._getCompletePortIndex();
1029
- if (completeIdx !== null) {
1030
- node._sendToPort(completeIdx, {
1031
- ...msg,
1032
- complete: {
1033
- source: { id: node.id, type: NC.type, name: node.name }
1034
- }
1035
- });
1036
- }
1037
- done();
1038
- this.log("Input processed");
1039
- } catch (error) {
1040
- const errorMsg = error instanceof Error ? error.message : "Unknown error during input handling";
1041
- const errorIdx = node._getErrorPortIndex();
1042
- if (errorIdx !== null) {
1043
- node._sendToPort(errorIdx, {
1044
- ...msg,
1045
- error: {
1046
- message: errorMsg,
1047
- source: { id: node.id, type: NC.type, name: node.name }
1048
- }
1049
- });
1050
- }
1051
- if (error instanceof Error) {
1052
- this.error("Error while processing input: " + error.message, msg);
1053
- done(error);
1054
- } else {
1055
- this.error("Unknown error occurred during input handling", msg);
1056
- done(new Error(errorMsg));
1057
- }
1058
- }
1059
- }
1060
- );
1061
- this.on(
1062
- "close",
1063
- async (removed, done) => {
1064
- try {
1065
- this.log("Calling closed");
1066
- await Promise.resolve(node._closed(removed));
1067
- this.log("Node was closed");
1068
- done();
1069
- } catch (error) {
1070
- if (error instanceof Error) {
1071
- this.error("Error while closing node: " + error.message);
1072
- done(error);
1073
- } else {
1074
- this.error("Unknown error occurred while closing node");
1075
- done(new Error("Unknown error occurred while closing node"));
1076
- }
1077
- }
1078
- }
1079
- );
1080
- },
1081
- {
1082
- credentials: NC.credentialsSchema ? getCredentialsFromSchema(NC.credentialsSchema) : {},
1083
- settings: NC._settings?.()
1084
- }
1085
- );
1086
- await Promise.resolve(NC._registered?.(RED));
1087
- RED.log.debug(`Type registered: ${NC.type}`);
1088
- }
1089
- function registerTypes(nodes) {
1090
- const fn = async function(RED) {
1091
- initValidator(RED);
1092
- initRoutes(RED);
1093
- try {
1094
- RED.log.info("Registering node types in series");
1095
- for (const NodeClass of nodes) {
1096
- await registerType(RED, NodeClass);
1097
- }
1098
- RED.log.info("All node types registered in series");
1099
- } catch (error) {
1100
- RED.log.error("Error registering node types:", error);
1101
- throw error;
1102
- }
1103
- };
1104
- fn.nodes = nodes;
1105
- return fn;
1106
- }
1107
1201
  function defineModule(definition) {
1108
1202
  return definition;
1109
1203
  }
1110
1204
  // Annotate the CommonJS export names for ESM import in node:
1111
1205
  0 && (module.exports = {
1206
+ CompletePortSchema,
1112
1207
  ConfigNode,
1208
+ ErrorPortSchema,
1113
1209
  IONode,
1114
1210
  Node,
1115
1211
  NrgError,
1116
1212
  SchemaType,
1213
+ StatusPortSchema,
1117
1214
  defineConfigNode,
1118
1215
  defineIONode,
1119
1216
  defineModule,
1120
1217
  defineSchema,
1121
- initValidator,
1122
1218
  registerType,
1123
1219
  registerTypes
1124
1220
  });