feat(Adalo Node): MVP Adalo node N8N-3263 (#3102)

*  Added Adalo node using declarative API.

* fix: Updated authentication headers.

* fix: Fixed node linting issues.

*  improvements

* fix: Fixed Adalo node pagination.

* fix: Fixed Adalo node linting issues.

Co-authored-by: Michael Kret <michael.k@radency.com>
This commit is contained in:
Alex Grozav 2022-09-14 13:36:53 +03:00 committed by GitHub
parent f70e6d2345
commit 9a59d0a5d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 432 additions and 0 deletions

View file

@ -0,0 +1,34 @@
import { IAuthenticateGeneric, ICredentialType, INodeProperties } from 'n8n-workflow';
export class AdaloApi implements ICredentialType {
name = 'adaloApi';
displayName = 'Adalo API';
documentationUrl = 'adalo';
properties: INodeProperties[] = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string',
default: '',
description:
'The Adalo API is available on paid Adalo plans, find more information <a href="https://help.adalo.com/integrations/the-adalo-api" target="_blank">here</a>',
},
{
displayName: 'App ID',
name: 'appId',
type: 'string',
default: '',
description:
'You can get App ID from the URL of your app. For example, if your app URL is <strong>https://app.adalo.com/apps/1234567890/screens</strong>, then your App ID is <strong>1234567890</strong>.',
},
];
authenticate: IAuthenticateGeneric = {
type: 'generic',
properties: {
headers: {
Authorization: '=Bearer {{$credentials.apiKey}}',
},
},
};
}

View file

@ -0,0 +1,20 @@
{
"node": "n8n-nodes-base.adalo",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": [
"Data & Storage"
],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/credentials/adalo"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.adalo/"
}
]
}
}

View file

@ -0,0 +1,212 @@
import {
IDataObject,
IExecuteSingleFunctions,
IHttpRequestOptions,
IN8nHttpFullResponse,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { collectionFields } from './CollectionDescription';
import { FieldsUiValues } from './types';
export class Adalo implements INodeType {
description: INodeTypeDescription = {
displayName: 'Adalo',
name: 'adalo',
icon: 'file:adalo.svg',
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["collectionId"]}}',
description: 'Consume Adalo API',
defaults: {
name: 'Adalo',
color: '#4f44d7',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'adaloApi',
required: true,
},
],
requestDefaults: {
baseURL: '=https://api.adalo.com/v0/apps/{{$credentials.appId}}',
},
requestOperations: {
pagination: {
type: 'offset',
properties: {
limitParameter: 'limit',
offsetParameter: 'offset',
pageSize: 100,
type: 'query',
},
},
},
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
default: 'collection',
options: [
{
name: 'Collection',
value: 'collection',
},
],
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Create',
value: 'create',
description: 'Create a row',
routing: {
send: {
preSend: [this.presendCreateUpdate],
},
request: {
method: 'POST',
url: '=/collections/{{$parameter["collectionId"]}}',
},
},
action: 'Create a row',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a row',
routing: {
request: {
method: 'DELETE',
url: '=/collections/{{$parameter["collectionId"]}}/{{$parameter["rowId"]}}',
},
output: {
postReceive: [
{
type: 'set',
properties: {
value: '={{ { "success": true } }}',
},
},
],
},
},
action: 'Delete a row',
},
{
name: 'Get',
value: 'get',
description: 'Retrieve a row',
routing: {
request: {
method: 'GET',
url: '=/collections/{{$parameter["collectionId"]}}/{{$parameter["rowId"]}}',
},
},
action: 'Retrieve a row',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Retrieve many rows',
routing: {
request: {
method: 'GET',
url: '=/collections/{{$parameter["collectionId"]}}',
qs: {
limit: '={{$parameter["limit"]}}',
},
},
send: {
paginate: '={{$parameter["returnAll"]}}',
},
output: {
postReceive: [
{
type: 'rootProperty',
properties: {
property: 'records',
},
},
],
},
},
action: 'Retrieve all rows',
},
{
name: 'Update',
value: 'update',
description: 'Update a row',
routing: {
send: {
preSend: [this.presendCreateUpdate],
},
request: {
method: 'PUT',
url: '=/collections/{{$parameter["collectionId"]}}/{{$parameter["rowId"]}}',
},
},
action: 'Update a row',
},
],
default: 'getAll',
},
{
displayName: 'Collection ID',
name: 'collectionId',
type: 'string',
required: true,
default: '',
description:
'Open your Adalo application and click on the three buttons beside the collection name, then select API Documentation',
hint: "You can find information about app's collections on https://app.adalo.com/apps/<strong>your-app-id</strong>/api-docs",
displayOptions: {
show: {
resource: ['collection'],
},
},
},
...collectionFields,
],
};
async presendCreateUpdate(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const dataToSend = this.getNodeParameter('dataToSend', 0) as 'defineBelow' | 'autoMapInputData';
requestOptions.body = {};
if (dataToSend === 'autoMapInputData') {
const inputData = this.getInputData();
const rawInputsToIgnore = this.getNodeParameter('inputsToIgnore') as string;
const inputKeysToIgnore = rawInputsToIgnore.split(',').map((c) => c.trim());
const inputKeys = Object.keys(inputData.json).filter(
(key) => !inputKeysToIgnore.includes(key),
);
for (const key of inputKeys) {
(requestOptions.body as IDataObject)[key] = inputData.json[key];
}
} else {
const fields = this.getNodeParameter('fieldsUi.fieldValues') as FieldsUiValues;
for (const field of fields) {
(requestOptions.body as IDataObject)[field.fieldId] = field.fieldValue;
}
}
return requestOptions;
}
}

View file

@ -0,0 +1,139 @@
import { INodeProperties } from 'n8n-workflow';
export const collectionFields: INodeProperties[] = [
{
displayName: 'Row ID',
name: 'rowId',
type: 'string',
displayOptions: {
show: {
operation: ['get', 'delete', 'update'],
resource: ['collection'],
},
},
default: '',
required: true,
},
/**
* create / update
*/
{
displayName: 'Data to Send',
name: 'dataToSend',
type: 'options',
options: [
{
name: 'Auto-Map Input Data to Columns',
value: 'autoMapInputData',
description: 'Use when node input properties match destination column names',
},
{
name: 'Define Below for Each Column',
value: 'defineBelow',
description: 'Set the value for each destination column',
},
],
displayOptions: {
show: {
operation: ['create', 'update'],
resource: ['collection'],
},
},
default: 'defineBelow',
description: 'Whether to insert the input data this node receives in the new row',
},
{
displayName: 'Inputs to Ignore',
name: 'inputsToIgnore',
type: 'string',
displayOptions: {
show: {
operation: ['create', 'update'],
dataToSend: ['autoMapInputData'],
resource: ['collection'],
},
},
default: '',
description:
'List of input properties to avoid sending, separated by commas. Leave empty to send all properties.',
placeholder: 'Enter properties...',
},
{
displayName: 'Fields to Send',
name: 'fieldsUi',
placeholder: 'Add Field',
type: 'fixedCollection',
description:
'Field must be defined in the collection, otherwise it will be ignored. If field defined in the collection is not set here, it will be set to null.',
typeOptions: {
multipleValueButtonText: 'Add Field to Send',
multipleValues: true,
},
displayOptions: {
show: {
operation: ['create', 'update'],
dataToSend: ['defineBelow'],
resource: ['collection'],
},
},
default: {},
options: [
{
displayName: 'Field',
name: 'fieldValues',
values: [
{
displayName: 'Field ID',
name: 'fieldId',
type: 'string',
default: '',
},
{
displayName: 'Field Value',
name: 'fieldValue',
type: 'string',
default: '',
},
],
},
],
},
/**
* getAll
*/
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: false,
description: 'Whether to return all results or only up to a given limit',
displayOptions: {
show: {
operation: ['getAll'],
resource: ['collection'],
},
},
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
default: 100,
typeOptions: {
minValue: 1,
maxValue: 100,
},
displayOptions: {
show: {
operation: ['getAll'],
resource: ['collection'],
returnAll: [false],
},
},
description: 'Max number of results to return',
},
];

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="438px" height="438px" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" xmlns:xlink="http://www.w3.org/1999/xlink">
<g><path style="opacity:0.999" fill="#09a595" d="M 199.5,-0.5 C 205.833,-0.5 212.167,-0.5 218.5,-0.5C 218.5,6.83333 218.5,14.1667 218.5,21.5C 134.502,25.3789 74.3355,65.0456 38,140.5C 14.1595,201.337 17.9928,260.337 49.5,317.5C 42.5393,320.647 35.8727,324.313 29.5,328.5C 12.4851,300.122 2.48507,269.455 -0.5,236.5C -0.5,224.5 -0.5,212.5 -0.5,200.5C 9.13611,119.029 50.1361,59.1953 122.5,21C 147.118,9.59915 172.784,2.43248 199.5,-0.5 Z"/></g>
<g><path style="opacity:0.999" fill="#ea4c2e" d="M 218.5,-0.5 C 224.5,-0.5 230.5,-0.5 236.5,-0.5C 323.23,10.0683 384.73,55.0683 421,134.5C 430.416,158.096 435.916,182.429 437.5,207.5C 437.5,214.5 437.5,221.5 437.5,228.5C 435.714,263.192 426.047,295.525 408.5,325.5C 401.549,321.863 394.882,317.863 388.5,313.5C 413.242,265.758 419.075,215.758 406,163.5C 383.782,95.6155 338.615,50.7822 270.5,29C 253.247,24.8432 235.914,22.3432 218.5,21.5C 218.5,14.1667 218.5,6.83333 218.5,-0.5 Z"/></g>
<g><path style="opacity:1" fill="#fefefe" d="M 218.5,21.5 C 235.914,22.3432 253.247,24.8432 270.5,29C 338.615,50.7822 383.782,95.6155 406,163.5C 419.075,215.758 413.242,265.758 388.5,313.5C 349.147,377.699 291.48,410.699 215.5,412.5C 143.597,409.23 88.2633,377.564 49.5,317.5C 17.9928,260.337 14.1595,201.337 38,140.5C 74.3355,65.0456 134.502,25.3789 218.5,21.5 Z"/></g>
<g><path style="opacity:1" fill="#343434" d="M 321.5,72.5 C 323.225,72.8425 323.725,73.8425 323,75.5C 319.373,81.9204 315.873,88.4204 312.5,95C 316.039,101.913 319.706,108.746 323.5,115.5C 322.277,116.614 320.944,116.781 319.5,116C 313.621,112.561 307.621,109.395 301.5,106.5C 295.129,109.57 288.795,112.736 282.5,116C 281.365,116.749 280.365,116.583 279.5,115.5C 282.992,108.684 286.325,101.851 289.5,95C 286.325,88.1488 282.992,81.3155 279.5,74.5C 280.365,73.4173 281.365,73.2506 282.5,74C 288.416,77.5421 294.583,80.7087 301,83.5C 308.277,80.448 315.111,76.7813 321.5,72.5 Z"/></g>
<g><path style="opacity:1" fill="#3b3b3b" d="M 226.5,80.5 C 229.392,82.7311 231.726,85.5644 233.5,89C 237.871,89.1725 242.204,89.6725 246.5,90.5C 243.5,92.5 240.5,94.5 237.5,96.5C 236.833,100.833 236.167,105.167 235.5,109.5C 233.289,106.244 230.955,103.078 228.5,100C 224.5,99.6667 220.5,99.3333 216.5,99C 219.36,96.6397 222.36,94.473 225.5,92.5C 226.491,88.5546 226.824,84.5546 226.5,80.5 Z"/></g>
<g><path style="opacity:1" fill="#323232" d="M 215.5,188.5 C 209.759,179.359 204.259,170.026 199,160.5C 198.503,157.854 198.336,155.187 198.5,152.5C 202.36,141.567 210.027,137.4 221.5,140C 224.266,141.467 226.766,143.3 229,145.5C 263.333,202.167 297.667,258.833 332,315.5C 333.521,331.312 326.354,338.479 310.5,337C 307.098,335.6 304.264,333.433 302,330.5C 273.338,283.018 244.505,235.684 215.5,188.5 Z"/></g>
<g><path style="opacity:1" fill="#373737" d="M 295.5,149.5 C 298.608,152.328 301.275,155.662 303.5,159.5C 308.132,160.328 312.798,160.828 317.5,161C 313.912,163.377 310.412,165.877 307,168.5C 306.828,172.871 306.328,177.204 305.5,181.5C 303.167,178.167 300.833,174.833 298.5,171.5C 294.204,170.672 289.871,170.172 285.5,170C 288.36,167.64 291.36,165.473 294.5,163.5C 295.493,158.88 295.827,154.214 295.5,149.5 Z"/></g>
<g><path style="opacity:1" fill="#bbbbbb" d="M 198.5,152.5 C 198.336,155.187 198.503,157.854 199,160.5C 204.259,170.026 209.759,179.359 215.5,188.5C 186.53,235.662 157.697,282.995 129,330.5C 122.315,337.854 114.482,339.354 105.5,335C 98.9153,329.643 96.7486,322.809 99,314.5C 131.941,260.269 165.108,206.269 198.5,152.5 Z"/></g>
<g><path style="opacity:1" fill="#333333" d="M 210.5,257.5 C 227.101,255.945 236.767,263.278 239.5,279.5C 237.449,297.376 227.449,304.876 209.5,302C 196.276,295.348 191.776,284.848 196,270.5C 199.785,264.875 204.618,260.542 210.5,257.5 Z"/></g>
<g><path style="opacity:0.999" fill="#f5b915" d="M 388.5,313.5 C 394.882,317.863 401.549,321.863 408.5,325.5C 384.936,367.891 350.936,399.058 306.5,419C 279.86,430.359 252.193,436.525 223.5,437.5C 219.833,437.5 216.167,437.5 212.5,437.5C 138.71,433.357 80.21,401.357 37,341.5C 33.9711,337.438 31.4711,333.105 29.5,328.5C 35.8727,324.313 42.5393,320.647 49.5,317.5C 88.2633,377.564 143.597,409.23 215.5,412.5C 291.48,410.699 349.147,377.699 388.5,313.5 Z"/></g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View file

@ -0,0 +1,11 @@
export type AdaloCredentials = {
apiKey: string;
appId: string;
};
export type FieldsUiValues = Array<{
fieldId: string;
fieldValue: string;
}>;
export type Operation = 'create' | 'delete' | 'update' | 'get' | 'getAll';

View file

@ -33,6 +33,7 @@
"dist/credentials/ActiveCampaignApi.credentials.js",
"dist/credentials/AcuitySchedulingApi.credentials.js",
"dist/credentials/AcuitySchedulingOAuth2Api.credentials.js",
"dist/credentials/AdaloApi.credentials.js",
"dist/credentials/AffinityApi.credentials.js",
"dist/credentials/AgileCrmApi.credentials.js",
"dist/credentials/AirtableApi.credentials.js",
@ -338,6 +339,7 @@
"dist/nodes/ActiveCampaign/ActiveCampaign.node.js",
"dist/nodes/ActiveCampaign/ActiveCampaignTrigger.node.js",
"dist/nodes/AcuityScheduling/AcuitySchedulingTrigger.node.js",
"dist/nodes/Adalo/Adalo.node.js",
"dist/nodes/Affinity/Affinity.node.js",
"dist/nodes/Affinity/AffinityTrigger.node.js",
"dist/nodes/AgileCrm/AgileCrm.node.js",