Add HaloPSA node (#2620)

* added node ui

* wip problems with auth

* updated authentication

* fixed linter error

* added haloPSA request function

* removed any return type

* fixed linter errors

* added CRUD functionalities

* updating branch from master

* updated create case for clients resource, added limit to getAll operation

* added required fields when creating clients and sites, added methods for fetching data to dynamicly populate  options when creating site or client

* added required fields for users and invoices when operation is create

* 🔨 Removed some commented code

* 🐛 Fix bug in url formating

* 🔨 fixed plural resources, fixed main for loop

* 🔨 fix trailing coma

* 🔨 fix for wrong resource endpoints

* 🔨 fixed linter complain in Jenkings node

* 🔨 replace custom fields with predefined

* 🔨 updating resources optional fields

*  Small improvement

* 🔨 replaced fixedCollection to collection in resources description

* 🔨 updated site and ticket descriptions, code clean up

* 🔨 fixed accordingly to PR review

*  Improvements

*  Improvements

*  Fix capitalization

* 👕 Fix trailing comma

* 🚧 node changes accordingly to review

*  lint errors fix

*  Activate simplify option by default

*  Fix some more issues

Co-authored-by: ricardo <ricardoespinoza105@gmail.com>
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
This commit is contained in:
Michael Kret 2022-02-11 20:00:30 +02:00 committed by GitHub
parent f35d123776
commit 66acaade29
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 2330 additions and 0 deletions

View file

@ -0,0 +1,81 @@
import {
ICredentialType,
INodeProperties,
} from 'n8n-workflow';
export class HaloPSAApi implements ICredentialType {
name = 'haloPSAApi';
displayName = 'HaloPSA API';
documentationUrl = 'halopsa';
properties: INodeProperties[] = [
{
displayName: 'Hosting Type',
name: 'hostingType',
type: 'options',
options: [
{
name: 'On-Premise Solution',
value: 'onPremise',
},
{
name: 'Hosted Solution Of Halo',
value: 'hostedHalo',
},
],
default: '',
description: 'Hosting Type',
},
{
displayName: 'HaloPSA Authorisation Server URL',
name: 'authUrl',
type: 'string',
default: '',
required: true,
},
{
displayName: 'Resource Server',
name: 'resourceApiUrl',
type: 'string',
default: '',
required: true,
description: `The Resource server is available at your "Halo Web Application url/api"`,
},
{
displayName: 'Client ID',
name: 'client_id',
type: 'string',
default: '',
required: true,
description: 'Must be your application client id',
},
{
displayName: 'Client Secret',
name: 'client_secret',
type: 'string',
default: '',
required: true,
description: 'Must be your application client secret',
},
{
displayName: 'Tenant',
name: 'tenant',
type: 'string',
displayOptions: {
show: {
hostingType: [
'hostedHalo',
],
},
},
default: '',
description: 'An additional tenant parameter for HaloPSA hosted solution',
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden',
default: 'admin edit:tickets edit:customers',
required: true,
},
];
}

View file

@ -0,0 +1,252 @@
import { IExecuteFunctions, IHookFunctions } from 'n8n-core';
import {
ICredentialDataDecryptedObject,
ICredentialTestFunctions,
IDataObject,
IExecuteSingleFunctions,
ILoadOptionsFunctions,
IPollFunctions,
JsonObject,
NodeApiError,
} from 'n8n-workflow';
import { OptionsWithUri } from 'request';
// Interfaces and Types -------------------------------------------------------------
interface IHaloPSATokens {
scope: string;
token_type: string;
access_token: string;
expires_in: number;
refresh_token: string;
id_token: string;
}
// API Requests ---------------------------------------------------------------------
export async function getAccessTokens(
this: IExecuteFunctions | ILoadOptionsFunctions,
): Promise<IHaloPSATokens> {
const credentials = (await this.getCredentials('haloPSAApi')) as IDataObject;
const options: OptionsWithUri = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
method: 'POST',
form: {
client_id: credentials.client_id,
client_secret: credentials.client_secret,
grant_type: 'client_credentials',
scope: credentials.scope,
},
uri: getAuthUrl(credentials),
json: true,
};
try {
const tokens = await this.helpers.request!(options);
return tokens;
} catch (error) {
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
export async function haloPSAApiRequest(
this:
| IHookFunctions
| IExecuteFunctions
| IExecuteSingleFunctions
| ILoadOptionsFunctions
| IPollFunctions,
method: string,
resource: string,
accessToken: string,
body: IDataObject | IDataObject[] = {},
qs: IDataObject = {},
option: IDataObject = {},
): Promise<any> { // tslint:disable-line:no-any
const resourceApiUrl = ((await this.getCredentials('haloPSAApi')) as IDataObject)
.resourceApiUrl as string;
try {
let options: OptionsWithUri = {
headers: {
Authorization: `Bearer ${accessToken}`,
'User-Agent': 'https://n8n.io',
Connection: 'keep-alive',
Accept: '*/*',
'Accept-Encoding': 'gzip, deflate, br',
'Content-Type': 'application/json',
},
method,
qs,
body,
uri: `${resourceApiUrl}${resource}`,
json: true,
};
options = Object.assign({}, options, option);
if (Object.keys(body).length === 0) {
delete options.body;
}
const result = await this.helpers.request!(options);
if (method === 'DELETE' && result.id) {
return { success: true };
}
return result;
} catch (error) {
const message = (error as JsonObject).message as string;
if (method === 'DELETE' || 'GET' || ('UPDATE' && message)) {
let newErrorMessage;
if (message.includes('400')) {
console.log(message);
newErrorMessage = JSON.parse(message.split(' - ')[1]);
(error as JsonObject).message = `For field ID, ${
newErrorMessage.id || newErrorMessage['[0].id']
}`;
}
if (message.includes('403')) {
(
error as JsonObject
).message = `You don\'t have permissions to ${method.toLowerCase()} ${resource
.split('/')[1]
.toLowerCase()}.`;
}
}
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
// export async function reasignTickets(
// this:
// | IHookFunctions
// | IExecuteFunctions
// | IExecuteSingleFunctions
// | ILoadOptionsFunctions
// | IPollFunctions,
// clientId: string,
// reasigmentCliendId: string,
// accessToken: string,
// ): Promise<any> {
// const response = (await haloPSAApiRequest.call(
// this,
// 'GET',
// `/tickets`,
// accessToken,
// {},
// { client_id: reasigmentCliendId },
// )) as IDataObject;
// const { tickets } = response;
// console.log((tickets as IDataObject[]).map(t => t.id));
// const body: IDataObject = {
// id: clientId,
// client_id: reasigmentCliendId,
// };
// for (const ticket of (tickets as IDataObject[])) {
// console.log(ticket.id);
// await haloPSAApiRequest.call(this, 'DELETE', `/tickets/${ticket.id}`, accessToken);
// }
// }
export async function haloPSAApiRequestAllItems(
this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions,
propertyName: string,
method: string,
endpoint: string,
accessToken: string,
body = {},
query: IDataObject = {},
): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData: IDataObject;
query.page_size = 100;
query.page_no = 1;
query.pageinate = true;
do {
responseData = (await haloPSAApiRequest.call(
this,
method,
endpoint,
accessToken,
body,
query,
)) as IDataObject;
returnData.push.apply(returnData, responseData[propertyName] as IDataObject[]);
query.page_no++;
//@ts-ignore
} while (returnData.length < responseData.record_count);
return returnData;
}
// Utilities ------------------------------------------------------------------------
function getAuthUrl(credentials: IDataObject) {
return credentials.hostingType === 'on-premise'
? `${credentials.appUrl}/auth/token`
: `${credentials.authUrl}/token?tenant=${credentials.tenant}`;
}
export function simplifyHaloPSAGetOutput(
response: IDataObject[],
fieldsList: string[],
): IDataObject[] {
const output = [];
for (const item of response) {
const simplifiedItem: IDataObject = {};
Object.keys(item).forEach((key: string) => {
if (fieldsList.includes(key)) {
simplifiedItem[key] = item[key];
}
});
output.push(simplifiedItem);
}
return output;
}
export function qsSetStatus(status: string) {
if (!status) return {};
const qs: IDataObject = {};
if (status === 'all') {
qs['includeinactive'] = true;
qs['includeactive'] = true;
} else if (status === 'active') {
qs['includeinactive'] = false;
qs['includeactive'] = true;
} else {
qs['includeinactive'] = true;
qs['includeactive'] = false;
}
return qs;
}
// Validation -----------------------------------------------------------------------
export async function validateCrendetials(
this: ICredentialTestFunctions,
decryptedCredentials: ICredentialDataDecryptedObject,
): Promise<IHaloPSATokens> {
const credentials = decryptedCredentials;
const options: OptionsWithUri = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
method: 'POST',
form: {
client_id: credentials.client_id,
client_secret: credentials.client_secret,
grant_type: 'client_credentials',
scope: credentials.scope,
},
uri: getAuthUrl(credentials),
json: true,
};
return (await this.helpers.request!(options)) as IHaloPSATokens;
}

View file

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

View file

@ -0,0 +1,683 @@
import { IExecuteFunctions } from 'n8n-core';
import {
ICredentialDataDecryptedObject,
ICredentialsDecrypted,
ICredentialTestFunctions,
IDataObject,
ILoadOptionsFunctions,
INodeCredentialTestResult,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
JsonObject,
} from 'n8n-workflow';
import {
clientFields,
clientOperations,
siteFields,
siteOperations,
ticketFields,
ticketOperations,
userFields,
userOperations,
} from './descriptions';
import {
getAccessTokens,
haloPSAApiRequest,
haloPSAApiRequestAllItems,
qsSetStatus,
simplifyHaloPSAGetOutput,
validateCrendetials,
} from './GenericFunctions';
export class HaloPSA implements INodeType {
description: INodeTypeDescription = {
displayName: 'HaloPSA',
name: 'haloPSA',
icon: 'file:halopsa.svg',
group: ['input'],
version: 1,
description: 'Consume HaloPSA API',
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
defaults: {
name: 'HaloPSA',
color: '#fd314e',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'haloPSAApi',
required: true,
testedBy: 'haloPSAApiCredentialTest',
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Client',
value: 'client',
},
{
name: 'Site',
value: 'site',
},
{
name: 'Ticket',
value: 'ticket',
},
{
name: 'User',
value: 'user',
},
],
default: 'client',
required: true,
},
...clientOperations,
...clientFields,
...ticketOperations,
...ticketFields,
...siteOperations,
...siteFields,
...userOperations,
...userFields,
],
};
methods = {
loadOptions: {
async getHaloPSASites(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const tokens = await getAccessTokens.call(this);
const response = (await haloPSAApiRequestAllItems.call(
this,
'sites',
'GET',
'/site',
tokens.access_token,
)) as IDataObject[];
const options = response.map((site) => {
return {
name: site.clientsite_name as string,
value: site.id as number,
};
});
return options.sort((a, b) => a.name.localeCompare(b.name));
},
async getHaloPSAClients(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const tokens = await getAccessTokens.call(this);
const response = (await haloPSAApiRequestAllItems.call(
this,
'clients',
'GET',
'/Client',
tokens.access_token,
)) as IDataObject[];
const options = response.map((client) => {
return {
name: client.name as string,
value: client.id as number,
};
});
return options.sort((a, b) => a.name.localeCompare(b.name));
},
async getHaloPSATicketsTypes(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const tokens = await getAccessTokens.call(this);
const response = (await haloPSAApiRequest.call(
this,
'GET',
`/TicketType`,
tokens.access_token,
{},
)) as IDataObject[];
const options = response.map((ticket) => {
return {
name: ticket.name as string,
value: ticket.id as number,
};
});
return options
.filter((ticket) => {
if (
// folowing types throws error 400 - "CODE:APP03/2 Please select the CAB members to approve"
ticket.name.includes('Request') ||
ticket.name.includes('Offboarding') ||
ticket.name.includes('Onboarding') ||
ticket.name.includes('Other Hardware') ||
ticket.name.includes('Software Change')
) {
return false;
}
return true;
})
.sort((a, b) => a.name.localeCompare(b.name));
},
async getHaloPSAAgents(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const tokens = await getAccessTokens.call(this);
const response = (await haloPSAApiRequest.call(
this,
'GET',
`/agent`,
tokens.access_token,
{},
)) as IDataObject[];
const options = response.map((agent) => {
return {
name: agent.name as string,
value: agent.id as number,
};
});
return options.sort((a, b) => a.name.localeCompare(b.name));
},
},
credentialTest: {
async haloPSAApiCredentialTest(
this: ICredentialTestFunctions,
credential: ICredentialsDecrypted,
): Promise<INodeCredentialTestResult> {
try {
await validateCrendetials.call(this, credential.data as ICredentialDataDecryptedObject);
} catch (error) {
return {
status: 'Error',
message: (error as JsonObject).message as string,
};
}
return {
status: 'OK',
message: 'Connection successful!',
};
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
let responseData;
const tokens = await getAccessTokens.call(this);
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
//====================================================================
// Main Loop
//====================================================================
for (let i = 0; i < items.length; i++) {
try {
if (resource === 'client') {
const simplifiedOutput = ['id', 'name', 'notes', 'is_vip', 'website'];
if (operation === 'create') {
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const name = this.getNodeParameter('clientName', i) as string;
const body: IDataObject = {
name,
...additionalFields,
};
responseData = await haloPSAApiRequest.call(
this,
'POST',
'/client',
tokens.access_token,
[body],
);
}
if (operation === 'delete') {
const clientId = this.getNodeParameter('clientId', i) as string;
// const reasign = this.getNodeParameter('reasign', i) as boolean;
// if (reasign) {
// const reasigmentCliendId = this.getNodeParameter('reasigmentCliendId', i) as string;
// await reasignTickets.call(this, clientId, reasigmentCliendId, tokens.access_token);
// }
responseData = await haloPSAApiRequest.call(
this,
'DELETE',
`/client/${clientId}`,
tokens.access_token,
);
}
if (operation === 'get') {
const clientId = this.getNodeParameter('clientId', i) as string;
const simplify = this.getNodeParameter('simplify', i) as boolean;
let response;
response = await haloPSAApiRequest.call(
this,
'GET',
`/client/${clientId}`,
tokens.access_token,
);
responseData = simplify
? simplifyHaloPSAGetOutput([response], simplifiedOutput)
: response;
}
if (operation === 'getAll') {
const filters = this.getNodeParameter('filters', i) as IDataObject;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const simplify = this.getNodeParameter('simplify', i) as boolean;
const qs: IDataObject = {};
let response;
Object.assign(qs, filters, qsSetStatus(filters.activeStatus as string));
if (returnAll) {
response = await haloPSAApiRequestAllItems.call(
this,
'clients',
'GET',
`/client`,
tokens.access_token,
{},
qs,
);
} else {
const limit = this.getNodeParameter('limit', i) as number;
qs.count = limit;
const { clients } = await haloPSAApiRequest.call(
this,
'GET',
`/client`,
tokens.access_token,
{},
qs,
);
response = clients;
}
responseData = simplify
? simplifyHaloPSAGetOutput(response, simplifiedOutput)
: response;
}
if (operation === 'update') {
const clientId = this.getNodeParameter('clientId', i) as IDataObject;
const updateFields = this.getNodeParameter('updateFields', i) as IDataObject;
const body: IDataObject = {
id: clientId,
...updateFields,
};
responseData = await haloPSAApiRequest.call(
this,
'POST',
'/client',
tokens.access_token,
[body],
);
}
}
if (resource === 'site') {
const simplifiedOutput = [
'id',
'name',
'client_id',
'maincontact_name',
'notes',
'phonenumber',
];
if (operation === 'create') {
const name = this.getNodeParameter('siteName', i) as string;
const clientId = this.getNodeParameter('clientId', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const body: IDataObject = {
name,
client_id: clientId,
...additionalFields,
};
responseData = await haloPSAApiRequest.call(
this,
'POST',
'/site',
tokens.access_token,
[body],
);
}
if (operation === 'delete') {
const siteId = this.getNodeParameter('siteId', i) as string;
responseData = await haloPSAApiRequest.call(
this,
'DELETE',
`/site/${siteId}`,
tokens.access_token,
);
}
if (operation === 'get') {
const siteId = this.getNodeParameter('siteId', i) as string;
const simplify = this.getNodeParameter('simplify', i) as boolean;
let response;
response = await haloPSAApiRequest.call(
this,
'GET',
`/site/${siteId}`,
tokens.access_token,
);
responseData = simplify
? simplifyHaloPSAGetOutput([response], simplifiedOutput)
: response;
}
if (operation === 'getAll') {
const filters = this.getNodeParameter('filters', i) as IDataObject;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const simplify = this.getNodeParameter('simplify', i) as boolean;
const qs: IDataObject = {};
let response;
Object.assign(qs, filters, qsSetStatus(filters.activeStatus as string));
if (returnAll) {
response = await haloPSAApiRequestAllItems.call(
this,
'sites',
'GET',
`/site`,
tokens.access_token,
{},
qs,
);
} else {
const limit = this.getNodeParameter('limit', i) as number;
qs.count = limit;
const { sites } = await haloPSAApiRequest.call(
this,
'GET',
`/site`,
tokens.access_token,
{},
qs,
);
response = sites;
}
responseData = simplify
? simplifyHaloPSAGetOutput(response, simplifiedOutput)
: response;
}
if (operation === 'update') {
const siteId = this.getNodeParameter('siteId', i) as IDataObject;
const updateFields = this.getNodeParameter('updateFields', i) as IDataObject;
const body: IDataObject = {
id: siteId,
...updateFields,
};
responseData = await haloPSAApiRequest.call(
this,
'POST',
'/site',
tokens.access_token,
[body],
);
}
}
if (resource === 'ticket') {
const simplifiedOutput = [
'id',
'summary',
'details',
'agent_id',
'startdate',
'targetdate',
];
if (operation === 'create') {
const summary = this.getNodeParameter('summary', i) as string;
const details = this.getNodeParameter('details', i) as string;
const ticketType = this.getNodeParameter('ticketType', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const body: IDataObject = {
tickettype_id: ticketType,
summary,
details,
...additionalFields,
};
responseData = await haloPSAApiRequest.call(
this,
'POST',
'/tickets',
tokens.access_token,
[body],
);
}
if (operation === 'delete') {
const ticketId = this.getNodeParameter('ticketId', i) as string;
responseData = await haloPSAApiRequest.call(
this,
'DELETE',
`/tickets/${ticketId}`,
tokens.access_token,
);
}
if (operation === 'get') {
const ticketId = this.getNodeParameter('ticketId', i) as string;
const simplify = this.getNodeParameter('simplify', i) as boolean;
let response;
response = await haloPSAApiRequest.call(
this,
'GET',
`/tickets/${ticketId}`,
tokens.access_token,
);
responseData = simplify
? simplifyHaloPSAGetOutput([response], simplifiedOutput)
: response;
}
if (operation === 'getAll') {
const filters = this.getNodeParameter('filters', i) as IDataObject;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const simplify = this.getNodeParameter('simplify', i) as boolean;
const qs: IDataObject = {};
let response;
Object.assign(qs, filters, qsSetStatus(filters.activeStatus as string));
if (returnAll) {
response = await haloPSAApiRequestAllItems.call(
this,
'tickets',
'GET',
`/tickets`,
tokens.access_token,
{},
qs,
);
} else {
const limit = this.getNodeParameter('limit', i) as number;
qs.count = limit;
const { tickets } = await haloPSAApiRequest.call(
this,
'GET',
`/tickets`,
tokens.access_token,
{},
qs,
);
response = tickets;
}
responseData = simplify
? simplifyHaloPSAGetOutput(response, simplifiedOutput)
: response;
}
if (operation === 'update') {
const ticketId = this.getNodeParameter('ticketId', i) as IDataObject;
const updateFields = this.getNodeParameter('updateFields', i) as IDataObject;
const body: IDataObject = {
id: ticketId,
...updateFields,
};
responseData = await haloPSAApiRequest.call(
this,
'POST',
'/tickets',
tokens.access_token,
[body],
);
}
}
if (resource === 'user') {
const simplifiedOutput = [
'id',
'name',
'site_id',
'emailaddress',
'notes',
'surname',
'inactive',
];
if (operation === 'create') {
const name = this.getNodeParameter('userName', i) as string;
const siteId = this.getNodeParameter('siteId', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const body: IDataObject = {
name,
site_id: siteId,
...additionalFields,
};
responseData = await haloPSAApiRequest.call(
this,
'POST',
'/users',
tokens.access_token,
[body],
);
}
if (operation === 'delete') {
const userId = this.getNodeParameter('userId', i) as string;
responseData = await haloPSAApiRequest.call(
this,
'DELETE',
`/users/${userId}`,
tokens.access_token,
);
}
if (operation === 'get') {
const userId = this.getNodeParameter('userId', i) as string;
const simplify = this.getNodeParameter('simplify', i) as boolean;
let response;
response = await haloPSAApiRequest.call(
this,
'GET',
`/users/${userId}`,
tokens.access_token,
);
responseData = simplify
? simplifyHaloPSAGetOutput([response], simplifiedOutput)
: response;
}
if (operation === 'getAll') {
const filters = this.getNodeParameter('filters', i) as IDataObject;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const simplify = this.getNodeParameter('simplify', i) as boolean;
const qs: IDataObject = {};
let response;
Object.assign(qs, filters, qsSetStatus(filters.activeStatus as string));
if (returnAll) {
response = await haloPSAApiRequestAllItems.call(
this,
'users',
'GET',
`/users`,
tokens.access_token,
{},
qs,
);
} else {
const limit = this.getNodeParameter('limit', i) as number;
qs.count = limit;
const { users } = await haloPSAApiRequest.call(
this,
'GET',
`/users`,
tokens.access_token,
{},
qs,
);
response = users;
}
responseData = simplify
? simplifyHaloPSAGetOutput(response, simplifiedOutput)
: response;
}
if (operation === 'update') {
const userId = this.getNodeParameter('userId', i) as IDataObject;
const updateFields = this.getNodeParameter('updateFields', i) as IDataObject;
const body: IDataObject = {
id: userId,
...updateFields,
};
responseData = await haloPSAApiRequest.call(
this,
'POST',
'/users',
tokens.access_token,
[body],
);
}
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData);
} else if (responseData !== undefined) {
returnData.push(responseData);
}
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ error: (error as JsonObject).message });
continue;
}
throw error;
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}

View file

@ -0,0 +1,340 @@
import { INodeProperties } from 'n8n-workflow';
export const clientOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: ['client'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a client',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a client',
},
{
name: 'Get',
value: 'get',
description: 'Get a client',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all clients',
},
{
name: 'Update',
value: 'update',
description: 'Update a client',
},
],
default: 'create',
},
];
export const clientFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* client:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Name',
name: 'clientName',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: ['create'],
resource: ['client'],
},
},
description: 'Enter client name',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
default: {},
placeholder: 'Add Field',
displayOptions: {
show: {
operation: ['create'],
resource: ['client'],
},
},
options: [
{
displayName: 'Account Status',
name: 'inactive',
type: 'options',
default: false,
options: [
{
name: 'Active',
value: false,
},
{
name: 'Inactive',
value: true,
},
],
},
{
displayName: 'Notes',
name: 'notes',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
},
{
displayName: 'VIP',
name: 'is_vip',
type: 'boolean',
default: false,
description: 'Whether the client is VIP or not',
},
{
displayName: 'Website',
name: 'website',
type: 'string',
default: '',
},
],
},
/* -------------------------------------------------------------------------- */
/* client:delete */
/* -------------------------------------------------------------------------- */
// {
// displayName: 'Reasign tickets before deleting',
// name: 'reasign',
// type: 'boolean',
// default: false,
// description: 'Whether tickets assigned to client sould be reasigned',
// displayOptions: {
// show: {
// resource: [
// 'client',
// ],
// operation: [
// 'delete',
// ],
// },
// },
// },
// {
// displayName: 'Client ID For Reasigment',
// name: 'reasigmentCliendId',
// type: 'string',
// default: '',
// displayOptions: {
// show: {
// resource: [
// 'client',
// ],
// operation: [
// 'delete',
// ],
// reasign: [
// true
// ]
// },
// },
// },
/* -------------------------------------------------------------------------- */
/* client:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Client ID',
name: 'clientId',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['client'],
operation: ['get', 'delete'],
},
},
},
{
displayName: 'Simplify Output',
name: 'simplify',
type: 'boolean',
default: true,
description: 'Whether output should be simplified',
displayOptions: {
show: {
resource: ['client'],
operation: ['get', 'getAll'],
},
},
},
/* -------------------------------------------------------------------------- */
/* client:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['client'],
operation: ['getAll'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
default: 50,
displayOptions: {
show: {
resource: ['client'],
operation: ['getAll'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 1000,
},
description: 'Max number of results to return',
},
{
displayName: 'Filters',
name: 'filters',
type: 'collection',
default: {},
placeholder: 'Add Field',
displayOptions: {
show: {
resource: ['client'],
operation: ['getAll'],
},
},
options: [
{
displayName: 'Active Status',
name: 'activeStatus',
type: 'options',
default: 'active',
options: [
{
name: 'Active only',
value: 'active',
description: 'Whether to include active customers in the response',
},
{
name: 'All',
value: 'all',
description: 'Whether to include active and inactive customers in the response',
},
{
name: 'Inactive only',
value: 'inactive',
description: 'Whether to include inactive Customers in the response',
},
],
},
{
displayName: 'Text To Filter By',
name: 'search',
type: 'string',
default: '',
description: 'Filter clients by your search string',
},
],
},
/* -------------------------------------------------------------------------- */
/* client:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Client ID',
name: 'clientId',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['client'],
operation: ['update'],
},
},
},
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
default: {},
placeholder: 'Add Field',
displayOptions: {
show: {
resource: ['client'],
operation: ['update'],
},
},
options: [
{
displayName: 'Account Status',
name: 'inactive',
type: 'options',
default: false,
options: [
{
name: 'Active',
value: false,
},
{
name: 'Inactive',
value: true,
},
],
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
},
{
displayName: 'Notes',
name: 'notes',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
},
{
displayName: 'VIP',
name: 'is_vip',
type: 'boolean',
default: false,
description: 'Whether the client is VIP or not',
},
{
displayName: 'Website',
name: 'website',
type: 'string',
default: '',
},
],
},
];

View file

@ -0,0 +1,314 @@
import { INodeProperties } from 'n8n-workflow';
export const siteOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: ['site'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a site',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a site',
},
{
name: 'Get',
value: 'get',
description: 'Get a site',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all sites',
},
{
name: 'Update',
value: 'update',
description: 'Update a site',
},
],
default: 'create',
},
];
export const siteFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* site:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Name',
name: 'siteName',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['site'],
operation: ['create'],
},
},
description: 'Enter site name',
},
{
displayName: 'Select Client by ID',
name: 'selectOption',
type: 'boolean',
default: false,
description: 'Whether client can be selected by id',
displayOptions: {
show: {
resource: ['site'],
operation: ['create'],
},
},
},
{
displayName: 'Client ID',
name: 'clientId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['site'],
operation: ['create'],
selectOption: [true],
},
},
},
{
displayName: 'Client Name',
name: 'clientId',
type: 'options',
default: '',
required: true,
typeOptions: {
loadOptionsMethod: 'getHaloPSAClients',
},
displayOptions: {
show: {
resource: ['site'],
operation: ['create'],
selectOption: [false],
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
default: {},
placeholder: 'Add Field',
displayOptions: {
show: {
resource: ['site'],
operation: ['create'],
},
},
options: [
{
displayName: 'Main Contact',
name: 'maincontact_name',
type: 'string',
default: '',
},
{
displayName: 'Notes',
name: 'notes',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
},
{
displayName: 'Phone Number',
name: 'phonenumber',
type: 'string',
default: '',
},
],
},
/* -------------------------------------------------------------------------- */
/* site:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Site ID',
name: 'siteId',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['site'],
operation: ['delete', 'get'],
},
},
},
{
displayName: 'Simplify Output',
name: 'simplify',
type: 'boolean',
default: true,
description: 'Whether output should be simplified',
displayOptions: {
show: {
resource: ['site'],
operation: ['get', 'getAll'],
},
},
},
/* -------------------------------------------------------------------------- */
/* site:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['site'],
operation: ['getAll'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
default: 50,
displayOptions: {
show: {
resource: ['site'],
operation: ['getAll'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 1000,
},
description: 'Max number of results to return',
},
{
displayName: 'Filters',
name: 'filters',
type: 'collection',
default: {},
placeholder: 'Add Field',
displayOptions: {
show: {
resource: ['site'],
operation: ['getAll'],
},
},
options: [
{
displayName: 'Active Status',
name: 'activeStatus',
type: 'options',
default: 'all',
options: [
{
name: 'Active only',
value: 'active',
description: 'Whether to include active sites in the response',
},
{
name: 'All',
value: 'all',
description: 'Whether to include active and inactive sites in the response',
},
{
name: 'Inactive only',
value: 'inactive',
description: 'Whether to include inactive sites in the response',
},
],
},
{
displayName: 'Text To Filter By',
name: 'search',
type: 'string',
default: '',
description: 'Filter sites by your search string',
},
],
},
/* -------------------------------------------------------------------------- */
/* site:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Site ID',
name: 'siteId',
type: 'string',
default: '',
displayOptions: {
show: {
resource: ['site'],
operation: ['update'],
},
},
},
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
default: {},
placeholder: 'Add Field',
displayOptions: {
show: {
resource: ['site'],
operation: ['update'],
},
},
options: [
{
displayName: 'Client ID',
name: 'client_id',
type: 'string',
default: '',
},
{
displayName: 'Main Contact',
name: 'maincontact_name',
type: 'string',
default: '',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'Enter site name',
},
{
displayName: 'Notes',
name: 'notes',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
},
{
displayName: 'Phone Number',
name: 'phonenumber',
type: 'string',
default: '',
},
],
},
];

View file

@ -0,0 +1,300 @@
import { INodeProperties } from 'n8n-workflow';
export const ticketOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: ['ticket'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a ticket',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a ticket',
},
{
name: 'Get',
value: 'get',
description: 'Get a ticket',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all tickets',
},
{
name: 'Update',
value: 'update',
description: 'Update a ticket',
},
],
default: 'delete',
},
];
export const ticketFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* ticket:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Ticket Type',
name: 'ticketType',
type: 'options',
default: '',
required: true,
typeOptions: {
loadOptionsMethod: 'getHaloPSATicketsTypes',
},
displayOptions: {
show: {
resource: ['ticket'],
operation: ['create'],
},
},
},
{
displayName: 'Summary',
name: 'summary',
type: 'string',
default: '',
placeholder: '',
required: true,
displayOptions: {
show: {
resource: ['ticket'],
operation: ['create'],
},
},
},
{
displayName: 'Details',
name: 'details',
type: 'string',
default: '',
placeholder: '',
required: true,
displayOptions: {
show: {
resource: ['ticket'],
operation: ['create'],
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
default: {},
placeholder: 'Add Field',
displayOptions: {
show: {
resource: ['ticket'],
operation: ['create'],
},
},
options: [
{
displayName: 'Assigned Agent Name/ID',
name: 'agent_id',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getHaloPSAAgents',
},
},
{
displayName: 'Start Date',
name: 'startdate',
type: 'dateTime',
default: '',
},
{
displayName: 'Target Date',
name: 'targetdate',
type: 'dateTime',
default: '',
},
],
},
/* -------------------------------------------------------------------------- */
/* site:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Ticket ID',
name: 'ticketId',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['ticket'],
operation: ['delete', 'get'],
},
},
},
{
displayName: 'Simplify Output',
name: 'simplify',
type: 'boolean',
default: true,
description: 'Whether output should be simplified',
displayOptions: {
show: {
resource: ['ticket'],
operation: ['get', 'getAll'],
},
},
},
/* -------------------------------------------------------------------------- */
/* ticket:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['ticket'],
operation: ['getAll'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
default: 50,
displayOptions: {
show: {
resource: ['ticket'],
operation: ['getAll'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 1000,
},
description: 'Max number of results to return',
},
{
displayName: 'Filters',
name: 'filters',
type: 'collection',
default: {},
placeholder: 'Add Field',
displayOptions: {
show: {
resource: ['ticket'],
operation: ['getAll'],
},
},
options: [
{
displayName: 'Active Status',
name: 'activeStatus',
type: 'options',
default: 'all',
options: [
{
name: 'Active only',
value: 'active',
description: 'Whether to include active customers in the response',
},
{
name: 'All',
value: 'all',
description: 'Whether to include active and inactive customers in the response',
},
{
name: 'Inactive only',
value: 'inactive',
description: 'Whether to include inactive Customers in the responsee',
},
],
},
{
displayName: 'Text To Filter By',
name: 'search',
type: 'string',
default: '',
description: 'Filter tickets by your search string',
},
],
},
/* -------------------------------------------------------------------------- */
/* ticket:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Ticket ID',
name: 'ticketId',
type: 'string',
default: '',
displayOptions: {
show: {
resource: ['ticket'],
operation: ['update'],
},
},
},
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
default: {},
placeholder: 'Add Field',
displayOptions: {
show: {
resource: ['ticket'],
operation: ['update'],
},
},
options: [
{
displayName: 'Assigned Agent Name/ID',
name: 'agent_id',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getHaloPSAAgents',
},
},
{
displayName: 'Details',
name: 'details',
type: 'string',
default: '',
},
{
displayName: 'Start Date',
name: 'startdate',
type: 'dateTime',
default: '',
},
{
displayName: 'Summary',
name: 'summary',
type: 'string',
default: '',
},
{
displayName: 'Target Date',
name: 'targetdate',
type: 'dateTime',
default: '',
},
],
},
];

View file

@ -0,0 +1,320 @@
import { INodeProperties } from 'n8n-workflow';
export const userOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: ['user'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a user',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a user',
},
{
name: 'Get',
value: 'get',
description: 'Get a user',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all users',
},
{
name: 'Update',
value: 'update',
description: 'Update a user',
},
],
default: 'create',
},
];
export const userFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* user:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Name',
name: 'userName',
type: 'string',
default: '',
description: 'Enter user name',
required: true,
displayOptions: {
show: {
resource: ['user'],
operation: ['create'],
},
},
},
{
displayName: 'Site Name/ID',
name: 'siteId',
type: 'options',
default: '',
required: true,
typeOptions: {
loadOptionsMethod: 'getHaloPSASites',
},
displayOptions: {
show: {
resource: ['user'],
operation: ['create'],
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
default: {},
placeholder: 'Add Field',
displayOptions: {
show: {
resource: ['user'],
operation: ['create'],
},
},
options: [
{
displayName: 'Email',
name: 'emailaddress',
type: 'string',
default: '',
},
{
displayName: 'Notes',
name: 'notes',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
},
{
displayName: 'Password',
name: 'password',
type: 'string',
default: '',
description:
'Your new password must be at least 8 characters long and contain at least one letter, one number or symbol, one upper case character and one lower case character',
},
{
displayName: 'Surname',
name: 'surname',
type: 'string',
default: '',
},
{
displayName: 'User is Inactive',
name: 'inactive',
type: 'boolean',
default: false,
},
],
},
/* -------------------------------------------------------------------------- */
/* user:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'User ID',
name: 'userId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['user'],
operation: ['delete', 'get'],
},
},
},
{
displayName: 'Simplify Output',
name: 'simplify',
type: 'boolean',
default: true,
description: 'Whether output should be simplified',
displayOptions: {
show: {
resource: ['user'],
operation: ['get', 'getAll'],
},
},
},
/* -------------------------------------------------------------------------- */
/* user:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['user'],
operation: ['getAll'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
default: 50,
displayOptions: {
show: {
resource: ['user'],
operation: ['getAll'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 1000,
},
description: 'Max number of results to return',
},
{
displayName: 'Filters',
name: 'filters',
type: 'collection',
default: {},
placeholder: 'Add Field',
displayOptions: {
show: {
resource: ['user'],
operation: ['getAll'],
},
},
options: [
{
displayName: 'Active Status',
name: 'activeStatus',
type: 'options',
default: 'all',
options: [
{
name: 'Active only',
value: 'active',
description: 'Whether to include active customers in the response',
},
{
name: 'All',
value: 'all',
description: 'Whether to include active and inactive customers in the response',
},
{
name: 'Inactive only',
value: 'inactive',
description: 'Whether to include inactive Customers in the response',
},
],
},
{
displayName: 'Text To Filter By',
name: 'search',
type: 'string',
default: '',
description: 'Filter users by your search string',
},
],
},
/* -------------------------------------------------------------------------- */
/* user:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'User ID',
name: 'userId',
type: 'string',
default: '',
displayOptions: {
show: {
resource: ['user'],
operation: ['update'],
},
},
},
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
default: {},
placeholder: 'Add Field',
displayOptions: {
show: {
resource: ['user'],
operation: ['update'],
},
},
options: [
{
displayName: 'Email',
name: 'emailaddress',
type: 'string',
default: '',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'Enter user name',
},
{
displayName: 'Notes',
name: 'notes',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
},
{
displayName: 'Password',
name: 'password',
type: 'string',
typeOptions: {
password: true,
},
default: '',
description:
'Your new password must be at least 8 characters long and contain at least one letter, one number or symbol, one upper case character and one lower case character',
},
{
displayName: 'Site ID',
name: 'site_id',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getHaloPSASites',
},
},
{
displayName: 'Surname',
name: 'surname',
type: 'string',
default: '',
},
{
displayName: 'User is Inactive',
name: 'inactive',
type: 'boolean',
default: false,
},
],
},
];

View file

@ -0,0 +1,15 @@
import { clientFields, clientOperations } from './ClientDescription';
import { siteFields, siteOperations } from './SiteDescription';
import { ticketFields, ticketOperations } from './TicketDescription';
import { userFields, userOperations } from './UserDescription';
export {
clientFields,
clientOperations,
siteFields,
siteOperations,
ticketFields,
ticketOperations,
userFields,
userOperations
};

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" style="enable-background:new 0 0 142.02 142.02" viewBox="0 0 142.02 142.02" xml:space="preserve"><path d="M71.01 0C31.79 0 0 31.79 0 71.01c0 39.22 31.79 71.01 71.01 71.01s71.01-31.79 71.01-71.01C142.02 31.79 110.23 0 71.01 0zm0 107.77c-4.72 0-9.22-.89-13.37-2.49l-13.12 6.34.23-14.74c-6.72-6.72-10.87-16-10.87-26.25 0-20.51 16.62-37.13 37.13-37.13s37.13 16.62 37.13 37.13c0 20.52-16.62 37.14-37.13 37.14z" style="fill:#f8384b"/></svg>

After

Width:  |  Height:  |  Size: 474 B

View file

@ -133,6 +133,7 @@
"dist/credentials/GrafanaApi.credentials.js",
"dist/credentials/GSuiteAdminOAuth2Api.credentials.js",
"dist/credentials/GumroadApi.credentials.js",
"dist/credentials/HaloPSAApi.credentials.js",
"dist/credentials/HarvestApi.credentials.js",
"dist/credentials/HarvestOAuth2Api.credentials.js",
"dist/credentials/HelpScoutOAuth2Api.credentials.js",
@ -458,6 +459,7 @@
"dist/nodes/Grist/Grist.node.js",
"dist/nodes/Gumroad/GumroadTrigger.node.js",
"dist/nodes/HackerNews/HackerNews.node.js",
"dist/nodes/HaloPSA/HaloPSA.node.js",
"dist/nodes/Harvest/Harvest.node.js",
"dist/nodes/HelpScout/HelpScout.node.js",
"dist/nodes/HelpScout/HelpScoutTrigger.node.js",