Where to define class to use in multiple nodes

Hi,

I'm using node-red for some time now and just started to create custom nodes.
Because of good documentation I was able to get pretty far up till now.
I created 3 custom nodes so far. These are passing json objects.
To create these objects I use multiple classes. This works fine as long as the class definition is inside the .js file.
But I whould like to use these class definitions in multiple nodes. Copying them to the single file seems to be worst idea :wink:
So is there any clever way to do a class definition so multiple nodes are able to use the classes? I was thinking maybe in the global context. but I didn't find anythink usefull on the web.

help appreciated
Chris

Just use "plain old" NodeJS modules.

Put all your classes into one .js file (or one file per class, if you want) and export them. Then use the require() function to import them in your nodes.

2 Likes

(moved to the creating nodes category)
Thanks Matthias, will definitely use that too :slight_smile:

1 Like

You can see an example in the uibuilder Node. I use two modules there as libraries of code - in my case, mainly to keep each code file to a manageable size but the principle is the same.

If you need to share code across multiple packages (e.g. different entries in npm), you should put the module into its own package and publish that to npm. Then you can make it a dependency in your Node's package.

The tilib module in uibuilder is very likely to end up in its own package as I've already used some of it in a second package.

3 Likes

Thanks for the advice. This helped me a lot.
I had some trouble to define multiple classes in one file but I found the solution here:

example for documentation:
class Jack{
//Member variables, functions, etc
}
class John{
//Member variables, functions, etc
}
module.exports = {
Jack : Jack,
John : John
}

access the classes by:

var People = require('./People.js');
var JackInstance = new People.Jack();
var JohnInstance = new People.John();

Thanks for sharing!

One small hint: you don't need to replace the exports object (but you can, of course)

The following is sufficient and provides better readability:

module.exports.Jack = Jack;
module.exports.John = John;

NOTE: exports.Jack = Jack; works too (module is not required)

after your hints I was able to move my class definition code from a function node to a external file.
this file (class_def.js) contains three classes, corresponding constructors and some methods.

I declared this in the node-red settings.js by:
functionGlobalContext: {
lodash : require('lodash'),
class_def : require('/dataFS/node_red_dev/node-red-contrib-deltacon-tradfri/nodes/class_def.js')
},
then I can use it by
const class_def = global.get('class_def');
So far everything fine, again, thanks to your help :slight_smile:

Now I'm facing two new problems:

  1. I used node.warn() and node.error() for debuging purpuses. In the class methods (now located in the external file) the object "node" is no longer known. (as a understand, not in skope)
    Is there any way to use this object in the external defined methods of my class? Or what is the common way of debugging in this scenario?

  2. I was using lodash in my function node. This was working fine in my function node after I declared lowdash in settings.js
    But again it's a problem in my external class definition file
    I tried to include it by:
    var _ = require('lodash')
    var _ = global.get('lodash')

but none was working. Now I'm at my wit's end.
How do I have to declare lodash to use it?

You need to read up on how to use node.js

console.log can be used.

That is Node-RED, you can't use it in your external code since that runs in a different context and it doesn't know about any of the NR functions like RED or global.get

var _ = require('lodash') should work though. Where have you put your external module file? Do a manual test - comment out most of the code and run the file manually - can it find lodash? If not, then you need to sort that first.

2 Likes

/dataFS/node_red_dev/node-red-contrib-deltacon-tradfri/nodes/class_def.js

Looking at your filename I see that your module is not in your Node-RED workspace (~/.node-red or so), where you have probably installed lodash as a dependency.

I guessing node-red-contrib-deltacon-tradfri is a custom node, so you need to install lodash there. Otherwise it cannot be found.

For reference, the NodeJS module loading algorithm is described here: Modules | Node.js v10.24.1 Documentation

1 Like

@kuema, you are right.
the module was not located in the user dir of node-red (specified by -u on startup)
I was following the howto "create your first node" fromnode-red docs
so I used npm install

But it's not working this way.
This method creates a symbolic link in ./node_modules but this seams not to be enough.
Declaring my file in settings.js by:

functionGlobalContext: {
		lodash		: require('lodash'),
		class_def	: require('./node_modules/node-red-contrib-deltacon-tradfri/nodes/class_def.js')
    },

Is not working with symlink.
But when I move the module folder to nodes_modules (and substituting the syslink by the orginal folder) its fine.
In node-reds function node I can use the global class definitions with

const class_def = global.get('class_def');
scene0 = new class_def.Scene(); 

in module files I declare by

const class_def = require('./class_def.js');

@TotallyInformation I did some reading. Thanks for the hint.
What I still not completely understand are the loglevels.
Acording to node.js docs I'm able to set different levels from debug to error.
But were do I set the level? I guess the node-red setting from settings.js doesn't apply.

You set the level of visible logging in settings.js.

To output your own messages, you use console.xxxx or, if you have access to the RED object, you can use Node-RED's logging. If you are using an external module, you won't have access to RED unless you pass it to your module function/class instance.

Really, you should take some time out and create a node.js app from scratch so that you get the basics of how node.js and its module system works first. Then it should click into place. Remember that Node-RED is the node.js app that you are running and it is then loading your code. So when used within NR, you are several layers deep into the app. If you don't understand the basics of the node.js module system, you will likely continue to struggle. It doesn't take long to learn, just choose a tutorial about it and work through that.

You are over complicating things by trying to use a package as well as a module. Put your code file in your userDir for now, it will be much simpler. Later on, when you have the basics working and you understand how modules work, you can choose to move it into its own package.

When you npm install, you are installing a package. That is separate to a module and you don't need a package to use a module.

1 Like

There is a much easier way.

3 Simple steps:

  1. put the class into an object
  2. put the object into a global variable
  3. call the object via the global variable

Since I use Iobroker and the NodeRed adapter on it, direct access to node.red and the files is very complicated, so I simply packed the classes into a new object "Import" and then made that accessible in a single function block via the initialization as global variabel.

that's how I solved the problem.

  1. Create the class in the "import" object and then pass that as a global variable to Node.Red.
const Import = {
    CreateDB: class CreateDB {
        /**
    * @param {string} dbName
    * @param {number} [outputCount]
    */
        constructor(dbName, outputCount) {
            this.dbName = dbName;
            this.outputCount = outputCount;
            //Objekte innerhalb des Objekt erstellen
            this.extern;
            this.intern;
            //Multi Output erstellen
            this.output = [outputCount];
            for (let i = 0; i < outputCount; i++) {
                this.output[i] = {};
            }
            //Kontrolle ob schon Einträge vorhanden sind
            if (context.get("dbInt" + this.dbName) === undefined) {
                this.intern = {};
            }
            else {
                this.intern = context.get("dbInt" + this.dbName);
            }
            if (global.get("dbExt" + this.dbName) === undefined) {
                this.extern = {};
            }
            else {
                this.extern = global.get("dbExt" + this.dbName);
            }
        }
        SaveDBintern() {
            context.set("dbInt" + this.dbName, this.intern);
        }
        SaveDBextern() {
            global.set("dbExt" + this.dbName, this.extern);
        }
        LoadDBintern() {
            let obj = context.get("dbInt" + this.dbName);
            return obj;
        }
        LoadDBextern() {
            let obj = global.get("dbExt" + this.dbName);
            return obj;
        }
        ResetDB() {
            this.extern = {};
            this.intern = {};
            global.set("dbExt" + this.dbName, this.extern);
            context.set("dbInt" + this.dbName, this.intern);
        }
        FormatOutput() {
            for (let i = 0; i < this.outputCount; i++) {
                if (this.output[i].hasOwnProperty("payload") || this.output[i].hasOwnProperty("topic")) {
                    if (this.output[i].payload === undefined || this.output[i].payload === null) {
                        this.output[i] = null;
                    }
                }
                else {
                    this.output[i] = null;
                }
            }
        }
    }
}

//Objekt als Globale Variabel anlegen
global.set("Import",Import);
  1. Call the import object inside any function block you want and use the class to create your own objects as often as you want.
//Importieren der Klasse über ein Objekt
const Import = global.get("Import");
//Erstellen des neuen Objekts über eine Klasse aus Import
let TestDB = new Import.CreateDB("test",3);


//Anwendung Beispiel

//TestDB.extern.essen = "Bifi";
//TestDB.SaveDBextern();

TestDB.ResetDB();
node.warn("--> "+TestDB.LoadDBextern().essen);


TestDB.output[0].payload = "lecker";
TestDB.output[0].topic = "Kekse";
TestDB.output[2].payload = "hunger";
TestDB.output[2].topic = null;

TestDB.FormatOutput();
node.done();
return(TestDB.output);


This is how it should look in the function block that is not actively used, it is important that you put the code in the initialization so that it is run again every time you start node.red.

I know this is not a nice solution but it works for me, you can still use the node.warn debug output inside the class and always quickly adjust the classes if you want to extend something.

Ironically, my example class is a class that manages Global, Context and Temporary objects. So most of the code is not needed, it's just an example of what it could look like in action.

1 Like