@bonsae/nrg 0.9.0 → 0.10.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 +1 -0
- package/package.json +5 -1
- package/server/index.cjs +29 -37
- package/test/index.js +440 -0
- package/types/server.d.ts +2 -12
- package/types/test.d.ts +39 -0
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
<p align="center">
|
|
6
6
|
<a href="https://www.npmjs.com/package/@bonsae/nrg"><img src="https://img.shields.io/npm/v/@bonsae/nrg.svg" alt="npm package"></a>
|
|
7
7
|
<a href="https://github.com/bonsaedev/nrg/actions/workflows/ci.yaml"><img src="https://github.com/bonsaedev/nrg/actions/workflows/ci.yaml/badge.svg?branch=main" alt="build status"></a>
|
|
8
|
+
<a href="https://socket.dev/npm/package/@bonsae/nrg"><img src="https://badge.socket.dev/npm/package/@bonsae/nrg" alt="Socket Badge"></a>
|
|
8
9
|
</p>
|
|
9
10
|
|
|
10
11
|
# nrg
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bonsae/nrg",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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",
|
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
"types": "./types/vite.d.ts",
|
|
34
34
|
"default": "./vite/index.js"
|
|
35
35
|
},
|
|
36
|
+
"./test": {
|
|
37
|
+
"types": "./types/test.d.ts",
|
|
38
|
+
"default": "./test/index.js"
|
|
39
|
+
},
|
|
36
40
|
"./tsconfig/base.json": "./tsconfig/base.json",
|
|
37
41
|
"./tsconfig/client.json": "./tsconfig/client.json",
|
|
38
42
|
"./tsconfig/server.json": "./tsconfig/server.json"
|
package/server/index.cjs
CHANGED
|
@@ -750,45 +750,37 @@ function defineConfigNode(def) {
|
|
|
750
750
|
return NodeClass;
|
|
751
751
|
}
|
|
752
752
|
|
|
753
|
-
// src/core/server/api/
|
|
753
|
+
// src/core/server/api/nrg-assets.ts
|
|
754
754
|
var import_path = __toESM(require("path"), 1);
|
|
755
755
|
var import_fs = __toESM(require("fs"), 1);
|
|
756
|
-
var
|
|
757
|
-
|
|
758
|
-
".
|
|
759
|
-
".
|
|
760
|
-
".
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
756
|
+
var RESOURCES_DIR = import_path.default.resolve(__dirname, "./resources");
|
|
757
|
+
var ALLOWED_FILES = /* @__PURE__ */ new Set([
|
|
758
|
+
"nrg-client.js",
|
|
759
|
+
"vue.esm-browser.prod.js",
|
|
760
|
+
"vue.esm-browser.js"
|
|
761
|
+
]);
|
|
762
|
+
var handleNrgAsset = (req, res, next) => {
|
|
763
|
+
let fileName = req.params[0];
|
|
764
|
+
if (fileName === "vue.esm-browser.prod.js" && process.env.NODE_ENV !== "production") {
|
|
765
|
+
fileName = "vue.esm-browser.js";
|
|
766
|
+
}
|
|
767
|
+
if (!ALLOWED_FILES.has(fileName)) return next();
|
|
768
|
+
const filePath = import_path.default.join(RESOURCES_DIR, fileName);
|
|
769
|
+
if (!import_fs.default.existsSync(filePath)) return next();
|
|
770
|
+
res.setHeader("Content-Type", "application/javascript");
|
|
771
|
+
import_fs.default.createReadStream(filePath).pipe(res);
|
|
764
772
|
};
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
let reqPath = req.path.slice(prefix.length);
|
|
777
|
-
if (reqPath === "vue.esm-browser.prod.js" && process.env.NODE_ENV !== "production") {
|
|
778
|
-
const devPath = import_path.default.resolve(clientDir, "vue.esm-browser.js");
|
|
779
|
-
if (import_fs.default.existsSync(devPath)) {
|
|
780
|
-
reqPath = "vue.esm-browser.js";
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
const filePath = import_path.default.resolve(clientDir, reqPath);
|
|
784
|
-
const rel = import_path.default.relative(clientDir, filePath);
|
|
785
|
-
if (rel.startsWith("..") || import_path.default.isAbsolute(rel)) return next();
|
|
786
|
-
if (!import_fs.default.existsSync(filePath) || !import_fs.default.statSync(filePath).isFile())
|
|
787
|
-
return next();
|
|
788
|
-
const ext = import_path.default.extname(filePath);
|
|
789
|
-
res.setHeader("Content-Type", MIME[ext] ?? "application/octet-stream");
|
|
790
|
-
import_fs.default.createReadStream(filePath).pipe(res);
|
|
791
|
-
});
|
|
773
|
+
function initNrgAssetsRoute(router) {
|
|
774
|
+
if (!import_fs.default.existsSync(RESOURCES_DIR)) return;
|
|
775
|
+
router.get("/nrg/assets/*", handleNrgAsset);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// src/core/server/api/routes.ts
|
|
779
|
+
var _initialized = false;
|
|
780
|
+
function initRoutes(RED) {
|
|
781
|
+
if (_initialized) return;
|
|
782
|
+
_initialized = true;
|
|
783
|
+
initNrgAssetsRoute(RED.httpAdmin);
|
|
792
784
|
}
|
|
793
785
|
|
|
794
786
|
// src/core/constants.ts
|
|
@@ -1022,7 +1014,7 @@ async function registerType(RED, NodeClass) {
|
|
|
1022
1014
|
function registerTypes(nodes) {
|
|
1023
1015
|
const fn = async function(RED) {
|
|
1024
1016
|
initValidator(RED);
|
|
1025
|
-
|
|
1017
|
+
initRoutes(RED);
|
|
1026
1018
|
try {
|
|
1027
1019
|
RED.log.info("Registering node types in series");
|
|
1028
1020
|
for (const NodeClass of nodes) {
|
package/test/index.js
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
// src/test/index.ts
|
|
2
|
+
import { vi as vi2 } from "vitest";
|
|
3
|
+
|
|
4
|
+
// src/test/mocks.ts
|
|
5
|
+
import { vi } from "vitest";
|
|
6
|
+
function createMockRED(options = {}) {
|
|
7
|
+
const { nodes = {}, settings = {} } = options;
|
|
8
|
+
return {
|
|
9
|
+
log: {
|
|
10
|
+
info: vi.fn(),
|
|
11
|
+
warn: vi.fn(),
|
|
12
|
+
error: vi.fn(),
|
|
13
|
+
debug: vi.fn()
|
|
14
|
+
},
|
|
15
|
+
nodes: {
|
|
16
|
+
getNode: vi.fn((id) => nodes[id]),
|
|
17
|
+
registerType: vi.fn(),
|
|
18
|
+
createNode: vi.fn()
|
|
19
|
+
},
|
|
20
|
+
httpAdmin: {
|
|
21
|
+
get: vi.fn(),
|
|
22
|
+
post: vi.fn(),
|
|
23
|
+
put: vi.fn(),
|
|
24
|
+
delete: vi.fn(),
|
|
25
|
+
use: vi.fn()
|
|
26
|
+
},
|
|
27
|
+
settings: { ...settings },
|
|
28
|
+
_: vi.fn((key, subs) => {
|
|
29
|
+
if (!subs) return key;
|
|
30
|
+
return Object.entries(subs).reduce(
|
|
31
|
+
(str, [k, v]) => str.replace(`__${k}__`, v),
|
|
32
|
+
key
|
|
33
|
+
);
|
|
34
|
+
}),
|
|
35
|
+
util: {
|
|
36
|
+
evaluateNodeProperty: vi.fn(
|
|
37
|
+
(value, type, _node, msg, callback) => {
|
|
38
|
+
try {
|
|
39
|
+
let result;
|
|
40
|
+
switch (type) {
|
|
41
|
+
case "str":
|
|
42
|
+
result = String(value);
|
|
43
|
+
break;
|
|
44
|
+
case "num":
|
|
45
|
+
result = Number(value);
|
|
46
|
+
break;
|
|
47
|
+
case "bool":
|
|
48
|
+
result = value === "true" || value === true;
|
|
49
|
+
break;
|
|
50
|
+
case "json":
|
|
51
|
+
result = typeof value === "string" ? JSON.parse(value) : value;
|
|
52
|
+
break;
|
|
53
|
+
case "msg":
|
|
54
|
+
result = msg ? getProperty(msg, value) : void 0;
|
|
55
|
+
break;
|
|
56
|
+
case "date":
|
|
57
|
+
result = Date.now();
|
|
58
|
+
break;
|
|
59
|
+
case "bin":
|
|
60
|
+
result = Buffer.from(value ?? "");
|
|
61
|
+
break;
|
|
62
|
+
case "re":
|
|
63
|
+
result = new RegExp(value);
|
|
64
|
+
break;
|
|
65
|
+
case "jsonata":
|
|
66
|
+
case "flow":
|
|
67
|
+
case "global":
|
|
68
|
+
case "env":
|
|
69
|
+
case "cred":
|
|
70
|
+
result = void 0;
|
|
71
|
+
break;
|
|
72
|
+
case "node":
|
|
73
|
+
result = value;
|
|
74
|
+
break;
|
|
75
|
+
default:
|
|
76
|
+
result = value;
|
|
77
|
+
}
|
|
78
|
+
callback(null, result);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
callback(err, void 0);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
},
|
|
85
|
+
events: {
|
|
86
|
+
on: vi.fn(),
|
|
87
|
+
emit: vi.fn()
|
|
88
|
+
},
|
|
89
|
+
hooks: {
|
|
90
|
+
add: vi.fn(),
|
|
91
|
+
remove: vi.fn()
|
|
92
|
+
},
|
|
93
|
+
version: vi.fn(() => "0.0.0-test")
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function getProperty(obj, path) {
|
|
97
|
+
return path.split(".").reduce((acc, key) => acc?.[key], obj);
|
|
98
|
+
}
|
|
99
|
+
function createContextStore() {
|
|
100
|
+
const store = {};
|
|
101
|
+
return {
|
|
102
|
+
get: vi.fn(
|
|
103
|
+
(key, _store, cb) => cb(null, store[key])
|
|
104
|
+
),
|
|
105
|
+
set: vi.fn(
|
|
106
|
+
(key, value, _store, cb) => {
|
|
107
|
+
store[key] = value;
|
|
108
|
+
cb(null);
|
|
109
|
+
}
|
|
110
|
+
),
|
|
111
|
+
keys: vi.fn(
|
|
112
|
+
(_store, cb) => cb(null, Object.keys(store))
|
|
113
|
+
)
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function createMockNodeRedNode(options = {}) {
|
|
117
|
+
const nodeCtx = createContextStore();
|
|
118
|
+
const flowCtx = createContextStore();
|
|
119
|
+
const globalCtx = createContextStore();
|
|
120
|
+
const context = {
|
|
121
|
+
...nodeCtx,
|
|
122
|
+
flow: flowCtx,
|
|
123
|
+
global: globalCtx
|
|
124
|
+
};
|
|
125
|
+
return {
|
|
126
|
+
id: options.id ?? `test-${Math.random().toString(36).slice(2, 10)}`,
|
|
127
|
+
type: options.type ?? "test-node",
|
|
128
|
+
name: options.name ?? "",
|
|
129
|
+
z: options.z ?? "flow-1",
|
|
130
|
+
x: 100,
|
|
131
|
+
y: 200,
|
|
132
|
+
g: void 0,
|
|
133
|
+
wires: options.wires ?? [[]],
|
|
134
|
+
credentials: options.credentials ?? {},
|
|
135
|
+
log: vi.fn(),
|
|
136
|
+
warn: vi.fn(),
|
|
137
|
+
error: vi.fn(),
|
|
138
|
+
on: vi.fn(),
|
|
139
|
+
send: vi.fn(),
|
|
140
|
+
status: vi.fn(),
|
|
141
|
+
updateWires: vi.fn(),
|
|
142
|
+
receive: vi.fn(),
|
|
143
|
+
context: vi.fn(() => context),
|
|
144
|
+
...options
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/core/validator.ts
|
|
149
|
+
import Ajv from "ajv";
|
|
150
|
+
import addFormats from "ajv-formats";
|
|
151
|
+
import addErrors from "ajv-errors";
|
|
152
|
+
var Validator = class {
|
|
153
|
+
ajv;
|
|
154
|
+
constructor(options) {
|
|
155
|
+
const { customKeywords, customFormats, ...ajvOptions } = options || {};
|
|
156
|
+
this.ajv = new Ajv({
|
|
157
|
+
allErrors: true,
|
|
158
|
+
code: {
|
|
159
|
+
source: false
|
|
160
|
+
},
|
|
161
|
+
coerceTypes: true,
|
|
162
|
+
removeAdditional: false,
|
|
163
|
+
strict: false,
|
|
164
|
+
strictSchema: false,
|
|
165
|
+
useDefaults: true,
|
|
166
|
+
validateFormats: true,
|
|
167
|
+
// NOTE: typebox handles validation via typescript
|
|
168
|
+
// NOTE: if true, types that are not serializable JSON, like Function, would not work
|
|
169
|
+
validateSchema: false,
|
|
170
|
+
verbose: true,
|
|
171
|
+
...ajvOptions
|
|
172
|
+
});
|
|
173
|
+
addFormats(this.ajv);
|
|
174
|
+
addErrors(this.ajv);
|
|
175
|
+
this.addCustomKeywords(customKeywords || []);
|
|
176
|
+
this.addCustomFormats(customFormats || {});
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Add custom keywords to the validator
|
|
180
|
+
*/
|
|
181
|
+
addCustomKeywords(keywords) {
|
|
182
|
+
if (!keywords) return;
|
|
183
|
+
keywords.forEach((keyword) => {
|
|
184
|
+
this.ajv.addKeyword(keyword);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Add custom formats to the validator
|
|
189
|
+
*/
|
|
190
|
+
addCustomFormats(formats) {
|
|
191
|
+
if (!formats) return;
|
|
192
|
+
Object.entries(formats).forEach(([name, validator2]) => {
|
|
193
|
+
if (validator2 instanceof RegExp) {
|
|
194
|
+
this.ajv.addFormat(name, validator2);
|
|
195
|
+
} else {
|
|
196
|
+
this.ajv.addFormat(name, { validate: validator2 });
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Create a validator function with caching
|
|
202
|
+
* @param schema - JSON Schema to validate against
|
|
203
|
+
* @param cacheKey - Optional cache key for reusing validators
|
|
204
|
+
*/
|
|
205
|
+
createValidator(schema, cacheKey) {
|
|
206
|
+
if (cacheKey && !schema.$id) {
|
|
207
|
+
schema.$id = cacheKey;
|
|
208
|
+
}
|
|
209
|
+
if (schema.$id) {
|
|
210
|
+
const cached = this.ajv.getSchema(schema.$id);
|
|
211
|
+
if (cached) return cached;
|
|
212
|
+
}
|
|
213
|
+
const validator2 = this.ajv.compile(schema);
|
|
214
|
+
return validator2;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Validate data against a schema and return a structured result
|
|
218
|
+
*/
|
|
219
|
+
validate(data, schema, options) {
|
|
220
|
+
const validator2 = this.createValidator(schema, options?.cacheKey);
|
|
221
|
+
const valid = validator2(data);
|
|
222
|
+
if (!valid) {
|
|
223
|
+
const errorMessage = this.formatErrors(validator2.errors);
|
|
224
|
+
if (options?.throwOnError) {
|
|
225
|
+
throw new ValidationError(errorMessage, validator2.errors || []);
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
valid: false,
|
|
229
|
+
errors: validator2.errors || void 0,
|
|
230
|
+
errorMessage
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
valid: true,
|
|
235
|
+
data
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Format errors into a human-readable string
|
|
240
|
+
*/
|
|
241
|
+
formatErrors(errors, options) {
|
|
242
|
+
if (!errors || errors.length === 0) {
|
|
243
|
+
return "No errors";
|
|
244
|
+
}
|
|
245
|
+
return this.ajv.errorsText(errors, {
|
|
246
|
+
separator: "; ",
|
|
247
|
+
dataVar: "data",
|
|
248
|
+
...options
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Get detailed error information
|
|
253
|
+
*/
|
|
254
|
+
getDetailedErrors(errors) {
|
|
255
|
+
if (!errors || errors.length === 0) return [];
|
|
256
|
+
return errors.map((error) => ({
|
|
257
|
+
field: error.instancePath || "/",
|
|
258
|
+
message: error.message || "Validation failed",
|
|
259
|
+
keyword: error.keyword,
|
|
260
|
+
params: error.params,
|
|
261
|
+
schemaPath: error.schemaPath
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Add a schema to the validator for reference
|
|
266
|
+
*/
|
|
267
|
+
addSchema(schema, key) {
|
|
268
|
+
this.ajv.addSchema(schema, key);
|
|
269
|
+
return this;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Remove a schema from the validator
|
|
273
|
+
*/
|
|
274
|
+
removeSchema(key) {
|
|
275
|
+
this.ajv.removeSchema(key);
|
|
276
|
+
return this;
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
var ValidationError = class _ValidationError extends Error {
|
|
280
|
+
constructor(message, errors) {
|
|
281
|
+
super(message);
|
|
282
|
+
this.errors = errors;
|
|
283
|
+
this.name = "ValidationError";
|
|
284
|
+
Object.setPrototypeOf(this, _ValidationError.prototype);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// src/core/server/validation.ts
|
|
289
|
+
var validator = void 0;
|
|
290
|
+
function initValidator(RED) {
|
|
291
|
+
validator = new Validator({
|
|
292
|
+
customKeywords: [
|
|
293
|
+
{
|
|
294
|
+
keyword: "x-nrg-skip-validation",
|
|
295
|
+
schemaType: "boolean",
|
|
296
|
+
valid: true
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
keyword: "x-nrg-node-type",
|
|
300
|
+
type: "string",
|
|
301
|
+
validate: (schemaValue, dataValue) => {
|
|
302
|
+
if (!dataValue) return true;
|
|
303
|
+
const node = RED.nodes.getNode(dataValue);
|
|
304
|
+
return node?.type === schemaValue;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
],
|
|
308
|
+
customFormats: {
|
|
309
|
+
"node-id": /^[a-zA-Z0-9-_]+$/,
|
|
310
|
+
"flow-id": /^[a-f0-9]{16}$/,
|
|
311
|
+
"topic-path": (data) => /^[a-zA-Z0-9/_-]+$/.test(data)
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// src/test/index.ts
|
|
317
|
+
function buildConfig(NodeClass, userConfig = {}) {
|
|
318
|
+
const defaults = {};
|
|
319
|
+
if (NodeClass.configSchema?.properties) {
|
|
320
|
+
for (const [key, prop] of Object.entries(
|
|
321
|
+
NodeClass.configSchema.properties
|
|
322
|
+
)) {
|
|
323
|
+
if (prop.default !== void 0) {
|
|
324
|
+
defaults[key] = prop.default;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return { ...defaults, ...userConfig };
|
|
329
|
+
}
|
|
330
|
+
function buildNodeRedNodes(configNodes) {
|
|
331
|
+
const nodes = {};
|
|
332
|
+
for (const [id, value] of Object.entries(configNodes)) {
|
|
333
|
+
if (value && typeof value === "object" && "id" in value) {
|
|
334
|
+
nodes[id] = { _node: value };
|
|
335
|
+
} else {
|
|
336
|
+
nodes[id] = value;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return nodes;
|
|
340
|
+
}
|
|
341
|
+
function attachHelpers(node, nodeRedNode) {
|
|
342
|
+
const sentMessages = [];
|
|
343
|
+
const statusCalls = [];
|
|
344
|
+
nodeRedNode.send.mockImplementation((msg) => {
|
|
345
|
+
sentMessages.push(msg);
|
|
346
|
+
});
|
|
347
|
+
nodeRedNode.status.mockImplementation((status) => {
|
|
348
|
+
statusCalls.push(status);
|
|
349
|
+
});
|
|
350
|
+
const nodeRef = node;
|
|
351
|
+
const helpers = {
|
|
352
|
+
async receive(msg) {
|
|
353
|
+
const sendFn = vi2.fn((outMsg) => {
|
|
354
|
+
nodeRedNode.send(outMsg);
|
|
355
|
+
});
|
|
356
|
+
await nodeRef._input(msg, sendFn);
|
|
357
|
+
},
|
|
358
|
+
async close(removed = false) {
|
|
359
|
+
await nodeRef._closed(removed);
|
|
360
|
+
},
|
|
361
|
+
reset() {
|
|
362
|
+
sentMessages.length = 0;
|
|
363
|
+
statusCalls.length = 0;
|
|
364
|
+
nodeRedNode.log.mockClear();
|
|
365
|
+
nodeRedNode.warn.mockClear();
|
|
366
|
+
nodeRedNode.error.mockClear();
|
|
367
|
+
},
|
|
368
|
+
sent(port) {
|
|
369
|
+
if (port === void 0) return [...sentMessages];
|
|
370
|
+
return sentMessages.map(
|
|
371
|
+
(msg) => Array.isArray(msg) ? msg[port] : port === 0 ? msg : void 0
|
|
372
|
+
).filter((msg) => msg != null);
|
|
373
|
+
},
|
|
374
|
+
statuses() {
|
|
375
|
+
return [...statusCalls];
|
|
376
|
+
},
|
|
377
|
+
logged(level) {
|
|
378
|
+
if (level) {
|
|
379
|
+
return nodeRedNode[level === "info" ? "log" : level].mock.calls.map(
|
|
380
|
+
(c) => c[0]
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
return [
|
|
384
|
+
...nodeRedNode.log.mock.calls.map((c) => c[0]),
|
|
385
|
+
...nodeRedNode.warn.mock.calls.map((c) => c[0]),
|
|
386
|
+
...nodeRedNode.error.mock.calls.map((c) => c[0])
|
|
387
|
+
];
|
|
388
|
+
},
|
|
389
|
+
warned() {
|
|
390
|
+
return nodeRedNode.warn.mock.calls.map((c) => c[0]);
|
|
391
|
+
},
|
|
392
|
+
errored() {
|
|
393
|
+
return nodeRedNode.error.mock.calls.map((c) => c[0]);
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
return Object.assign(node, helpers);
|
|
397
|
+
}
|
|
398
|
+
function isConfigNode(NodeClass) {
|
|
399
|
+
return NodeClass.category === "config";
|
|
400
|
+
}
|
|
401
|
+
async function createNode(NodeClass, options = {}) {
|
|
402
|
+
const {
|
|
403
|
+
config: userConfig = {},
|
|
404
|
+
credentials = {},
|
|
405
|
+
configNodes = {},
|
|
406
|
+
settings = {},
|
|
407
|
+
overrides: overrideOpts = {}
|
|
408
|
+
} = options;
|
|
409
|
+
const redNodes = buildNodeRedNodes(configNodes);
|
|
410
|
+
const RED = createMockRED({ nodes: redNodes, settings });
|
|
411
|
+
initValidator(RED);
|
|
412
|
+
const configDefaults = {
|
|
413
|
+
id: overrideOpts.id ?? `test-${Math.random().toString(36).slice(2, 10)}`,
|
|
414
|
+
type: NodeClass.type
|
|
415
|
+
};
|
|
416
|
+
if (isConfigNode(NodeClass)) {
|
|
417
|
+
configDefaults._users = [];
|
|
418
|
+
}
|
|
419
|
+
const config = buildConfig(NodeClass, {
|
|
420
|
+
...configDefaults,
|
|
421
|
+
...userConfig
|
|
422
|
+
});
|
|
423
|
+
const nodeRedNode = createMockNodeRedNode({
|
|
424
|
+
id: config.id,
|
|
425
|
+
type: NodeClass.type,
|
|
426
|
+
name: config.name ?? "",
|
|
427
|
+
credentials,
|
|
428
|
+
...overrideOpts
|
|
429
|
+
});
|
|
430
|
+
await Promise.resolve(
|
|
431
|
+
NodeClass._registered?.(RED) ?? NodeClass.registered?.(RED)
|
|
432
|
+
);
|
|
433
|
+
const node = new NodeClass(RED, nodeRedNode, config, credentials);
|
|
434
|
+
const augmented = attachHelpers(node, nodeRedNode);
|
|
435
|
+
await Promise.resolve(augmented.created?.());
|
|
436
|
+
return { node: augmented, RED };
|
|
437
|
+
}
|
|
438
|
+
export {
|
|
439
|
+
createNode
|
|
440
|
+
};
|
package/types/server.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Generated by dts-bundle-generator v9.5.1
|
|
2
2
|
|
|
3
3
|
import { EventEmitter } from 'events';
|
|
4
|
+
import { Express as Express$1 } from 'express';
|
|
4
5
|
import { Http2ServerRequest } from 'http2';
|
|
5
6
|
|
|
6
7
|
declare const TransformKind: unique symbol;
|
|
@@ -2006,18 +2007,7 @@ export interface NodeRedHooks {
|
|
|
2006
2007
|
has(hookId: string): boolean;
|
|
2007
2008
|
clear(): void;
|
|
2008
2009
|
}
|
|
2009
|
-
export type
|
|
2010
|
-
export interface NodeRedExpressApp {
|
|
2011
|
-
get(path: string, ...handlers: NodeRedRequestHandler[]): void;
|
|
2012
|
-
post(path: string, ...handlers: NodeRedRequestHandler[]): void;
|
|
2013
|
-
put(path: string, ...handlers: NodeRedRequestHandler[]): void;
|
|
2014
|
-
delete(path: string, ...handlers: NodeRedRequestHandler[]): void;
|
|
2015
|
-
patch(path: string, ...handlers: NodeRedRequestHandler[]): void;
|
|
2016
|
-
options(path: string, ...handlers: NodeRedRequestHandler[]): void;
|
|
2017
|
-
head(path: string, ...handlers: NodeRedRequestHandler[]): void;
|
|
2018
|
-
use(path: string | NodeRedRequestHandler, ...handlers: NodeRedRequestHandler[]): void;
|
|
2019
|
-
all(path: string, ...handlers: NodeRedRequestHandler[]): void;
|
|
2020
|
-
}
|
|
2010
|
+
export type NodeRedExpressApp = Express;
|
|
2021
2011
|
export interface RED {
|
|
2022
2012
|
/** Internationalization function */
|
|
2023
2013
|
_(key: string, substitutions?: Record<string, string>): string;
|
package/types/test.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
|
|
2
|
+
export interface CreateNodeOptions {
|
|
3
|
+
config?: Record<string, any>;
|
|
4
|
+
credentials?: Record<string, any>;
|
|
5
|
+
configNodes?: Record<string, any>;
|
|
6
|
+
settings?: Record<string, any>;
|
|
7
|
+
overrides?: Record<string, any>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type ExtractInput<T> = T extends { input(msg: infer I): any } ? I : any;
|
|
11
|
+
type ExtractOutput<T> = T extends { send(msg: infer O): any } ? O : any;
|
|
12
|
+
|
|
13
|
+
export interface TestNodeHelpers<TInput = any, TOutput = any> {
|
|
14
|
+
receive(msg: TInput): Promise<void>;
|
|
15
|
+
close(removed?: boolean): Promise<void>;
|
|
16
|
+
reset(): void;
|
|
17
|
+
sent(): TOutput[];
|
|
18
|
+
sent(port: number): any[];
|
|
19
|
+
statuses(): any[];
|
|
20
|
+
logged(level?: "info" | "warn" | "error" | "debug"): string[];
|
|
21
|
+
warned(): string[];
|
|
22
|
+
errored(): string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CreateNodeResult<T> {
|
|
26
|
+
node: T & TestNodeHelpers<ExtractInput<T>, ExtractOutput<T>>;
|
|
27
|
+
RED: any;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface NodeClass {
|
|
31
|
+
readonly type: string;
|
|
32
|
+
readonly category?: string;
|
|
33
|
+
readonly configSchema?: any;
|
|
34
|
+
registered?(RED: any): void | Promise<void>;
|
|
35
|
+
_registered?(RED: any): void | Promise<void>;
|
|
36
|
+
new (...args: any[]): any;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export declare function createNode<T extends NodeClass>(NodeClass: T, options?: CreateNodeOptions): Promise<CreateNodeResult<InstanceType<T>>>;
|