This commit is contained in:
Rupenieks 2020-07-14 11:23:19 +02:00
commit 52b5eb4a1e
26 changed files with 3373 additions and 21 deletions

View file

@ -0,0 +1,25 @@
# Folder where data should be saved
DATA_FOLDER=/root/n8n/
# The top level domain to serve from
DOMAIN_NAME=example.com
# The subfolder to serve from
SUBFOLDER=app1
N8N_PATH=/app1/
# DOMAIN_NAME and SUBDOMAIN combined decide where n8n will be reachable from
# above example would result in: https://example.com/n8n/
# The user name to use for autentication - IMPORTANT ALWAYS CHANGE!
N8N_BASIC_AUTH_USER=user
# The password to use for autentication - IMPORTANT ALWAYS CHANGE!
N8N_BASIC_AUTH_PASSWORD=password
# Optional timezone to set which gets used by Cron-Node by default
# If not set New York time will be used
GENERIC_TIMEZONE=Europe/Berlin
# The email address to use for the SSL certificate creation
SSL_EMAIL=user@example.com

View file

@ -0,0 +1,26 @@
# n8n on Subfolder with SSL
Starts n8n and deployes it on a subfolder
## Start
To start n8n in a subfolder simply start docker-compose by executing the following
command in the current folder.
**IMPORTANT:** But before you do that change the default users and passwords in the `.env` file!
```
docker-compose up -d
```
To stop it execute:
```
docker-compose stop
```
## Configuration
The default name of the database, user and password for MongoDB can be changed in the `.env` file in the current directory.

View file

@ -0,0 +1,57 @@
version: "3"
services:
traefik:
image: "traefik"
command:
- "--api=true"
- "--api.insecure=true"
- "--api.dashboard=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.mytlschallenge.acme.tlschallenge=true"
- "--certificatesresolvers.mytlschallenge.acme.email=${SSL_EMAIL}"
- "--certificatesresolvers.mytlschallenge.acme.storage=/letsencrypt/acme.json"
- /home/jan/www/n8n/n8n:/data
ports:
- "443:443"
- "80:80"
volumes:
- ${DATA_FOLDER}/letsencrypt:/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock:ro
n8n:
image: n8nio/n8n
ports:
- "127.0.0.1:5678:5678"
labels:
- traefik.enable=true
- traefik.http.routers.n8n.rule=Host(`${DOMAIN_NAME}`)
- traefik.http.routers.n8n.tls=true
- traefik.http.routers.n8n.entrypoints=websecure
- "traefik.http.routers.n8n.rule=PathPrefix(`/${SUBFOLDER}{regex:$$|/.*}`)"
- "traefik.http.middlewares.n8n-stripprefix.stripprefix.prefixes=/${SUBFOLDER}"
- "traefik.http.routers.n8n.middlewares=n8n-stripprefix"
- traefik.http.routers.n8n.tls.certresolver=mytlschallenge
- traefik.http.middlewares.n8n.headers.SSLRedirect=true
- traefik.http.middlewares.n8n.headers.STSSeconds=315360000
- traefik.http.middlewares.n8n.headers.browserXSSFilter=true
- traefik.http.middlewares.n8n.headers.contentTypeNosniff=true
- traefik.http.middlewares.n8n.headers.forceSTSHeader=true
- traefik.http.middlewares.n8n.headers.SSLHost=${DOMAIN_NAME}
- traefik.http.middlewares.n8n.headers.STSIncludeSubdomains=true
- traefik.http.middlewares.n8n.headers.STSPreload=true
environment:
- N8N_BASIC_AUTH_ACTIVE=true
- N8N_BASIC_AUTH_USER
- N8N_BASIC_AUTH_PASSWORD
- N8N_HOST=${DOMAIN_NAME}
- N8N_PORT=5678
- N8N_PROTOCOL=https
- NODE_ENV=production
- N8N_PATH
- WEBHOOK_TUNNEL_URL=http://${DOMAIN_NAME}${N8N_PATH}
- VUE_APP_URL_BASE_API=http://${DOMAIN_NAME}${N8N_PATH}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ${DATA_FOLDER}/.n8n:/root/.n8n

View file

@ -161,8 +161,8 @@ const config = convict({
// If a workflow executes all the data gets saved by default. This // If a workflow executes all the data gets saved by default. This
// could be a problem when a workflow gets executed a lot and processes // could be a problem when a workflow gets executed a lot and processes
// a lot of data. To not write the database full it is possible to // a lot of data. To not exceed the database's capacity it is possible to
// not save the execution at all. // prune the database regularly or to not save the execution at all.
// Depending on if the execution did succeed or error a different // Depending on if the execution did succeed or error a different
// save behaviour can be set. // save behaviour can be set.
saveDataOnError: { saveDataOnError: {
@ -188,6 +188,27 @@ const config = convict({
default: false, default: false,
env: 'EXECUTIONS_DATA_SAVE_MANUAL_EXECUTIONS' env: 'EXECUTIONS_DATA_SAVE_MANUAL_EXECUTIONS'
}, },
// To not exceed the database's capacity and keep its size moderate
// the execution data gets pruned regularly (default: 1 hour interval).
// All saved execution data older than the max age will be deleted.
// Pruning is currently not activated by default, which will change in
// a future version.
pruneData: {
doc: 'Delete data of past executions on a rolling basis',
default: false,
env: 'EXECUTIONS_DATA_PRUNE'
},
pruneDataMaxAge: {
doc: 'How old (hours) the execution data has to be to get deleted',
default: 336,
env: 'EXECUTIONS_DATA_MAX_AGE'
},
pruneDataTimeout: {
doc: 'Timeout (seconds) after execution data has been pruned',
default: 3600,
env: 'EXECUTIONS_DATA_PRUNE_TIMEOUT'
},
}, },
generic: { generic: {

View file

@ -40,11 +40,12 @@ export function getBaseUrl(): string {
const protocol = config.get('protocol') as string; const protocol = config.get('protocol') as string;
const host = config.get('host') as string; const host = config.get('host') as string;
const port = config.get('port') as number; const port = config.get('port') as number;
const path = config.get('path') as string;
if (protocol === 'http' && port === 80 || protocol === 'https' && port === 443) { if (protocol === 'http' && port === 80 || protocol === 'https' && port === 443) {
return `${protocol}://${host}/`; return `${protocol}://${host}${path}`;
} }
return `${protocol}://${host}:${port}/`; return `${protocol}://${host}:${port}${path}`;
} }

View file

@ -931,7 +931,8 @@ class App {
// Authorize OAuth Data // Authorize OAuth Data
this.app.get(`/${this.restEndpoint}/oauth1-credential/auth`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<string> => { this.app.get(`/${this.restEndpoint}/oauth1-credential/auth`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<string> => {
if (req.query.id === undefined) { if (req.query.id === undefined) {
throw new Error('Required credential id is missing!'); res.status(500).send('Required credential id is missing!');
return '';
} }
const result = await Db.collections.Credentials!.findOne(req.query.id as string); const result = await Db.collections.Credentials!.findOne(req.query.id as string);
@ -943,7 +944,8 @@ class App {
let encryptionKey = undefined; let encryptionKey = undefined;
encryptionKey = await UserSettings.getEncryptionKey(); encryptionKey = await UserSettings.getEncryptionKey();
if (encryptionKey === undefined) { if (encryptionKey === undefined) {
throw new Error('No encryption key got found to decrypt the credentials!'); res.status(500).send('No encryption key got found to decrypt the credentials!');
return '';
} }
// Decrypt the currently saved credentials // Decrypt the currently saved credentials
@ -1015,7 +1017,8 @@ class App {
const { oauth_verifier, oauth_token, cid } = req.query; const { oauth_verifier, oauth_token, cid } = req.query;
if (oauth_verifier === undefined || oauth_token === undefined) { if (oauth_verifier === undefined || oauth_token === undefined) {
throw new Error('Insufficient parameters for OAuth1 callback'); const errorResponse = new ResponseHelper.ResponseError('Insufficient parameters for OAuth1 callback. Received following query parameters: ' + JSON.stringify(req.query), undefined, 503);
return ResponseHelper.sendErrorResponse(res, errorResponse);
} }
const result = await Db.collections.Credentials!.findOne(cid as any); // tslint:disable-line:no-any const result = await Db.collections.Credentials!.findOne(cid as any); // tslint:disable-line:no-any
@ -1085,7 +1088,8 @@ class App {
// Authorize OAuth Data // Authorize OAuth Data
this.app.get(`/${this.restEndpoint}/oauth2-credential/auth`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<string> => { this.app.get(`/${this.restEndpoint}/oauth2-credential/auth`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<string> => {
if (req.query.id === undefined) { if (req.query.id === undefined) {
throw new Error('Required credential id is missing!'); res.status(500).send('Required credential id is missing.');
return '';
} }
const result = await Db.collections.Credentials!.findOne(req.query.id as string); const result = await Db.collections.Credentials!.findOne(req.query.id as string);
@ -1097,7 +1101,8 @@ class App {
let encryptionKey = undefined; let encryptionKey = undefined;
encryptionKey = await UserSettings.getEncryptionKey(); encryptionKey = await UserSettings.getEncryptionKey();
if (encryptionKey === undefined) { if (encryptionKey === undefined) {
throw new Error('No encryption key got found to decrypt the credentials!'); res.status(500).send('No encryption key got found to decrypt the credentials!');
return '';
} }
// Decrypt the currently saved credentials // Decrypt the currently saved credentials
@ -1161,7 +1166,8 @@ class App {
const {code, state: stateEncoded } = req.query; const {code, state: stateEncoded } = req.query;
if (code === undefined || stateEncoded === undefined) { if (code === undefined || stateEncoded === undefined) {
throw new Error('Insufficient parameters for OAuth2 callback'); const errorResponse = new ResponseHelper.ResponseError('Insufficient parameters for OAuth2 callback. Received following query parameters: ' + JSON.stringify(req.query), undefined, 503);
return ResponseHelper.sendErrorResponse(res, errorResponse);
} }
let state; let state;
@ -1211,17 +1217,20 @@ class App {
}, },
}; };
} }
const redirectUri = `${WebhookHelpers.getWebhookBaseUrl()}${this.restEndpoint}/oauth2-credential/callback`;
const oAuthObj = new clientOAuth2({ const oAuthObj = new clientOAuth2({
clientId: _.get(oauthCredentials, 'clientId') as string, clientId: _.get(oauthCredentials, 'clientId') as string,
clientSecret: _.get(oauthCredentials, 'clientSecret', '') as string, clientSecret: _.get(oauthCredentials, 'clientSecret', '') as string,
accessTokenUri: _.get(oauthCredentials, 'accessTokenUrl', '') as string, accessTokenUri: _.get(oauthCredentials, 'accessTokenUrl', '') as string,
authorizationUri: _.get(oauthCredentials, 'authUrl', '') as string, authorizationUri: _.get(oauthCredentials, 'authUrl', '') as string,
redirectUri: `${WebhookHelpers.getWebhookBaseUrl()}${this.restEndpoint}/oauth2-credential/callback`, redirectUri,
scopes: _.split(_.get(oauthCredentials, 'scope', 'openid,') as string, ',') scopes: _.split(_.get(oauthCredentials, 'scope', 'openid,') as string, ',')
}); });
const oauthToken = await oAuthObj.code.getToken(req.originalUrl, options); const queryParameters = req.originalUrl.split('?').splice(1, 1).join('');
const oauthToken = await oAuthObj.code.getToken(`${redirectUri}?${queryParameters}`, options);
if (oauthToken === undefined) { if (oauthToken === undefined) {
const errorResponse = new ResponseHelper.ResponseError('Unable to get access tokens!', undefined, 404); const errorResponse = new ResponseHelper.ResponseError('Unable to get access tokens!', undefined, 404);

View file

@ -41,6 +41,8 @@ import {
import * as config from '../config'; import * as config from '../config';
import { LessThanOrEqual } from "typeorm";
/** /**
* Checks if there was an error and if errorWorkflow is defined. If so it collects * Checks if there was an error and if errorWorkflow is defined. If so it collects
@ -79,6 +81,30 @@ function executeErrorWorkflow(workflowData: IWorkflowBase, fullRunData: IRun, mo
} }
} }
/**
* Prunes Saved Execution which are older than configured.
* Throttled to be executed just once in configured timeframe.
*
*/
let throttling = false;
function pruneExecutionData(): void {
if (!throttling) {
throttling = true;
const timeout = config.get('executions.pruneDataTimeout') as number; // in seconds
const maxAge = config.get('executions.pruneDataMaxAge') as number; // in h
const date = new Date(); // today
date.setHours(date.getHours() - maxAge);
// throttle just on success to allow for self healing on failure
Db.collections.Execution!.delete({ stoppedAt: LessThanOrEqual(date.toISOString()) })
.then(data =>
setTimeout(() => {
throttling = false;
}, timeout * 1000)
).catch(err => throttling = false);
}
}
/** /**
* Pushes the execution out to all connected clients * Pushes the execution out to all connected clients
@ -189,6 +215,11 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
workflowExecuteAfter: [ workflowExecuteAfter: [
async function (this: WorkflowHooks, fullRunData: IRun, newStaticData: IDataObject): Promise<void> { async function (this: WorkflowHooks, fullRunData: IRun, newStaticData: IDataObject): Promise<void> {
// Prune old execution data
if (config.get('executions.pruneData')) {
pruneExecutionData();
}
const isManualMode = [this.mode, parentProcessMode].includes('manual'); const isManualMode = [this.mode, parentProcessMode].includes('manual');
try { try {

View file

@ -1,6 +1,6 @@
# n8n-core # n8n-core
![n8n.io - Workflow Automation](https://raw.githubusercontent.com/n8n-io/n8n/master/docs/images/n8n-logo.png) ![n8n.io - Workflow Automation](https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-logo.png)
Core components for n8n Core components for n8n

View file

@ -1,6 +1,6 @@
# n8n-editor-ui # n8n-editor-ui
![n8n.io - Workflow Automation](https://n8n.io/n8n-logo.png) ![n8n.io - Workflow Automation](https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-logo.png)
The UI to create and update n8n workflows The UI to create and update n8n workflows

View file

@ -16,7 +16,7 @@
<el-menu-item index="logo" class="logo-item"> <el-menu-item index="logo" class="logo-item">
<a href="https://n8n.io" target="_blank" class="logo"> <a href="https://n8n.io" target="_blank" class="logo">
<img :src="basePath + '/n8n-icon-small.png'" class="icon" alt="n8n.io"/> <img :src="basePath + 'n8n-icon-small.png'" class="icon" alt="n8n.io"/>
<span class="logo-text" slot="title">n8n.io</span> <span class="logo-text" slot="title">n8n.io</span>
</a> </a>
</el-menu-item> </el-menu-item>

View file

@ -1,6 +1,6 @@
# n8n-node-dev # n8n-node-dev
![n8n.io - Workflow Automation](https://raw.githubusercontent.com/n8n-io/n8n/master/docs/images/n8n-logo.png) ![n8n.io - Workflow Automation](https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-logo.png)
Currently very simple and not very sophisticated CLI which makes it easier Currently very simple and not very sophisticated CLI which makes it easier
to create credentials and nodes in TypeScript for n8n. to create credentials and nodes in TypeScript for n8n.

View file

@ -1,6 +1,6 @@
# n8n-nodes-base # n8n-nodes-base
![n8n.io - Workflow Automation](https://raw.githubusercontent.com/n8n-io/n8n/master/docs/images/n8n-logo.png) ![n8n.io - Workflow Automation](https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-logo.png)
The nodes which are included by default in n8n The nodes which are included by default in n8n

View file

@ -0,0 +1,51 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
const scopes = [
'offline_access',
'accounting.transactions',
'accounting.settings',
'accounting.contacts',
];
export class XeroOAuth2Api implements ICredentialType {
name = 'xeroOAuth2Api';
extends = [
'oAuth2Api',
];
displayName = 'Xero OAuth2 API';
properties = [
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://login.xero.com/identity/connect/authorize',
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://identity.xero.com/connect/token',
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
default: scopes.join(' '),
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden' as NodePropertyTypes,
default: '',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden' as NodePropertyTypes,
default: 'header',
},
];
}

View file

@ -0,0 +1,80 @@
import {
IExecuteFunctions,
IHookFunctions,
} from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
} from 'n8n-workflow';
import {
OptionsWithUri,
} from 'request';
/**
* Make an API request to HackerNews
*
* @param {IHookFunctions} this
* @param {string} method
* @param {string} endpoint
* @param {IDataObject} qs
* @returns {Promise<any>}
*/
export async function hackerNewsApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, qs: IDataObject): Promise<any> { // tslint:disable-line:no-any
const options: OptionsWithUri = {
method,
qs,
uri: `http://hn.algolia.com/api/v1/${endpoint}`,
json: true,
};
try {
return await this.helpers.request!(options);
} catch (error) {
if (error.response && error.response.body && error.response.body.error) {
// Try to return the error prettier
throw new Error(`Hacker News error response [${error.statusCode}]: ${error.response.body.error}`);
}
throw error;
}
}
/**
* Make an API request to HackerNews
* and return all results
*
* @export
* @param {(IHookFunctions | IExecuteFunctions)} this
* @param {string} method
* @param {string} endpoint
* @param {IDataObject} qs
* @returns {Promise<any>}
*/
export async function hackerNewsApiRequestAllItems(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, qs: IDataObject): Promise<any> { // tslint:disable-line:no-any
qs.hitsPerPage = 100;
const returnData: IDataObject[] = [];
let responseData;
let itemsReceived = 0;
do {
responseData = await hackerNewsApiRequest.call(this, method, endpoint, qs);
returnData.push.apply(returnData, responseData.hits);
if (returnData !== undefined) {
itemsReceived += returnData.length;
}
} while (
responseData.nbHits > itemsReceived
);
return returnData;
}

View file

@ -0,0 +1,384 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
INodeExecutionData,
INodeType,
INodeTypeDescription,
IDataObject,
} from 'n8n-workflow';
import {
hackerNewsApiRequest,
hackerNewsApiRequestAllItems,
} from './GenericFunctions';
export class HackerNews implements INodeType {
description: INodeTypeDescription = {
displayName: 'Hacker News',
name: 'hackerNews',
icon: 'file:hackernews.png',
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Hacker News API',
defaults: {
name: 'Hacker News',
color: '#ff6600',
},
inputs: ['main'],
outputs: ['main'],
properties: [
// ----------------------------------
// Resources
// ----------------------------------
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'All',
value: 'all',
},
{
name: 'Article',
value: 'article',
},
{
name: 'User',
value: 'user',
},
],
default: 'article',
description: 'Resource to consume.',
},
// ----------------------------------
// Operations
// ----------------------------------
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'all',
],
},
},
options: [
{
name: 'Get All',
value: 'getAll',
description: 'Get all items',
},
],
default: 'getAll',
description: 'Operation to perform.',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'article',
],
},
},
options: [
{
name: 'Get',
value: 'get',
description: 'Get a Hacker News article',
},
],
default: 'get',
description: 'Operation to perform.',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'user',
],
},
},
options: [
{
name: 'Get',
value: 'get',
description: 'Get a Hacker News user',
},
],
default: 'get',
description: 'Operation to perform.',
},
// ----------------------------------
// Fields
// ----------------------------------
{
displayName: 'Article ID',
name: 'articleId',
type: 'string',
required: true,
default: '',
description: 'The ID of the Hacker News article to be returned',
displayOptions: {
show: {
resource: [
'article',
],
operation: [
'get',
],
},
},
},
{
displayName: 'Username',
name: 'username',
type: 'string',
required: true,
default: '',
description: 'The Hacker News user to be returned',
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'get',
],
},
},
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: false,
description: 'Whether to return all results for the query or only up to a limit.',
displayOptions: {
show: {
resource: [
'all',
],
operation: [
'getAll',
],
},
},
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
default: 100,
description: 'Limit of Hacker News articles to be returned for the query.',
displayOptions: {
show: {
resource: [
'all',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'article',
],
operation: [
'get',
],
},
},
options: [
{
displayName: 'Include comments',
name: 'includeComments',
type: 'boolean',
default: false,
description: 'Whether to include all the comments in a Hacker News article.',
},
],
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'all',
],
operation: [
'getAll',
],
},
},
options: [
{
displayName: 'Keyword',
name: 'keyword',
type: 'string',
default: '',
description: 'The keyword for filtering the results of the query.',
},
{
displayName: 'Tags',
name: 'tags',
type: 'multiOptions',
options: [
{
name: 'Story',
value: 'story',
description: 'Returns query results filtered by story tag',
},
{
name: 'Comment',
value: 'comment',
description: 'Returns query results filtered by comment tag',
},
{
name: 'Poll',
value: 'poll',
description: 'Returns query results filtered by poll tag',
},
{
name: 'Show HN',
value: 'show_hn', // snake case per HN tags
description: 'Returns query results filtered by Show HN tag',
},
{
name: 'Ask HN',
value: 'ask_hn', // snake case per HN tags
description: 'Returns query results filtered by Ask HN tag',
},
{
name: 'Front Page',
value: 'front_page', // snake case per HN tags
description: 'Returns query results filtered by Front Page tag',
},
],
default: '',
description: 'Tags for filtering the results of the query.',
},
],
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
let returnAll = false;
for (let i = 0; i < items.length; i++) {
let qs: IDataObject = {};
let endpoint = '';
let includeComments = false;
if (resource === 'all') {
if (operation === 'getAll') {
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const keyword = additionalFields.keyword as string;
const tags = additionalFields.tags as string[];
qs = {
query: keyword,
tags: tags ? tags.join() : '',
};
returnAll = this.getNodeParameter('returnAll', i) as boolean;
if (!returnAll) {
qs.hitsPerPage = this.getNodeParameter('limit', i) as number;
}
endpoint = 'search?';
} else {
throw new Error(`The operation '${operation}' is unknown!`);
}
} else if (resource === 'article') {
if (operation === 'get') {
endpoint = `items/${this.getNodeParameter('articleId', i)}`;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
includeComments = additionalFields.includeComments as boolean;
} else {
throw new Error(`The operation '${operation}' is unknown!`);
}
} else if (resource === 'user') {
if (operation === 'get') {
endpoint = `users/${this.getNodeParameter('username', i)}`;
} else {
throw new Error(`The operation '${operation}' is unknown!`);
}
} else {
throw new Error(`The resource '${resource}' is unknown!`);
}
let responseData;
if (returnAll === true) {
responseData = await hackerNewsApiRequestAllItems.call(this, 'GET', endpoint, qs);
} else {
responseData = await hackerNewsApiRequest.call(this, 'GET', endpoint, qs);
if (resource === 'all' && operation === 'getAll') {
responseData = responseData.hits;
}
}
if (resource === 'article' && operation === 'get' && !includeComments) {
delete responseData.children;
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else {
returnData.push(responseData as IDataObject);
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -113,7 +113,9 @@ export class Uplead implements INodeType {
if (Array.isArray(responseData.data)) { if (Array.isArray(responseData.data)) {
returnData.push.apply(returnData, responseData.data as IDataObject[]); returnData.push.apply(returnData, responseData.data as IDataObject[]);
} else { } else {
returnData.push(responseData.data as IDataObject); if (responseData.data !== null) {
returnData.push(responseData.data as IDataObject);
}
} }
} }
return [this.helpers.returnJsonArray(returnData)]; return [this.helpers.returnJsonArray(returnData)];

View file

@ -0,0 +1,838 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const contactOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'contact',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'create a contact',
},
{
name: 'Get',
value: 'get',
description: 'Get a contact',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all contacts',
},
{
name: 'Update',
value: 'update',
description: 'Update a contact',
},
],
default: 'create',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const contactFields = [
/* -------------------------------------------------------------------------- */
/* contact:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Organization ID',
name: 'organizationId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTenants',
},
default: '',
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'create',
],
},
},
required: true,
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'create',
],
},
},
description: 'Full name of contact/organisation',
required: true,
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'create',
],
},
},
options: [
{
displayName: 'Account Number',
name: 'accountNumber',
type: 'string',
default: '',
description: 'A user defined account number',
},
// {
// displayName: 'Addresses',
// name: 'addressesUi',
// type: 'fixedCollection',
// typeOptions: {
// multipleValues: true,
// },
// default: '',
// placeholder: 'Add Address',
// options: [
// {
// name: 'addressesValues',
// displayName: 'Address',
// values: [
// {
// displayName: 'Type',
// name: 'type',
// type: 'options',
// options: [
// {
// name: 'PO Box',
// value: 'POBOX',
// },
// {
// name: 'Street',
// value: 'STREET',
// },
// ],
// default: '',
// },
// {
// displayName: 'Line 1',
// name: 'line1',
// type: 'string',
// default: '',
// },
// {
// displayName: 'Line 2',
// name: 'line2',
// type: 'string',
// default: '',
// },
// {
// displayName: 'City',
// name: 'city',
// type: 'string',
// default: '',
// },
// {
// displayName: 'Region',
// name: 'region',
// type: 'string',
// default: '',
// },
// {
// displayName: 'Postal Code',
// name: 'postalCode',
// type: 'string',
// default: '',
// },
// {
// displayName: 'Country',
// name: 'country',
// type: 'string',
// default: '',
// },
// {
// displayName: 'Attention To',
// name: 'attentionTo',
// type: 'string',
// default: '',
// },
// ],
// },
// ],
// },
{
displayName: 'Bank Account Details',
name: 'bankAccountDetails',
type: 'string',
default: '',
description: 'Bank account number of contact',
},
{
displayName: 'Contact Number',
name: 'contactNumber',
type: 'string',
default: '',
description: 'This field is read only on the Xero contact screen, used to identify contacts in external systems',
},
{
displayName: 'Contact Status',
name: 'contactStatus',
type: 'options',
options: [
{
name: 'Active',
value: 'ACTIVE',
description: 'The Contact is active and can be used in transactions',
},
{
name: 'Archived',
value: 'ARCHIVED',
description: 'The Contact is archived and can no longer be used in transactions',
},
{
name: 'GDPR Request',
value: 'GDPRREQUEST',
description: 'The Contact is the subject of a GDPR erasure request',
},
],
default: '',
description: 'Current status of a contact - see contact status types',
},
{
displayName: 'Default Currency',
name: 'defaultCurrency',
type: 'string',
default: '',
description: 'Default currency for raising invoices against contact',
},
{
displayName: 'Email',
name: 'emailAddress',
type: 'string',
default: '',
description: 'Email address of contact person (umlauts not supported) (max length = 255)',
},
{
displayName: 'First Name',
name: 'firstName',
type: 'string',
default: '',
description: 'First name of contact person (max length = 255)',
},
{
displayName: 'Last Name',
name: 'lastName',
type: 'string',
default: '',
description: 'Last name of contact person (max length = 255)',
},
// {
// displayName: 'Phones',
// name: 'phonesUi',
// type: 'fixedCollection',
// typeOptions: {
// multipleValues: true,
// },
// default: '',
// placeholder: 'Add Phone',
// options: [
// {
// name: 'phonesValues',
// displayName: 'Phones',
// values: [
// {
// displayName: 'Type',
// name: 'type',
// type: 'options',
// options: [
// {
// name: 'Default',
// value: 'DEFAULT',
// },
// {
// name: 'DDI',
// value: 'DDI',
// },
// {
// name: 'Mobile',
// value: 'MOBILE',
// },
// {
// name: 'Fax',
// value: 'FAX',
// },
// ],
// default: '',
// },
// {
// displayName: 'Number',
// name: 'phoneNumber',
// type: 'string',
// default: '',
// },
// {
// displayName: 'Area Code',
// name: 'phoneAreaCode',
// type: 'string',
// default: '',
// },
// {
// displayName: 'Country Code',
// name: 'phoneCountryCode',
// type: 'string',
// default: '',
// },
// ],
// },
// ],
// },
{
displayName: 'Purchase Default Account Code',
name: 'purchasesDefaultAccountCode',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getAccountCodes',
},
default: '',
description: 'The default purchases account code for contacts',
},
{
displayName: 'Sales Default Account Code',
name: 'salesDefaultAccountCode',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getAccountCodes',
},
default: '',
description: 'The default sales account code for contacts',
},
{
displayName: 'Skype',
name: 'skypeUserName',
type: 'string',
default: '',
description: 'Skype user name of contact',
},
{
displayName: 'Tax Number',
name: 'taxNumber',
type: 'string',
default: '',
description: 'Tax number of contact',
},
{
displayName: 'Xero Network Key',
name: 'xeroNetworkKey',
type: 'string',
default: '',
description: 'Store XeroNetworkKey for contacts',
},
],
},
/* -------------------------------------------------------------------------- */
/* contact:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Organization ID',
name: 'organizationId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTenants',
},
default: '',
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'get',
],
},
},
required: true,
},
{
displayName: 'Contact ID',
name: 'contactId',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'get',
],
},
},
required: true,
},
/* -------------------------------------------------------------------------- */
/* contact:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Organization ID',
name: 'organizationId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTenants',
},
default: '',
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'getAll',
],
},
},
required: true,
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'getAll',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 100,
description: 'How many results to return.',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'getAll',
],
},
},
options: [
{
displayName: 'Include Archived',
name: 'includeArchived',
type: 'boolean',
default: false,
description: `Contacts with a status of ARCHIVED will be included in the response`,
},
{
displayName: 'Order By',
name: 'orderBy',
type: 'string',
placeholder: 'contactID',
default: '',
description: 'Order by any element returned',
},
{
displayName: 'Sort Order',
name: 'sortOrder',
type: 'options',
options: [
{
name: 'Asc',
value: 'ASC',
},
{
name: 'Desc',
value: 'DESC',
},
],
default: '',
description: 'Sort order',
},
{
displayName: 'Where',
name: 'where',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
placeholder: 'EmailAddress!=null&&EmailAddress.StartsWith("boom")',
default: '',
description: `The where parameter allows you to filter on endpoints and elements that don't have explicit parameters. <a href="https://developer.xero.com/documentation/api/requests-and-responses#get-modified" target="_blank">Examples Here</a>`,
},
],
},
/* -------------------------------------------------------------------------- */
/* contact:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Organization ID',
name: 'organizationId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTenants',
},
default: '',
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'update',
],
},
},
required: true,
},
{
displayName: 'Contact ID',
name: 'contactId',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'update',
],
},
},
required: true,
},
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'update',
],
},
},
options: [
{
displayName: 'Account Number',
name: 'accountNumber',
type: 'string',
default: '',
description: 'A user defined account number',
},
// {
// displayName: 'Addresses',
// name: 'addressesUi',
// type: 'fixedCollection',
// typeOptions: {
// multipleValues: true,
// },
// default: '',
// placeholder: 'Add Address',
// options: [
// {
// name: 'addressesValues',
// displayName: 'Address',
// values: [
// {
// displayName: 'Type',
// name: 'type',
// type: 'options',
// options: [
// {
// name: 'PO Box',
// value: 'POBOX',
// },
// {
// name: 'Street',
// value: 'STREET',
// },
// ],
// default: '',
// },
// {
// displayName: 'Line 1',
// name: 'line1',
// type: 'string',
// default: '',
// },
// {
// displayName: 'Line 2',
// name: 'line2',
// type: 'string',
// default: '',
// },
// {
// displayName: 'City',
// name: 'city',
// type: 'string',
// default: '',
// },
// {
// displayName: 'Region',
// name: 'region',
// type: 'string',
// default: '',
// },
// {
// displayName: 'Postal Code',
// name: 'postalCode',
// type: 'string',
// default: '',
// },
// {
// displayName: 'Country',
// name: 'country',
// type: 'string',
// default: '',
// },
// {
// displayName: 'Attention To',
// name: 'attentionTo',
// type: 'string',
// default: '',
// },
// ],
// },
// ],
// },
{
displayName: 'Bank Account Details',
name: 'bankAccountDetails',
type: 'string',
default: '',
description: 'Bank account number of contact',
},
{
displayName: 'Contact Number',
name: 'contactNumber',
type: 'string',
default: '',
description: 'This field is read only on the Xero contact screen, used to identify contacts in external systems',
},
{
displayName: 'Contact Status',
name: 'contactStatus',
type: 'options',
options: [
{
name: 'Active',
value: 'ACTIVE',
description: 'The Contact is active and can be used in transactions',
},
{
name: 'Archived',
value: 'ARCHIVED',
description: 'The Contact is archived and can no longer be used in transactions',
},
{
name: 'GDPR Request',
value: 'GDPRREQUEST',
description: 'The Contact is the subject of a GDPR erasure request',
},
],
default: '',
description: 'Current status of a contact - see contact status types',
},
{
displayName: 'Default Currency',
name: 'defaultCurrency',
type: 'string',
default: '',
description: 'Default currency for raising invoices against contact',
},
{
displayName: 'Email',
name: 'emailAddress',
type: 'string',
default: '',
description: 'Email address of contact person (umlauts not supported) (max length = 255)',
},
{
displayName: 'First Name',
name: 'firstName',
type: 'string',
default: '',
description: 'First name of contact person (max length = 255)',
},
{
displayName: 'Last Name',
name: 'lastName',
type: 'string',
default: '',
description: 'Last name of contact person (max length = 255)',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'Full name of contact/organisation',
},
// {
// displayName: 'Phones',
// name: 'phonesUi',
// type: 'fixedCollection',
// typeOptions: {
// multipleValues: true,
// },
// default: '',
// placeholder: 'Add Phone',
// options: [
// {
// name: 'phonesValues',
// displayName: 'Phones',
// values: [
// {
// displayName: 'Type',
// name: 'type',
// type: 'options',
// options: [
// {
// name: 'Default',
// value: 'DEFAULT',
// },
// {
// name: 'DDI',
// value: 'DDI',
// },
// {
// name: 'Mobile',
// value: 'MOBILE',
// },
// {
// name: 'Fax',
// value: 'FAX',
// },
// ],
// default: '',
// },
// {
// displayName: 'Number',
// name: 'phoneNumber',
// type: 'string',
// default: '',
// },
// {
// displayName: 'Area Code',
// name: 'phoneAreaCode',
// type: 'string',
// default: '',
// },
// {
// displayName: 'Country Code',
// name: 'phoneCountryCode',
// type: 'string',
// default: '',
// },
// ],
// },
// ],
// },
{
displayName: 'Purchase Default Account Code',
name: 'purchasesDefaultAccountCode',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getAccountCodes',
},
default: '',
description: 'The default purchases account code for contacts',
},
{
displayName: 'Sales Default Account Code',
name: 'salesDefaultAccountCode',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getAccountCodes',
},
default: '',
description: 'The default sales account code for contacts',
},
{
displayName: 'Skype',
name: 'skypeUserName',
type: 'string',
default: '',
description: 'Skype user name of contact',
},
{
displayName: 'Tax Number',
name: 'taxNumber',
type: 'string',
default: '',
description: 'Tax number of contact',
},
{
displayName: 'Xero Network Key',
name: 'xeroNetworkKey',
type: 'string',
default: '',
description: 'Store XeroNetworkKey for contacts',
},
],
},
] as INodeProperties[];

View file

@ -0,0 +1,76 @@
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
IExecuteSingleFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IDataObject,
} from 'n8n-workflow';
export async function xeroApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, headers: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const options: OptionsWithUri = {
headers: {
'Content-Type': 'application/json',
},
method,
body,
qs,
uri: uri || `https://api.xero.com/api.xro/2.0${resource}`,
json: true
};
try {
if (body.organizationId) {
options.headers = { ...options.headers, 'Xero-tenant-id': body.organizationId };
delete body.organizationId;
}
if (Object.keys(headers).length !== 0) {
options.headers = Object.assign({}, options.headers, headers);
}
if (Object.keys(body).length === 0) {
delete options.body;
}
//@ts-ignore
return await this.helpers.requestOAuth2.call(this, 'xeroOAuth2Api', options);
} catch (error) {
let errorMessage;
if (error.response && error.response.body && error.response.body.Message) {
errorMessage = error.response.body.Message;
if (error.response.body.Elements) {
const elementErrors = [];
for (const element of error.response.body.Elements) {
elementErrors.push(element.ValidationErrors.map((error: IDataObject) => error.Message).join('|'));
}
errorMessage = elementErrors.join('-');
}
// Try to return the error prettier
throw new Error(`Xero error response [${error.statusCode}]: ${errorMessage}`);
}
throw error;
}
}
export async function xeroApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
query.page = 1;
do {
responseData = await xeroApiRequest.call(this, method, endpoint, body, query);
query.page++;
returnData.push.apply(returnData, responseData[propertyName]);
} while (
responseData[propertyName].length !== 0
);
return returnData;
}

View file

@ -0,0 +1,44 @@
export interface IAddress {
Type?: string;
AddressLine1?: string;
AddressLine2?: string;
City?: string;
Region?: string;
PostalCode?: string;
Country?: string;
AttentionTo?: string;
}
export interface IPhone {
Type?: string;
PhoneNumber?: string;
PhoneAreaCode?: string;
PhoneCountryCode?: string;
}
export interface IContact extends ITenantId {
AccountNumber?: string;
Addresses?: IAddress[];
BankAccountDetails?: string;
ContactId?: string;
ContactNumber?: string;
ContactStatus?: string;
DefaultCurrency?: string;
EmailAddress?: string;
FirstName?: string;
LastName?: string;
Name?: string;
Phones?: IPhone[];
PurchaseTrackingCategory?: string;
PurchasesDefaultAccountCode?: string;
SalesDefaultAccountCode?: string;
SalesTrackingCategory?: string;
SkypeUserName?: string;
taxNumber?: string;
xeroNetworkKey?: string;
}
export interface ITenantId {
organizationId?: string;
}

View file

@ -0,0 +1,983 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const invoiceOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'invoice',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a invoice',
},
{
name: 'Get',
value: 'get',
description: 'Get a invoice',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all invoices',
},
{
name: 'Update',
value: 'update',
description: 'Update a invoice',
},
],
default: 'create',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const invoiceFields = [
/* -------------------------------------------------------------------------- */
/* invoice:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Organization ID',
name: 'organizationId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTenants',
},
default: '',
displayOptions: {
show: {
resource: [
'invoice',
],
operation: [
'create',
],
},
},
required: true,
},
{
displayName: 'Type',
name: 'type',
type: 'options',
options: [
{
name: 'Bill',
value: 'ACCPAY',
description: 'Accounts Payable or supplier invoice'
},
{
name: 'Sales Invoice',
value: 'ACCREC',
description: ' Accounts Receivable or customer invoice'
},
],
default: '',
displayOptions: {
show: {
resource: [
'invoice',
],
operation: [
'create',
],
},
},
required: true,
description: 'Invoice Type',
},
{
displayName: 'Contact ID',
name: 'contactId',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'invoice',
],
operation: [
'create',
],
},
},
required: true,
description: 'Contact ID',
},
{
displayName: 'Line Items',
name: 'lineItemsUi',
placeholder: 'Add Line Item',
type: 'fixedCollection',
default: '',
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
resource: [
'invoice',
],
operation: [
'create',
]
},
},
description: 'Line item data',
options: [
{
name: 'lineItemsValues',
displayName: 'Line Item',
values: [
{
displayName: 'Description',
name: 'description',
type: 'string',
default: '',
description: 'A line item with just a description',
},
{
displayName: 'Quantity',
name: 'quantity',
type: 'number',
default: 1,
typeOptions: {
minValue: 1,
},
description: 'LineItem Quantity',
},
{
displayName: 'Unit Amount',
name: 'unitAmount',
type: 'string',
default: '',
description: 'Lineitem unit amount. By default, unit amount will be rounded to two decimal places.',
},
{
displayName: 'Item Code',
name: 'itemCode',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getItemCodes',
loadOptionsDependsOn: [
'organizationId',
],
},
default: '',
},
{
displayName: 'Account Code',
name: 'accountCode',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getAccountCodes',
loadOptionsDependsOn: [
'organizationId',
],
},
default: '',
},
{
displayName: 'Tax Type',
name: 'taxType',
type: 'options',
options: [
{
name: 'Tax on Purchases',
value: 'INPUT',
},
{
name: 'Tax Exempt',
value: 'NONE',
},
{
name: 'Tax on Sales',
value: 'OUTPUT',
},
{
name: 'Sales Tax on Imports ',
value: 'GSTONIMPORTS',
},
],
default: '',
required: true,
description: 'Tax Type',
},
{
displayName: 'Tax Amount',
name: 'taxAmount',
type: 'string',
default: '',
description: 'The tax amount is auto calculated as a percentage of the line amount based on the tax rate.',
},
{
displayName: 'Line Amount',
name: 'lineAmount',
type: 'string',
default: '',
description: 'The line amount reflects the discounted price if a DiscountRate has been used',
},
{
displayName: 'Discount Rate',
name: 'discountRate',
type: 'string',
default: '',
description: 'Percentage discount or discount amount being applied to a line item. Only supported on ACCREC invoices - ACCPAY invoices and credit notes in Xero do not support discounts',
},
// {
// displayName: 'Tracking',
// name: 'trackingUi',
// placeholder: 'Add Tracking',
// description: 'Any LineItem can have a maximum of 2 TrackingCategory elements.',
// type: 'fixedCollection',
// typeOptions: {
// multipleValues: true,
// },
// default: {},
// options: [
// {
// name: 'trackingValues',
// displayName: 'Tracking',
// values: [
// {
// displayName: 'Name',
// name: 'name',
// type: 'options',
// typeOptions: {
// loadOptionsMethod: 'getTrakingCategories',
// loadOptionsDependsOn: [
// 'organizationId',
// ],
// },
// default: '',
// description: 'Name of the tracking category',
// },
// {
// displayName: 'Option',
// name: 'option',
// type: 'options',
// typeOptions: {
// loadOptionsMethod: 'getTrakingOptions',
// loadOptionsDependsOn: [
// '/name',
// ],
// },
// default: '',
// description: 'Name of the option',
// },
// ],
// },
// ],
// },
],
},
],
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'invoice',
],
operation: [
'create',
],
},
},
options: [
{
displayName: 'Branding Theme ID',
name: 'brandingThemeId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getBrandingThemes',
loadOptionsDependsOn: [
'organizationId',
],
},
default: '',
},
{
displayName: 'Currency',
name: 'currency',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getCurrencies',
loadOptionsDependsOn: [
'organizationId',
],
},
default: '',
},
{
displayName: 'Currency Rate',
name: 'currencyRate',
type: 'string',
default: '',
description: 'The currency rate for a multicurrency invoice. If no rate is specified, the XE.com day rate is used.',
},
{
displayName: 'Date',
name: 'date',
type: 'dateTime',
default: '',
description: 'Date invoice was issued - YYYY-MM-DD. If the Date element is not specified it will default to the current date based on the timezone setting of the organisation',
},
{
displayName: 'Due Date',
name: 'dueDate',
type: 'dateTime',
default: '',
description: 'Date invoice is due - YYYY-MM-DD',
},
{
displayName: 'Expected Payment Date',
name: 'expectedPaymentDate',
type: 'dateTime',
default: '',
description: 'Shown on sales invoices (Accounts Receivable) when this has been set',
},
{
displayName: 'Invoice Number',
name: 'invoiceNumber',
type: 'string',
default: '',
},
{
displayName: 'Line Amount Type',
name: 'lineAmountType',
type: 'options',
options: [
{
name: 'Exclusive',
value: 'Exclusive',
description: 'Line items are exclusive of tax',
},
{
name: 'Inclusive',
value: 'Inclusive',
description: 'Line items are inclusive tax',
},
{
name: 'NoTax',
value: 'NoTax',
description: 'Line have no tax',
},
],
default: 'Exclusive',
},
{
displayName: 'Planned Payment Date ',
name: 'plannedPaymentDate',
type: 'dateTime',
default: '',
description: 'Shown on bills (Accounts Payable) when this has been set',
},
{
displayName: 'Reference',
name: 'reference',
type: 'string',
default: '',
description: 'ACCREC only - additional reference number (max length = 255)',
},
{
displayName: 'Send To Contact',
name: 'sendToContact',
type: 'boolean',
default: false,
description: 'Whether the invoice in the Xero app should be marked as "sent". This can be set only on invoices that have been approved',
},
{
displayName: 'Status',
name: 'status',
type: 'options',
options: [
{
name: 'Draft',
value: 'DRAFT',
},
{
name: 'Submitted',
value: 'SUBMITTED',
},
{
name: 'Authorised',
value: 'AUTHORISED',
},
],
default: 'DRAFT',
},
{
displayName: 'URL',
name: 'url',
type: 'string',
default: '',
description: 'URL link to a source document - shown as "Go to [appName]" in the Xero app',
},
],
},
/* -------------------------------------------------------------------------- */
/* invoice:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Organization ID',
name: 'organizationId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTenants',
},
default: '',
displayOptions: {
show: {
resource: [
'invoice',
],
operation: [
'update',
],
},
},
required: true,
},
{
displayName: 'Invoice ID',
name: 'invoiceId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'invoice',
],
operation: [
'update',
],
},
},
description: 'Invoice ID',
},
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'invoice',
],
operation: [
'update',
],
},
},
options: [
{
displayName: 'Branding Theme ID',
name: 'brandingThemeId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getBrandingThemes',
loadOptionsDependsOn: [
'organizationId',
],
},
default: '',
},
{
displayName: 'Contact ID',
name: 'contactId',
type: 'string',
default: '',
description: 'Contact ID',
},
{
displayName: 'Currency',
name: 'currency',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getCurrencies',
loadOptionsDependsOn: [
'organizationId',
],
},
default: '',
},
{
displayName: 'Currency Rate',
name: 'currencyRate',
type: 'string',
default: '',
description: 'The currency rate for a multicurrency invoice. If no rate is specified, the XE.com day rate is used.',
},
{
displayName: 'Date',
name: 'date',
type: 'dateTime',
default: '',
description: 'Date invoice was issued - YYYY-MM-DD. If the Date element is not specified it will default to the current date based on the timezone setting of the organisation',
},
{
displayName: 'Due Date',
name: 'dueDate',
type: 'dateTime',
default: '',
description: 'Date invoice is due - YYYY-MM-DD',
},
{
displayName: 'Expected Payment Date',
name: 'expectedPaymentDate',
type: 'dateTime',
default: '',
description: 'Shown on sales invoices (Accounts Receivable) when this has been set',
},
{
displayName: 'Invoice Number',
name: 'invoiceNumber',
type: 'string',
default: '',
},
{
displayName: 'Line Amount Type',
name: 'lineAmountType',
type: 'options',
options: [
{
name: 'Exclusive',
value: 'Exclusive',
description: 'Line items are exclusive of tax',
},
{
name: 'Inclusive',
value: 'Inclusive',
description: 'Line items are inclusive tax',
},
{
name: 'NoTax',
value: 'NoTax',
description: 'Line have no tax',
},
],
default: 'Exclusive',
},
{
displayName: 'Line Items',
name: 'lineItemsUi',
placeholder: 'Add Line Item',
type: 'fixedCollection',
default: '',
typeOptions: {
multipleValues: true,
},
description: 'Line item data',
options: [
{
name: 'lineItemsValues',
displayName: 'Line Item',
values: [
{
displayName: 'Line Item ID',
name: 'lineItemId',
type: 'string',
default: '',
description: 'The Xero generated identifier for a LineItem',
},
{
displayName: 'Description',
name: 'description',
type: 'string',
default: '',
description: 'A line item with just a description',
},
{
displayName: 'Quantity',
name: 'quantity',
type: 'number',
default: 1,
typeOptions: {
minValue: 1,
},
description: 'LineItem Quantity',
},
{
displayName: 'Unit Amount',
name: 'unitAmount',
type: 'string',
default: '',
description: 'Lineitem unit amount. By default, unit amount will be rounded to two decimal places.',
},
{
displayName: 'Item Code',
name: 'itemCode',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getItemCodes',
loadOptionsDependsOn: [
'organizationId',
],
},
default: '',
},
{
displayName: 'Account Code',
name: 'accountCode',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getAccountCodes',
loadOptionsDependsOn: [
'organizationId',
],
},
default: '',
},
{
displayName: 'Tax Type',
name: 'taxType',
type: 'options',
options: [
{
name: 'Tax on Purchases',
value: 'INPUT',
},
{
name: 'Tax Exempt',
value: 'NONE',
},
{
name: 'Tax on Sales',
value: 'OUTPUT',
},
{
name: 'Sales Tax on Imports ',
value: 'GSTONIMPORTS',
},
],
default: '',
required: true,
description: 'Tax Type',
},
{
displayName: 'Tax Amount',
name: 'taxAmount',
type: 'string',
default: '',
description: 'The tax amount is auto calculated as a percentage of the line amount based on the tax rate.',
},
{
displayName: 'Line Amount',
name: 'lineAmount',
type: 'string',
default: '',
description: 'The line amount reflects the discounted price if a DiscountRate has been used',
},
{
displayName: 'Discount Rate',
name: 'discountRate',
type: 'string',
default: '',
description: 'Percentage discount or discount amount being applied to a line item. Only supported on ACCREC invoices - ACCPAY invoices and credit notes in Xero do not support discounts',
},
// {
// displayName: 'Tracking',
// name: 'trackingUi',
// placeholder: 'Add Tracking',
// description: 'Any LineItem can have a maximum of 2 TrackingCategory elements.',
// type: 'fixedCollection',
// typeOptions: {
// multipleValues: true,
// },
// default: {},
// options: [
// {
// name: 'trackingValues',
// displayName: 'Tracking',
// values: [
// {
// displayName: 'Name',
// name: 'name',
// type: 'options',
// typeOptions: {
// loadOptionsMethod: 'getTrakingCategories',
// loadOptionsDependsOn: [
// 'organizationId',
// ],
// },
// default: '',
// description: 'Name of the tracking category',
// },
// {
// displayName: 'Option',
// name: 'option',
// type: 'options',
// typeOptions: {
// loadOptionsMethod: 'getTrakingOptions',
// loadOptionsDependsOn: [
// '/name',
// ],
// },
// default: '',
// description: 'Name of the option',
// },
// ],
// },
// ],
// },
],
},
],
},
{
displayName: 'Planned Payment Date ',
name: 'plannedPaymentDate',
type: 'dateTime',
default: '',
description: 'Shown on bills (Accounts Payable) when this has been set',
},
{
displayName: 'Reference',
name: 'reference',
type: 'string',
default: '',
description: 'ACCREC only - additional reference number (max length = 255)',
},
{
displayName: 'Send To Contact',
name: 'sendToContact',
type: 'boolean',
default: false,
description: 'Whether the invoice in the Xero app should be marked as "sent". This can be set only on invoices that have been approved',
},
{
displayName: 'Status',
name: 'status',
type: 'options',
options: [
{
name: 'Draft',
value: 'DRAFT',
},
{
name: 'Submitted',
value: 'SUBMITTED',
},
{
name: 'Authorised',
value: 'AUTHORISED',
},
],
default: 'DRAFT',
},
{
displayName: 'URL',
name: 'url',
type: 'string',
default: '',
description: 'URL link to a source document - shown as "Go to [appName]" in the Xero app',
},
],
},
/* -------------------------------------------------------------------------- */
/* invoice:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Organization ID',
name: 'organizationId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTenants',
},
default: '',
displayOptions: {
show: {
resource: [
'invoice',
],
operation: [
'get',
],
},
},
required: true,
},
{
displayName: 'Invoice ID',
name: 'invoiceId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'invoice',
],
operation: [
'get',
],
},
},
description: 'Invoice ID',
},
/* -------------------------------------------------------------------------- */
/* invoice:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Organization ID',
name: 'organizationId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTenants',
},
default: '',
displayOptions: {
show: {
resource: [
'invoice',
],
operation: [
'getAll',
],
},
},
required: true,
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'invoice',
],
operation: [
'getAll',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: [
'invoice',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 100,
description: 'How many results to return.',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
displayOptions: {
show: {
resource: [
'invoice',
],
operation: [
'getAll',
],
},
},
options: [
{
displayName: 'Created By My App',
name: 'createdByMyApp',
type: 'boolean',
default: false,
description: `When set to true you'll only retrieve Invoices created by your app`,
},
{
displayName: 'Order By',
name: 'orderBy',
type: 'string',
placeholder: 'InvoiceID',
default: '',
description: 'Order by any element returned',
},
{
displayName: 'Sort Order',
name: 'sortOrder',
type: 'options',
options: [
{
name: 'Asc',
value: 'ASC',
},
{
name: 'Desc',
value: 'DESC',
},
],
default: '',
description: 'Sort order',
},
{
displayName: 'Statuses',
name: 'statuses',
type: 'multiOptions',
options: [
{
name: 'Draft',
value: 'DRAFT',
},
{
name: 'Submitted',
value: 'SUBMITTED',
},
{
name: 'Authorised',
value: 'AUTHORISED',
},
],
default: [],
},
{
displayName: 'Where',
name: 'where',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
placeholder: 'EmailAddress!=null&&EmailAddress.StartsWith("boom")',
default: '',
description: `The where parameter allows you to filter on endpoints and elements that don't have explicit parameters. <a href="https://developer.xero.com/documentation/api/requests-and-responses#get-modified" target="_blank">Examples Here</a>`,
},
],
},
] as INodeProperties[];

View file

@ -0,0 +1,40 @@
import {
IDataObject,
} from 'n8n-workflow';
export interface ILineItem {
Description?: string;
Quantity?: string;
UnitAmount?: string;
ItemCode?: string;
AccountCode?: string;
LineItemID?: string;
TaxType?: string;
TaxAmount?: string;
LineAmount?: string;
DiscountRate?: string;
Tracking?: IDataObject[];
}
export interface IInvoice extends ITenantId {
Type?: string;
LineItems?: ILineItem[];
Contact?: IDataObject;
Date?: string;
DueDate?: string;
LineAmountType?: string;
InvoiceNumber?: string;
Reference?: string;
BrandingThemeID?: string;
Url?: string;
CurrencyCode?: string;
CurrencyRate?: string;
Status?: string;
SentToContact?: boolean;
ExpectedPaymentDate?: string;
PlannedPaymentDate?: string;
}
export interface ITenantId {
organizationId?: string;
}

View file

@ -0,0 +1,681 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeTypeDescription,
INodeExecutionData,
INodeType,
ILoadOptionsFunctions,
INodePropertyOptions,
} from 'n8n-workflow';
import {
xeroApiRequest,
xeroApiRequestAllItems,
} from './GenericFunctions';
import {
invoiceFields,
invoiceOperations
} from './InvoiceDescription';
import {
contactFields,
contactOperations,
} from './ContactDescription';
import {
IInvoice,
ILineItem,
} from './InvoiceInterface';
import {
IContact,
// IPhone,
// IAddress,
} from './IContactInterface';
export class Xero implements INodeType {
description: INodeTypeDescription = {
displayName: 'Xero',
name: 'xero',
icon: 'file:xero.png',
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Xero API',
defaults: {
name: 'Xero',
color: '#13b5ea',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'xeroOAuth2Api',
required: true,
}
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Contact',
value: 'contact',
},
{
name: 'Invoice',
value: 'invoice',
},
],
default: 'invoice',
description: 'Resource to consume.',
},
// CONTACT
...contactOperations,
...contactFields,
// INVOICE
...invoiceOperations,
...invoiceFields,
],
};
methods = {
loadOptions: {
// Get all the item codes to display them to user so that he can
// select them easily
async getItemCodes(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const organizationId = this.getCurrentNodeParameter('organizationId');
const returnData: INodePropertyOptions[] = [];
const { Items: items } = await xeroApiRequest.call(this, 'GET', '/items', { organizationId });
for (const item of items) {
const itemName = item.Description;
const itemId = item.Code;
returnData.push({
name: itemName,
value: itemId,
});
}
return returnData;
},
// Get all the account codes to display them to user so that he can
// select them easily
async getAccountCodes(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const organizationId = this.getCurrentNodeParameter('organizationId');
const returnData: INodePropertyOptions[] = [];
const { Accounts: accounts } = await xeroApiRequest.call(this, 'GET', '/Accounts', { organizationId });
for (const account of accounts) {
const accountName = account.Name;
const accountId = account.Code;
returnData.push({
name: accountName,
value: accountId,
});
}
return returnData;
},
// Get all the tenants to display them to user so that he can
// select them easily
async getTenants(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const tenants = await xeroApiRequest.call(this, 'GET', '', {}, {}, 'https://api.xero.com/connections');
for (const tenant of tenants) {
const tenantName = tenant.tenantName;
const tenantId = tenant.tenantId;
returnData.push({
name: tenantName,
value: tenantId,
});
}
return returnData;
},
// Get all the brading themes to display them to user so that he can
// select them easily
async getBrandingThemes(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const organizationId = this.getCurrentNodeParameter('organizationId');
const returnData: INodePropertyOptions[] = [];
const { BrandingThemes: themes } = await xeroApiRequest.call(this, 'GET', '/BrandingThemes', { organizationId });
for (const theme of themes) {
const themeName = theme.Name;
const themeId = theme.BrandingThemeID;
returnData.push({
name: themeName,
value: themeId,
});
}
return returnData;
},
// Get all the brading themes to display them to user so that he can
// select them easily
async getCurrencies(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const organizationId = this.getCurrentNodeParameter('organizationId');
const returnData: INodePropertyOptions[] = [];
const { Currencies: currencies } = await xeroApiRequest.call(this, 'GET', '/Currencies', { organizationId });
for (const currency of currencies) {
const currencyName = currency.Code;
const currencyId = currency.Description;
returnData.push({
name: currencyName,
value: currencyId,
});
}
return returnData;
},
// Get all the tracking categories to display them to user so that he can
// select them easily
async getTrakingCategories(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const organizationId = this.getCurrentNodeParameter('organizationId');
const returnData: INodePropertyOptions[] = [];
const { TrackingCategories: categories } = await xeroApiRequest.call(this, 'GET', '/TrackingCategories', { organizationId });
for (const category of categories) {
const categoryName = category.Name;
const categoryId = category.TrackingCategoryID;
returnData.push({
name: categoryName,
value: categoryId,
});
}
return returnData;
},
// // Get all the tracking categories to display them to user so that he can
// // select them easily
// async getTrakingOptions(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
// const organizationId = this.getCurrentNodeParameter('organizationId');
// const name = this.getCurrentNodeParameter('name');
// const returnData: INodePropertyOptions[] = [];
// const { TrackingCategories: categories } = await xeroApiRequest.call(this, 'GET', '/TrackingCategories', { organizationId });
// const { Options: options } = categories.filter((category: IDataObject) => category.Name === name)[0];
// for (const option of options) {
// const optionName = option.Name;
// const optionId = option.TrackingOptionID;
// returnData.push({
// name: optionName,
// value: optionId,
// });
// }
// return returnData;
// },
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = items.length as unknown as number;
const qs: IDataObject = {};
let responseData;
for (let i = 0; i < length; i++) {
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
//https://developer.xero.com/documentation/api/invoices
if (resource === 'invoice') {
if (operation === 'create') {
const organizationId = this.getNodeParameter('organizationId', i) as string;
const type = this.getNodeParameter('type', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const contactId = this.getNodeParameter('contactId', i) as string;
const lineItemsValues = ((this.getNodeParameter('lineItemsUi', i) as IDataObject).lineItemsValues as IDataObject[]);
const body: IInvoice = {
organizationId,
Type: type,
Contact: { ContactID: contactId },
};
if (lineItemsValues) {
const lineItems: ILineItem[] = [];
for (const lineItemValue of lineItemsValues) {
const lineItem: ILineItem = {
Tracking: [],
};
lineItem.AccountCode = lineItemValue.accountCode as string;
lineItem.Description = lineItemValue.description as string;
lineItem.DiscountRate = lineItemValue.discountRate as string;
lineItem.ItemCode = lineItemValue.itemCode as string;
lineItem.LineAmount = lineItemValue.lineAmount as string;
lineItem.Quantity = (lineItemValue.quantity as number).toString();
lineItem.TaxAmount = lineItemValue.taxAmount as string;
lineItem.TaxType = lineItemValue.taxType as string;
lineItem.UnitAmount = lineItemValue.unitAmount as string;
// if (lineItemValue.trackingUi) {
// //@ts-ignore
// const { trackingValues } = lineItemValue.trackingUi as IDataObject[];
// if (trackingValues) {
// for (const trackingValue of trackingValues) {
// const tracking: IDataObject = {};
// tracking.Name = trackingValue.name as string;
// tracking.Option = trackingValue.option as string;
// lineItem.Tracking!.push(tracking);
// }
// }
// }
lineItems.push(lineItem);
}
body.LineItems = lineItems;
}
if (additionalFields.brandingThemeId) {
body.BrandingThemeID = additionalFields.brandingThemeId as string;
}
if (additionalFields.currency) {
body.CurrencyCode = additionalFields.currency as string;
}
if (additionalFields.currencyRate) {
body.CurrencyRate = additionalFields.currencyRate as string;
}
if (additionalFields.date) {
body.Date = additionalFields.date as string;
}
if (additionalFields.dueDate) {
body.DueDate = additionalFields.dueDate as string;
}
if (additionalFields.dueDate) {
body.DueDate = additionalFields.dueDate as string;
}
if (additionalFields.expectedPaymentDate) {
body.ExpectedPaymentDate = additionalFields.expectedPaymentDate as string;
}
if (additionalFields.invoiceNumber) {
body.InvoiceNumber = additionalFields.invoiceNumber as string;
}
if (additionalFields.lineAmountType) {
body.LineAmountType = additionalFields.lineAmountType as string;
}
if (additionalFields.plannedPaymentDate) {
body.PlannedPaymentDate = additionalFields.plannedPaymentDate as string;
}
if (additionalFields.reference) {
body.Reference = additionalFields.reference as string;
}
if (additionalFields.sendToContact) {
body.SentToContact = additionalFields.sendToContact as boolean;
}
if (additionalFields.status) {
body.Status = additionalFields.status as string;
}
if (additionalFields.url) {
body.Url = additionalFields.url as string;
}
responseData = await xeroApiRequest.call(this, 'POST', '/Invoices', body);
responseData = responseData.Invoices;
}
if (operation === 'update') {
const invoiceId = this.getNodeParameter('invoiceId', i) as string;
const organizationId = this.getNodeParameter('organizationId', i) as string;
const updateFields = this.getNodeParameter('updateFields', i) as IDataObject;
const body: IInvoice = {
organizationId,
};
if (updateFields.lineItemsUi) {
const lineItemsValues = (updateFields.lineItemsUi as IDataObject).lineItemsValues as IDataObject[];
if (lineItemsValues) {
const lineItems: ILineItem[] = [];
for (const lineItemValue of lineItemsValues) {
const lineItem: ILineItem = {
Tracking: [],
};
lineItem.AccountCode = lineItemValue.accountCode as string;
lineItem.Description = lineItemValue.description as string;
lineItem.DiscountRate = lineItemValue.discountRate as string;
lineItem.ItemCode = lineItemValue.itemCode as string;
lineItem.LineAmount = lineItemValue.lineAmount as string;
lineItem.Quantity = (lineItemValue.quantity as number).toString();
lineItem.TaxAmount = lineItemValue.taxAmount as string;
lineItem.TaxType = lineItemValue.taxType as string;
lineItem.UnitAmount = lineItemValue.unitAmount as string;
// if (lineItemValue.trackingUi) {
// //@ts-ignore
// const { trackingValues } = lineItemValue.trackingUi as IDataObject[];
// if (trackingValues) {
// for (const trackingValue of trackingValues) {
// const tracking: IDataObject = {};
// tracking.Name = trackingValue.name as string;
// tracking.Option = trackingValue.option as string;
// lineItem.Tracking!.push(tracking);
// }
// }
// }
lineItems.push(lineItem);
}
body.LineItems = lineItems;
}
}
if (updateFields.type) {
body.Type = updateFields.type as string;
}
if (updateFields.Contact) {
body.Contact = { ContactID: updateFields.contactId as string };
}
if (updateFields.brandingThemeId) {
body.BrandingThemeID = updateFields.brandingThemeId as string;
}
if (updateFields.currency) {
body.CurrencyCode = updateFields.currency as string;
}
if (updateFields.currencyRate) {
body.CurrencyRate = updateFields.currencyRate as string;
}
if (updateFields.date) {
body.Date = updateFields.date as string;
}
if (updateFields.dueDate) {
body.DueDate = updateFields.dueDate as string;
}
if (updateFields.dueDate) {
body.DueDate = updateFields.dueDate as string;
}
if (updateFields.expectedPaymentDate) {
body.ExpectedPaymentDate = updateFields.expectedPaymentDate as string;
}
if (updateFields.invoiceNumber) {
body.InvoiceNumber = updateFields.invoiceNumber as string;
}
if (updateFields.lineAmountType) {
body.LineAmountType = updateFields.lineAmountType as string;
}
if (updateFields.plannedPaymentDate) {
body.PlannedPaymentDate = updateFields.plannedPaymentDate as string;
}
if (updateFields.reference) {
body.Reference = updateFields.reference as string;
}
if (updateFields.sendToContact) {
body.SentToContact = updateFields.sendToContact as boolean;
}
if (updateFields.status) {
body.Status = updateFields.status as string;
}
if (updateFields.url) {
body.Url = updateFields.url as string;
}
responseData = await xeroApiRequest.call(this, 'POST', `/Invoices/${invoiceId}`, body);
responseData = responseData.Invoices;
}
if (operation === 'get') {
const organizationId = this.getNodeParameter('organizationId', i) as string;
const invoiceId = this.getNodeParameter('invoiceId', i) as string;
responseData = await xeroApiRequest.call(this, 'GET', `/Invoices/${invoiceId}`, { organizationId });
responseData = responseData.Invoices;
}
if (operation === 'getAll') {
const organizationId = this.getNodeParameter('organizationId', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const options = this.getNodeParameter('options', i) as IDataObject;
if (options.statuses) {
qs.statuses = (options.statuses as string[]).join(',');
}
if (options.orderBy) {
qs.order = `${options.orderBy} ${(options.sortOrder === undefined) ? 'DESC' : options.sortOrder}`;
}
if (options.where) {
qs.where = options.where;
}
if (options.createdByMyApp) {
qs.createdByMyApp = options.createdByMyApp as boolean;
}
if (returnAll) {
responseData = await xeroApiRequestAllItems.call(this, 'Invoices', 'GET', '/Invoices', { organizationId }, qs);
} else {
const limit = this.getNodeParameter('limit', i) as number;
responseData = await xeroApiRequest.call(this, 'GET', `/Invoices`, { organizationId }, qs);
responseData = responseData.Invoices;
responseData = responseData.splice(0, limit);
}
}
}
if (resource === 'contact') {
}
if (operation === 'create') {
const organizationId = this.getNodeParameter('organizationId', i) as string;
const name = this.getNodeParameter('name', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
// const addressesUi = additionalFields.addressesUi as IDataObject;
// const phonesUi = additionalFields.phonesUi as IDataObject;
const body: IContact = {
Name: name,
};
if (additionalFields.accountNumber) {
body.AccountNumber = additionalFields.accountNumber as string;
}
if (additionalFields.bankAccountDetails) {
body.BankAccountDetails = additionalFields.bankAccountDetails as string;
}
if (additionalFields.contactNumber) {
body.ContactNumber = additionalFields.contactNumber as string;
}
if (additionalFields.contactStatus) {
body.ContactStatus = additionalFields.contactStatus as string;
}
if (additionalFields.defaultCurrency) {
body.DefaultCurrency = additionalFields.defaultCurrency as string;
}
if (additionalFields.emailAddress) {
body.EmailAddress = additionalFields.emailAddress as string;
}
if (additionalFields.firstName) {
body.FirstName = additionalFields.firstName as string;
}
if (additionalFields.lastName) {
body.LastName = additionalFields.lastName as string;
}
if (additionalFields.purchasesDefaultAccountCode) {
body.PurchasesDefaultAccountCode = additionalFields.purchasesDefaultAccountCode as string;
}
if (additionalFields.salesDefaultAccountCode) {
body.SalesDefaultAccountCode = additionalFields.salesDefaultAccountCode as string;
}
if (additionalFields.skypeUserName) {
body.SkypeUserName = additionalFields.skypeUserName as string;
}
if (additionalFields.taxNumber) {
body.taxNumber = additionalFields.taxNumber as string;
}
if (additionalFields.xeroNetworkKey) {
body.xeroNetworkKey = additionalFields.xeroNetworkKey as string;
}
// if (phonesUi) {
// const phoneValues = phonesUi?.phonesValues as IDataObject[];
// if (phoneValues) {
// const phones: IPhone[] = [];
// for (const phoneValue of phoneValues) {
// const phone: IPhone = {};
// phone.Type = phoneValue.type as string;
// phone.PhoneNumber = phoneValue.PhoneNumber as string;
// phone.PhoneAreaCode = phoneValue.phoneAreaCode as string;
// phone.PhoneCountryCode = phoneValue.phoneCountryCode as string;
// phones.push(phone);
// }
// body.Phones = phones;
// }
// }
// if (addressesUi) {
// const addressValues = addressesUi?.addressesValues as IDataObject[];
// if (addressValues) {
// const addresses: IAddress[] = [];
// for (const addressValue of addressValues) {
// const address: IAddress = {};
// address.Type = addressValue.type as string;
// address.AddressLine1 = addressValue.line1 as string;
// address.AddressLine2 = addressValue.line2 as string;
// address.City = addressValue.city as string;
// address.Region = addressValue.region as string;
// address.PostalCode = addressValue.postalCode as string;
// address.Country = addressValue.country as string;
// address.AttentionTo = addressValue.attentionTo as string;
// addresses.push(address);
// }
// body.Addresses = addresses;
// }
// }
responseData = await xeroApiRequest.call(this, 'POST', '/Contacts', { organizationId, Contacts: [body] });
responseData = responseData.Contacts;
}
if (operation === 'get') {
const organizationId = this.getNodeParameter('organizationId', i) as string;
const contactId = this.getNodeParameter('contactId', i) as string;
responseData = await xeroApiRequest.call(this, 'GET', `/Contacts/${contactId}`, { organizationId });
responseData = responseData.Contacts;
}
if (operation === 'getAll') {
const organizationId = this.getNodeParameter('organizationId', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const options = this.getNodeParameter('options', i) as IDataObject;
if (options.includeArchived) {
qs.includeArchived = options.includeArchived as boolean;
}
if (options.orderBy) {
qs.order = `${options.orderBy} ${(options.sortOrder === undefined) ? 'DESC' : options.sortOrder}`;
}
if (options.where) {
qs.where = options.where;
}
if (returnAll) {
responseData = await xeroApiRequestAllItems.call(this, 'Contacts', 'GET', '/Contacts', { organizationId }, qs);
} else {
const limit = this.getNodeParameter('limit', i) as number;
responseData = await xeroApiRequest.call(this, 'GET', `/Contacts`, { organizationId }, qs);
responseData = responseData.Contacts;
responseData = responseData.splice(0, limit);
}
}
if (operation === 'update') {
const organizationId = this.getNodeParameter('organizationId', i) as string;
const contactId = this.getNodeParameter('contactId', i) as string;
const updateFields = this.getNodeParameter('updateFields', i) as IDataObject;
// const addressesUi = updateFields.addressesUi as IDataObject;
// const phonesUi = updateFields.phonesUi as IDataObject;
const body: IContact = {};
if (updateFields.accountNumber) {
body.AccountNumber = updateFields.accountNumber as string;
}
if (updateFields.name) {
body.Name = updateFields.name as string;
}
if (updateFields.bankAccountDetails) {
body.BankAccountDetails = updateFields.bankAccountDetails as string;
}
if (updateFields.contactNumber) {
body.ContactNumber = updateFields.contactNumber as string;
}
if (updateFields.contactStatus) {
body.ContactStatus = updateFields.contactStatus as string;
}
if (updateFields.defaultCurrency) {
body.DefaultCurrency = updateFields.defaultCurrency as string;
}
if (updateFields.emailAddress) {
body.EmailAddress = updateFields.emailAddress as string;
}
if (updateFields.firstName) {
body.FirstName = updateFields.firstName as string;
}
if (updateFields.lastName) {
body.LastName = updateFields.lastName as string;
}
if (updateFields.purchasesDefaultAccountCode) {
body.PurchasesDefaultAccountCode = updateFields.purchasesDefaultAccountCode as string;
}
if (updateFields.salesDefaultAccountCode) {
body.SalesDefaultAccountCode = updateFields.salesDefaultAccountCode as string;
}
if (updateFields.skypeUserName) {
body.SkypeUserName = updateFields.skypeUserName as string;
}
if (updateFields.taxNumber) {
body.taxNumber = updateFields.taxNumber as string;
}
if (updateFields.xeroNetworkKey) {
body.xeroNetworkKey = updateFields.xeroNetworkKey as string;
}
// if (phonesUi) {
// const phoneValues = phonesUi?.phonesValues as IDataObject[];
// if (phoneValues) {
// const phones: IPhone[] = [];
// for (const phoneValue of phoneValues) {
// const phone: IPhone = {};
// phone.Type = phoneValue.type as string;
// phone.PhoneNumber = phoneValue.PhoneNumber as string;
// phone.PhoneAreaCode = phoneValue.phoneAreaCode as string;
// phone.PhoneCountryCode = phoneValue.phoneCountryCode as string;
// phones.push(phone);
// }
// body.Phones = phones;
// }
// }
// if (addressesUi) {
// const addressValues = addressesUi?.addressesValues as IDataObject[];
// if (addressValues) {
// const addresses: IAddress[] = [];
// for (const addressValue of addressValues) {
// const address: IAddress = {};
// address.Type = addressValue.type as string;
// address.AddressLine1 = addressValue.line1 as string;
// address.AddressLine2 = addressValue.line2 as string;
// address.City = addressValue.city as string;
// address.Region = addressValue.region as string;
// address.PostalCode = addressValue.postalCode as string;
// address.Country = addressValue.country as string;
// address.AttentionTo = addressValue.attentionTo as string;
// addresses.push(address);
// }
// body.Addresses = addresses;
// }
// }
responseData = await xeroApiRequest.call(this, 'POST', `/Contacts/${contactId}`, { organizationId, Contacts: [body] });
responseData = responseData.Contacts;
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else {
returnData.push(responseData as IDataObject);
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View file

@ -37,8 +37,8 @@
"dist/credentials/BannerbearApi.credentials.js", "dist/credentials/BannerbearApi.credentials.js",
"dist/credentials/BitbucketApi.credentials.js", "dist/credentials/BitbucketApi.credentials.js",
"dist/credentials/BitlyApi.credentials.js", "dist/credentials/BitlyApi.credentials.js",
"dist/credentials/CircleCiApi.credentials.js",
"dist/credentials/ChargebeeApi.credentials.js", "dist/credentials/ChargebeeApi.credentials.js",
"dist/credentials/CircleCiApi.credentials.js",
"dist/credentials/ClearbitApi.credentials.js", "dist/credentials/ClearbitApi.credentials.js",
"dist/credentials/ClickUpApi.credentials.js", "dist/credentials/ClickUpApi.credentials.js",
"dist/credentials/ClockifyApi.credentials.js", "dist/credentials/ClockifyApi.credentials.js",
@ -143,6 +143,7 @@
"dist/credentials/WebflowOAuth2Api.credentials.js", "dist/credentials/WebflowOAuth2Api.credentials.js",
"dist/credentials/WooCommerceApi.credentials.js", "dist/credentials/WooCommerceApi.credentials.js",
"dist/credentials/WordpressApi.credentials.js", "dist/credentials/WordpressApi.credentials.js",
"dist/credentials/XeroOAuth2Api.credentials.js",
"dist/credentials/ZendeskApi.credentials.js", "dist/credentials/ZendeskApi.credentials.js",
"dist/credentials/ZendeskOAuth2Api.credentials.js", "dist/credentials/ZendeskOAuth2Api.credentials.js",
"dist/credentials/ZohoOAuth2Api.credentials.js", "dist/credentials/ZohoOAuth2Api.credentials.js",
@ -171,9 +172,9 @@
"dist/nodes/Bitbucket/BitbucketTrigger.node.js", "dist/nodes/Bitbucket/BitbucketTrigger.node.js",
"dist/nodes/Bitly/Bitly.node.js", "dist/nodes/Bitly/Bitly.node.js",
"dist/nodes/Calendly/CalendlyTrigger.node.js", "dist/nodes/Calendly/CalendlyTrigger.node.js",
"dist/nodes/CircleCi/CircleCi.node.js",
"dist/nodes/Chargebee/Chargebee.node.js", "dist/nodes/Chargebee/Chargebee.node.js",
"dist/nodes/Chargebee/ChargebeeTrigger.node.js", "dist/nodes/Chargebee/ChargebeeTrigger.node.js",
"dist/nodes/CircleCi/CircleCi.node.js",
"dist/nodes/Clearbit/Clearbit.node.js", "dist/nodes/Clearbit/Clearbit.node.js",
"dist/nodes/ClickUp/ClickUp.node.js", "dist/nodes/ClickUp/ClickUp.node.js",
"dist/nodes/ClickUp/ClickUpTrigger.node.js", "dist/nodes/ClickUp/ClickUpTrigger.node.js",
@ -212,6 +213,7 @@
"dist/nodes/Google/Task/GoogleTasks.node.js", "dist/nodes/Google/Task/GoogleTasks.node.js",
"dist/nodes/GraphQL/GraphQL.node.js", "dist/nodes/GraphQL/GraphQL.node.js",
"dist/nodes/Gumroad/GumroadTrigger.node.js", "dist/nodes/Gumroad/GumroadTrigger.node.js",
"dist/nodes/HackerNews/HackerNews.node.js",
"dist/nodes/Harvest/Harvest.node.js", "dist/nodes/Harvest/Harvest.node.js",
"dist/nodes/HelpScout/HelpScout.node.js", "dist/nodes/HelpScout/HelpScout.node.js",
"dist/nodes/HelpScout/HelpScoutTrigger.node.js", "dist/nodes/HelpScout/HelpScoutTrigger.node.js",
@ -304,6 +306,7 @@
"dist/nodes/WooCommerce/WooCommerce.node.js", "dist/nodes/WooCommerce/WooCommerce.node.js",
"dist/nodes/WooCommerce/WooCommerceTrigger.node.js", "dist/nodes/WooCommerce/WooCommerceTrigger.node.js",
"dist/nodes/WriteBinaryFile.node.js", "dist/nodes/WriteBinaryFile.node.js",
"dist/nodes/Xero/Xero.node.js",
"dist/nodes/Xml.node.js", "dist/nodes/Xml.node.js",
"dist/nodes/Zendesk/Zendesk.node.js", "dist/nodes/Zendesk/Zendesk.node.js",
"dist/nodes/Zendesk/ZendeskTrigger.node.js", "dist/nodes/Zendesk/ZendeskTrigger.node.js",

View file

@ -1,6 +1,6 @@
# n8n-workflow # n8n-workflow
![n8n.io - Workflow Automation](https://n8n.io/n8n-logo.png) ![n8n.io - Workflow Automation](https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-logo.png)
Workflow base code for n8n Workflow base code for n8n