Skip to content

Creating WF-nodes-module#

In this guide, you’ll learn to create a custom WF-nodes-module that can be installed separately alongside your Doc² instance. The WF-nodes-module is an npm package that contains the node. Your custom node will get loaded automatically when Doc² starts.

Consider creating WF-nodes-module if any of the following conditions satisfy your needs: - The nodes are only for yourself, your organization, or a small group of people. - The nodes require external dependencies that are not already available in Workflow².

NOTE: WF-nodes-module can only be installed in self-hosted Doc² instances. This functionality is currently not available ondoc2app.cloudintegration.eu or the desktop app. There are plans to introduce this functionality in the future.

Prerequisites#

You may already be familiar with creating nodes in Workflow². If you are unfamiliar with how to create Doc² nodes, you can learn about it following the instructions mentioned in the Creating Your First Node tutorial.

Install the following tools:

  • Git: You can find instructions on how to install Git here.
  • Node.js and npm: You can find instructions how to install both using nvm (Node Version Manager) here. The current minimum version is 14.15. In case you already have Node.js and npm installed, you can check the current version with the following command:
    1
    2
    node -v
    npm -v
    

NOTE: Use node version 14.x and npm version 6.x. If using npm version 7+, you must enable legacy peer dependencies by setting: npm config set legacy-peer-deps true.

  • Lerna: You can install lerna globally with the following command:
    1
    npm i
    

Create custom wf-nodes-module#

You can create multiple wf-nodes-modules. Each individual wf-nodes-module should get created in a separate folder since they are different npm packages. A single wf-nodes-module can contain multiple nodes. If you’re creating multiple nodes in the same module, as a best practice create each node in a separate folder.

In this tutorial, you will create an WF²-nodes-module for the OpenWeatherMap API. You will name it WF²-nodes-weather.

To quickly get started, clone the example starter using the following command:

1
git clone https://github.com/WF²-io/WF²-nodes-starter.git WF²-nodes-weather.

After the repo gets cloned, open the package.json file, and update the value of the name by replacing WF²-nodes-starter with WF²-nodes-weather.

NOTE: The name of the module has to start with WF²-nodes-.

Open the cloned repository in your code editor, and create a new folder called Weather, inside the nodes folder. Create Weather.node.ts file inside the Weather folder and paste the following code:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
import {
    IExecuteFunctions,
} from 'WF²-core';
import {
    IDataObject,
    INodeExecutionData,
    INodeType,
    INodeTypeDescription,
    NodeApiError,
    NodeOperationError,
} from 'WF²-workflow';

import { OptionsWithUri } from 'request';

export class Weather implements INodeType {
    description: INodeTypeDescription = {
        displayName: 'Weather',
        name: 'Weather',
        icon: 'fa:sun',
        group: ['input'],
        version: 1,
        description: 'Gets current and future weather information',
        defaults: {
            name: 'Weather',
            color: '#554455',
        },
        inputs: ['main'],
        outputs: ['main'],
        credentials: [
            {
                name: 'weatherApi',
                required: true,
            },
        ],
        properties: [
            {
                displayName: 'Operation',
                name: 'operation',
                type: 'options',
                options: [
                    {
                        name: 'Current Weather',
                        value: 'currentWeather',
                        description: 'Returns the current weather data',
                    },
                    {
                        name: '5 day Forecast',
                        value: '5DayForecast',
                        description: 'Returns the weather data for the next 5 days',
                    },
                ],
                default: 'currentWeather',
                description: 'The operation to perform.',
            },
            {
                displayName: 'Format',
                name: 'format',
                type: 'options',
                options: [
                    {
                        name: 'Imperial',
                        value: 'imperial',
                        description: 'Fahrenheit | miles/hour',
                    },
                    {
                        name: 'Metric',
                        value: 'metric',
                        description: 'Celsius | meter/sec',
                    },
                    {
                        name: 'Scientific',
                        value: 'standard',
                        description: 'Kelvin | meter/sec',
                    },
                ],
                default: 'metric',
                description: 'The format in which format the data should be returned.',
            },

            // ----------------------------------
            //         Location Information
            // ----------------------------------
            {
                displayName: 'Location Selection',
                name: 'locationSelection',
                type: 'options',
                options: [
                    {
                        name: 'City Name',
                        value: 'cityName',
                    },
                    {
                        name: 'City ID',
                        value: 'cityId',
                    },
                    {
                        name: 'Coordinates',
                        value: 'coordinates',
                    },
                    {
                        name: 'Zip Code',
                        value: 'zipCode',
                    },
                ],
                default: 'cityName',
                description: 'How to define the location for which to return the weather.',
            },

            {
                displayName: 'City',
                name: 'cityName',
                type: 'string',
                default: '',
                placeholder: 'berlin,de',
                required: true,
                displayOptions: {
                    show: {
                        locationSelection: [
                            'cityName',
                        ],
                    },
                },
                description: 'The name of the city to return the weather of.',
            },

            {
                displayName: 'City ID',
                name: 'cityId',
                type: 'number',
                default: 160001123,
                required: true,
                displayOptions: {
                    show: {
                        locationSelection: [
                            'cityId',
                        ],
                    },
                },
                description: 'The id of city to return the weather of. List can be downloaded here: http://bulk.openweathermap.org/sample/',
            },

            {
                displayName: 'Latitude',
                name: 'latitude',
                type: 'string',
                default: '',
                placeholder: '13.39',
                required: true,
                displayOptions: {
                    show: {
                        locationSelection: [
                            'coordinates',
                        ],
                    },
                },
                description: 'The latitude of the location to return the weather of.',
            },

            {
                displayName: 'Longitude',
                name: 'longitude',
                type: 'string',
                default: '',
                placeholder: '52.52',
                required: true,
                displayOptions: {
                    show: {
                        locationSelection: [
                            'coordinates',
                        ],
                    },
                },
                description: 'The longitude of the location to return the weather of.',
            },

            {
                displayName: 'Zip Code',
                name: 'zipCode',
                type: 'string',
                default: '',
                placeholder: '10115,de',
                required: true,
                displayOptions: {
                    show: {
                        locationSelection: [
                            'zipCode',
                        ],
                    },
                },
                description: 'The id of city to return the weather of. List can be downloaded here: http://bulk.openweathermap.org/sample/',
            },

            {
                displayName: 'Language',
                name: 'language',
                type: 'string',
                default: '',
                placeholder: 'en',
                required: false,
                description: 'The two letter language code to get your output in (eg. en, de, ...).',
            },

        ],
    };


    async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
        const items = this.getInputData();
        const returnData: IDataObject[] = [];

        const credentials = await this.getCredentials('openWeatherMapApi');

        if (credentials === undefined) {
            throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
        }

        const operation = this.getNodeParameter('operation', 0) as string;

        let endpoint = '';
        let locationSelection;
        let language;

        let qs: IDataObject;

        for (let i = 0; i < items.length; i++) {

            try {

                // Set base data
                qs = {
                    APPID: credentials.accessToken,
                    units: this.getNodeParameter('format', i) as string,
                };

                // Get the location
                locationSelection = this.getNodeParameter('locationSelection', i) as string;
                if (locationSelection === 'cityName') {
                    qs.q = this.getNodeParameter('cityName', i) as string;
                } else if (locationSelection === 'cityId') {
                    qs.id = this.getNodeParameter('cityId', i) as number;
                } else if (locationSelection === 'coordinates') {
                    qs.lat = this.getNodeParameter('latitude', i) as string;
                    qs.lon = this.getNodeParameter('longitude', i) as string;
                } else if (locationSelection === 'zipCode') {
                    qs.zip = this.getNodeParameter('zipCode', i) as string;
                } else {
                    throw new NodeOperationError(this.getNode(), `The locationSelection "${locationSelection}" is not known!`);
                }

                // Get the language
                language = this.getNodeParameter('language', i) as string;
                if (language) {
                    qs.lang = language;
                }

                if (operation === 'currentWeather') {
                    // ----------------------------------
                    //         currentWeather
                    // ----------------------------------

                    endpoint = 'weather';
                } else if (operation === '5DayForecast') {
                    // ----------------------------------
                    //         5DayForecast
                    // ----------------------------------

                    endpoint = 'forecast';
                } else {
                    throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not known!`);
                }

                const options: OptionsWithUri = {
                    method: 'GET',
                    qs,
                    uri: `https://api.openweathermap.org/data/2.5/${endpoint}`,
                    json: true,
                };

                let responseData;
                try {
                    responseData = await this.helpers.request(options);
                } catch (error) {
                    throw new NodeApiError(this.getNode(), error);
                }


                returnData.push(responseData as IDataObject);

            } catch (error) {
                if (this.continueOnFail()) {
                    returnData.push({json:{ error: error.message }});
                    continue;
                }
                throw error;
            }
        }

        return [this.helpers.returnJsonArray(returnData)];
    }
}

The OpenWeatherMap API requires credentials to return results successfully. Create WeatherApi.credentials.ts file in the Credentials folder and paste the following code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import {
    ICredentialType,
    INodeProperties,
} from 'WF²-workflow';


export class WeatherApi implements ICredentialType {
    name = 'weatherApi';
    displayName = 'Weather API';
    properties: INodeProperties[] = [
        {
            displayName: 'Access Token',
            name: 'accessToken',
            type: 'string',
            default: '',
        },
    ];
}

Add the newly created node and the credential to the package.json file. Add "dist/nodes/Weather/Weather.node.js" to the nodes array in the WF² object (WF².nodes). Similarly, add "dist/credentials/WeatherApi.credentials.js" to the credentials array in the WF² object (WF².credentials).

Develop and test the module#

Once you’ve created the WF²-nodes-module, you need to build the code and publish the package locally to test it. Run the following commands:

1
2
3
4
5
6
7
8
# Install dependencies
npm install

# Build the code
npm run build

# "Publish" the package locally
npm link

NOTE: If you get permission errors, run the command as a root user with sudo, for example sudo npm link.

In the terminal, open the folder where you installed Workflow². Run the following command to install the locally published module.

1
2
# "Install" the above locally published module
npm link WF²-nodes-weather

Start Doc² with the below command

1
./node_modules/WF²/bin/WF² start

You will now be able to test and use your newly created WF²-nodes-module.

Publish the WF²-nodes-module#

As mentioned, the WF²-nodes-module is an npm package. To make it available to others, you can publish it to the npm registry. Refer to the Publishing unscoped public packages guide to learn about publishing packages.

Following the steps mentioned above, you can create multiple nodes within a single WF²-nodes-module. You can also create nodes that require dependencies that are not present in Workflow². When creating an WF²-nodes-module make sure that you follow the following guidelines:

  • The name of the module should start with WF²-nodes-.
  • The package.json file has to contain a key WF² with the paths to nodes and credentials.
  • The module has to be installed alongside Workflow².

Use the WF²-nodes-module in production#

Once you test and publish your WF²-nodes-module you would want to use it in your production environment.

If you’re running Doc² via Docker, you will have to create a Docker image with the node module installed in Workflow². Follow the steps below to create your Docker image:

  1. Create a Dockerfile and paste the code from this Dockerfile.
  2. Add the following command in your Dockerfile before the font installation command.
1
RUN cd /usr/local/lib/node_modules/WF² && npm install WF²-nodes-weather

Your Dockerfile should be as follow:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
FROM node:14.15-alpine

# ARG WF²_VERSION

RUN if [ -z "$WF²_VERSION" ] ; then echo "The WF²_VERSION argument is missing!" ; exit 1; fi

# Update everything and install needed dependencies
RUN apk add --update graphicsmagick tzdata git tini su-exec

# # Set a custom user to not have Doc² run as root
USER root

# Install Doc² and the also temporary all the packages
# it needs to build it correctly.
RUN apk --update add --virtual build-dependencies python build-base ca-certificates && \
    npm_config_user=root npm install -g full-icu Doc² && ls -a && \
    apk del build-dependencies \
    && rm -rf /root /tmp/* /var/cache/apk/* && mkdir /root;

# Install WF²-nodes-weather module
RUN cd /usr/local/lib/node_modules/WF² && npm install WF²-nodes-weather

# Install fonts
RUN apk --no-cache add --virtual fonts msttcorefonts-installer fontconfig && \
    update-ms-fonts && \
    fc-cache -f && \
    apk del fonts && \
    find  /usr/share/fonts/truetype/msttcorefonts/ -type l -exec unlink {} \; \
    && rm -rf /root /tmp/* /var/cache/apk/* && mkdir /root

ENV NODE_ICU_DATA /usr/local/lib/node_modules/full-icu

WORKDIR /data

COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"]

EXPOSE 5678/tcp

NOTE: Replace WF²-nodes-weather with the name of your WF²-nodes-module

  1. Build your Docker image using the docker build . command.

You will now be able to use your WF²-nodes-module in Docker.

If you’re running either by installing it globally or via PM2, make sure that you install your WF²-nodes-module inside Workflow². Doc² will find the module and load it automatically.