@bonsae/nrg 0.10.0 → 0.10.1

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 CHANGED
@@ -155,6 +155,9 @@ src/
155
155
  │ │ └── index.ts # registerTypes, exports
156
156
  │ ├── constants.ts
157
157
  │ └── validator.ts # AJV-based validation
158
+ ├── test/ # Test utilities for consumers
159
+ │ ├── index.ts # createNode, receive, close, reset
160
+ │ └── mocks.ts # RED and Node-RED node mocks
158
161
  ├── vite/ # Build tooling
159
162
  │ ├── plugin.ts # Vite plugin factory
160
163
  │ ├── plugins/ # Dev server, build orchestration
@@ -167,6 +170,37 @@ src/
167
170
  └── server.json
168
171
  ```
169
172
 
173
+ ## Testing
174
+
175
+ Test your nodes' server-side logic with `@bonsae/nrg/test`:
176
+
177
+ ```bash
178
+ pnpm add -D vitest
179
+ ```
180
+
181
+ ```typescript
182
+ // tests/my-node.test.ts
183
+ import { describe, it, expect } from "vitest";
184
+ import { createNode } from "@bonsae/nrg/test";
185
+ import MyNode from "../src/server/nodes/my-node";
186
+
187
+ describe("my-node", () => {
188
+ it("should process messages", async () => {
189
+ const { node } = await createNode(MyNode, {
190
+ config: { greeting: "hello" },
191
+ });
192
+
193
+ await node.receive({ payload: "world" });
194
+
195
+ expect(node.sent(0)).toEqual([{ payload: "hello world" }]);
196
+ });
197
+ });
198
+ ```
199
+
200
+ ```bash
201
+ npx vitest run
202
+ ```
203
+
170
204
  ## Development
171
205
 
172
206
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bonsae/nrg",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
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",
package/server/index.cjs CHANGED
@@ -39,6 +39,7 @@ __export(index_exports, {
39
39
  defineIONode: () => defineIONode,
40
40
  defineModule: () => defineModule,
41
41
  defineSchema: () => defineSchema,
42
+ initValidator: () => initValidator,
42
43
  registerType: () => registerType,
43
44
  registerTypes: () => registerTypes
44
45
  });
@@ -1043,6 +1044,7 @@ function defineModule(definition) {
1043
1044
  defineIONode,
1044
1045
  defineModule,
1045
1046
  defineSchema,
1047
+ initValidator,
1046
1048
  registerType,
1047
1049
  registerTypes
1048
1050
  });
package/test/index.js CHANGED
@@ -145,175 +145,8 @@ function createMockNodeRedNode(options = {}) {
145
145
  };
146
146
  }
147
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
148
  // src/test/index.ts
149
+ import { initValidator } from "@bonsae/nrg/server";
317
150
  function buildConfig(NodeClass, userConfig = {}) {
318
151
  const defaults = {};
319
152
  if (NodeClass.configSchema?.properties) {