@bonsae/nrg 0.25.0 → 0.26.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.
Files changed (34) hide show
  1. package/package.json +6 -4
  2. package/server/index.cjs +1413 -2
  3. package/server/resources/nrg-client.js +10 -0
  4. package/test/client/component/config.js +9 -9
  5. package/test/client/component/index.js +230 -42
  6. package/test/client/component/nrg.css +1 -0
  7. package/test/client/component/schemas.js +37 -0
  8. package/test/client/component/setup.js +1473 -198
  9. package/test/client/e2e/index.js +33 -35
  10. package/test/client/unit/index.js +17 -2
  11. package/test/server/integration/index.js +1222 -11
  12. package/test/server/unit/index.js +184 -2
  13. package/types/client.d.ts +1 -1
  14. package/types/server.d.ts +8 -2
  15. package/types/shims/components.d.ts +8 -8
  16. package/types/shims/{client → core/client}/types.d.ts +2 -2
  17. package/types/shims/core/schema-options.d.ts +24 -0
  18. package/types/shims/schema-options.d.ts +17 -17
  19. package/types/test-client-component-schemas.d.ts +73 -0
  20. package/types/test-client-component.d.ts +153 -7
  21. package/types/test-client-unit.d.ts +105 -4
  22. package/types/test-server-integration.d.ts +64 -63
  23. package/types/test-server-unit.d.ts +2 -1
  24. package/vite/index.js +33 -35
  25. /package/types/shims/{client → core/client}/form/components/node-red-config-input.vue.d.ts +0 -0
  26. /package/types/shims/{client → core/client}/form/components/node-red-editor-input.vue.d.ts +0 -0
  27. /package/types/shims/{client → core/client}/form/components/node-red-input-label.vue.d.ts +0 -0
  28. /package/types/shims/{client → core/client}/form/components/node-red-input.vue.d.ts +0 -0
  29. /package/types/shims/{client → core/client}/form/components/node-red-json-schema-form.vue.d.ts +0 -0
  30. /package/types/shims/{client → core/client}/form/components/node-red-select-input.vue.d.ts +0 -0
  31. /package/types/shims/{client → core/client}/form/components/node-red-toggle.vue.d.ts +0 -0
  32. /package/types/shims/{client → core/client}/form/components/node-red-typed-input.vue.d.ts +0 -0
  33. /package/types/shims/{constants.d.ts → core/constants.d.ts} +0 -0
  34. /package/types/shims/{brands.d.ts → core/types.d.ts} +0 -0
@@ -235,9 +235,191 @@ function createNodeRedNode(options = {}) {
235
235
  };
236
236
  }
237
237
 
238
+ // src/core/validator.ts
239
+ import Ajv from "ajv";
240
+ import addFormats from "ajv-formats";
241
+ import addErrors from "ajv-errors";
242
+ var Validator = class {
243
+ ajv;
244
+ constructor(options) {
245
+ const { customKeywords, customFormats, ...ajvOptions } = options || {};
246
+ this.ajv = new Ajv({
247
+ allErrors: true,
248
+ code: {
249
+ source: false
250
+ },
251
+ coerceTypes: true,
252
+ removeAdditional: false,
253
+ strict: false,
254
+ strictSchema: false,
255
+ useDefaults: true,
256
+ validateFormats: true,
257
+ // NOTE: typebox handles validation via typescript
258
+ // NOTE: if true, types that are not serializable JSON, like Function, would not work
259
+ validateSchema: false,
260
+ verbose: true,
261
+ ...ajvOptions
262
+ });
263
+ addFormats(this.ajv);
264
+ addErrors(this.ajv);
265
+ this.addCustomKeywords(customKeywords || []);
266
+ this.addCustomFormats(customFormats || {});
267
+ }
268
+ /**
269
+ * Add custom keywords to the validator
270
+ */
271
+ addCustomKeywords(keywords) {
272
+ if (!keywords) return;
273
+ keywords.forEach((keyword) => {
274
+ this.ajv.addKeyword(keyword);
275
+ });
276
+ }
277
+ /**
278
+ * Add custom formats to the validator
279
+ */
280
+ addCustomFormats(formats) {
281
+ if (!formats) return;
282
+ Object.entries(formats).forEach(([name, validator]) => {
283
+ if (validator instanceof RegExp) {
284
+ this.ajv.addFormat(name, validator);
285
+ } else {
286
+ this.ajv.addFormat(name, { validate: validator });
287
+ }
288
+ });
289
+ }
290
+ /**
291
+ * Create a validator function with caching
292
+ * @param schema - JSON Schema to validate against
293
+ * @param cacheKey - Optional cache key for reusing validators
294
+ */
295
+ createValidator(schema, cacheKey) {
296
+ if (cacheKey && !schema.$id) {
297
+ schema.$id = cacheKey;
298
+ }
299
+ if (schema.$id) {
300
+ const cached = this.ajv.getSchema(schema.$id);
301
+ if (cached) return cached;
302
+ }
303
+ const validator = this.ajv.compile(schema);
304
+ return validator;
305
+ }
306
+ /**
307
+ * Validate data against a schema and return a structured result
308
+ */
309
+ validate(data, schema, options) {
310
+ const validator = this.createValidator(schema, options?.cacheKey);
311
+ const valid = validator(data);
312
+ if (!valid) {
313
+ const errorMessage = this.formatErrors(validator.errors);
314
+ if (options?.throwOnError) {
315
+ throw new ValidationError(errorMessage, validator.errors || []);
316
+ }
317
+ return {
318
+ valid: false,
319
+ errors: validator.errors || void 0,
320
+ errorMessage
321
+ };
322
+ }
323
+ return {
324
+ valid: true,
325
+ data
326
+ };
327
+ }
328
+ /**
329
+ * Format errors into a human-readable string
330
+ */
331
+ formatErrors(errors, options) {
332
+ if (!errors || errors.length === 0) {
333
+ return "No errors";
334
+ }
335
+ return this.ajv.errorsText(errors, {
336
+ separator: "; ",
337
+ dataVar: "data",
338
+ ...options
339
+ });
340
+ }
341
+ /**
342
+ * Get detailed error information
343
+ */
344
+ getDetailedErrors(errors) {
345
+ if (!errors || errors.length === 0) return [];
346
+ return errors.map((error) => ({
347
+ field: error.instancePath || "/",
348
+ message: error.message || "Validation failed",
349
+ keyword: error.keyword,
350
+ params: error.params,
351
+ schemaPath: error.schemaPath
352
+ }));
353
+ }
354
+ /**
355
+ * Add a schema to the validator for reference
356
+ */
357
+ addSchema(schema, key) {
358
+ this.ajv.addSchema(schema, key);
359
+ return this;
360
+ }
361
+ /**
362
+ * Remove a schema from the validator
363
+ */
364
+ removeSchema(key) {
365
+ this.ajv.removeSchema(key);
366
+ return this;
367
+ }
368
+ };
369
+ var ValidationError = class _ValidationError extends Error {
370
+ constructor(message, errors) {
371
+ super(message);
372
+ this.errors = errors;
373
+ this.name = "ValidationError";
374
+ Object.setPrototypeOf(this, _ValidationError.prototype);
375
+ }
376
+ };
377
+
378
+ // src/core/server/validation.ts
379
+ function initValidator(RED) {
380
+ if (RED.validator) return;
381
+ const nrg = {
382
+ validator: new Validator({
383
+ customKeywords: [
384
+ {
385
+ keyword: "x-nrg-skip-validation",
386
+ schemaType: "boolean",
387
+ valid: true
388
+ },
389
+ {
390
+ keyword: "x-nrg-node-type",
391
+ type: "string",
392
+ validate: (schemaValue, dataValue) => {
393
+ if (!dataValue) return true;
394
+ const node = RED.nodes.getNode(dataValue);
395
+ return node?.type === schemaValue;
396
+ }
397
+ }
398
+ ],
399
+ customFormats: {
400
+ "node-id": /^[a-zA-Z0-9-_]+$/,
401
+ "flow-id": /^[a-f0-9]{16}$/,
402
+ "topic-path": (data) => /^[a-zA-Z0-9/_-]+$/.test(data)
403
+ }
404
+ })
405
+ };
406
+ Object.defineProperty(RED, "_nrg", {
407
+ value: nrg,
408
+ writable: false,
409
+ enumerable: false,
410
+ configurable: false
411
+ });
412
+ Object.defineProperty(RED, "validator", {
413
+ get: () => nrg.validator,
414
+ enumerable: false,
415
+ configurable: false
416
+ });
417
+ }
418
+
419
+ // src/core/server/nodes/symbols.ts
420
+ var WIRE_HANDLERS = Symbol.for("nrg.wireHandlers");
421
+
238
422
  // src/test/server/unit/index.ts
239
- import { initValidator } from "@bonsae/nrg-runtime/internal/server";
240
- import { WIRE_HANDLERS } from "@bonsae/nrg-runtime/internal/server";
241
423
  import { Kind } from "@sinclair/typebox";
242
424
  function builtinPortIndex(node, name) {
243
425
  if (name !== "error" && name !== "complete" && name !== "status") {
package/types/client.d.ts CHANGED
@@ -228,7 +228,7 @@ export interface TypedInputValue {
228
228
  /**
229
229
  * Maps a schema's static type to the raw values the editor form holds.
230
230
  * The server counterpart (`ResolvedStatic` in server/schemas/types) maps the
231
- * same brands — shared via core/brands — to resolved runtime values instead.
231
+ * same brands — shared via core/types — to resolved runtime values instead.
232
232
  * - `NodeRef<T>` → `string` (the referenced node's id)
233
233
  * - `TypedInput<T>` → `TypedInputValue` (raw value + type pair)
234
234
  * - Functions pass through, arrays and objects map recursively
package/types/server.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /// <reference path="./shims/typebox.d.ts" />
2
2
  // Generated by dts-bundle-generator v9.5.1
3
3
 
4
- import { Kind, ObjectOptions, SchemaOptions, Static, TArray, TBoolean, TConst, TEnum, TFunction, TInteger, TIntersect, TLiteral, TNull, TNumber, TObject, TOptional, TProperties, TRecord, TRef, TSchema, TString, TTuple, TUnion } from '@sinclair/typebox';
4
+ import { Kind, ObjectOptions, SchemaOptions, Static, StringFormatOption, StringOptions, TArray, TBoolean, TConst, TEnum, TFunction, TInteger, TIntersect, TLiteral, TNull, TNumber, TObject, TOptional, TProperties, TRecord, TRef, TSchema, TString, TTuple, TUnion } from '@sinclair/typebox';
5
5
  import { EventEmitter } from 'events';
6
6
  import { Express as Express$1 } from 'express';
7
7
  import { Http2ServerRequest } from 'http2';
@@ -508,9 +508,14 @@ declare function NodeRef<T extends (new (...args: any[]) => any) & {
508
508
  type: string;
509
509
  }>(nodeClass: T, options?: NrgSchemaOptions): TNodeRef<InstanceType<T>>;
510
510
  declare function TypedInput$1<T = unknown>(options?: NrgSchemaOptions): TTypedInput<T>;
511
+ type NrgStringFormat = StringFormatOption | "password" | "byte" | "binary" | "url" | "duration" | "iso-time" | "iso-date-time" | "json-pointer-uri-fragment";
512
+ interface NrgStringOptions extends Omit<StringOptions, "format"> {
513
+ format?: NrgStringFormat;
514
+ }
515
+ declare function NrgString(options?: NrgStringOptions): TString;
511
516
  declare function OutputReturnProperties(options?: NrgSchemaOptions & {
512
517
  default?: Record<number, string>;
513
- }): import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TNumber, import("@sinclair/typebox").TString>;
518
+ }): import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TNumber, TString>;
514
519
  declare function OutputContextModes(options?: NrgSchemaOptions & {
515
520
  default?: Record<number, "carry" | "trace" | "reset">;
516
521
  }): import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TNumber, import("@sinclair/typebox").TUnion<[
@@ -530,6 +535,7 @@ declare function OutputContextModes(options?: NrgSchemaOptions & {
530
535
  * use {@link NodeRef} / {@link TypedInput} instead. See the Schemas guide.
531
536
  */
532
537
  export declare const SchemaType: import("@sinclair/typebox").JavaScriptTypeBuilder & {
538
+ String: typeof NrgString;
533
539
  NodeRef: typeof NodeRef;
534
540
  TypedInput: typeof TypedInput$1;
535
541
  OutputReturnProperties: typeof OutputReturnProperties;
@@ -11,13 +11,13 @@ declare module "vue" {
11
11
  }
12
12
 
13
13
  export interface GlobalComponents {
14
- NodeRedConfigInput: (typeof import("./client/form/components/node-red-config-input.vue"))["default"];
15
- NodeRedEditorInput: (typeof import("./client/form/components/node-red-editor-input.vue"))["default"];
16
- NodeRedInputLabel: (typeof import("./client/form/components/node-red-input-label.vue"))["default"];
17
- NodeRedInput: (typeof import("./client/form/components/node-red-input.vue"))["default"];
18
- NodeRedJsonSchemaForm: (typeof import("./client/form/components/node-red-json-schema-form.vue"))["default"];
19
- NodeRedSelectInput: (typeof import("./client/form/components/node-red-select-input.vue"))["default"];
20
- NodeRedToggle: (typeof import("./client/form/components/node-red-toggle.vue"))["default"];
21
- NodeRedTypedInput: (typeof import("./client/form/components/node-red-typed-input.vue"))["default"];
14
+ NodeRedConfigInput: (typeof import("./core/client/form/components/node-red-config-input.vue"))["default"];
15
+ NodeRedEditorInput: (typeof import("./core/client/form/components/node-red-editor-input.vue"))["default"];
16
+ NodeRedInputLabel: (typeof import("./core/client/form/components/node-red-input-label.vue"))["default"];
17
+ NodeRedInput: (typeof import("./core/client/form/components/node-red-input.vue"))["default"];
18
+ NodeRedJsonSchemaForm: (typeof import("./core/client/form/components/node-red-json-schema-form.vue"))["default"];
19
+ NodeRedSelectInput: (typeof import("./core/client/form/components/node-red-select-input.vue"))["default"];
20
+ NodeRedToggle: (typeof import("./core/client/form/components/node-red-toggle.vue"))["default"];
21
+ NodeRedTypedInput: (typeof import("./core/client/form/components/node-red-typed-input.vue"))["default"];
22
22
  }
23
23
  }
@@ -1,7 +1,7 @@
1
1
  import type { Component, App } from "vue";
2
2
  import type { TSchema, Static } from "@sinclair/typebox";
3
3
  import type { SchemaObject } from "ajv";
4
- import type { NodeRefResolved, TypedInputResolved } from "../brands";
4
+ import type { NodeRefResolved, TypedInputResolved } from "../types";
5
5
  import type { JsonSchemaObjectExtensions } from "../schema-options";
6
6
  export interface NodeStateCredentials {
7
7
  [key: string]: any;
@@ -200,7 +200,7 @@ export interface TypedInputValue {
200
200
  /**
201
201
  * Maps a schema's static type to the raw values the editor form holds.
202
202
  * The server counterpart (`ResolvedStatic` in server/schemas/types) maps the
203
- * same brands — shared via core/brands — to resolved runtime values instead.
203
+ * same brands — shared via core/types — to resolved runtime values instead.
204
204
  * - `NodeRef<T>` → `string` (the referenced node's id)
205
205
  * - `TypedInput<T>` → `TypedInputValue` (raw value + type pair)
206
206
  * - Functions pass through, arrays and objects map recursively
@@ -0,0 +1,24 @@
1
+ /**
2
+ * NRG's JSON Schema vocabulary — the custom keywords the server emits onto
3
+ * serialized schemas and the client consumes for form rendering and
4
+ * validation. Shared at the core root (like types.ts) so both planes derive
5
+ * from one definition instead of drifting copies.
6
+ */
7
+ export interface JsonSchemaObjectExtensions {
8
+ format?: "node-id" | "flow-id" | "topic-path" | (string & {});
9
+ /** expose this settings property to the editor via RED.settings */
10
+ exportable?: boolean;
11
+ /** set by SchemaType.NodeRef — the referenced config node type */
12
+ "x-nrg-node-type"?: string;
13
+ /** set by SchemaType.TypedInput — marks a TypedInput value/type pair */
14
+ "x-nrg-typed-input"?: boolean;
15
+ /** set by markNonValidatable — ajv skips this property */
16
+ "x-nrg-skip-validation"?: boolean;
17
+ /** form rendering hints consumed by the auto-generated editor form */
18
+ "x-nrg-form"?: {
19
+ icon?: string;
20
+ typedInputTypes?: string[];
21
+ editorLanguage?: string;
22
+ toggle?: boolean;
23
+ };
24
+ }
@@ -1,24 +1,24 @@
1
1
  /**
2
2
  * NRG's JSON Schema vocabulary — the custom keywords the server emits onto
3
3
  * serialized schemas and the client consumes for form rendering and
4
- * validation. Shared at the core root (like brands.ts) so both planes derive
4
+ * validation. Shared at the core root (like types.ts) so both planes derive
5
5
  * from one definition instead of drifting copies.
6
6
  */
7
7
  export interface JsonSchemaObjectExtensions {
8
- format?: "node-id" | "flow-id" | "topic-path" | (string & {});
9
- /** expose this settings property to the editor via RED.settings */
10
- exportable?: boolean;
11
- /** set by SchemaType.NodeRef — the referenced config node type */
12
- "x-nrg-node-type"?: string;
13
- /** set by SchemaType.TypedInput — marks a TypedInput value/type pair */
14
- "x-nrg-typed-input"?: boolean;
15
- /** set by markNonValidatable — ajv skips this property */
16
- "x-nrg-skip-validation"?: boolean;
17
- /** form rendering hints consumed by the auto-generated editor form */
18
- "x-nrg-form"?: {
19
- icon?: string;
20
- typedInputTypes?: string[];
21
- editorLanguage?: string;
22
- toggle?: boolean;
23
- };
8
+ format?: "node-id" | "flow-id" | "topic-path" | (string & {});
9
+ /** expose this settings property to the editor via RED.settings */
10
+ exportable?: boolean;
11
+ /** set by SchemaType.NodeRef — the referenced config node type */
12
+ "x-nrg-node-type"?: string;
13
+ /** set by SchemaType.TypedInput — marks a TypedInput value/type pair */
14
+ "x-nrg-typed-input"?: boolean;
15
+ /** set by markNonValidatable — ajv skips this property */
16
+ "x-nrg-skip-validation"?: boolean;
17
+ /** form rendering hints consumed by the auto-generated editor form */
18
+ "x-nrg-form"?: {
19
+ icon?: string;
20
+ typedInputTypes?: string[];
21
+ editorLanguage?: string;
22
+ toggle?: boolean;
23
+ };
24
24
  }
@@ -0,0 +1,73 @@
1
+ // Generated by dts-bundle-generator v9.5.1
2
+
3
+ import { ProvidedContext } from 'vitest';
4
+
5
+ interface GlobalSetupContext {
6
+ provide<K extends keyof ProvidedContext>(key: K, value: ProvidedContext[K]): void;
7
+ }
8
+ /**
9
+ * A node's schemas, serialized to plain JSON. This is the exact shape the vite
10
+ * plugin injects into the production editor bundle (`JSON.stringify` of the
11
+ * TypeBox schema), so component tests validate against the same data the real
12
+ * form does — with no TypeBox `Kind` symbols or server runtime in the browser.
13
+ */
14
+ export interface SerializedNodeSchemas {
15
+ configSchema?: Record<string, unknown>;
16
+ credentialsSchema?: Record<string, unknown>;
17
+ }
18
+ interface NodeClass {
19
+ type?: string;
20
+ configSchema?: unknown;
21
+ credentialsSchema?: unknown;
22
+ }
23
+ interface Registry {
24
+ nodes?: NodeClass[];
25
+ }
26
+ /**
27
+ * Serializes every node in a registry (`defineModule({ nodes })`) to a map
28
+ * keyed by node `type`. Pure data in, pure data out — runs in Node.
29
+ */
30
+ export declare function serializeRegistry(registry: Registry | undefined): Record<string, SerializedNodeSchemas>;
31
+ /**
32
+ * Builds a vitest `globalSetup` that serializes an explicitly-provided node
33
+ * registry and provides it to component tests. Use this when your registry is
34
+ * not at the conventional `src/server` entry; otherwise reference the default
35
+ * export of this module directly from your config.
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * // tests/client/component/schemas.ts
40
+ * import { provideSchemas } from "@bonsae/nrg/test/client/component/schemas";
41
+ * import registry from "../../../src/server";
42
+ * export default provideSchemas(registry);
43
+ * ```
44
+ */
45
+ export declare function provideSchemas(registry: Registry): ({ provide }: GlobalSetupContext) => void;
46
+ /**
47
+ * Imports a package's node registry from the conventional `src/server` entry.
48
+ * Resolves the default export (`defineModule({ nodes })`), or the module itself
49
+ * if it exposes `nodes` directly. Runs in Node, so the server import is safe.
50
+ *
51
+ * @param cwd Package root to resolve `src/server/index.ts` against. Defaults to
52
+ * the working directory (where vitest runs), which is the package root.
53
+ */
54
+ export declare function loadRegistry(cwd?: string): Promise<Registry>;
55
+ /**
56
+ * Convention `globalSetup`: serializes the package's node registry — the
57
+ * default export of `src/server` — and provides every node's schemas as data,
58
+ * so component tests validate against the real schema WITHOUT value-importing
59
+ * the server runtime into the browser. `createNode({ type })` reads it back.
60
+ *
61
+ * Reference it directly from your vitest config — no per-package file needed:
62
+ *
63
+ * ```ts
64
+ * test: { globalSetup: ["@bonsae/nrg/test/client/component/schemas"] }
65
+ * ```
66
+ */
67
+ declare function _default({ provide }: GlobalSetupContext): Promise<void>;
68
+
69
+ export {
70
+ _default as default,
71
+ };
72
+
73
+ export {};
@@ -1,13 +1,125 @@
1
1
  // Generated by dts-bundle-generator v9.5.1
2
2
 
3
- import { TSchema } from '@sinclair/typebox';
3
+ import { Static, TSchema } from '@sinclair/typebox';
4
+ import { SchemaObject } from 'ajv';
5
+ import { App } from 'vue';
4
6
 
5
- export declare function useFormNode<TConfig extends TSchema = TSchema, TCredentials extends TSchema = TSchema>(): {
6
- node: Record<string, any>;
7
- schema: Record<string, any>;
8
- errors: Record<string, string>;
9
- };
10
- type JsonSchemaObject = Record<string, any>;
7
+ interface NodeRefResolved<T = any> {
8
+ readonly __nrg_node_ref: true;
9
+ readonly __instance: T;
10
+ }
11
+ interface TypedInputResolved {
12
+ resolve(...args: any[]): any;
13
+ value: unknown;
14
+ type: string;
15
+ }
16
+ interface JsonSchemaObjectExtensions {
17
+ format?: "node-id" | "flow-id" | "topic-path" | (string & {});
18
+ /** expose this settings property to the editor via RED.settings */
19
+ exportable?: boolean;
20
+ /** set by SchemaType.NodeRef — the referenced config node type */
21
+ "x-nrg-node-type"?: string;
22
+ /** set by SchemaType.TypedInput — marks a TypedInput value/type pair */
23
+ "x-nrg-typed-input"?: boolean;
24
+ /** set by markNonValidatable — ajv skips this property */
25
+ "x-nrg-skip-validation"?: boolean;
26
+ /** form rendering hints consumed by the auto-generated editor form */
27
+ "x-nrg-form"?: {
28
+ icon?: string;
29
+ typedInputTypes?: string[];
30
+ editorLanguage?: string;
31
+ toggle?: boolean;
32
+ };
33
+ }
34
+ interface NodeRedNodeButtonDefinition {
35
+ toggle: string;
36
+ onclick: () => void;
37
+ enabled?: () => boolean;
38
+ visible?: () => boolean;
39
+ }
40
+ interface NodeRedNode {
41
+ id: string;
42
+ type: string;
43
+ name: string;
44
+ category: string;
45
+ x: string;
46
+ y: string;
47
+ g: string;
48
+ z: string;
49
+ credentials: Record<string, any>;
50
+ _def: {
51
+ defaults: Record<string, {
52
+ value: string;
53
+ type?: string;
54
+ label?: string;
55
+ required?: boolean;
56
+ }>;
57
+ credentials: Record<string, {
58
+ value: string;
59
+ type?: "password" | "text";
60
+ label?: string;
61
+ required?: boolean;
62
+ }>;
63
+ category: string;
64
+ color?: string;
65
+ icon?: string;
66
+ label?: ((this: NodeRedNode) => string) | string;
67
+ inputs?: number;
68
+ outputs?: number;
69
+ paletteLabel?: ((this: NodeRedNode) => string) | string;
70
+ labelStyle?: ((this: NodeRedNode) => string) | string;
71
+ inputLabels?: ((this: NodeRedNode, index: number) => string) | string;
72
+ outputLabels?: ((this: NodeRedNode, index: number) => string) | string;
73
+ align?: "left" | "right";
74
+ button?: NodeRedNodeButtonDefinition;
75
+ };
76
+ _newState?: NodeRedNode;
77
+ _app?: App | null;
78
+ _: (str: string) => string;
79
+ /** dynamic port count (base outputs + enabled built-in ports) */
80
+ outputs?: number;
81
+ /** injected when the node has an inputSchema */
82
+ validateInput?: boolean;
83
+ /** built-in port toggles, present when declared in the configSchema */
84
+ errorPort?: boolean;
85
+ completePort?: boolean;
86
+ statusPort?: boolean;
87
+ /**
88
+ * Per-port output settings, indexed by base-output port. `validateOutputs` is
89
+ * injected (empty) when the node has an outputsSchema; `outputReturnProperties`
90
+ * and `outputContextModes` are author-declared (SchemaType.*) — present only
91
+ * when the node opts into per-port return keys / context modes. Read at
92
+ * runtime by IONode.
93
+ */
94
+ validateOutputs?: Record<number, boolean>;
95
+ outputContextModes?: Record<number, "carry" | "trace" | "reset">;
96
+ outputReturnProperties?: Record<number, string>;
97
+ [key: string]: any;
98
+ }
99
+ interface JsonPropertySchema extends JsonSchemaObjectExtensions {
100
+ type?: string | string[];
101
+ properties?: Record<string, JsonPropertySchema>;
102
+ required?: string[];
103
+ enum?: unknown[];
104
+ anyOf?: JsonPropertySchema[];
105
+ const?: unknown;
106
+ items?: JsonPropertySchema;
107
+ title?: string;
108
+ description?: string;
109
+ default?: unknown;
110
+ }
111
+ interface JsonSchemaObject extends SchemaObject {
112
+ type: "object";
113
+ properties?: Record<string, JsonPropertySchema>;
114
+ required?: string[];
115
+ }
116
+ interface TypedInputValue {
117
+ value: string;
118
+ type: string;
119
+ }
120
+ type EditorStatic<T> = T extends NodeRefResolved<any> ? string : T extends TypedInputResolved ? TypedInputValue : T extends (...args: any[]) => any ? T : T extends Array<infer I> ? EditorStatic<I>[] : T extends object ? {
121
+ [K in keyof T]: EditorStatic<T[K]>;
122
+ } : T;
11
123
  export interface MockEditor {
12
124
  getValue(): string;
13
125
  setValue(val: string): void;
@@ -104,6 +216,30 @@ export interface MockRED {
104
216
  settings: MockSettings;
105
217
  notify(message: any, options?: Record<string, any>): MockNotification;
106
218
  }
219
+ interface FormNode<TConfig extends TSchema = TSchema, TCredentials extends TSchema = TSchema> {
220
+ node: NodeRedNode & EditorStatic<Static<TConfig>> & {
221
+ credentials: EditorStatic<Static<TCredentials>> & Record<string, any>;
222
+ };
223
+ schema: Record<string, any>;
224
+ errors: Record<string, string>;
225
+ }
226
+ /**
227
+ * Composable that provides typed access to the form node, schema, and errors.
228
+ * Replaces `defineProps` in custom form components — no props declaration needed.
229
+ *
230
+ * @example
231
+ * ```vue
232
+ * <script setup lang="ts">
233
+ * import { useFormNode } from "@bonsae/nrg/client";
234
+ * import type { ConfigsSchema, CredentialsSchema } from "../../server/schemas/my-node";
235
+ *
236
+ * const { node, errors } = useFormNode<typeof ConfigsSchema, typeof CredentialsSchema>();
237
+ * node.name // string — typed from ConfigsSchema
238
+ * node.credentials.apiKey // string — typed from CredentialsSchema
239
+ * </script>
240
+ * ```
241
+ */
242
+ export declare function useFormNode<TConfig extends TSchema = TSchema, TCredentials extends TSchema = TSchema>(): FormNode<TConfig, TCredentials>;
107
243
  export interface TestNode {
108
244
  id: string;
109
245
  type: string;
@@ -125,6 +261,16 @@ export interface CreateNodeResult {
125
261
  provide: FormProvide;
126
262
  }
127
263
  export interface CreateNodeOptions {
264
+ /**
265
+ * The node's registered `type`. When set, createNode resolves each schema not
266
+ * passed explicitly (configSchema/credentialsSchema) from the map serialized
267
+ * by the `schemas` globalSetup — so the test validates against the production
268
+ * schema without importing it. Used only for schema lookup; the harness keeps
269
+ * its own unique internal node type. Note: this is a reserved options key — a
270
+ * raw-shorthand config object that itself carries a `type` field must be
271
+ * passed via the explicit `{ configs }` form.
272
+ */
273
+ type?: string;
128
274
  configs?: Record<string, any>;
129
275
  credentials?: Record<string, any>;
130
276
  configSchema?: JsonSchemaObject;