@bonsae/nrg 0.5.2 → 0.5.3
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/build/server/index.cjs +65 -33
- package/package.json +1 -1
- package/src/core/errors.ts +9 -0
- package/src/core/server/index.ts +19 -12
- package/src/core/server/nodes/node.ts +5 -1
- package/src/core/server/nodes/utils.ts +50 -27
package/build/server/index.cjs
CHANGED
|
@@ -33,6 +33,7 @@ __export(index_exports, {
|
|
|
33
33
|
ConfigNode: () => ConfigNode,
|
|
34
34
|
IONode: () => IONode,
|
|
35
35
|
Node: () => Node,
|
|
36
|
+
NrgError: () => NrgError,
|
|
36
37
|
SchemaType: () => SchemaType,
|
|
37
38
|
defineSchema: () => defineSchema,
|
|
38
39
|
registerType: () => registerType,
|
|
@@ -231,6 +232,15 @@ function initValidator(RED) {
|
|
|
231
232
|
validator2 = new NodeRedValidator(RED);
|
|
232
233
|
}
|
|
233
234
|
|
|
235
|
+
// src/core/errors.ts
|
|
236
|
+
var NrgError = class _NrgError extends Error {
|
|
237
|
+
constructor(message) {
|
|
238
|
+
super(message);
|
|
239
|
+
this.name = "NrgError";
|
|
240
|
+
Object.setPrototypeOf(this, _NrgError.prototype);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
234
244
|
// src/core/server/nodes/utils.ts
|
|
235
245
|
function setupContext(context, store) {
|
|
236
246
|
return {
|
|
@@ -254,40 +264,51 @@ function setupContext(context, store) {
|
|
|
254
264
|
)
|
|
255
265
|
};
|
|
256
266
|
}
|
|
257
|
-
function setupConfigProxy(RED, config) {
|
|
267
|
+
function setupConfigProxy(RED, config, schema) {
|
|
258
268
|
const SKIP_PROPS = /* @__PURE__ */ new Set(["id", "_id", "_users"]);
|
|
269
|
+
const nodeRefProps = /* @__PURE__ */ new Set();
|
|
270
|
+
if (schema?.properties) {
|
|
271
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
272
|
+
if (propSchema?.["x-nrg-node-type"]) {
|
|
273
|
+
nodeRefProps.add(key);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const cache = /* @__PURE__ */ new WeakMap();
|
|
259
278
|
const createProxy = (obj) => {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
return target[prop];
|
|
279
|
+
const cached = cache.get(obj);
|
|
280
|
+
if (cached) return cached;
|
|
281
|
+
if (Array.isArray(obj)) {
|
|
282
|
+
const mapped = obj.map((item) => {
|
|
283
|
+
if (item && typeof item === "object") {
|
|
284
|
+
return createProxy(item);
|
|
267
285
|
}
|
|
286
|
+
return item;
|
|
287
|
+
});
|
|
288
|
+
cache.set(obj, mapped);
|
|
289
|
+
return mapped;
|
|
290
|
+
}
|
|
291
|
+
const proxy = new Proxy(obj, {
|
|
292
|
+
get(target, prop) {
|
|
293
|
+
if (typeof prop === "symbol") return target[prop];
|
|
294
|
+
if (SKIP_PROPS.has(prop)) return target[prop];
|
|
268
295
|
const value = target[prop];
|
|
269
|
-
if (typeof value === "string" && value.length > 0) {
|
|
270
|
-
|
|
271
|
-
return node || value;
|
|
272
|
-
}
|
|
273
|
-
if (Array.isArray(value)) {
|
|
274
|
-
return value.map((item) => {
|
|
275
|
-
if (typeof item === "string") {
|
|
276
|
-
const node = RED.nodes.getNode(item)?._node;
|
|
277
|
-
return node || item;
|
|
278
|
-
}
|
|
279
|
-
if (item && typeof item === "object") {
|
|
280
|
-
return createProxy(item);
|
|
281
|
-
}
|
|
282
|
-
return item;
|
|
283
|
-
});
|
|
296
|
+
if (typeof value === "string" && value.length > 0 && nodeRefProps.has(prop)) {
|
|
297
|
+
return RED.nodes.getNode(value)?._node ?? value;
|
|
284
298
|
}
|
|
285
299
|
if (value && typeof value === "object") {
|
|
286
300
|
return createProxy(value);
|
|
287
301
|
}
|
|
288
302
|
return value;
|
|
303
|
+
},
|
|
304
|
+
set(_target, prop) {
|
|
305
|
+
throw new NrgError(
|
|
306
|
+
`Cannot set property '${String(prop)}' on read-only node config`
|
|
307
|
+
);
|
|
289
308
|
}
|
|
290
309
|
});
|
|
310
|
+
cache.set(obj, proxy);
|
|
311
|
+
return proxy;
|
|
291
312
|
};
|
|
292
313
|
return createProxy(config);
|
|
293
314
|
}
|
|
@@ -369,7 +390,11 @@ var Node = class {
|
|
|
369
390
|
);
|
|
370
391
|
}
|
|
371
392
|
}
|
|
372
|
-
this.config = setupConfigProxy(
|
|
393
|
+
this.config = setupConfigProxy(
|
|
394
|
+
RED,
|
|
395
|
+
config,
|
|
396
|
+
constructor.configSchema
|
|
397
|
+
);
|
|
373
398
|
if (constructor.credentialsSchema && credentials) {
|
|
374
399
|
this.log("Validating credentials");
|
|
375
400
|
const credResult = validator2.validate(
|
|
@@ -785,7 +810,7 @@ function serveNrgResources(RED) {
|
|
|
785
810
|
httpAdmin.use(function(req, res, next) {
|
|
786
811
|
const prefix = "/nrg/assets/";
|
|
787
812
|
if (!req.path.startsWith(prefix)) return next();
|
|
788
|
-
let reqPath = req.path.slice(prefix.length)
|
|
813
|
+
let reqPath = req.path.slice(prefix.length);
|
|
789
814
|
if (reqPath === "vue.esm-browser.prod.js" && process.env.NODE_ENV !== "production") {
|
|
790
815
|
const devPath = import_path.default.resolve(clientDir, "vue.esm-browser.js");
|
|
791
816
|
if (import_fs.default.existsSync(devPath)) {
|
|
@@ -793,7 +818,8 @@ function serveNrgResources(RED) {
|
|
|
793
818
|
}
|
|
794
819
|
}
|
|
795
820
|
const filePath = import_path.default.resolve(clientDir, reqPath);
|
|
796
|
-
|
|
821
|
+
const rel = import_path.default.relative(clientDir, filePath);
|
|
822
|
+
if (rel.startsWith("..") || import_path.default.isAbsolute(rel)) return next();
|
|
797
823
|
if (!import_fs.default.existsSync(filePath) || !import_fs.default.statSync(filePath).isFile())
|
|
798
824
|
return next();
|
|
799
825
|
const ext = import_path.default.extname(filePath);
|
|
@@ -805,23 +831,23 @@ async function registerType(RED, NodeClass) {
|
|
|
805
831
|
const NC = NodeClass;
|
|
806
832
|
RED.log.debug(`Registering Type: ${NC.type}`);
|
|
807
833
|
if (!(NC.prototype instanceof Node)) {
|
|
808
|
-
throw new
|
|
834
|
+
throw new NrgError(`${NC.name} must extend IONode or ConfigNode classes`);
|
|
809
835
|
}
|
|
810
836
|
if (!NC.type) {
|
|
811
|
-
throw new
|
|
837
|
+
throw new NrgError("type must be provided when registering the node");
|
|
812
838
|
}
|
|
813
839
|
if (NC.color && !/^#[0-9A-Fa-f]{6}$/.test(NC.color)) {
|
|
814
|
-
throw new
|
|
840
|
+
throw new NrgError(
|
|
815
841
|
`Invalid color for ${NodeClass.type}: ${NC.color} color must be in hex format`
|
|
816
842
|
);
|
|
817
843
|
}
|
|
818
|
-
if (NC.inputs !== void 0 && (!Number.isInteger(NC.inputs) || NC.inputs
|
|
819
|
-
throw new
|
|
844
|
+
if (NC.inputs !== void 0 && (!Number.isInteger(NC.inputs) || NC.inputs !== 0 && NC.inputs !== 1)) {
|
|
845
|
+
throw new NrgError(
|
|
820
846
|
`Invalid number of inputs for ${NodeClass.type}: inputs must be 0 or 1`
|
|
821
847
|
);
|
|
822
848
|
}
|
|
823
849
|
if (NC.outputs !== void 0 && (!Number.isInteger(NC.outputs) || NC.outputs < 0)) {
|
|
824
|
-
throw new
|
|
850
|
+
throw new NrgError(
|
|
825
851
|
`Invalid number of outputs for ${NodeClass.type}: outputs must be a positive integer`
|
|
826
852
|
);
|
|
827
853
|
}
|
|
@@ -830,7 +856,12 @@ async function registerType(RED, NodeClass) {
|
|
|
830
856
|
function(config) {
|
|
831
857
|
RED.nodes.createNode(this, config);
|
|
832
858
|
const node = new NC(RED, this, config, this.credentials);
|
|
833
|
-
this
|
|
859
|
+
Object.defineProperty(this, "_node", {
|
|
860
|
+
value: node,
|
|
861
|
+
writable: false,
|
|
862
|
+
configurable: false,
|
|
863
|
+
enumerable: false
|
|
864
|
+
});
|
|
834
865
|
const createdPromise = Promise.resolve(node.created?.()).catch(
|
|
835
866
|
(error) => {
|
|
836
867
|
this.error("Error during created hook: " + error.message);
|
|
@@ -913,6 +944,7 @@ function registerTypes(nodes) {
|
|
|
913
944
|
ConfigNode,
|
|
914
945
|
IONode,
|
|
915
946
|
Node,
|
|
947
|
+
NrgError,
|
|
916
948
|
SchemaType,
|
|
917
949
|
defineSchema,
|
|
918
950
|
registerType,
|
package/package.json
CHANGED
package/src/core/server/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { getCredentialsFromSchema } from "./utils";
|
|
|
4
4
|
import { Node } from "./nodes";
|
|
5
5
|
import { type RED } from "./types";
|
|
6
6
|
import { initValidator } from "./validator";
|
|
7
|
+
import { NrgError } from "../errors";
|
|
7
8
|
|
|
8
9
|
const MIME: Record<string, string> = {
|
|
9
10
|
".js": "application/javascript",
|
|
@@ -32,9 +33,7 @@ function serveNrgResources(RED: RED): void {
|
|
|
32
33
|
httpAdmin.use(function (req: any, res: any, next: any) {
|
|
33
34
|
const prefix = "/nrg/assets/";
|
|
34
35
|
if (!(req.path as string).startsWith(prefix)) return next();
|
|
35
|
-
let reqPath = (req.path as string)
|
|
36
|
-
.slice(prefix.length)
|
|
37
|
-
.replace(/\.\./g, "");
|
|
36
|
+
let reqPath = (req.path as string).slice(prefix.length);
|
|
38
37
|
// Serve the Vue dev build in development for devtools support
|
|
39
38
|
if (
|
|
40
39
|
reqPath === "vue.esm-browser.prod.js" &&
|
|
@@ -46,7 +45,8 @@ function serveNrgResources(RED: RED): void {
|
|
|
46
45
|
}
|
|
47
46
|
}
|
|
48
47
|
const filePath = path.resolve(clientDir, reqPath);
|
|
49
|
-
|
|
48
|
+
const rel = path.relative(clientDir, filePath);
|
|
49
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) return next();
|
|
50
50
|
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile())
|
|
51
51
|
return next();
|
|
52
52
|
const ext = path.extname(filePath);
|
|
@@ -70,24 +70,24 @@ async function registerType(RED: RED, NodeClass: AnyNodeClass) {
|
|
|
70
70
|
const NC = NodeClass as any;
|
|
71
71
|
RED.log.debug(`Registering Type: ${NC.type}`);
|
|
72
72
|
if (!(NC.prototype instanceof Node)) {
|
|
73
|
-
throw new
|
|
73
|
+
throw new NrgError(`${NC.name} must extend IONode or ConfigNode classes`);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
if (!NC.type) {
|
|
77
|
-
throw new
|
|
77
|
+
throw new NrgError("type must be provided when registering the node");
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
if (NC.color && !/^#[0-9A-Fa-f]{6}$/.test(NC.color)) {
|
|
81
|
-
throw new
|
|
81
|
+
throw new NrgError(
|
|
82
82
|
`Invalid color for ${NodeClass.type}: ${NC.color} color must be in hex format`,
|
|
83
83
|
);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
if (
|
|
87
87
|
NC.inputs !== undefined &&
|
|
88
|
-
(!Number.isInteger(NC.inputs) || (NC.inputs
|
|
88
|
+
(!Number.isInteger(NC.inputs) || (NC.inputs !== 0 && NC.inputs !== 1))
|
|
89
89
|
) {
|
|
90
|
-
throw new
|
|
90
|
+
throw new NrgError(
|
|
91
91
|
`Invalid number of inputs for ${NodeClass.type}: inputs must be 0 or 1`,
|
|
92
92
|
);
|
|
93
93
|
}
|
|
@@ -96,7 +96,7 @@ async function registerType(RED: RED, NodeClass: AnyNodeClass) {
|
|
|
96
96
|
NC.outputs !== undefined &&
|
|
97
97
|
(!Number.isInteger(NC.outputs) || NC.outputs < 0)
|
|
98
98
|
) {
|
|
99
|
-
throw new
|
|
99
|
+
throw new NrgError(
|
|
100
100
|
`Invalid number of outputs for ${NodeClass.type}: outputs must be a positive integer`,
|
|
101
101
|
);
|
|
102
102
|
}
|
|
@@ -106,8 +106,14 @@ async function registerType(RED: RED, NodeClass: AnyNodeClass) {
|
|
|
106
106
|
function (this: any, config: any) {
|
|
107
107
|
RED.nodes.createNode(this, config);
|
|
108
108
|
const node = new NC(RED, this, config, this.credentials);
|
|
109
|
-
// NOTE: save node
|
|
110
|
-
|
|
109
|
+
// NOTE: save node instance inside node-red's node so that the proxy can resolve it lazily.
|
|
110
|
+
// Non-writable to prevent accidental clobbering by other code in the process.
|
|
111
|
+
Object.defineProperty(this, "_node", {
|
|
112
|
+
value: node,
|
|
113
|
+
writable: false,
|
|
114
|
+
configurable: false,
|
|
115
|
+
enumerable: false,
|
|
116
|
+
});
|
|
111
117
|
|
|
112
118
|
// NOTE: created promise must be here because we only want it to start after the whole object creation chain has been completed: child -> IONode -> Node -> IONode -> child -> done
|
|
113
119
|
const createdPromise = Promise.resolve(node.created?.()).catch(
|
|
@@ -214,6 +220,7 @@ function registerTypes(nodes: AnyNodeClass[]): NodeRedPackageFunction {
|
|
|
214
220
|
|
|
215
221
|
export { registerType, registerTypes };
|
|
216
222
|
export { Node, IONode, ConfigNode } from "./nodes";
|
|
223
|
+
export { NrgError } from "../errors";
|
|
217
224
|
export type { RED } from "./types";
|
|
218
225
|
export { SchemaType, defineSchema } from "./schemas";
|
|
219
226
|
export type { Schema, Infer } from "./schemas/types";
|
|
@@ -115,7 +115,11 @@ abstract class Node<TConfig = any, TCredentials = any, TSettings = any> {
|
|
|
115
115
|
);
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
-
(this as any).config = setupConfigProxy(
|
|
118
|
+
(this as any).config = setupConfigProxy(
|
|
119
|
+
RED,
|
|
120
|
+
config,
|
|
121
|
+
constructor.configSchema,
|
|
122
|
+
);
|
|
119
123
|
|
|
120
124
|
if (constructor.credentialsSchema && credentials) {
|
|
121
125
|
this.log("Validating credentials");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ResolveNodeRefs } from "../schemas/types";
|
|
2
2
|
import type { RED, NodeRedContextStore } from "../types";
|
|
3
3
|
import type { NodeContextStore } from "./types";
|
|
4
|
+
import { NrgError } from "../../errors";
|
|
4
5
|
|
|
5
6
|
function setupContext(
|
|
6
7
|
context: NodeRedContextStore,
|
|
@@ -31,50 +32,72 @@ function setupContext(
|
|
|
31
32
|
function setupConfigProxy<T extends object>(
|
|
32
33
|
RED: RED,
|
|
33
34
|
config: T,
|
|
35
|
+
schema?: any,
|
|
34
36
|
): ResolveNodeRefs<T> {
|
|
35
|
-
// NOTE: must not proxy its own id or parents ids
|
|
36
37
|
const SKIP_PROPS = new Set(["id", "_id", "_users"]);
|
|
37
38
|
|
|
39
|
+
// Build a set of property names that are node references based on the schema.
|
|
40
|
+
// Only these properties will have their string values resolved via RED.nodes.getNode().
|
|
41
|
+
const nodeRefProps = new Set<string>();
|
|
42
|
+
if (schema?.properties) {
|
|
43
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
44
|
+
if ((propSchema as any)?.["x-nrg-node-type"]) {
|
|
45
|
+
nodeRefProps.add(key);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Per-node-instance cache: original object/array -> proxy or mapped array.
|
|
51
|
+
// This preserves reference equality: config.server === config.server
|
|
52
|
+
const cache = new WeakMap<object, any>();
|
|
53
|
+
|
|
38
54
|
const createProxy = <O extends object>(obj: O): any => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (typeof prop === "symbol") {
|
|
42
|
-
return target[prop];
|
|
43
|
-
}
|
|
55
|
+
const cached = cache.get(obj);
|
|
56
|
+
if (cached) return cached;
|
|
44
57
|
|
|
45
|
-
|
|
46
|
-
|
|
58
|
+
if (Array.isArray(obj)) {
|
|
59
|
+
// Map once, cache the result array so identity is stable across reads
|
|
60
|
+
const mapped = obj.map((item) => {
|
|
61
|
+
if (item && typeof item === "object") {
|
|
62
|
+
return createProxy(item);
|
|
47
63
|
}
|
|
64
|
+
return item;
|
|
65
|
+
});
|
|
66
|
+
cache.set(obj, mapped);
|
|
67
|
+
return mapped;
|
|
68
|
+
}
|
|
48
69
|
|
|
49
|
-
|
|
70
|
+
const proxy = new Proxy(obj, {
|
|
71
|
+
get(target: any, prop: string | symbol): any {
|
|
72
|
+
if (typeof prop === "symbol") return target[prop];
|
|
73
|
+
if (SKIP_PROPS.has(prop)) return target[prop];
|
|
50
74
|
|
|
51
|
-
|
|
52
|
-
// NOTE: using the instance provided by the user instead of node-red's internal one
|
|
53
|
-
const node = RED.nodes.getNode(value)?._node;
|
|
54
|
-
return node || value;
|
|
55
|
-
}
|
|
75
|
+
const value = target[prop];
|
|
56
76
|
|
|
57
|
-
if
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (item && typeof item === "object") {
|
|
65
|
-
return createProxy(item);
|
|
66
|
-
}
|
|
67
|
-
return item;
|
|
68
|
-
});
|
|
77
|
+
// Only resolve strings as node references if the schema marks the property
|
|
78
|
+
if (
|
|
79
|
+
typeof value === "string" &&
|
|
80
|
+
value.length > 0 &&
|
|
81
|
+
nodeRefProps.has(prop)
|
|
82
|
+
) {
|
|
83
|
+
return RED.nodes.getNode(value)?._node ?? value;
|
|
69
84
|
}
|
|
70
85
|
|
|
71
86
|
if (value && typeof value === "object") {
|
|
72
|
-
return createProxy(value);
|
|
87
|
+
return createProxy(value); // hits the cache on repeat access
|
|
73
88
|
}
|
|
74
89
|
|
|
75
90
|
return value;
|
|
76
91
|
},
|
|
92
|
+
set(_target: any, prop: string | symbol): boolean {
|
|
93
|
+
throw new NrgError(
|
|
94
|
+
`Cannot set property '${String(prop)}' on read-only node config`,
|
|
95
|
+
);
|
|
96
|
+
},
|
|
77
97
|
});
|
|
98
|
+
|
|
99
|
+
cache.set(obj, proxy);
|
|
100
|
+
return proxy;
|
|
78
101
|
};
|
|
79
102
|
|
|
80
103
|
return createProxy(config) as ResolveNodeRefs<T>;
|