Integration of Dolibarr to the n8n automation platform

From Dolibarr ERP CRM Wiki
Revision as of 16:04, 4 May 2023 by AceBasket (talk | contribs) (Creation of page explaining how to create a node for Dolibarr on n8n)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

In n8n, there are two types of nodes: actions and triggers. In this tutorial, we will focus on building a declarative-style action node that creates an event in Dolibarr's agenda module.

Setting up n8n

As an average user, there are many ways through which you can use n8n. However, as a developer, you need to be able to test your node locally. As a result, this tutorial will assume you will be developing your node on n8n self-hosted through npm.

Building a node

On this page, you will find a tutorial for creating a node (n8n app integration). This includes :

  • An explanation of the different styles to build a node (programmatic or declarative)
  • A tutorial for each building style
  • A page that lists the different UI elements you will be able to use in your node

From now on, this tutorial will focus on explaining how to build the operation "Create an event" which covers a wide range of what is possible with n8n.

Setting up your environment

This tutorial assumes you are at least vaguely familiar with JavaScript, TypeScript, and REST APIs. You should also know how to use git : at the very least to be able to publish your node once it is done. Finally, you should know how to create and submit packages on npm.

On your computer, you should have :

  • Node.js and npm (minimum version Node 16)
  • A local instance of n8n (run npm install n8n -g on your terminal to install it)

n8n recommends that you use Visual Studio Code as your IDE and that you install the following extensions:

  • ESLint
  • EditorConfig
  • Prettier

By using VS Code and these extensions, you get access to the n8n node linter's warnings as you code

Node file structure

Must be included in your file structure:

  • A package.json file (necessary for any npm module)
  • A nodes directory containing all your source files for your node
    • This directory must contain a base file named <node-name>.node.ts (in our case, it will be called Dolibarr.node.ts)
    • It is recommended to have a codex file, containing metadata for your node and matching the node base file name (Dolibarr.node.json)
  • A credentials directory containing your credentials code (one file per node). In our case, it will be called Dolibarr.credentials.ts

Once the node starts to become complicated, n8n recommends the following structure :

  • actions: directory with description and implementation of each possible resource and operation.
    • In the actions folder, n8n recommends using resources and operations as the names of the sub-folders.
    • For the implementation and description you can use separate files. Use execute.ts and description.ts as filenames. This makes browsing through the code a lot easier. You can simplify this for nodes that have a less complicated structure.
  • methods: an optional directory dynamic parameters' functions.
  • transport: a directory containing the communication implementation.

Should you include more than one node in the npm package, each node should have its own directory in nodes

Setting up the project

The easiest way to start a new project is to generate a repository from the n8n-nodes-starter template, clone your new repository and then delete the following files and directories :

  • nodes/ExampleNode/
  • nodes/HTTPBin/
  • credentials/ExampleCredentials.credentials.ts
  • credentials/HttpBinApi.credentials.ts

From there, you can create the directory and files necessary for our node:

  • nodes/Dolibarr/
  • nodes/Dolibarr/Dolibarr.node.ts
  • nodes/Dolibarr/Dolibarr.node.json
  • credentials/DolibarrApi.credentials.ts

Now, install the project dependencies:

1 npm i

Save the Dolibarr logo as an svg file in the nodes folder.

Creating the node

Imports

Start by adding the import statement:

1 import { INodeProperties, INodeType, INodeTypeDescription } from 'n8n-workflow';

Creating the main class

A node must export an interface that implements INodeType. We need to have a description interface that contains the properties array.

1 export class Dolibarr implements INodeType {
2     description: INodeTypeDescription = {
3         // Basic node details will go here
4         properties: [
5         // Resources and operations will go here
6         ]
7     };
8 }

Adding node details

Now, we add basic parameters such as the display name, the icon, and the basic information for making a request using our node. In description, just before properties, we add:

 1         displayName: 'Dolibarr',
 2         name: 'dolibarr',
 3         icon: 'file:dolibarr_logo.svg',
 4         group: [],
 5         version: 1,
 6         subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
 7         description: 'Add data to Dolibarr',
 8         defaults: {
 9             name: 'Dolibarr',
10         },
11         inputs: ['main'],
12         outputs: ['main'],
13         credentials: [
14             {
15                 name: 'dolibarrApi',
16                 required: true,
17             },
18         ],
19         requestDefaults: {
20             returnFullResponse: true,
21             baseURL: '={{$credentials.baseUrl}}/api/index.php',
22             headers: {
23                 Accept: 'application/json',
24                 'Content-Type': 'application/json',
25             },
26         },

Here is what each field does:

  • displayName, `icon, `description, and `subtitle are used to render the node in the Editor UI
  • version: the version number of our node
  • defaults: in case the displayName is too long and isn't able to be displayed (in our case, the display name is short enough that we can have the same default name)
  • inputs and outputs: where the node inputs and outputs its data (should be 'main' in most cases)
  • credentials: the name of our credentials (should match up with the name that will be given in the credentials file)
  • requestDefaults: what model the HTTP requests will be based upon. In our case, it should match the HTTP requests given when you try out the APIs on swagger.

Add resources

The resource object defines the API resource that the node uses. Here, we are creating a node that will create an event in the agenda module in a Dolibarr instance. Update the properties array with the resource object:

 1 properties: [
 2     // In our case, we should have one resource per Dolibarr module
 3     {
 4         displayName: 'Resource',
 5         name: 'resource',
 6         type: 'options',
 7         noDataExpression: true,
 8         options: [
 9             {
10                 name: 'Agenda',
11                 value: 'agenda',
12             },
13             // Add as many options as resources
14         ],
15         default: 'agenda',
16     },
17     // Operations will go here
18 ]

type controls which UI element n8n displays for the resource, and tells n8n what type of data to expect from the user. options results in n8n adding a dropdown that allows users to choose one option. More informations here.

Adding operations

The operations object defines the available operations on a resource. In our case, we will probably want one operation per available API. For example, for the agenda module, we have 5 different types of APIs:

  • GET /agendaevents
  • POST /agendaevents
  • DELETE /agendaevents{id}
  • GET /agendaevents{id}
  • PUT /agendaevents{id}

For this tutorial, the one that interests us is POST /agendaevents. > In a declarative-style node, the operations object includes routing (within the options array). This sets up the details of the API call.

Add the following to the properties array, after the resource object (will add an operation):

 1 {
 2     displayName: 'Operation',
 3     name: 'operation',
 4     type: 'options',
 5     noDataExpression: true,
 6     default: 'createEvent',
 7     displayOptions: {
 8         show: {
 9             resource: ['agenda'],
10         },
11     },
12 
13     options: [
14         {
15             name: 'Create Event',
16             value: 'createEvent',
17             action: 'Create an event',
18             routing: {
19                 request: {
20                     method: 'POST',
21                     url: '/agendaevents',
22                 },
23             },
24         },
25         // Add the other operations for the agenda resource here
26     ],
27 },

After that, you can add the following, which will create the fields the user will see in the Editor UI:

  1 {
  2     displayName: 'Event Name',
  3     name: 'label',
  4     type: 'string',
  5     default: '',
  6     placeholder: 'Name of the event',
  7     required: true,
  8     displayOptions: {
  9         show: {
 10             resource: ['agenda'],
 11             operation: ['createEvent'],
 12         },
 13     },
 14     routing: {
 15         send: {
 16             type: 'body',
 17             property: 'label',
 18         },
 19     },
 20 },
 21 {
 22     displayName: 'Description',
 23     name: 'note',
 24     type: 'string',
 25     default: '',
 26     placeholder: 'Description of the event',
 27     required: true,
 28     displayOptions: {
 29         show: {
 30             resource: ['agenda'],
 31             operation: ['createEvent'],
 32         },
 33     },
 34     routing: {
 35         send: {
 36             type: 'body',
 37             property: 'note',
 38         },
 39     },
 40 },
 41 {
 42     displayName: 'Event Type',
 43     name: 'type_code',
 44     type: 'options',
 45     default: 'AC_INT',
 46     placeholder: 'Type of event',
 47     required: true,
 48     displayOptions: {
 49         show: {
 50             resource: ['agenda'],
 51             operation: ['createEvent'],
 52         },
 53     },
 54     options: [
 55         {
 56             name: 'Onsite Intervention',
 57             value: 'AC_INT',
 58         },
 59         {
 60             name: 'Meeting',
 61             value: 'AC_RDV',
 62         },
 63         {
 64             name: 'Receiving Mail',
 65             value: 'AC_EMAIL_IN',
 66         },
 67     ],
 68     routing: {
 69         send: {
 70             type: 'body',
 71             property: 'type_code',
 72         },
 73     },
 74 },
 75 {
 76     displayName: 'Beginning Date of Event',
 77     name: 'start_date',
 78     type: 'dateTime',
 79     default: '',
 80     required: true,
 81     displayOptions: {
 82         show: {
 83             resource: ['agenda'],
 84             operation: ['createEvent'],
 85         },
 86     },
 87     routing: {
 88         request: {
 89             body: {
 90                 datep: '={{$value}}',
 91             },
 92         },
 93         send: {
 94             preSend: [formatDateP],
 95         },
 96     },
 97 },
 98 {
 99     displayName: 'End Date of Event',
100     name: 'end_date',
101     type: 'dateTime',
102     default: '',
103     required: true,
104     displayOptions: {
105         show: {
106             resource: ['agenda'],
107             operation: ['createEvent'],
108         },
109     },
110     routing: {
111         request: {
112             body: {
113                 datef: '={{$value}}',
114             },
115         },
116         send: {
117             preSend: [formatDateF],
118         },
119     },
120 },

Each object corresponds to one user input. Let's explain what each field does using the following example:

 1 {
 2         displayName: 'Event Name',
 3         name: 'label',
 4         type: 'string',
 5         default: '',
 6         placeholder: 'Name of the event',
 7         required: true,
 8         displayOptions: {
 9             show: {
10                 resource: ['agenda'],
11                 operation: ['createEvent'],
12             },
13         },
14         routing: {
15             send: {
16                 type: 'body',
17                 property: 'label',
18             },
19         },
20     },

This object corresponds to the following user input :

   <img src="screenshots/Event_name.png">
  • displayName is the label just above the user input box
  • name is the internal name of the object
  • The placeholder is what the user can see before starting to type in the box
  • In displayOptions you should put the values for which that user input is shown. In this case, the user input for the name of an event will only appear is the user hase selected the agenda resource and the createEvent operation.
  • In routing, you deal with how your HTTP request will be built. In our case, we will send an HTTP request with a JSON body and the user input will be associated with the field label (given by the property field).
  • type is the Node UI element

Hidden fields

You might have noticed that in the [Add Resources section](#add-resources), we are missing a field for userownerid that is a mandatory field when making the API request. However, an average user would have no way of knowing his user id on Dolibarr. That is why we hid the user input and fill it in automatically. Add this object after all the others:

 1 {
 2         displayName: 'Event Creator',
 3         name: 'userownerid',
 4         type: 'hidden',
 5         default: '',
 6         displayOptions: {
 7             show: {
 8                 resource: ['agenda'],
 9                 operation: ['createEvent'],
10             },
11         },
12         routing: {
13             send: {
14                 preSend: [setUserOwnerId],
15             },
16         },
17     },

It's value is set before being sent (hence the preSend field) in a function called setUserOwnerId found in another file. More about this in [Functions section](#functions).

Adding optional fields

If you want your node to have optional fields, you can do so easily and n8n will display those under Additional Fields in the UI. To add an additional field, it should look like this:

 1 {
 2     displayName: 'Additional Fields',
 3     name: 'additionalFields',
 4     type: 'collection',
 5     default: {},
 6     placeholder: 'Add Field',
 7     displayOptions: {
 8         show: {
 9             resource: [
10                 'agenda',
11             ],
12             operation: [
13                 'createEvent',
14             ],
15         },
16     },
17     options: [
18         {
19             displayName: '____',
20             name: '____',
21             type: '____',
22             default: '',
23             routing: {
24                 // What you want to do for routing
25             },
26         },
27     ],
28 }

Functions

The declarative-style for creating nodes is really easy to use but you might want to do some more complex things such as pre-filling a field before sending the HTTP request. This is done by creating functions that you can then use in your object just like the object handling the userownerid field in the API. Let's take a look as how that function works:

1 export async function setUserOwnerId(this: IExecuteSingleFunctions, requestOptions: IHttpRequestOptions): Promise<IHttpRequestOptions> {
2     const responseData = await apiRequest.call(this, 'GET', 'users/info');
3     if (requestOptions.body != undefined) {
4         Object.assign(requestOptions.body, { userownerid: responseData.id });
5     }
6     return requestOptions;
7 }

This function takes the HTTP request options that would have been sent on the API call as an argument. In the body of the function, we aim to modify the body of the HTTP request. First, through a separate API call, we get the user ID. Then, after checking that requestOptions.body isn't undefined, we set userownerid to the value that was received from the API call. Lastly, we return the HTTP request options that were modified.

You might have noticed that the biggest part of that function is done by apiRequest.call. The code for apiRequest can be found below:

 1 export async function apiRequest(
 2     this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IExecuteSingleFunctions,
 3     method: IHttpRequestMethods,
 4     endpoint: string,
 5     body: IDataObject | GenericValue | GenericValue[] = {},
 6     query: IDataObject = {},
 7 ): Promise<any> {
 8     query = query || {};
 9 
10     type DolibarrApiCredentials = {
11         apiKey: string;
12         baseUrl: string;
13     };
14 
15     const credentials = (await this.getCredentials('dolibarrApi')) as DolibarrApiCredentials;
16 
17     const options: IHttpRequestOptions = {
18         method,
19         body,
20         qs: query,
21         url: `${credentials.baseUrl}/api/index.php/${endpoint}`,
22         headers: {
23             'content-type': 'application/json',
24         },
25     };
26 
27     try {
28         return this.helpers.httpRequestWithAuthentication.call(this, 'dolibarrApi', options);
29     } catch (error) {
30         if (error instanceof NodeApiError) {
31             throw error;
32         }
33         throw new NodeApiError(this.getNode(), error as JsonObject);
34     }
35 }

This function is rather simple as well. We define a type for our credentials and we retrieve them. We define the HTTP request options and then use the helper function from n8n to return the result of an HTTP request with authentication.

Setting up authentication

As you might remember, we created a DolibarrApi.credentials.ts file earlier in the beginning of this tutorial. Head to that file and add the following:

 1 import type {
 2     ICredentialType,
 3     INodeProperties,
 4     IAuthenticateGeneric,
 5     ICredentialTestRequest,
 6 } from 'n8n-workflow';
 7 
 8 export class DolibarrApi implements ICredentialType {
 9     name = 'dolibarrApi';
10 
11     displayName = 'Dolibarr API';
12 
13     documentationUrl =
14         'https://wiki.dolibarr.org/index.php?title=Module_Web_Services_API_REST_(developer)';
15 
16     properties: INodeProperties[] = [
17         {
18             displayName: 'Base URL',
19             name: 'baseUrl',
20             type: 'string',
21             required: true,
22             default: '',
23             placeholder: 'https://dolibarr.domain-name.com',
24         },
25         {
26             displayName: 'API Key',
27             name: 'apiKey',
28             type: 'string',
29             required: true,
30             typeOptions: { password: true },
31             default: '',
32         },
33     ];
34 
35     authenticate: IAuthenticateGeneric = {
36         type: 'generic',
37         properties: {
38             headers: {
39                 DOLAPIKEY: '={{$credentials.apiKey}}',
40             },
41         },
42     };
43 
44     test: ICredentialTestRequest = {
45         request: {
46             baseURL: '={{$credentials.baseUrl}}/api/index.php',
47             url: '/users/info',
48             method: 'GET',
49         },
50     };
51 }
  • name: internal name of the object, notably used in the `Dolibarr.node.ts` to access the credentials.
  • documentationUrl: link to credentials documentation.
  • properties: fields we want the user to fill in to authenticate themselves. In our case, that is the base URL to their Dolibarr instance and the API key associated with their user profile.
  • authenticate: how to inject the authentication data as part of the API request. In our case, we need a DOLAPIKEY in the headers.
  • test: how n8n checks that the credentials input are correct.
  • more information here.

Adding node metadata

This is what you need to add to the Dolibarr.node.json file:

 1 {
 2     "node": "n8n-nodes-base.Dolibarr",
 3     "nodeVersion": "1.0",
 4     "codexVersion": "1.0",
 5     "categories": [
 6         "Miscellaneous"
 7     ],
 8     "resources": {
 9         "credentialDocumentation": [
10             {
11                 "url": ""
12             }
13         ],
14         "primaryDocumentation": [
15             {
16                 "url": ""
17             }
18         ]
19     }
20 }

In the resources, we should have link to the necessary documentation so that n8n can automatically add help links.

Updating the npm package details

Update package.json at the root of the project to include the following information:

 1 {
 2     // All node names must start with "n8n-nodes-"
 3     "name": "n8n-nodes-dolibarr",
 4     "version": "0.1.0",
 5     "description": "n8n node to use Dolibarr services.",
 6     "keywords": [
 7         // This keyword is required for community nodes
 8         "n8n-community-node-package"
 9     ],
10     "license": "MIT",
11     "homepage": "https://www.dolibarr.org/",
12     "author": {
13         "name": "Test"
14     },
15     "repository": {
16         "type": "git",
17         "url": "https://github.com/Dolibarr/dolibarr-integration-resources"
18     },
19     "main": "index.js",
20     "scripts": {
21         // don't change
22     },
23     "files": [
24         "dist"
25     ],
26     // Link the credentials and node
27     "n8n": {
28         "n8nNodesApiVersion": 1,
29         "credentials": [
30             "dist/credentials/DolibarrApi.credentials.js"
31         ],
32         "nodes": [
33             "dist/nodes/Dolibarr/Dolibarr.node.js"
34         ]
35     },
36     "devDependencies": {
37         // don't change
38     },
39     "dependencies": {
40         // don't change
41     }
42 }

Testing the node locally

  1. Make sure you have installe n8n. If not, you can run
1       npm install n8n -g
  1. Publish your node locally:
1       # In your node directory
2       npm run build
3       npm link
  You can also run  
1       npm run dev
  So that your node will be built each time you make a change
  1. Install the node into your local n8n instance:
1       npm list -g
   Should give you something like this

$HOME/.nvm/versions/node/v18.14.0/lib

├── corepack@0.15.3

├── n8n@0.218.0

├── npm@9.5.1

└── uuid@9.0.0

   Do the following to create a link between your local node and the nodes directory within your n8n installation:
1       cd $HOME/.nvm/versions/node/v18.14.0/lib/node_modules/n8n
2       npm link <node-package-name>
  1. Start n8n
1      n8n start
  1. Open n8n in your browser. You should be able to find your node when you search for them in the nodes panel (just type Dolibarr).

Conclusion

This tutorial helped you build an n8n node that will create an event in Dolibarr. However, this code has already been published to n8n and a node that can create an event, create a task and create a document can be found in n8n.