@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 +34 -0
- package/package.json +1 -1
- package/server/index.cjs +2 -0
- package/test/index.js +1 -168
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
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) {
|