@bonsae/nrg 0.13.1 → 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/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 createMockRED(options = {}) {
7
- const { nodes = {}, settings = {} } = options;
8
- return {
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
- events: {
86
- on: vi.fn(),
87
- emit: vi.fn()
145
+ version: vi.fn(() => "0.0.0-test"),
146
+ validator: void 0,
147
+ registerNode(id, nodeRedNode) {
148
+ nodes[id] = nodeRedNode;
88
149
  },
89
- hooks: {
90
- add: vi.fn(),
91
- remove: vi.fn()
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 createMockNodeRedNode(options = {}) {
176
+ function createNodeRedNode(options = {}) {
117
177
  const nodeCtx = createContextStore();
118
178
  const flowCtx = createContextStore();
119
179
  const globalCtx = createContextStore();
@@ -123,14 +183,14 @@ function createMockNodeRedNode(options = {}) {
123
183
  global: globalCtx
124
184
  };
125
185
  return {
126
- id: options.id ?? `test-${Math.random().toString(36).slice(2, 10)}`,
186
+ id: options.id ?? `node-${Math.random().toString(36).slice(2, 10)}`,
127
187
  type: options.type ?? "test-node",
128
- name: options.name ?? "",
188
+ name: options.name ?? "test-node",
129
189
  z: options.z ?? "flow-1",
130
190
  x: 100,
131
191
  y: 200,
132
- g: void 0,
133
- wires: options.wires ?? [[]],
192
+ g: "group-1",
193
+ wires: options.wires ?? [["node-2"]],
134
194
  credentials: options.credentials ?? {},
135
195
  log: vi.fn(),
136
196
  warn: vi.fn(),
@@ -145,32 +205,201 @@ function createMockNodeRedNode(options = {}) {
145
205
  };
146
206
  }
147
207
 
208
+ // src/core/validator.ts
209
+ import Ajv from "ajv";
210
+ import addFormats from "ajv-formats";
211
+ import addErrors from "ajv-errors";
212
+ var Validator = class {
213
+ ajv;
214
+ constructor(options) {
215
+ const { customKeywords, customFormats, ...ajvOptions } = options || {};
216
+ this.ajv = new Ajv({
217
+ allErrors: true,
218
+ code: {
219
+ source: false
220
+ },
221
+ coerceTypes: true,
222
+ removeAdditional: false,
223
+ strict: false,
224
+ strictSchema: false,
225
+ useDefaults: true,
226
+ validateFormats: true,
227
+ // NOTE: typebox handles validation via typescript
228
+ // NOTE: if true, types that are not serializable JSON, like Function, would not work
229
+ validateSchema: false,
230
+ verbose: true,
231
+ ...ajvOptions
232
+ });
233
+ addFormats(this.ajv);
234
+ addErrors(this.ajv);
235
+ this.addCustomKeywords(customKeywords || []);
236
+ this.addCustomFormats(customFormats || {});
237
+ }
238
+ /**
239
+ * Add custom keywords to the validator
240
+ */
241
+ addCustomKeywords(keywords) {
242
+ if (!keywords) return;
243
+ keywords.forEach((keyword) => {
244
+ this.ajv.addKeyword(keyword);
245
+ });
246
+ }
247
+ /**
248
+ * Add custom formats to the validator
249
+ */
250
+ addCustomFormats(formats) {
251
+ if (!formats) return;
252
+ Object.entries(formats).forEach(([name, validator]) => {
253
+ if (validator instanceof RegExp) {
254
+ this.ajv.addFormat(name, validator);
255
+ } else {
256
+ this.ajv.addFormat(name, { validate: validator });
257
+ }
258
+ });
259
+ }
260
+ /**
261
+ * Create a validator function with caching
262
+ * @param schema - JSON Schema to validate against
263
+ * @param cacheKey - Optional cache key for reusing validators
264
+ */
265
+ createValidator(schema, cacheKey) {
266
+ if (cacheKey && !schema.$id) {
267
+ schema.$id = cacheKey;
268
+ }
269
+ if (schema.$id) {
270
+ const cached = this.ajv.getSchema(schema.$id);
271
+ if (cached) return cached;
272
+ }
273
+ const validator = this.ajv.compile(schema);
274
+ return validator;
275
+ }
276
+ /**
277
+ * Validate data against a schema and return a structured result
278
+ */
279
+ validate(data, schema, options) {
280
+ const validator = this.createValidator(schema, options?.cacheKey);
281
+ const valid = validator(data);
282
+ if (!valid) {
283
+ const errorMessage = this.formatErrors(validator.errors);
284
+ if (options?.throwOnError) {
285
+ throw new ValidationError(errorMessage, validator.errors || []);
286
+ }
287
+ return {
288
+ valid: false,
289
+ errors: validator.errors || void 0,
290
+ errorMessage
291
+ };
292
+ }
293
+ return {
294
+ valid: true,
295
+ data
296
+ };
297
+ }
298
+ /**
299
+ * Format errors into a human-readable string
300
+ */
301
+ formatErrors(errors, options) {
302
+ if (!errors || errors.length === 0) {
303
+ return "No errors";
304
+ }
305
+ return this.ajv.errorsText(errors, {
306
+ separator: "; ",
307
+ dataVar: "data",
308
+ ...options
309
+ });
310
+ }
311
+ /**
312
+ * Get detailed error information
313
+ */
314
+ getDetailedErrors(errors) {
315
+ if (!errors || errors.length === 0) return [];
316
+ return errors.map((error) => ({
317
+ field: error.instancePath || "/",
318
+ message: error.message || "Validation failed",
319
+ keyword: error.keyword,
320
+ params: error.params,
321
+ schemaPath: error.schemaPath
322
+ }));
323
+ }
324
+ /**
325
+ * Add a schema to the validator for reference
326
+ */
327
+ addSchema(schema, key) {
328
+ this.ajv.addSchema(schema, key);
329
+ return this;
330
+ }
331
+ /**
332
+ * Remove a schema from the validator
333
+ */
334
+ removeSchema(key) {
335
+ this.ajv.removeSchema(key);
336
+ return this;
337
+ }
338
+ };
339
+ var ValidationError = class _ValidationError extends Error {
340
+ constructor(message, errors) {
341
+ super(message);
342
+ this.errors = errors;
343
+ this.name = "ValidationError";
344
+ Object.setPrototypeOf(this, _ValidationError.prototype);
345
+ }
346
+ };
347
+
348
+ // src/core/server/validation.ts
349
+ function initValidator(RED) {
350
+ const nrg = {
351
+ validator: new Validator({
352
+ customKeywords: [
353
+ {
354
+ keyword: "x-nrg-skip-validation",
355
+ schemaType: "boolean",
356
+ valid: true
357
+ },
358
+ {
359
+ keyword: "x-nrg-node-type",
360
+ type: "string",
361
+ validate: (schemaValue, dataValue) => {
362
+ if (!dataValue) return true;
363
+ const node = RED.nodes.getNode(dataValue);
364
+ return node?.type === schemaValue;
365
+ }
366
+ }
367
+ ],
368
+ customFormats: {
369
+ "node-id": /^[a-zA-Z0-9-_]+$/,
370
+ "flow-id": /^[a-f0-9]{16}$/,
371
+ "topic-path": (data) => /^[a-zA-Z0-9/_-]+$/.test(data)
372
+ }
373
+ })
374
+ };
375
+ Object.defineProperty(RED, "_nrg", {
376
+ value: nrg,
377
+ writable: false,
378
+ enumerable: false,
379
+ configurable: false
380
+ });
381
+ Object.defineProperty(RED, "validator", {
382
+ get: () => nrg.validator,
383
+ enumerable: false,
384
+ configurable: false
385
+ });
386
+ }
387
+
148
388
  // src/test/index.ts
149
- import { initValidator } from "@bonsae/nrg/server";
150
389
  function buildConfig(NodeClass, userConfig = {}) {
151
390
  const defaults = {};
152
391
  if (NodeClass.configSchema?.properties) {
153
392
  for (const [key, prop] of Object.entries(
154
393
  NodeClass.configSchema.properties
155
394
  )) {
156
- if (prop.default !== void 0) {
157
- defaults[key] = prop.default;
395
+ const schemaProp = prop;
396
+ if (schemaProp.default !== void 0) {
397
+ defaults[key] = schemaProp.default;
158
398
  }
159
399
  }
160
400
  }
161
401
  return { ...defaults, ...userConfig };
162
402
  }
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
403
  function attachHelpers(node, nodeRedNode) {
175
404
  const sentMessages = [];
176
405
  const statusCalls = [];
@@ -248,9 +477,11 @@ async function createNode(NodeClass, options = {}) {
248
477
  resolvedConfig[key] = value;
249
478
  }
250
479
  }
251
- const redNodes = buildNodeRedNodes(configNodes);
252
- const RED = createMockRED({ nodes: redNodes, settings });
480
+ const RED = createNodeRedRuntime({ settings });
253
481
  initValidator(RED);
482
+ for (const [id, value] of Object.entries(configNodes)) {
483
+ RED.registerNrgNode(id, value);
484
+ }
254
485
  const configDefaults = {
255
486
  id: overrideOpts.id ?? `test-${Math.random().toString(36).slice(2, 10)}`,
256
487
  type: NodeClass.type
@@ -262,7 +493,7 @@ async function createNode(NodeClass, options = {}) {
262
493
  ...configDefaults,
263
494
  ...resolvedConfig
264
495
  });
265
- const nodeRedNode = createMockNodeRedNode({
496
+ const nodeRedNode = createNodeRedNode({
266
497
  id: config.id,
267
498
  type: NodeClass.type,
268
499
  name: config.name ?? "",