Writing Custom Modules and Formatters (NodeJS)

@sid I kindly ask someone who managed to make a module, show in a video how to do it step by step.

I believe it is not as simple as it seems, I did several unsuccessful tests

2 Likes

It would be pretty simple i’m sure. The post is just lacking some information. Some more details would help to get it just right.
I hope I can post something, if I get it working before you do. :sweat_smile:

Should be pretty straight forward to do, if you need more info just keep asking and @patrick can also extend the docs with those questions afterwards

Got the module to work. :slight_smile:
Will configure the custom package now.
Will also try the .hjson part soon.

@patrick Was able to get Wappler to call the custom module from server action, but the file path I am supplying is not working. Its unable to find the file.

The value I am passing is /public/uploads/abc.png which is valid.
image

I am getting a no response. Is there a specific way to access the public folder stuff from the custom module js file?
I tried to add multiple ../ - hoping it will change path to root, and then try to access the file, but that did not work either.

P.S. I understood the method & action thing I asked earlier. So please ignore that question.

You need to convert the path to system path first. Here the exists action of the fs module with a little modification to use it in the extensions folder.

const fs = require('fs-extra');
const { toSystemPath } = require('../../lib/core/path');

exports.exists = async function(options) {
  let path = toSystemPath(this.parseRequired(options.path, 'string', 'fs.exists: path is required.'));

  if (fs.existsSync(path)) {
    if (options.then) this.exec(options.then, true);
    return true;
  } else {
    if (options.else) this.exec(options.else, true);
    return false;
  }
}
3 Likes

An update on the questions asked above:
George answered 1 & 2.
3. What is setvalue in your example for module?
A - Its the name of the action.
5. Can you please share a complete module example like you have for formatter?
A - I will try to share it myself, now that I am able to at least run the module

Still unanswered:
4. What does this.parse do? I tried Googleing, but could not find anything.

About the module name and action name, its clear now too. A step in server action is an action, which is inside a parent module. This module is basically the file where action definition resides.

The file path thing is also working now as expected. Thank you Patrick. :slightly_smiling_face:
One typo is the require path for path will be ../../../lib/core/path.

Next: .hjson.

1 Like

Just a note for those that already had custom formatters for nodejs.

I had to change manually /lib/formatters/index.js for backwards compatibility. I defined custom formatters in lib/formatters/custom.js

So you index.js would look something like this.

const collections = require('./collections');
const conditional = require('./conditional');
const core = require('./core');
const crypto = require('./crypto');
const date = require('./date');
const number = require('./number');
const string = require('./string');
const custom = require('./custom');

module.exports = {
  ...collections,
  ...conditional,
  ...core,
  ...crypto,
  ...date,
  ...number,
  ...string,
  ...custom
};

I will be migrating them to the current approach but just in case someone notices that something broke with 3.4.1

This file will be overwritten in future updates so make sure you migrate your formatters to the new approach or remember to discard changes(if you are using GIT) or redoing the change.

1 Like

The function runs in the context of Server Connect and has access to several of its internal methods. this.parse is like dmx.parse in App Connect, it parses an expression and returns the result. The input can also be a complete object, it will then check all the properties if it contains a string with an expression to parse and parses them all.

Other maybe handy methods/properties:

// parse an option or throw an error if not a valid string
this.parseRequired(options.value, 'string', 'parameter value is required.');
// parse an option, a default value will be used if it is not a valid string
this.parseOptional(options.value, 'string', 'default value');
// parse an option, like above a default value will be used if it is not set, it doesn't check the type
this.parseOptional(options.value, '*', 'default value');

// execute sub steps/actions
// always set the second parameter to true
await this.exec(options.then, true);

// express req object
this.req
// express res object
this.res

// set session variable
this.setSession(key, value);
// get session variable
this.getSession(key);
// remove session variable
this.removeSession(key);

// set cookie
this.setCookie(name, value, opts);
// get cookie
this.getCookie(name, opts);
// remove cookie
this.removeCookie(name);

I think these will be the most important ones. These only applies to custom modules, they are not available with formatters.

4 Likes

Edit: Or that :point_up_2:

It’s an internal function from Wappler. It parses data we send from the UI so that the framework understands how to treat it. Is it a string, a number, a reference to a variable, an sql query, etc

Tried out .hjson. Its amazing. :exploding_head:
Stuck with output though:

  1. There is no output checkbox. How to enable that? Enabling from the option up top does work, but having a checkbox would be great.
  2. The output is not showing up. The response in the custom module is just a string. Do I have to wrap it up in a JSON saying {"path": response}? With dataPickObject set to true, this does show up in the picker, but there is no actual output.
 dataScheme: [
    {name: 'path', type: 'text'}
  ],

Just have an extra field in your variables with optionName: ‘output’ , like this:

{ name: 'myActionOutput', optionName: 'output', title: 'Output', type: 'boolean', defaultValue: false }
1 Like

This works. Thanks. :slightly_smiling_face:

An update on the output issue.
I added a “name” parameter and I started seeing the output from my module.

image

image

Specifically, optionName: 'name'. With any value other than name, the output disappears again.

The UI still shows my dataScheme variable path as the output data item. But, the name img64 is what appears in the response. This is confusing.
What I was expecting is something like img64.path to have the data. Also, does this mean a property variable with optionName: 'name' is compulsory if I need to access an output from the custom module?

Yes optionName: ‘name’ is required if you want output.

Thanks for clearing that up.

One last clarification remains for now - for the string output.
Giving this a bit of a though, I realize that if the output is a direct string, it would be available as a variable, and not via some nested param like img64.path… similar to how set value works. If the value set is an object, we can expect {{var1.key1}}, but if its a simple text, {{var1}} is used.

I think this wraps up my custom image to base64 module for NodeJS while giving a brief understanding of how it works. Plan to make a new post in detail tomorrow. :slight_smile:

Hope to delve into more complex stuff soon. This is really really cool. :ok_hand:
Wappler: A super powerful and extensible tool, with easy custom UI components - unheard of.

3 Likes

@George - On an existing NodeJS (Docker) project, I cannot get the extensions folder to appear, and I’m not sure if just manually creating it would work? I’ve tried opening targets and re-saving as in another post - but no luck.

Please advise?

You can create them manually. No problem with that.

1 Like

I have a module, it is working great except that I am returning a value before a file write is finished.

The module is this:

exports.write_binary = function (options) {
    //for converting file paths provided in UI
    const { toSystemPath } = require('../../../lib/core/path');

    //uuid package
    const { v4: uuidv4 } = require('uuid');

    //required for retrieving and saving file
    const http = require('https');
    const fs = require('fs');

    //convert the user local_path provided in the API action to useable in the module
    let path = toSystemPath(this.parseRequired(this.parse(options.local_path), 'string', 'fs.exists: path is required.'));

    //evaluate the data binding for remoteURL
    let remoteURL = this.parseRequired(options.file_url, 'string', 'parameter value is required.');

    //generate uuid
    let uuid = uuidv4(); // ⇨ '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed'

    //set the filename
    let file_name = path + '/' + uuid;

    //open up the stream for writing the file
    const file = fs.createWriteStream(file_name);



    async function fetchURL() {

        await http.get(remoteURL, async function (response) {
            response.pipe(file);
            await new Promise((r, j) => {
                file.on('close', r);
            });

        })
            .on('error', function (err) { // Handle errors
                fs.unlink(file_name); // Delete the file async. (But we don't check the result)

            });
    }

    fetchURL();

    return { "file_path": uuid };

};

I am attempting to retrieve a file from a URL and return the file name (currently a uuid) for use within standard Wappler actions.

I thought my async function fetchURL would block the return from happening until it was finished, but I’m clearly doing something wrong.

@patrick does this.exec help me here?

If it’s not obvious, Node and async are very new to me! :laughing:

Add await before the fetchURL function call :slight_smile:

1 Like

Like this?

async function fetchURL() {

        await http.get(remoteURL, async function (response) {
            response.pipe(file);
            await new Promise((r, j) => {
                file.on('close', r);
            });

        })
            .on('error', function (err) { // Handle errors
                fs.unlink(file_name); // Delete the file async. (But we don't check the result)

            });
    }

    await fetchURL();

    return { "file_path": uuid };

await has to be within an async function, so that wouldn’t work???