@bonsae/nrg 0.5.4 → 0.6.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.
@@ -619,6 +619,87 @@ function getNodeTypeExports(filePath) {
619
619
  }
620
620
  return result;
621
621
  }
622
+ var SCHEMA_PROP_SEMANTICS = {
623
+ configSchema: "ConfigSchema",
624
+ credentialsSchema: "CredentialsSchema",
625
+ inputSchema: "InputSchema",
626
+ outputsSchema: "OutputsSchema",
627
+ settingsSchema: "SettingsSchema"
628
+ };
629
+ function getSchemaReferences(filePath) {
630
+ const content = fs3.readFileSync(filePath, "utf-8");
631
+ const source = ts.createSourceFile(
632
+ filePath,
633
+ content,
634
+ ts.ScriptTarget.ESNext,
635
+ true
636
+ );
637
+ const importMap = /* @__PURE__ */ new Map();
638
+ for (const stmt of source.statements) {
639
+ if (ts.isImportDeclaration(stmt) && stmt.importClause) {
640
+ const moduleSpecifier = stmt.moduleSpecifier.text;
641
+ const namedBindings = stmt.importClause.namedBindings;
642
+ if (namedBindings && ts.isNamedImports(namedBindings)) {
643
+ for (const el of namedBindings.elements) {
644
+ importMap.set(el.name.text, moduleSpecifier);
645
+ }
646
+ }
647
+ }
648
+ }
649
+ const schemaRefs = /* @__PURE__ */ new Map();
650
+ function extractIdentifiers(node) {
651
+ if (ts.isIdentifier(node)) return [node.text];
652
+ if (ts.isArrayLiteralExpression(node)) {
653
+ return node.elements.filter(ts.isIdentifier).map((el) => el.text);
654
+ }
655
+ return [];
656
+ }
657
+ for (const stmt of source.statements) {
658
+ if (ts.isClassDeclaration(stmt)) {
659
+ for (const member of stmt.members) {
660
+ if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name) && member.name.text in SCHEMA_PROP_SEMANTICS && member.initializer) {
661
+ const ids = extractIdentifiers(member.initializer);
662
+ if (ids.length > 0) {
663
+ schemaRefs.set(member.name.text, ids);
664
+ }
665
+ }
666
+ }
667
+ }
668
+ if (ts.isExportAssignment(stmt) && stmt.expression && ts.isCallExpression(stmt.expression)) {
669
+ const callee = stmt.expression.expression;
670
+ if (ts.isIdentifier(callee) && (callee.text === "defineIONode" || callee.text === "defineConfigNode")) {
671
+ const arg = stmt.expression.arguments[0];
672
+ if (arg && ts.isObjectLiteralExpression(arg)) {
673
+ for (const prop of arg.properties) {
674
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text in SCHEMA_PROP_SEMANTICS) {
675
+ const ids = extractIdentifiers(prop.initializer);
676
+ if (ids.length > 0) {
677
+ schemaRefs.set(prop.name.text, ids);
678
+ }
679
+ }
680
+ }
681
+ }
682
+ }
683
+ }
684
+ }
685
+ const result = [];
686
+ for (const [propName, identifiers] of schemaRefs) {
687
+ const semanticName = SCHEMA_PROP_SEMANTICS[propName];
688
+ const isArray = identifiers.length > 1;
689
+ for (const identifier of identifiers) {
690
+ const importSource = importMap.get(identifier);
691
+ if (importSource) {
692
+ result.push({
693
+ localName: identifier,
694
+ semanticName: isArray ? identifier : semanticName,
695
+ importSource,
696
+ tupleProp: isArray ? semanticName : void 0
697
+ });
698
+ }
699
+ }
700
+ }
701
+ return result;
702
+ }
622
703
  function buildNodeReexports(srcDir, entryFile) {
623
704
  const nodesDir = path3.join(srcDir, "nodes");
624
705
  const nodeFiles = collectTsFiles(nodesDir);
@@ -635,6 +716,20 @@ function buildNodeReexports(srcDir, entryFile) {
635
716
  ).join(", ");
636
717
  lines.push(`export type { ${prefixed} } from "${specifier}";`);
637
718
  }
719
+ const schemaRefs = getSchemaReferences(file);
720
+ const bySource = /* @__PURE__ */ new Map();
721
+ for (const ref of schemaRefs) {
722
+ const resolvedSource = path3.relative(
723
+ path3.dirname(entryFile),
724
+ path3.resolve(path3.dirname(file), ref.importSource)
725
+ ).replace(/\\/g, "/");
726
+ const sourceSpecifier = resolvedSource.startsWith(".") ? resolvedSource : `./${resolvedSource}`;
727
+ if (!bySource.has(sourceSpecifier)) bySource.set(sourceSpecifier, []);
728
+ bySource.get(sourceSpecifier).push(`${ref.localName} as ${ns}${ref.semanticName}`);
729
+ }
730
+ for (const [source, names] of bySource) {
731
+ lines.push(`export { ${names.join(", ")} } from "${source}";`);
732
+ }
638
733
  return lines.join("\n");
639
734
  }).join("\n");
640
735
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bonsae/nrg",
3
- "version": "0.5.4",
3
+ "version": "0.6.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",
@@ -46,6 +46,9 @@
46
46
  "require": "./build/server/index.cjs",
47
47
  "default": "./build/server/index.cjs"
48
48
  },
49
+ "./server/nodes": {
50
+ "types": "./src/core/server/nodes/index.ts"
51
+ },
49
52
  "./client": "./src/core/client/index.ts",
50
53
  "./schemas": "./src/core/server/schemas/index.ts",
51
54
  "./schemas/types": "./src/core/server/schemas/types/index.ts",
@@ -2,32 +2,22 @@
2
2
  <div
3
3
  v-if="features.hasInputSchema || features.hasOutputSchema"
4
4
  class="form-row"
5
- style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap"
5
+ style="display: flex; align-items: center; gap: 12px"
6
6
  >
7
- <template v-if="features.hasInputSchema">
8
- <input
9
- :id="`node-input-validateInput-${localNode.id}`"
10
- type="checkbox"
11
- :checked="localNode.validateInput"
12
- style="width: auto; margin: 0"
13
- @change="
14
- localNode.validateInput = ($event.target as HTMLInputElement).checked
15
- "
16
- />
17
- <NodeRedInputLabel label="Validate Input" style="width: auto" />
18
- </template>
19
- <template v-if="features.hasOutputSchema">
20
- <input
21
- :id="`node-input-validateOutput-${localNode.id}`"
22
- type="checkbox"
23
- :checked="localNode.validateOutput"
24
- style="width: auto; margin: 0"
25
- @change="
26
- localNode.validateOutput = ($event.target as HTMLInputElement).checked
27
- "
28
- />
29
- <NodeRedInputLabel label="Validate Output" style="width: auto" />
30
- </template>
7
+ <NodeRedToggle
8
+ v-if="features.hasInputSchema"
9
+ :model-value="localNode.validateInput"
10
+ label="Validate Input"
11
+ style="flex: 1"
12
+ @update:model-value="localNode.validateInput = $event"
13
+ />
14
+ <NodeRedToggle
15
+ v-if="features.hasOutputSchema"
16
+ :model-value="localNode.validateOutput"
17
+ label="Validate Output"
18
+ style="flex: 1"
19
+ @update:model-value="localNode.validateOutput = $event"
20
+ />
31
21
  </div>
32
22
  <div style="width: 100%; padding-bottom: 12px">
33
23
  <NodeRedNodeForm
@@ -12,12 +12,20 @@
12
12
  @update:value="node[field.key] = $event"
13
13
  />
14
14
 
15
+ <div v-else-if="field.inputType === 'boolean' && field.toggle">
16
+ <NodeRedToggle
17
+ :model-value="node[field.key]"
18
+ :label="field.label"
19
+ :icon="field.icon"
20
+ @update:model-value="node[field.key] = $event"
21
+ />
22
+ </div>
23
+
15
24
  <div v-else-if="field.inputType === 'boolean'">
16
25
  <NodeRedInputLabel
17
26
  :label="field.label"
18
27
  :icon="field.icon"
19
28
  :required="field.required"
20
- style="width: auto"
21
29
  />
22
30
  <input
23
31
  type="checkbox"
@@ -144,6 +152,7 @@
144
152
  import type { PropType } from "vue";
145
153
  import { defineComponent } from "vue";
146
154
  import NodeRedInputLabel from "./node-red-input-label.vue";
155
+ import NodeRedToggle from "./node-red-toggle.vue";
147
156
  import NodeRedInput from "./node-red-input.vue";
148
157
  import NodeRedSelectInput from "./node-red-select-input.vue";
149
158
  import NodeRedTypedInput from "./node-red-typed-input.vue";
@@ -169,6 +178,7 @@ interface NrgFormOptions {
169
178
  icon?: string;
170
179
  typedInputTypes?: string[];
171
180
  editorLanguage?: string;
181
+ toggle?: boolean;
172
182
  }
173
183
 
174
184
  interface FieldSchema {
@@ -206,6 +216,7 @@ interface FormField {
206
216
  types?: string[];
207
217
  configType?: string;
208
218
  language?: string;
219
+ toggle?: boolean;
209
220
  }
210
221
 
211
222
  function formatLabel(key: string): string {
@@ -292,7 +303,14 @@ function buildField(
292
303
 
293
304
  switch (rawType) {
294
305
  case "boolean":
295
- return { key, label, icon, inputType: "boolean", required };
306
+ return {
307
+ key,
308
+ label,
309
+ icon,
310
+ inputType: "boolean",
311
+ required,
312
+ toggle: form.toggle,
313
+ };
296
314
 
297
315
  case "number":
298
316
  case "integer":
@@ -368,6 +386,7 @@ export default defineComponent({
368
386
  name: "NodeRedJsonSchemaForm",
369
387
  components: {
370
388
  NodeRedInputLabel,
389
+ NodeRedToggle,
371
390
  NodeRedInput,
372
391
  NodeRedSelectInput,
373
392
  NodeRedTypedInput,
@@ -0,0 +1,115 @@
1
+ <template>
2
+ <div class="nrg-toggle-wrapper">
3
+ <label class="nrg-toggle" :class="{ 'nrg-toggle--checked': modelValue }">
4
+ <input
5
+ type="checkbox"
6
+ :checked="modelValue"
7
+ class="nrg-toggle__input"
8
+ @change="
9
+ $emit(
10
+ 'update:modelValue',
11
+ ($event.target as HTMLInputElement).checked,
12
+ )
13
+ "
14
+ />
15
+ <span v-if="icon || label" class="nrg-toggle__label">
16
+ <i v-if="icon" :class="iconClass"></i>
17
+ {{ label }}
18
+ </span>
19
+ <span class="nrg-toggle__slider"></span>
20
+ </label>
21
+ </div>
22
+ </template>
23
+
24
+ <script lang="ts">
25
+ import { defineComponent } from "vue";
26
+
27
+ export default defineComponent({
28
+ name: "NodeRedToggle",
29
+ props: {
30
+ modelValue: {
31
+ type: Boolean,
32
+ default: false,
33
+ },
34
+ label: {
35
+ type: String,
36
+ default: "",
37
+ },
38
+ icon: {
39
+ type: String,
40
+ default: "",
41
+ },
42
+ },
43
+ emits: ["update:modelValue"],
44
+ computed: {
45
+ iconClass(): string {
46
+ if (!this.icon) return "";
47
+ const name = this.icon.startsWith("fa-") ? this.icon : `fa-${this.icon}`;
48
+ return `fa ${name}`;
49
+ },
50
+ },
51
+ });
52
+ </script>
53
+
54
+ <style scoped>
55
+ .nrg-toggle-wrapper {
56
+ display: inline-flex;
57
+ align-items: center;
58
+ }
59
+
60
+ .nrg-toggle {
61
+ position: relative;
62
+ display: inline-flex;
63
+ align-items: center;
64
+ cursor: pointer;
65
+ gap: 8px;
66
+ user-select: none;
67
+ }
68
+
69
+ .nrg-toggle__input {
70
+ position: absolute;
71
+ opacity: 0;
72
+ width: 0;
73
+ height: 0;
74
+ }
75
+
76
+ .nrg-toggle__slider {
77
+ position: relative;
78
+ display: inline-block;
79
+ width: 36px;
80
+ min-width: 36px;
81
+ height: 20px;
82
+ background-color: var(--red-ui-secondary-border-color, #ccc);
83
+ border-radius: 10px;
84
+ transition: background-color 0.2s ease;
85
+ }
86
+
87
+ .nrg-toggle__slider::after {
88
+ content: "";
89
+ position: absolute;
90
+ top: 2px;
91
+ left: 2px;
92
+ width: 16px;
93
+ height: 16px;
94
+ background-color: white;
95
+ border-radius: 50%;
96
+ transition: transform 0.2s ease;
97
+ }
98
+
99
+ .nrg-toggle--checked .nrg-toggle__slider {
100
+ background-color: var(--red-ui-text-color-link, #0070d2);
101
+ }
102
+
103
+ .nrg-toggle--checked .nrg-toggle__slider::after {
104
+ transform: translateX(16px);
105
+ }
106
+
107
+ .nrg-toggle__label {
108
+ cursor: default;
109
+ white-space: nowrap;
110
+ }
111
+
112
+ .nrg-toggle__label i {
113
+ margin-right: 2px;
114
+ }
115
+ </style>
@@ -9,6 +9,7 @@ import NodeRedConfigInput from "./components/node-red-config-input.vue";
9
9
  import NodeRedSelectInput from "./components/node-red-select-input.vue";
10
10
  import NodeRedEditorInput from "./components/node-red-editor-input.vue";
11
11
  import NodeRedInputLabel from "./components/node-red-input-label.vue";
12
+ import NodeRedToggle from "./components/node-red-toggle.vue";
12
13
  import NodeRedJsonSchemaForm from "./components/node-red-json-schema-form.vue";
13
14
 
14
15
  const _schemas: Record<string, any> = {};
@@ -165,6 +166,7 @@ function createNodeRedVueApp(
165
166
  });
166
167
 
167
168
  app.component("NodeRedInputLabel", NodeRedInputLabel);
169
+ app.component("NodeRedToggle", NodeRedToggle);
168
170
  app.component("NodeRedInput", NodeRedInput);
169
171
  app.component("NodeRedTypedInput", NodeRedTypedInput);
170
172
  app.component("NodeRedConfigInput", NodeRedConfigInput);
@@ -0,0 +1 @@
1
+ export { serveNrgResources } from "./serve-nrg-resources";
@@ -0,0 +1,54 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+ import type { RED } from "../types";
4
+
5
+ const MIME: Record<string, string> = {
6
+ ".js": "application/javascript",
7
+ ".mjs": "application/javascript",
8
+ ".css": "text/css",
9
+ ".json": "application/json",
10
+ ".map": "application/json",
11
+ ".png": "image/png",
12
+ ".svg": "image/svg+xml",
13
+ };
14
+
15
+ let _registered = false;
16
+
17
+ function serveNrgResources(RED: RED): void {
18
+ if (_registered) return;
19
+ _registered = true;
20
+
21
+ const clientDir = path.resolve(__dirname, "./resources");
22
+ if (!fs.existsSync(clientDir)) return;
23
+
24
+ const httpAdmin = (RED as any).httpAdmin;
25
+ if (!httpAdmin) return;
26
+
27
+ // /nrg/assets/ is not handled by Node-RED's editorApp, so our handler
28
+ // appended via use() is reached normally without any stack manipulation.
29
+ httpAdmin.use(function (req: any, res: any, next: any) {
30
+ const prefix = "/nrg/assets/";
31
+ if (!(req.path as string).startsWith(prefix)) return next();
32
+ let reqPath = (req.path as string).slice(prefix.length);
33
+ // Serve the Vue dev build in development for devtools support
34
+ if (
35
+ reqPath === "vue.esm-browser.prod.js" &&
36
+ process.env.NODE_ENV !== "production"
37
+ ) {
38
+ const devPath = path.resolve(clientDir, "vue.esm-browser.js");
39
+ if (fs.existsSync(devPath)) {
40
+ reqPath = "vue.esm-browser.js";
41
+ }
42
+ }
43
+ const filePath = path.resolve(clientDir, reqPath);
44
+ const rel = path.relative(clientDir, filePath);
45
+ if (rel.startsWith("..") || path.isAbsolute(rel)) return next();
46
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile())
47
+ return next();
48
+ const ext = path.extname(filePath);
49
+ res.setHeader("Content-Type", MIME[ext] ?? "application/octet-stream");
50
+ fs.createReadStream(filePath).pipe(res);
51
+ });
52
+ }
53
+
54
+ export { serveNrgResources };
@@ -1,60 +1,10 @@
1
- import path from "path";
2
- import fs from "fs";
3
1
  import { getCredentialsFromSchema } from "./utils";
4
2
  import { Node } from "./nodes";
5
3
  import { type RED } from "./types";
6
4
  import { initValidator } from "./validator";
5
+ import { serveNrgResources } from "./api";
7
6
  import { NrgError } from "../errors";
8
7
 
9
- const MIME: Record<string, string> = {
10
- ".js": "application/javascript",
11
- ".mjs": "application/javascript",
12
- ".css": "text/css",
13
- ".json": "application/json",
14
- ".map": "application/json",
15
- ".png": "image/png",
16
- ".svg": "image/svg+xml",
17
- };
18
-
19
- let _nrgResourcesRegistered = false;
20
-
21
- function serveNrgResources(RED: RED): void {
22
- if (_nrgResourcesRegistered) return;
23
- _nrgResourcesRegistered = true;
24
-
25
- const clientDir = path.resolve(__dirname, "./resources");
26
- if (!fs.existsSync(clientDir)) return;
27
-
28
- const httpAdmin = (RED as any).httpAdmin;
29
- if (!httpAdmin) return;
30
-
31
- // /nrg/assets/ is not handled by Node-RED's editorApp, so our handler
32
- // appended via use() is reached normally without any stack manipulation.
33
- httpAdmin.use(function (req: any, res: any, next: any) {
34
- const prefix = "/nrg/assets/";
35
- if (!(req.path as string).startsWith(prefix)) return next();
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
- }
47
- const filePath = path.resolve(clientDir, reqPath);
48
- const rel = path.relative(clientDir, filePath);
49
- if (rel.startsWith("..") || path.isAbsolute(rel)) return next();
50
- if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile())
51
- return next();
52
- const ext = path.extname(filePath);
53
- res.setHeader("Content-Type", MIME[ext] ?? "application/octet-stream");
54
- fs.createReadStream(filePath).pipe(res);
55
- });
56
- }
57
-
58
8
  type AnyNodeClass = (abstract new (...args: any[]) => Node) &
59
9
  Partial<typeof Node>;
60
10
 
@@ -187,7 +137,7 @@ async function registerType(RED: RED, NodeClass: AnyNodeClass) {
187
137
  RED.log.debug(`Type registered: ${NC.type}`);
188
138
  }
189
139
 
190
- type NodeRedPackageFunction = ((RED: RED) => Promise<void>) & {
140
+ type RegistrationFunction = ((RED: RED) => Promise<void>) & {
191
141
  nodes: AnyNodeClass[];
192
142
  };
193
143
 
@@ -199,7 +149,7 @@ type NodeRedPackageFunction = ((RED: RED) => Promise<void>) & {
199
149
  *
200
150
  * @param nodes - Array of node classes to register
201
151
  */
202
- function registerTypes(nodes: AnyNodeClass[]): NodeRedPackageFunction {
152
+ function registerTypes(nodes: AnyNodeClass[]): RegistrationFunction {
203
153
  const fn = async function (RED: RED) {
204
154
  initValidator(RED);
205
155
  serveNrgResources(RED);
@@ -214,12 +164,27 @@ function registerTypes(nodes: AnyNodeClass[]): NodeRedPackageFunction {
214
164
  throw error;
215
165
  }
216
166
  };
217
- (fn as NodeRedPackageFunction).nodes = nodes;
218
- return fn as NodeRedPackageFunction;
167
+ (fn as RegistrationFunction).nodes = nodes;
168
+ return fn as RegistrationFunction;
169
+ }
170
+
171
+ interface ModuleDefinition {
172
+ nodes: AnyNodeClass[];
173
+ }
174
+
175
+ function defineModule(definition: ModuleDefinition): ModuleDefinition {
176
+ return definition;
219
177
  }
220
178
 
221
- export { registerType, registerTypes };
222
- export { Node, IONode, ConfigNode } from "./nodes";
179
+ export { registerType, registerTypes, defineModule };
180
+ export type { ModuleDefinition };
181
+ export {
182
+ Node,
183
+ IONode,
184
+ ConfigNode,
185
+ defineIONode,
186
+ defineConfigNode,
187
+ } from "./nodes";
223
188
  export { NrgError } from "../errors";
224
189
  export type { RED } from "./types";
225
190
  export { SchemaType, defineSchema } from "./schemas";
@@ -0,0 +1,136 @@
1
+ import { type TSchema } from "@sinclair/typebox";
2
+ import type { Schema } from "../schemas/types";
3
+ import type { RED } from "../types";
4
+ import type {
5
+ InferOr,
6
+ InferOutputs,
7
+ IONodeDefinition,
8
+ ConfigNodeDefinition,
9
+ NodeClassBase,
10
+ HexColor,
11
+ } from "./types";
12
+ import { IONode } from "./io-node";
13
+ import { ConfigNode } from "./config-node";
14
+
15
+ function defineIONode<
16
+ TConfigSchema extends TSchema | undefined = undefined,
17
+ TCredsSchema extends TSchema | undefined = undefined,
18
+ TSettingsSchema extends TSchema | undefined = undefined,
19
+ TInputSchema extends TSchema | undefined = undefined,
20
+ TOutputsSchema extends TSchema | readonly TSchema[] | undefined = undefined,
21
+ >(
22
+ def: IONodeDefinition<
23
+ TConfigSchema,
24
+ TCredsSchema,
25
+ TSettingsSchema,
26
+ TInputSchema,
27
+ TOutputsSchema
28
+ >,
29
+ ): NodeClassBase {
30
+ const NodeClass = class extends IONode<
31
+ InferOr<TConfigSchema, any>,
32
+ InferOr<TCredsSchema, any>,
33
+ InferOr<TInputSchema, any>,
34
+ InferOutputs<TOutputsSchema>,
35
+ InferOr<TSettingsSchema, any>
36
+ > {
37
+ static override readonly type: string = def.type;
38
+ static override readonly category: string = def.category ?? "function";
39
+ static override readonly color: HexColor = def.color ?? "#a6bbcf";
40
+ static override readonly inputs: number = def.inputs ?? 1;
41
+ static override readonly outputs: number = def.outputs ?? 1;
42
+ static override readonly paletteLabel = def.paletteLabel;
43
+ static override readonly inputLabels = def.inputLabels;
44
+ static override readonly outputLabels = def.outputLabels;
45
+ static override readonly align = def.align;
46
+ static override readonly labelStyle = def.labelStyle;
47
+
48
+ static override readonly configSchema: Schema | undefined =
49
+ def.configSchema as Schema | undefined;
50
+ static override readonly credentialsSchema: Schema | undefined =
51
+ def.credentialsSchema as Schema | undefined;
52
+ static override readonly settingsSchema: Schema | undefined =
53
+ def.settingsSchema as Schema | undefined;
54
+ static override readonly inputSchema: Schema | undefined =
55
+ def.inputSchema as Schema | undefined;
56
+ static override readonly outputsSchema: Schema | Schema[] | undefined =
57
+ def.outputsSchema as Schema | Schema[] | undefined;
58
+ static override readonly validateInput: boolean =
59
+ def.validateInput ?? false;
60
+ static override readonly validateOutput: boolean =
61
+ def.validateOutput ?? false;
62
+
63
+ static override _registered(RED: RED) {
64
+ this.validateSettings(RED);
65
+ return def.registered?.(RED);
66
+ }
67
+
68
+ async input(msg: InferOr<TInputSchema, any>) {
69
+ if (def.input) return def.input.call(this as any, msg);
70
+ }
71
+
72
+ override async created() {
73
+ if (def.created) return def.created.call(this as any);
74
+ }
75
+
76
+ override async closed(removed?: boolean) {
77
+ if (def.closed) return def.closed.call(this as any, removed);
78
+ }
79
+ };
80
+
81
+ Object.defineProperty(NodeClass, "name", {
82
+ value: def.type.replace(/(^|-)(\w)/g, (_, __, c: string) =>
83
+ c.toUpperCase(),
84
+ ),
85
+ configurable: true,
86
+ });
87
+
88
+ return NodeClass as unknown as NodeClassBase;
89
+ }
90
+
91
+ function defineConfigNode<
92
+ TConfigSchema extends TSchema | undefined = undefined,
93
+ TCredsSchema extends TSchema | undefined = undefined,
94
+ TSettingsSchema extends TSchema | undefined = undefined,
95
+ >(
96
+ def: ConfigNodeDefinition<TConfigSchema, TCredsSchema, TSettingsSchema>,
97
+ ): NodeClassBase {
98
+ const NodeClass = class extends ConfigNode<
99
+ InferOr<TConfigSchema, any>,
100
+ InferOr<TCredsSchema, any>,
101
+ InferOr<TSettingsSchema, any>
102
+ > {
103
+ static override readonly type: string = def.type;
104
+
105
+ static override readonly configSchema: Schema | undefined =
106
+ def.configSchema as Schema | undefined;
107
+ static override readonly credentialsSchema: Schema | undefined =
108
+ def.credentialsSchema as Schema | undefined;
109
+ static override readonly settingsSchema: Schema | undefined =
110
+ def.settingsSchema as Schema | undefined;
111
+
112
+ static override _registered(RED: RED) {
113
+ this.validateSettings(RED);
114
+ return def.registered?.(RED);
115
+ }
116
+
117
+ override async created() {
118
+ if (def.created) return def.created.call(this as any);
119
+ }
120
+
121
+ override async closed(removed?: boolean) {
122
+ if (def.closed) return def.closed.call(this as any, removed);
123
+ }
124
+ };
125
+
126
+ Object.defineProperty(NodeClass, "name", {
127
+ value: def.type.replace(/(^|-)(\w)/g, (_, __, c: string) =>
128
+ c.toUpperCase(),
129
+ ),
130
+ configurable: true,
131
+ });
132
+
133
+ return NodeClass as unknown as NodeClassBase;
134
+ }
135
+
136
+ export { defineIONode, defineConfigNode };
@@ -1,4 +1,5 @@
1
1
  export { Node } from "./node";
2
2
  export { IONode } from "./io-node";
3
3
  export { ConfigNode } from "./config-node";
4
+ export { defineIONode, defineConfigNode } from "./factories";
4
5
  export type * from "./types";