@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/README.md +3 -40
- package/package.json +3 -2
- package/server/index.cjs +549 -453
- package/server/resources/nrg-client.js +2114 -2097
- package/test/index.js +295 -44
- package/types/server.d.ts +360 -1854
- 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 +414 -32
- package/types/vite.d.ts +164 -7
- package/vite/index.js +289 -40
- package/server/resources/vue.esm-browser.js +0 -18708
- package/server/resources/vue.esm-browser.prod.js +0 -13
package/test/index.js
CHANGED
|
@@ -3,19 +3,44 @@ import { vi as vi2 } from "vitest";
|
|
|
3
3
|
|
|
4
4
|
// src/test/mocks.ts
|
|
5
5
|
import { vi } from "vitest";
|
|
6
|
-
function
|
|
7
|
-
const {
|
|
8
|
-
|
|
6
|
+
function createNodeRedRuntime(options = {}) {
|
|
7
|
+
const { settings = {} } = options;
|
|
8
|
+
const nodes = {};
|
|
9
|
+
const red = {
|
|
9
10
|
log: {
|
|
10
11
|
info: vi.fn(),
|
|
11
12
|
warn: vi.fn(),
|
|
12
13
|
error: vi.fn(),
|
|
13
|
-
debug: vi.fn()
|
|
14
|
+
debug: vi.fn(),
|
|
15
|
+
trace: vi.fn(),
|
|
16
|
+
log: vi.fn(),
|
|
17
|
+
metric: vi.fn(() => false),
|
|
18
|
+
audit: vi.fn(),
|
|
19
|
+
addHandler: vi.fn(),
|
|
20
|
+
removeHandler: vi.fn(),
|
|
21
|
+
FATAL: 10,
|
|
22
|
+
ERROR: 20,
|
|
23
|
+
WARN: 30,
|
|
24
|
+
INFO: 40,
|
|
25
|
+
DEBUG: 50,
|
|
26
|
+
TRACE: 60,
|
|
27
|
+
AUDIT: 98,
|
|
28
|
+
METRIC: 99
|
|
14
29
|
},
|
|
15
30
|
nodes: {
|
|
16
31
|
getNode: vi.fn((id) => nodes[id]),
|
|
17
32
|
registerType: vi.fn(),
|
|
18
|
-
createNode: vi.fn()
|
|
33
|
+
createNode: vi.fn(),
|
|
34
|
+
getCredentials: vi.fn(),
|
|
35
|
+
eachNode: vi.fn(),
|
|
36
|
+
getType: vi.fn(),
|
|
37
|
+
getNodeInfo: vi.fn(),
|
|
38
|
+
getNodeList: vi.fn(() => []),
|
|
39
|
+
getModuleInfo: vi.fn(),
|
|
40
|
+
installModule: vi.fn(),
|
|
41
|
+
uninstallModule: vi.fn(),
|
|
42
|
+
enableNode: vi.fn(),
|
|
43
|
+
disableNode: vi.fn()
|
|
19
44
|
},
|
|
20
45
|
httpAdmin: {
|
|
21
46
|
get: vi.fn(),
|
|
@@ -24,6 +49,25 @@ function createMockRED(options = {}) {
|
|
|
24
49
|
delete: vi.fn(),
|
|
25
50
|
use: vi.fn()
|
|
26
51
|
},
|
|
52
|
+
httpNode: {
|
|
53
|
+
get: vi.fn(),
|
|
54
|
+
post: vi.fn(),
|
|
55
|
+
put: vi.fn(),
|
|
56
|
+
delete: vi.fn(),
|
|
57
|
+
use: vi.fn()
|
|
58
|
+
},
|
|
59
|
+
hooks: {
|
|
60
|
+
add: vi.fn(),
|
|
61
|
+
remove: vi.fn(),
|
|
62
|
+
trigger: vi.fn(),
|
|
63
|
+
has: vi.fn(() => false),
|
|
64
|
+
clear: vi.fn()
|
|
65
|
+
},
|
|
66
|
+
events: {
|
|
67
|
+
on: vi.fn(),
|
|
68
|
+
emit: vi.fn(),
|
|
69
|
+
removeListener: vi.fn()
|
|
70
|
+
},
|
|
27
71
|
settings: { ...settings },
|
|
28
72
|
_: vi.fn((key, subs) => {
|
|
29
73
|
if (!subs) return key;
|
|
@@ -80,18 +124,34 @@ function createMockRED(options = {}) {
|
|
|
80
124
|
callback(err, void 0);
|
|
81
125
|
}
|
|
82
126
|
}
|
|
83
|
-
)
|
|
127
|
+
),
|
|
128
|
+
generateId: vi.fn(() => "mock-id"),
|
|
129
|
+
cloneMessage: vi.fn((msg) => ({ ...msg })),
|
|
130
|
+
ensureString: vi.fn((o) => String(o)),
|
|
131
|
+
ensureBuffer: vi.fn(),
|
|
132
|
+
compareObjects: vi.fn(),
|
|
133
|
+
getMessageProperty: vi.fn(),
|
|
134
|
+
setMessageProperty: vi.fn(),
|
|
135
|
+
getObjectProperty: vi.fn(),
|
|
136
|
+
setObjectProperty: vi.fn(),
|
|
137
|
+
normalisePropertyExpression: vi.fn(),
|
|
138
|
+
normaliseNodeTypeName: vi.fn(),
|
|
139
|
+
prepareJSONataExpression: vi.fn(),
|
|
140
|
+
evaluateJSONataExpression: vi.fn(),
|
|
141
|
+
parseContextStore: vi.fn(),
|
|
142
|
+
getSetting: vi.fn(),
|
|
143
|
+
encodeObject: vi.fn()
|
|
84
144
|
},
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
145
|
+
version: vi.fn(() => "0.0.0-test"),
|
|
146
|
+
validator: void 0,
|
|
147
|
+
registerNode(id, nodeRedNode) {
|
|
148
|
+
nodes[id] = nodeRedNode;
|
|
88
149
|
},
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
},
|
|
93
|
-
version: vi.fn(() => "0.0.0-test")
|
|
150
|
+
registerNrgNode(id, nrgInstance) {
|
|
151
|
+
nodes[id] = createNodeRedNode({ id, _node: nrgInstance });
|
|
152
|
+
}
|
|
94
153
|
};
|
|
154
|
+
return red;
|
|
95
155
|
}
|
|
96
156
|
function getProperty(obj, path) {
|
|
97
157
|
return path.split(".").reduce((acc, key) => acc?.[key], obj);
|
|
@@ -113,7 +173,7 @@ function createContextStore() {
|
|
|
113
173
|
)
|
|
114
174
|
};
|
|
115
175
|
}
|
|
116
|
-
function
|
|
176
|
+
function createNodeRedNode(options = {}) {
|
|
117
177
|
const nodeCtx = createContextStore();
|
|
118
178
|
const flowCtx = createContextStore();
|
|
119
179
|
const globalCtx = createContextStore();
|
|
@@ -122,20 +182,29 @@ function createMockNodeRedNode(options = {}) {
|
|
|
122
182
|
flow: flowCtx,
|
|
123
183
|
global: globalCtx
|
|
124
184
|
};
|
|
185
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
125
186
|
return {
|
|
126
|
-
id: options.id ?? `
|
|
187
|
+
id: options.id ?? `node-${Math.random().toString(36).slice(2, 10)}`,
|
|
127
188
|
type: options.type ?? "test-node",
|
|
128
|
-
name: options.name ?? "",
|
|
189
|
+
name: options.name ?? "test-node",
|
|
129
190
|
z: options.z ?? "flow-1",
|
|
130
191
|
x: 100,
|
|
131
192
|
y: 200,
|
|
132
|
-
g:
|
|
133
|
-
wires: options.wires ?? [[]],
|
|
193
|
+
g: "group-1",
|
|
194
|
+
wires: options.wires ?? [["node-2"]],
|
|
134
195
|
credentials: options.credentials ?? {},
|
|
135
196
|
log: vi.fn(),
|
|
136
197
|
warn: vi.fn(),
|
|
137
198
|
error: vi.fn(),
|
|
138
|
-
on: vi.fn()
|
|
199
|
+
on: vi.fn((event, handler) => {
|
|
200
|
+
if (!handlers.has(event)) handlers.set(event, []);
|
|
201
|
+
handlers.get(event).push(handler);
|
|
202
|
+
}),
|
|
203
|
+
emit: vi.fn(async (event, ...args) => {
|
|
204
|
+
for (const handler of handlers.get(event) ?? []) {
|
|
205
|
+
await handler(...args);
|
|
206
|
+
}
|
|
207
|
+
}),
|
|
139
208
|
send: vi.fn(),
|
|
140
209
|
status: vi.fn(),
|
|
141
210
|
updateWires: vi.fn(),
|
|
@@ -145,32 +214,204 @@ function createMockNodeRedNode(options = {}) {
|
|
|
145
214
|
};
|
|
146
215
|
}
|
|
147
216
|
|
|
217
|
+
// src/core/validator.ts
|
|
218
|
+
import Ajv from "ajv";
|
|
219
|
+
import addFormats from "ajv-formats";
|
|
220
|
+
import addErrors from "ajv-errors";
|
|
221
|
+
var Validator = class {
|
|
222
|
+
ajv;
|
|
223
|
+
constructor(options) {
|
|
224
|
+
const { customKeywords, customFormats, ...ajvOptions } = options || {};
|
|
225
|
+
this.ajv = new Ajv({
|
|
226
|
+
allErrors: true,
|
|
227
|
+
code: {
|
|
228
|
+
source: false
|
|
229
|
+
},
|
|
230
|
+
coerceTypes: true,
|
|
231
|
+
removeAdditional: false,
|
|
232
|
+
strict: false,
|
|
233
|
+
strictSchema: false,
|
|
234
|
+
useDefaults: true,
|
|
235
|
+
validateFormats: true,
|
|
236
|
+
// NOTE: typebox handles validation via typescript
|
|
237
|
+
// NOTE: if true, types that are not serializable JSON, like Function, would not work
|
|
238
|
+
validateSchema: false,
|
|
239
|
+
verbose: true,
|
|
240
|
+
...ajvOptions
|
|
241
|
+
});
|
|
242
|
+
addFormats(this.ajv);
|
|
243
|
+
addErrors(this.ajv);
|
|
244
|
+
this.addCustomKeywords(customKeywords || []);
|
|
245
|
+
this.addCustomFormats(customFormats || {});
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Add custom keywords to the validator
|
|
249
|
+
*/
|
|
250
|
+
addCustomKeywords(keywords) {
|
|
251
|
+
if (!keywords) return;
|
|
252
|
+
keywords.forEach((keyword) => {
|
|
253
|
+
this.ajv.addKeyword(keyword);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Add custom formats to the validator
|
|
258
|
+
*/
|
|
259
|
+
addCustomFormats(formats) {
|
|
260
|
+
if (!formats) return;
|
|
261
|
+
Object.entries(formats).forEach(([name, validator]) => {
|
|
262
|
+
if (validator instanceof RegExp) {
|
|
263
|
+
this.ajv.addFormat(name, validator);
|
|
264
|
+
} else {
|
|
265
|
+
this.ajv.addFormat(name, { validate: validator });
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Create a validator function with caching
|
|
271
|
+
* @param schema - JSON Schema to validate against
|
|
272
|
+
* @param cacheKey - Optional cache key for reusing validators
|
|
273
|
+
*/
|
|
274
|
+
createValidator(schema, cacheKey) {
|
|
275
|
+
if (cacheKey && !schema.$id) {
|
|
276
|
+
schema.$id = cacheKey;
|
|
277
|
+
}
|
|
278
|
+
if (schema.$id) {
|
|
279
|
+
const cached = this.ajv.getSchema(schema.$id);
|
|
280
|
+
if (cached) return cached;
|
|
281
|
+
}
|
|
282
|
+
const validator = this.ajv.compile(schema);
|
|
283
|
+
return validator;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Validate data against a schema and return a structured result
|
|
287
|
+
*/
|
|
288
|
+
validate(data, schema, options) {
|
|
289
|
+
const validator = this.createValidator(schema, options?.cacheKey);
|
|
290
|
+
const valid = validator(data);
|
|
291
|
+
if (!valid) {
|
|
292
|
+
const errorMessage = this.formatErrors(validator.errors);
|
|
293
|
+
if (options?.throwOnError) {
|
|
294
|
+
throw new ValidationError(errorMessage, validator.errors || []);
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
valid: false,
|
|
298
|
+
errors: validator.errors || void 0,
|
|
299
|
+
errorMessage
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
valid: true,
|
|
304
|
+
data
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Format errors into a human-readable string
|
|
309
|
+
*/
|
|
310
|
+
formatErrors(errors, options) {
|
|
311
|
+
if (!errors || errors.length === 0) {
|
|
312
|
+
return "No errors";
|
|
313
|
+
}
|
|
314
|
+
return this.ajv.errorsText(errors, {
|
|
315
|
+
separator: "; ",
|
|
316
|
+
dataVar: "data",
|
|
317
|
+
...options
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Get detailed error information
|
|
322
|
+
*/
|
|
323
|
+
getDetailedErrors(errors) {
|
|
324
|
+
if (!errors || errors.length === 0) return [];
|
|
325
|
+
return errors.map((error) => ({
|
|
326
|
+
field: error.instancePath || "/",
|
|
327
|
+
message: error.message || "Validation failed",
|
|
328
|
+
keyword: error.keyword,
|
|
329
|
+
params: error.params,
|
|
330
|
+
schemaPath: error.schemaPath
|
|
331
|
+
}));
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Add a schema to the validator for reference
|
|
335
|
+
*/
|
|
336
|
+
addSchema(schema, key) {
|
|
337
|
+
this.ajv.addSchema(schema, key);
|
|
338
|
+
return this;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Remove a schema from the validator
|
|
342
|
+
*/
|
|
343
|
+
removeSchema(key) {
|
|
344
|
+
this.ajv.removeSchema(key);
|
|
345
|
+
return this;
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
var ValidationError = class _ValidationError extends Error {
|
|
349
|
+
constructor(message, errors) {
|
|
350
|
+
super(message);
|
|
351
|
+
this.errors = errors;
|
|
352
|
+
this.name = "ValidationError";
|
|
353
|
+
Object.setPrototypeOf(this, _ValidationError.prototype);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
// src/core/server/validation.ts
|
|
358
|
+
function initValidator(RED) {
|
|
359
|
+
const nrg = {
|
|
360
|
+
validator: new Validator({
|
|
361
|
+
customKeywords: [
|
|
362
|
+
{
|
|
363
|
+
keyword: "x-nrg-skip-validation",
|
|
364
|
+
schemaType: "boolean",
|
|
365
|
+
valid: true
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
keyword: "x-nrg-node-type",
|
|
369
|
+
type: "string",
|
|
370
|
+
validate: (schemaValue, dataValue) => {
|
|
371
|
+
if (!dataValue) return true;
|
|
372
|
+
const node = RED.nodes.getNode(dataValue);
|
|
373
|
+
return node?.type === schemaValue;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
],
|
|
377
|
+
customFormats: {
|
|
378
|
+
"node-id": /^[a-zA-Z0-9-_]+$/,
|
|
379
|
+
"flow-id": /^[a-f0-9]{16}$/,
|
|
380
|
+
"topic-path": (data) => /^[a-zA-Z0-9/_-]+$/.test(data)
|
|
381
|
+
}
|
|
382
|
+
})
|
|
383
|
+
};
|
|
384
|
+
Object.defineProperty(RED, "_nrg", {
|
|
385
|
+
value: nrg,
|
|
386
|
+
writable: false,
|
|
387
|
+
enumerable: false,
|
|
388
|
+
configurable: false
|
|
389
|
+
});
|
|
390
|
+
Object.defineProperty(RED, "validator", {
|
|
391
|
+
get: () => nrg.validator,
|
|
392
|
+
enumerable: false,
|
|
393
|
+
configurable: false
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// src/core/server/nodes/symbols.ts
|
|
398
|
+
var WIRE_HANDLERS = Symbol("wireHandlers");
|
|
399
|
+
|
|
148
400
|
// src/test/index.ts
|
|
149
|
-
import { initValidator } from "@bonsae/nrg/server";
|
|
150
401
|
function buildConfig(NodeClass, userConfig = {}) {
|
|
151
402
|
const defaults = {};
|
|
152
403
|
if (NodeClass.configSchema?.properties) {
|
|
153
404
|
for (const [key, prop] of Object.entries(
|
|
154
405
|
NodeClass.configSchema.properties
|
|
155
406
|
)) {
|
|
156
|
-
|
|
157
|
-
|
|
407
|
+
const schemaProp = prop;
|
|
408
|
+
if (schemaProp.default !== void 0) {
|
|
409
|
+
defaults[key] = schemaProp.default;
|
|
158
410
|
}
|
|
159
411
|
}
|
|
160
412
|
}
|
|
161
413
|
return { ...defaults, ...userConfig };
|
|
162
414
|
}
|
|
163
|
-
function buildNodeRedNodes(configNodes) {
|
|
164
|
-
const nodes = {};
|
|
165
|
-
for (const [id, value] of Object.entries(configNodes)) {
|
|
166
|
-
if (value && typeof value === "object" && "id" in value) {
|
|
167
|
-
nodes[id] = { _node: value };
|
|
168
|
-
} else {
|
|
169
|
-
nodes[id] = value;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
return nodes;
|
|
173
|
-
}
|
|
174
415
|
function attachHelpers(node, nodeRedNode) {
|
|
175
416
|
const sentMessages = [];
|
|
176
417
|
const statusCalls = [];
|
|
@@ -180,16 +421,23 @@ function attachHelpers(node, nodeRedNode) {
|
|
|
180
421
|
nodeRedNode.status.mockImplementation((status) => {
|
|
181
422
|
statusCalls.push(status);
|
|
182
423
|
});
|
|
183
|
-
const nodeRef = node;
|
|
184
424
|
const helpers = {
|
|
185
425
|
async receive(msg) {
|
|
186
426
|
const sendFn = vi2.fn((outMsg) => {
|
|
187
427
|
nodeRedNode.send(outMsg);
|
|
188
428
|
});
|
|
189
|
-
|
|
429
|
+
const doneFn = vi2.fn();
|
|
430
|
+
await nodeRedNode.emit("input", msg, sendFn, doneFn);
|
|
431
|
+
if (doneFn.mock.calls[0]?.[0] instanceof Error) {
|
|
432
|
+
throw doneFn.mock.calls[0][0];
|
|
433
|
+
}
|
|
190
434
|
},
|
|
191
435
|
async close(removed = false) {
|
|
192
|
-
|
|
436
|
+
const doneFn = vi2.fn();
|
|
437
|
+
await nodeRedNode.emit("close", removed, doneFn);
|
|
438
|
+
if (doneFn.mock.calls[0]?.[0] instanceof Error) {
|
|
439
|
+
throw doneFn.mock.calls[0][0];
|
|
440
|
+
}
|
|
193
441
|
},
|
|
194
442
|
reset() {
|
|
195
443
|
sentMessages.length = 0;
|
|
@@ -248,9 +496,11 @@ async function createNode(NodeClass, options = {}) {
|
|
|
248
496
|
resolvedConfig[key] = value;
|
|
249
497
|
}
|
|
250
498
|
}
|
|
251
|
-
const
|
|
252
|
-
const RED = createMockRED({ nodes: redNodes, settings });
|
|
499
|
+
const RED = createNodeRedRuntime({ settings });
|
|
253
500
|
initValidator(RED);
|
|
501
|
+
for (const [id, value] of Object.entries(configNodes)) {
|
|
502
|
+
RED.registerNrgNode(id, value);
|
|
503
|
+
}
|
|
254
504
|
const configDefaults = {
|
|
255
505
|
id: overrideOpts.id ?? `test-${Math.random().toString(36).slice(2, 10)}`,
|
|
256
506
|
type: NodeClass.type
|
|
@@ -262,19 +512,20 @@ async function createNode(NodeClass, options = {}) {
|
|
|
262
512
|
...configDefaults,
|
|
263
513
|
...resolvedConfig
|
|
264
514
|
});
|
|
265
|
-
const nodeRedNode =
|
|
515
|
+
const nodeRedNode = createNodeRedNode({
|
|
266
516
|
id: config.id,
|
|
267
517
|
type: NodeClass.type,
|
|
268
518
|
name: config.name ?? "",
|
|
269
519
|
credentials,
|
|
270
520
|
...overrideOpts
|
|
271
521
|
});
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
);
|
|
522
|
+
NodeClass.validateSettings(RED);
|
|
523
|
+
await Promise.resolve(NodeClass.registered?.(RED));
|
|
275
524
|
const node = new NodeClass(RED, nodeRedNode, config, credentials);
|
|
525
|
+
const createdPromise = Promise.resolve(node.created?.());
|
|
526
|
+
node[WIRE_HANDLERS](nodeRedNode, createdPromise);
|
|
527
|
+
await createdPromise;
|
|
276
528
|
const augmented = attachHelpers(node, nodeRedNode);
|
|
277
|
-
await Promise.resolve(augmented.created?.());
|
|
278
529
|
return { node: augmented, RED };
|
|
279
530
|
}
|
|
280
531
|
export {
|