@bonsae/nrg 0.5.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bonsae/nrg",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
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",
@@ -0,0 +1,9 @@
1
+ class NrgError extends Error {
2
+ constructor(message: string) {
3
+ super(message);
4
+ this.name = "NrgError";
5
+ Object.setPrototypeOf(this, NrgError.prototype);
6
+ }
7
+ }
8
+
9
+ export { NrgError };
@@ -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,11 +33,20 @@ 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
- const reqPath = (req.path as string)
36
- .slice(prefix.length)
37
- .replace(/\.\./g, "");
36
+ let reqPath = (req.path as string).slice(prefix.length);
37
+ // Serve the Vue dev build in development for devtools support
38
+ if (
39
+ reqPath === "vue.esm-browser.prod.js" &&
40
+ process.env.NODE_ENV !== "production"
41
+ ) {
42
+ const devPath = path.resolve(clientDir, "vue.esm-browser.js");
43
+ if (fs.existsSync(devPath)) {
44
+ reqPath = "vue.esm-browser.js";
45
+ }
46
+ }
38
47
  const filePath = path.resolve(clientDir, reqPath);
39
- if (!filePath.startsWith(clientDir)) return next();
48
+ const rel = path.relative(clientDir, filePath);
49
+ if (rel.startsWith("..") || path.isAbsolute(rel)) return next();
40
50
  if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile())
41
51
  return next();
42
52
  const ext = path.extname(filePath);
@@ -60,24 +70,24 @@ async function registerType(RED: RED, NodeClass: AnyNodeClass) {
60
70
  const NC = NodeClass as any;
61
71
  RED.log.debug(`Registering Type: ${NC.type}`);
62
72
  if (!(NC.prototype instanceof Node)) {
63
- throw new Error(`${NC.name} must extend IONode or ConfigNode classes`);
73
+ throw new NrgError(`${NC.name} must extend IONode or ConfigNode classes`);
64
74
  }
65
75
 
66
76
  if (!NC.type) {
67
- throw new Error("type must be provided when registering the node");
77
+ throw new NrgError("type must be provided when registering the node");
68
78
  }
69
79
 
70
80
  if (NC.color && !/^#[0-9A-Fa-f]{6}$/.test(NC.color)) {
71
- throw new Error(
81
+ throw new NrgError(
72
82
  `Invalid color for ${NodeClass.type}: ${NC.color} color must be in hex format`,
73
83
  );
74
84
  }
75
85
 
76
86
  if (
77
87
  NC.inputs !== undefined &&
78
- (!Number.isInteger(NC.inputs) || (NC.inputs != 0 && NC.inputs != 1))
88
+ (!Number.isInteger(NC.inputs) || (NC.inputs !== 0 && NC.inputs !== 1))
79
89
  ) {
80
- throw new Error(
90
+ throw new NrgError(
81
91
  `Invalid number of inputs for ${NodeClass.type}: inputs must be 0 or 1`,
82
92
  );
83
93
  }
@@ -86,7 +96,7 @@ async function registerType(RED: RED, NodeClass: AnyNodeClass) {
86
96
  NC.outputs !== undefined &&
87
97
  (!Number.isInteger(NC.outputs) || NC.outputs < 0)
88
98
  ) {
89
- throw new Error(
99
+ throw new NrgError(
90
100
  `Invalid number of outputs for ${NodeClass.type}: outputs must be a positive integer`,
91
101
  );
92
102
  }
@@ -96,8 +106,14 @@ async function registerType(RED: RED, NodeClass: AnyNodeClass) {
96
106
  function (this: any, config: any) {
97
107
  RED.nodes.createNode(this, config);
98
108
  const node = new NC(RED, this, config, this.credentials);
99
- // NOTE: save node intance inside node-red's node so that the proxy can resolve it lazyly
100
- this._node = node;
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
+ });
101
117
 
102
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
103
119
  const createdPromise = Promise.resolve(node.created?.()).catch(
@@ -204,6 +220,7 @@ function registerTypes(nodes: AnyNodeClass[]): NodeRedPackageFunction {
204
220
 
205
221
  export { registerType, registerTypes };
206
222
  export { Node, IONode, ConfigNode } from "./nodes";
223
+ export { NrgError } from "../errors";
207
224
  export type { RED } from "./types";
208
225
  export { SchemaType, defineSchema } from "./schemas";
209
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(RED, config);
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
- return new Proxy(obj, {
40
- get(target: any, prop: string | symbol): any {
41
- if (typeof prop === "symbol") {
42
- return target[prop];
43
- }
55
+ const cached = cache.get(obj);
56
+ if (cached) return cached;
44
57
 
45
- if (SKIP_PROPS.has(prop)) {
46
- return target[prop];
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
- const value = target[prop];
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
- if (typeof value === "string" && value.length > 0) {
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 (Array.isArray(value)) {
58
- return value.map((item) => {
59
- if (typeof item === "string") {
60
- // NOTE: using the instance provided by the user instead of node-red's internal one
61
- const node = RED.nodes.getNode(item)?._node;
62
- return node || item;
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>;