@bonsae/nrg 0.13.0 → 0.14.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/README.md +3 -40
- package/package.json +3 -2
- package/server/index.cjs +248 -237
- package/server/resources/nrg-client.js +514 -500
- package/test/index.js +131 -67
- package/types/server.d.ts +290 -1847
- package/types/shims/components.d.ts +13 -10
- package/types/shims/form/components/node-red-config-input.vue.d.ts +112 -0
- package/types/shims/form/components/node-red-editor-input.vue.d.ts +105 -0
- package/types/shims/form/components/node-red-input-label.vue.d.ts +34 -0
- package/types/shims/form/components/node-red-input.vue.d.ts +111 -0
- package/types/shims/form/components/node-red-json-schema-form.vue.d.ts +651 -0
- package/types/shims/form/components/node-red-select-input.vue.d.ts +117 -0
- package/types/shims/form/components/node-red-toggle.vue.d.ts +36 -0
- package/types/shims/form/components/node-red-typed-input.vue.d.ts +106 -0
- package/types/shims/globals.d.ts +1 -1
- package/types/shims/schema-options.d.ts +11 -0
- package/types/shims/typebox.d.ts +10 -0
- package/types/test.d.ts +391 -21
- package/types/vite.d.ts +164 -7
- package/vite/index.js +223 -31
- package/server/resources/vue.esm-browser.js +0 -18708
- package/server/resources/vue.esm-browser.prod.js +0 -13
package/README.md
CHANGED
|
@@ -17,10 +17,10 @@ Build Node-RED nodes with Vue 3, TypeScript, and JSON Schema validation.
|
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
pnpm add @bonsae/nrg
|
|
20
|
-
pnpm add -D vite
|
|
20
|
+
pnpm add -D vite
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
> `vite`
|
|
23
|
+
> `vite` is a dev dependency because it is only needed at build time. Vue is included as a dependency of nrg and served automatically at runtime.
|
|
24
24
|
|
|
25
25
|
## Package Exports
|
|
26
26
|
|
|
@@ -37,7 +37,7 @@ pnpm add -D vite vue
|
|
|
37
37
|
```bash
|
|
38
38
|
# In your Node-RED package project
|
|
39
39
|
pnpm add @bonsae/nrg
|
|
40
|
-
pnpm add -D vite
|
|
40
|
+
pnpm add -D vite
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
**vite.config.ts**
|
|
@@ -135,43 +135,6 @@ export default defineModule({
|
|
|
135
135
|
|
|
136
136
|
See the [consumer template](https://github.com/AllanOricil/node-red-vue-template) for a complete example.
|
|
137
137
|
|
|
138
|
-
## Project Structure
|
|
139
|
-
|
|
140
|
-
```
|
|
141
|
-
src/
|
|
142
|
-
├── core/ # Runtime framework
|
|
143
|
-
│ ├── client/ # Vue 3 editor components
|
|
144
|
-
│ │ ├── app.vue # Root form wrapper (validation, toggles)
|
|
145
|
-
│ │ ├── components/ # Reusable form inputs
|
|
146
|
-
│ │ │ ├── node-red-input.vue
|
|
147
|
-
│ │ │ ├── node-red-typed-input.vue
|
|
148
|
-
│ │ │ ├── node-red-config-input.vue
|
|
149
|
-
│ │ │ ├── node-red-select-input.vue
|
|
150
|
-
│ │ │ ├── node-red-editor-input.vue
|
|
151
|
-
│ │ │ └── node-red-json-schema-form.vue
|
|
152
|
-
│ │ └── index.ts # registerType, defineNode
|
|
153
|
-
│ ├── server/ # Node.js server runtime
|
|
154
|
-
│ │ ├── nodes/ # Node, IONode, ConfigNode classes
|
|
155
|
-
│ │ ├── schemas/ # TypeBox schema system
|
|
156
|
-
│ │ ├── types/ # RED, context store types
|
|
157
|
-
│ │ └── index.ts # registerTypes, exports
|
|
158
|
-
│ ├── constants.ts
|
|
159
|
-
│ └── validator.ts # AJV-based validation
|
|
160
|
-
├── test/ # Test utilities for consumers
|
|
161
|
-
│ ├── index.ts # createNode, receive, close, reset
|
|
162
|
-
│ └── mocks.ts # RED and Node-RED node mocks
|
|
163
|
-
├── vite/ # Build tooling
|
|
164
|
-
│ ├── plugin.ts # Vite plugin factory
|
|
165
|
-
│ ├── plugins/ # Dev server, build orchestration
|
|
166
|
-
│ ├── server/ # Server build (CJS/ESM + bridge)
|
|
167
|
-
│ ├── client/ # Client build (Vue + auto-wiring)
|
|
168
|
-
│ └── index.ts # nodeRed(), defineRuntimeSettings()
|
|
169
|
-
└── tsconfig/ # Shared configs for consumers
|
|
170
|
-
├── base.json
|
|
171
|
-
├── client.json
|
|
172
|
-
└── server.json
|
|
173
|
-
```
|
|
174
|
-
|
|
175
138
|
## Testing
|
|
176
139
|
|
|
177
140
|
Test your nodes' server-side logic with `@bonsae/nrg/test`:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bonsae/nrg",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "NRG framework — build Node-RED nodes with Vue 3, TypeScript, and JSON Schema",
|
|
5
5
|
"author": "Allan Oricil <allanoricil@duck.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
"tree-kill": "^1.2.2",
|
|
63
63
|
"typescript": "^5.8.3",
|
|
64
64
|
"vite-plugin-dts": "^4.5.4",
|
|
65
|
-
"vite-plugin-static-copy": "^3.1.0"
|
|
65
|
+
"vite-plugin-static-copy": "^3.1.0",
|
|
66
|
+
"vue": "^3.5.14"
|
|
66
67
|
}
|
|
67
68
|
}
|
package/server/index.cjs
CHANGED
|
@@ -39,7 +39,6 @@ __export(index_exports, {
|
|
|
39
39
|
defineIONode: () => defineIONode,
|
|
40
40
|
defineModule: () => defineModule,
|
|
41
41
|
defineSchema: () => defineSchema,
|
|
42
|
-
initValidator: () => initValidator,
|
|
43
42
|
registerType: () => registerType,
|
|
44
43
|
registerTypes: () => registerTypes
|
|
45
44
|
});
|
|
@@ -60,174 +59,6 @@ function getCredentialsFromSchema(schema) {
|
|
|
60
59
|
return result;
|
|
61
60
|
}
|
|
62
61
|
|
|
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
|
-
}
|
|
230
|
-
|
|
231
62
|
// src/core/errors.ts
|
|
232
63
|
var NrgError = class _NrgError extends Error {
|
|
233
64
|
constructor(message) {
|
|
@@ -402,7 +233,7 @@ var Node = class {
|
|
|
402
233
|
}
|
|
403
234
|
}
|
|
404
235
|
}
|
|
405
|
-
validator.validate(settings, this.settingsSchema, {
|
|
236
|
+
RED.validator.validate(settings, this.settingsSchema, {
|
|
406
237
|
cacheKey: this.settingsSchema.$id || `${this.type}:settings`,
|
|
407
238
|
throwOnError: true
|
|
408
239
|
});
|
|
@@ -421,7 +252,7 @@ var Node = class {
|
|
|
421
252
|
const constructor = this.constructor;
|
|
422
253
|
if (constructor.configSchema) {
|
|
423
254
|
this.log("Validating configs");
|
|
424
|
-
const configResult = validator.validate(
|
|
255
|
+
const configResult = this.RED.validator.validate(
|
|
425
256
|
config,
|
|
426
257
|
constructor.configSchema,
|
|
427
258
|
{
|
|
@@ -443,7 +274,7 @@ var Node = class {
|
|
|
443
274
|
});
|
|
444
275
|
if (constructor.credentialsSchema && credentials) {
|
|
445
276
|
this.log("Validating credentials");
|
|
446
|
-
const credResult = validator.validate(
|
|
277
|
+
const credResult = this.RED.validator.validate(
|
|
447
278
|
credentials,
|
|
448
279
|
constructor.credentialsSchema,
|
|
449
280
|
{
|
|
@@ -572,7 +403,7 @@ var IONode = class extends Node {
|
|
|
572
403
|
const shouldValidateInput = this.config.validateInput ?? NodeClass.validateInput;
|
|
573
404
|
if (shouldValidateInput && NodeClass.inputSchema) {
|
|
574
405
|
this.log("Validating input");
|
|
575
|
-
validator.validate(msg, NodeClass.inputSchema, {
|
|
406
|
+
this.RED.validator.validate(msg, NodeClass.inputSchema, {
|
|
576
407
|
cacheKey: NodeClass.inputSchema.$id || `${NodeClass.type}:input-schema`,
|
|
577
408
|
throwOnError: true
|
|
578
409
|
});
|
|
@@ -595,7 +426,7 @@ var IONode = class extends Node {
|
|
|
595
426
|
const msgs = msg;
|
|
596
427
|
for (let i = 0; i < schemas.length; i++) {
|
|
597
428
|
if (msgs[i] == null) continue;
|
|
598
|
-
validator.validate(msgs[i], schemas[i], {
|
|
429
|
+
this.RED.validator.validate(msgs[i], schemas[i], {
|
|
599
430
|
cacheKey: schemas[i].$id || `${NodeClass.type}:output-schema:${i}`,
|
|
600
431
|
throwOnError: true
|
|
601
432
|
});
|
|
@@ -603,13 +434,13 @@ var IONode = class extends Node {
|
|
|
603
434
|
} else if (Array.isArray(msg)) {
|
|
604
435
|
for (let i = 0; i < msg.length; i++) {
|
|
605
436
|
if (msg[i] == null) continue;
|
|
606
|
-
validator.validate(msg[i], schemas, {
|
|
437
|
+
this.RED.validator.validate(msg[i], schemas, {
|
|
607
438
|
cacheKey: schemas.$id || `${NodeClass.type}:output-schema`,
|
|
608
439
|
throwOnError: true
|
|
609
440
|
});
|
|
610
441
|
}
|
|
611
442
|
} else {
|
|
612
|
-
validator.validate(msg, schemas, {
|
|
443
|
+
this.RED.validator.validate(msg, schemas, {
|
|
613
444
|
cacheKey: schemas.$id || `${NodeClass.type}:output-schema`,
|
|
614
445
|
throwOnError: true
|
|
615
446
|
});
|
|
@@ -635,31 +466,40 @@ var IONode = class extends Node {
|
|
|
635
466
|
if (this.config.emitStatus) count++;
|
|
636
467
|
return count;
|
|
637
468
|
}
|
|
469
|
+
/**
|
|
470
|
+
* Send a message to a specific output port by index or name.
|
|
471
|
+
* Named ports: `"error"`, `"complete"`, `"status"` — resolved automatically
|
|
472
|
+
* based on the node's emit port configuration.
|
|
473
|
+
* Numeric indices refer to the base output ports (0-based).
|
|
474
|
+
*/
|
|
475
|
+
sendToPort(port, msg) {
|
|
476
|
+
this._sendToPort(port, msg);
|
|
477
|
+
}
|
|
638
478
|
/** @internal */
|
|
639
|
-
_sendToPort(
|
|
479
|
+
_sendToPort(port, msg) {
|
|
480
|
+
let portIndex;
|
|
481
|
+
if (typeof port === "number") {
|
|
482
|
+
portIndex = port;
|
|
483
|
+
} else {
|
|
484
|
+
portIndex = this._getEmitPortIndex(port);
|
|
485
|
+
if (portIndex === null) return;
|
|
486
|
+
}
|
|
640
487
|
const out = Array(this._totalOutputs).fill(null);
|
|
641
488
|
out[portIndex] = msg;
|
|
642
489
|
this.node.send(out);
|
|
643
490
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
if (
|
|
647
|
-
|
|
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;
|
|
491
|
+
_getEmitPortIndex(name) {
|
|
492
|
+
const config = this.config;
|
|
493
|
+
if (name === "error") {
|
|
494
|
+
return config.emitError ? this._baseOutputs : null;
|
|
495
|
+
}
|
|
659
496
|
let idx = this._baseOutputs;
|
|
660
|
-
if (
|
|
661
|
-
if (
|
|
662
|
-
|
|
497
|
+
if (config.emitError) idx++;
|
|
498
|
+
if (name === "complete") {
|
|
499
|
+
return config.emitComplete ? idx : null;
|
|
500
|
+
}
|
|
501
|
+
if (config.emitComplete) idx++;
|
|
502
|
+
return config.emitStatus ? idx : null;
|
|
663
503
|
}
|
|
664
504
|
_nodeSource() {
|
|
665
505
|
return {
|
|
@@ -670,19 +510,15 @@ var IONode = class extends Node {
|
|
|
670
510
|
}
|
|
671
511
|
status(status) {
|
|
672
512
|
this.node.status(status);
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
this.
|
|
676
|
-
|
|
677
|
-
source: this._nodeSource()
|
|
678
|
-
});
|
|
679
|
-
}
|
|
513
|
+
this._sendToPort("status", {
|
|
514
|
+
status,
|
|
515
|
+
source: this._nodeSource()
|
|
516
|
+
});
|
|
680
517
|
}
|
|
681
518
|
error(message, msg) {
|
|
682
519
|
super.error(message, msg);
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
this._sendToPort(portIdx, {
|
|
520
|
+
if (msg) {
|
|
521
|
+
this._sendToPort("error", {
|
|
686
522
|
...msg,
|
|
687
523
|
error: {
|
|
688
524
|
message,
|
|
@@ -815,29 +651,208 @@ function defineConfigNode(def) {
|
|
|
815
651
|
return NodeClass;
|
|
816
652
|
}
|
|
817
653
|
|
|
818
|
-
// src/core/
|
|
654
|
+
// src/core/validator.ts
|
|
655
|
+
var import_ajv = __toESM(require("ajv"), 1);
|
|
656
|
+
var import_ajv_formats = __toESM(require("ajv-formats"), 1);
|
|
657
|
+
var import_ajv_errors = __toESM(require("ajv-errors"), 1);
|
|
658
|
+
var Validator = class {
|
|
659
|
+
ajv;
|
|
660
|
+
constructor(options) {
|
|
661
|
+
const { customKeywords, customFormats, ...ajvOptions } = options || {};
|
|
662
|
+
this.ajv = new import_ajv.default({
|
|
663
|
+
allErrors: true,
|
|
664
|
+
code: {
|
|
665
|
+
source: false
|
|
666
|
+
},
|
|
667
|
+
coerceTypes: true,
|
|
668
|
+
removeAdditional: false,
|
|
669
|
+
strict: false,
|
|
670
|
+
strictSchema: false,
|
|
671
|
+
useDefaults: true,
|
|
672
|
+
validateFormats: true,
|
|
673
|
+
// NOTE: typebox handles validation via typescript
|
|
674
|
+
// NOTE: if true, types that are not serializable JSON, like Function, would not work
|
|
675
|
+
validateSchema: false,
|
|
676
|
+
verbose: true,
|
|
677
|
+
...ajvOptions
|
|
678
|
+
});
|
|
679
|
+
(0, import_ajv_formats.default)(this.ajv);
|
|
680
|
+
(0, import_ajv_errors.default)(this.ajv);
|
|
681
|
+
this.addCustomKeywords(customKeywords || []);
|
|
682
|
+
this.addCustomFormats(customFormats || {});
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Add custom keywords to the validator
|
|
686
|
+
*/
|
|
687
|
+
addCustomKeywords(keywords) {
|
|
688
|
+
if (!keywords) return;
|
|
689
|
+
keywords.forEach((keyword) => {
|
|
690
|
+
this.ajv.addKeyword(keyword);
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Add custom formats to the validator
|
|
695
|
+
*/
|
|
696
|
+
addCustomFormats(formats) {
|
|
697
|
+
if (!formats) return;
|
|
698
|
+
Object.entries(formats).forEach(([name, validator]) => {
|
|
699
|
+
if (validator instanceof RegExp) {
|
|
700
|
+
this.ajv.addFormat(name, validator);
|
|
701
|
+
} else {
|
|
702
|
+
this.ajv.addFormat(name, { validate: validator });
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Create a validator function with caching
|
|
708
|
+
* @param schema - JSON Schema to validate against
|
|
709
|
+
* @param cacheKey - Optional cache key for reusing validators
|
|
710
|
+
*/
|
|
711
|
+
createValidator(schema, cacheKey) {
|
|
712
|
+
if (cacheKey && !schema.$id) {
|
|
713
|
+
schema.$id = cacheKey;
|
|
714
|
+
}
|
|
715
|
+
if (schema.$id) {
|
|
716
|
+
const cached = this.ajv.getSchema(schema.$id);
|
|
717
|
+
if (cached) return cached;
|
|
718
|
+
}
|
|
719
|
+
const validator = this.ajv.compile(schema);
|
|
720
|
+
return validator;
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Validate data against a schema and return a structured result
|
|
724
|
+
*/
|
|
725
|
+
validate(data, schema, options) {
|
|
726
|
+
const validator = this.createValidator(schema, options?.cacheKey);
|
|
727
|
+
const valid = validator(data);
|
|
728
|
+
if (!valid) {
|
|
729
|
+
const errorMessage = this.formatErrors(validator.errors);
|
|
730
|
+
if (options?.throwOnError) {
|
|
731
|
+
throw new ValidationError(errorMessage, validator.errors || []);
|
|
732
|
+
}
|
|
733
|
+
return {
|
|
734
|
+
valid: false,
|
|
735
|
+
errors: validator.errors || void 0,
|
|
736
|
+
errorMessage
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
return {
|
|
740
|
+
valid: true,
|
|
741
|
+
data
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Format errors into a human-readable string
|
|
746
|
+
*/
|
|
747
|
+
formatErrors(errors, options) {
|
|
748
|
+
if (!errors || errors.length === 0) {
|
|
749
|
+
return "No errors";
|
|
750
|
+
}
|
|
751
|
+
return this.ajv.errorsText(errors, {
|
|
752
|
+
separator: "; ",
|
|
753
|
+
dataVar: "data",
|
|
754
|
+
...options
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Get detailed error information
|
|
759
|
+
*/
|
|
760
|
+
getDetailedErrors(errors) {
|
|
761
|
+
if (!errors || errors.length === 0) return [];
|
|
762
|
+
return errors.map((error) => ({
|
|
763
|
+
field: error.instancePath || "/",
|
|
764
|
+
message: error.message || "Validation failed",
|
|
765
|
+
keyword: error.keyword,
|
|
766
|
+
params: error.params,
|
|
767
|
+
schemaPath: error.schemaPath
|
|
768
|
+
}));
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Add a schema to the validator for reference
|
|
772
|
+
*/
|
|
773
|
+
addSchema(schema, key) {
|
|
774
|
+
this.ajv.addSchema(schema, key);
|
|
775
|
+
return this;
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Remove a schema from the validator
|
|
779
|
+
*/
|
|
780
|
+
removeSchema(key) {
|
|
781
|
+
this.ajv.removeSchema(key);
|
|
782
|
+
return this;
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
var ValidationError = class _ValidationError extends Error {
|
|
786
|
+
constructor(message, errors) {
|
|
787
|
+
super(message);
|
|
788
|
+
this.errors = errors;
|
|
789
|
+
this.name = "ValidationError";
|
|
790
|
+
Object.setPrototypeOf(this, _ValidationError.prototype);
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
// src/core/server/validation.ts
|
|
795
|
+
function initValidator(RED) {
|
|
796
|
+
const nrg = {
|
|
797
|
+
validator: new Validator({
|
|
798
|
+
customKeywords: [
|
|
799
|
+
{
|
|
800
|
+
keyword: "x-nrg-skip-validation",
|
|
801
|
+
schemaType: "boolean",
|
|
802
|
+
valid: true
|
|
803
|
+
},
|
|
804
|
+
{
|
|
805
|
+
keyword: "x-nrg-node-type",
|
|
806
|
+
type: "string",
|
|
807
|
+
validate: (schemaValue, dataValue) => {
|
|
808
|
+
if (!dataValue) return true;
|
|
809
|
+
const node = RED.nodes.getNode(dataValue);
|
|
810
|
+
return node?.type === schemaValue;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
],
|
|
814
|
+
customFormats: {
|
|
815
|
+
"node-id": /^[a-zA-Z0-9-_]+$/,
|
|
816
|
+
"flow-id": /^[a-f0-9]{16}$/,
|
|
817
|
+
"topic-path": (data) => /^[a-zA-Z0-9/_-]+$/.test(data)
|
|
818
|
+
}
|
|
819
|
+
})
|
|
820
|
+
};
|
|
821
|
+
Object.defineProperty(RED, "_nrg", {
|
|
822
|
+
value: nrg,
|
|
823
|
+
writable: false,
|
|
824
|
+
enumerable: false,
|
|
825
|
+
configurable: false
|
|
826
|
+
});
|
|
827
|
+
Object.defineProperty(RED, "validator", {
|
|
828
|
+
get: () => nrg.validator,
|
|
829
|
+
enumerable: false,
|
|
830
|
+
configurable: false
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// src/core/server/api/assets.ts
|
|
819
835
|
var import_path = __toESM(require("path"), 1);
|
|
820
836
|
var import_fs = __toESM(require("fs"), 1);
|
|
821
|
-
var
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
const
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
router.get("/nrg/assets/*", handleNrgAsset);
|
|
837
|
+
var import_module = require("module");
|
|
838
|
+
function serveFile(filePath) {
|
|
839
|
+
return (_req, res, next) => {
|
|
840
|
+
if (!import_fs.default.existsSync(filePath)) return next();
|
|
841
|
+
res.setHeader("Content-Type", "application/javascript");
|
|
842
|
+
import_fs.default.createReadStream(filePath).pipe(res);
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
function initAssetsRoutes(router) {
|
|
846
|
+
const resourcesDir = import_path.default.resolve(__dirname, "./resources");
|
|
847
|
+
if (!import_fs.default.existsSync(resourcesDir)) return;
|
|
848
|
+
const _require = (0, import_module.createRequire)(import_path.default.join(__dirname, "package.json"));
|
|
849
|
+
const vueFile = process.env.NODE_ENV !== "production" ? _require.resolve("vue/dist/vue.esm-browser.js") : _require.resolve("vue/dist/vue.esm-browser.prod.js");
|
|
850
|
+
router.get(
|
|
851
|
+
"/nrg/assets/nrg-client.js",
|
|
852
|
+
serveFile(import_path.default.join(resourcesDir, "nrg-client.js"))
|
|
853
|
+
);
|
|
854
|
+
router.get("/nrg/assets/vue.esm-browser.prod.js", serveFile(vueFile));
|
|
855
|
+
router.get("/nrg/assets/vue.esm-browser.js", serveFile(vueFile));
|
|
841
856
|
}
|
|
842
857
|
|
|
843
858
|
// src/core/server/api/routes.ts
|
|
@@ -845,7 +860,7 @@ var _initialized = false;
|
|
|
845
860
|
function initRoutes(RED) {
|
|
846
861
|
if (_initialized) return;
|
|
847
862
|
_initialized = true;
|
|
848
|
-
|
|
863
|
+
initAssetsRoutes(RED.httpAdmin);
|
|
849
864
|
}
|
|
850
865
|
|
|
851
866
|
// src/core/constants.ts
|
|
@@ -1025,15 +1040,12 @@ async function registerType(RED, NodeClass) {
|
|
|
1025
1040
|
try {
|
|
1026
1041
|
this.log("Calling input");
|
|
1027
1042
|
await Promise.resolve(node._input(msg, send));
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
}
|
|
1035
|
-
});
|
|
1036
|
-
}
|
|
1043
|
+
node._sendToPort("complete", {
|
|
1044
|
+
...msg,
|
|
1045
|
+
complete: {
|
|
1046
|
+
source: { id: node.id, type: NC.type, name: node.name }
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1037
1049
|
done();
|
|
1038
1050
|
this.log("Input processed");
|
|
1039
1051
|
} catch (error) {
|
|
@@ -1118,7 +1130,6 @@ function defineModule(definition) {
|
|
|
1118
1130
|
defineIONode,
|
|
1119
1131
|
defineModule,
|
|
1120
1132
|
defineSchema,
|
|
1121
|
-
initValidator,
|
|
1122
1133
|
registerType,
|
|
1123
1134
|
registerTypes
|
|
1124
1135
|
});
|