n8n/packages/nodes-base/nodes/Iterable/Iterable.node.ts

327 lines
8.6 KiB
TypeScript
Raw Normal View History

import { IExecuteFunctions } from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
:sparkles: Improve node error handling (#1309) * Add path mapping and response error interfaces * Add error handling and throwing functionality * Refactor error handling into a single function * Re-implement error handling in Hacker News node * Fix linting details * Re-implement error handling in Spotify node * Re-implement error handling in G Suite Admin node * :construction: create basic setup NodeError * :construction: add httpCodes * :construction: add path priolist * :construction: handle statusCode in error, adjust interfaces * :construction: fixing type issues w/Ivan * :construction: add error exploration * 👔 fix linter issues * :wrench: improve object check * :construction: remove path passing from NodeApiError * :construction: add multi error + refactor findProperty method * 👔 allow any * :wrench: handle multi error message callback * :zap: change return type of callback * :zap: add customCallback to MultiError * :construction: refactor to use INode * :hammer: handle arrays, continue search after first null property found * 🚫 refactor method access * :construction: setup NodeErrorView * :zap: change timestamp to Date.now * :books: Add documentation for methods and constants * :construction: change message setting * 🚚 move NodeErrors to workflow * :sparkles: add new ErrorView for Nodes * :art: improve error notification * :art: refactor interfaces * :zap: add WorkflowOperationError, refactor error throwing * 👕 fix linter issues * :art: rename param * :bug: fix handling normal errors * :zap: add usage of NodeApiError * :art: fix throw new error instead of constructor * :art: remove unnecessary code/comments * :art: adjusted spacing + updated status messages * :art: fix tab indentation * ✨ Replace current errors with custom errors (#1576) * :zap: Introduce NodeApiError in catch blocks * :zap: Introduce NodeOperationError in nodes * :zap: Add missing errors and remove incompatible * :zap: Fix NodeOperationError in incompatible nodes * :wrench: Adjust error handling in missed nodes PayPal, FileMaker, Reddit, Taiga and Facebook Graph API nodes * :hammer: Adjust Strava Trigger node error handling * :hammer: Adjust AWS nodes error handling * :hammer: Remove duplicate instantiation of NodeApiError * :bug: fix strava trigger node error handling * Add XML parsing to NodeApiError constructor (#1633) * :bug: Remove type annotation from catch variable * :sparkles: Add XML parsing to NodeApiError * :zap: Simplify error handling in Rekognition node * :zap: Pass in XML flag in generic functions * :fire: Remove try/catch wrappers at call sites * :hammer: Refactor setting description from XML * :hammer: Refactor let to const in resource loaders * :zap: Find property in parsed XML * :zap: Change let to const * :fire: Remove unneeded try/catch block * :shirt: Fix linting issues * :bug: Fix errors from merge conflict resolution * :zap: Add custom errors to latest contributions * :shirt: Fix linting issues * :zap: Refactor MongoDB helpers for custom errors * :bug: Correct custom error type * :zap: Apply feedback to A nodes * :zap: Apply feedback to missed A node * :zap: Apply feedback to B-D nodes * :zap: Apply feedback to E-F nodes * :zap: Apply feedback to G nodes * :zap: Apply feedback to H-L nodes * :zap: Apply feedback to M nodes * :zap: Apply feedback to P nodes * :zap: Apply feedback to R nodes * :zap: Apply feedback to S nodes * :zap: Apply feedback to T nodes * :zap: Apply feedback to V-Z nodes * :zap: Add HTTP code to iterable node error * :hammer: Standardize e as error * :hammer: Standardize err as error * :zap: Fix error handling for non-standard nodes Co-authored-by: Ben Hesseldieck <b.hesseldieck@gmail.com> Co-authored-by: Ben Hesseldieck <b.hesseldieck@gmail.com> Co-authored-by: Ben Hesseldieck <1849459+BHesseldieck@users.noreply.github.com>
2021-04-16 09:33:36 -07:00
NodeApiError,
NodeOperationError,
} from 'n8n-workflow';
import { iterableApiRequest } from './GenericFunctions';
import { eventFields, eventOperations } from './EventDescription';
import { userFields, userOperations } from './UserDescription';
import { userListFields, userListOperations } from './UserListDescription';
import moment from 'moment-timezone';
export class Iterable implements INodeType {
description: INodeTypeDescription = {
displayName: 'Iterable',
name: 'iterable',
refactor: Apply more `eslint-plugin-n8n-nodes-base` rules (#3534) * :zap: Update `lintfix` script * :zap: Run baseline `lintfix` * :fire: Remove unneeded exceptions (#3538) * :fire: Remove exceptions for `node-param-default-wrong-for-simplify` * :fire: Remove exceptions for `node-param-placeholder-miscased-id` * :zap: Update version * :shirt: Apply `node-param-placeholder-missing` (#3542) * :shirt: Apply `filesystem-wrong-cred-filename` (#3543) * :shirt: Apply `node-param-description-missing-from-dynamic-options` (#3545) Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :shirt: Apply `node-class-description-empty-string` (#3546) * :shirt: Apply `node-class-description-icon-not-svg` (#3548) * :shirt: Apply `filesystem-wrong-node-filename` (#3549) Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :shirt: Expand lintings to credentials (#3550) * :shirt: Apply `node-param-multi-options-type-unsorted-items` (#3552) * :zap: fix * :zap: Minor fixes Co-authored-by: Michael Kret <michael.k@radency.com> * :shirt: Apply `node-param-description-wrong-for-dynamic-multi-options` (#3541) * :zap: Add new lint rule, node-param-description-wrong-for-dynamic-multi-options * :zap: Fix with updated linting rules * :zap: Minor fixes Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :shirt: Apply `node-param-description-boolean-without-whether` (#3553) * :zap: fix * Update packages/nodes-base/nodes/Clockify/ProjectDescription.ts Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :shirt: Apply node-param-display-name-wrong-for-dynamic-multi-options (#3537) * :shirt: Add exceptions * :shirt: Add exception * :pencil2: Alphabetize rules * :zap: Restore `lintfix` command Co-authored-by: agobrech <45268029+agobrech@users.noreply.github.com> Co-authored-by: Omar Ajoue <krynble@gmail.com> Co-authored-by: Michael Kret <michael.k@radency.com> Co-authored-by: brianinoa <54530642+brianinoa@users.noreply.github.com> Co-authored-by: Michael Kret <88898367+michael-radency@users.noreply.github.com>
2022-06-20 07:54:01 -07:00
// eslint-disable-next-line n8n-nodes-base/node-class-description-icon-not-svg
icon: 'file:iterable.png',
group: ['input'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Iterable API',
defaults: {
name: 'Iterable',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'iterableApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
refactor: Apply more nodelinting rules (#3324) * :pencil2: Alphabetize lint rules * :fire: Remove duplicates * :zap: Update `lintfix` script * :shirt: Apply `node-param-operation-without-no-data-expression` (#3329) * :shirt: Apply `node-param-operation-without-no-data-expression` * :shirt: Add exceptions * :shirt: Apply `node-param-description-weak` (#3328) Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :shirt: Apply `node-param-option-value-duplicate` (#3331) * :shirt: Apply `node-param-description-miscased-json` (#3337) * :shirt: Apply `node-param-display-name-excess-inner-whitespace` (#3335) Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :shirt: Apply `node-param-type-options-missing-from-limit` (#3336) * Rule workig as intended * :pencil2: Uncomment rules Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :shirt: Apply `node-param-option-name-duplicate` (#3338) Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :shirt: Apply `node-param-description-wrong-for-simplify` (#3334) * :zap: fix * :zap: exceptions * :zap: changed rule ignoring from file to line * :shirt: Apply `node-param-resource-without-no-data-expression` (#3339) * :shirt: Apply `node-param-display-name-untrimmed` (#3341) * :shirt: Apply `node-param-display-name-miscased-id` (#3340) Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :shirt: Apply `node-param-resource-with-plural-option` (#3342) * :shirt: Apply `node-param-description-wrong-for-upsert` (#3333) * :zap: fix * :zap: replaced record with contact in description * :zap: fix Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :shirt: Apply `node-param-option-description-identical-to-name` (#3343) * :shirt: Apply `node-param-option-name-containing-star` (#3347) * :shirt: Apply `node-param-display-name-wrong-for-update-fields` (#3348) * :shirt: Apply `node-param-option-name-wrong-for-get-all` (#3345) * :zap: fix * :zap: exceptions * :shirt: Apply node-param-display-name-wrong-for-simplify (#3344) * Rule working as intended * Uncomented other rules * :shirt: Undo and add exceptions Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :zap: Alphabetize lint rules * :zap: Restore `lintfix` script Co-authored-by: Michael Kret <88898367+michael-radency@users.noreply.github.com> Co-authored-by: agobrech <45268029+agobrech@users.noreply.github.com>
2022-05-20 14:47:24 -07:00
noDataExpression: true,
options: [
{
name: 'Event',
value: 'event',
},
{
name: 'User',
value: 'user',
},
{
name: 'User List',
value: 'userList',
},
],
default: 'user',
},
...eventOperations,
...eventFields,
...userOperations,
...userFields,
...userListOperations,
...userListFields,
],
};
methods = {
loadOptions: {
// Get all the lists available channels
async getLists(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const { lists } = await iterableApiRequest.call(this, 'GET', '/lists');
const returnData: INodePropertyOptions[] = [];
for (const list of lists) {
returnData.push({
name: list.name,
value: list.id,
});
}
return returnData;
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
refactor: Apply `eslint-plugin-n8n-nodes-base` autofixable rules (#3174) * :zap: Initial setup * :shirt: Update `.eslintignore` * :shirt: Autofix node-param-default-missing (#3173) * :fire: Remove duplicate key * :shirt: Add exceptions * :package: Update package-lock.json * :shirt: Apply `node-class-description-inputs-wrong-trigger-node` (#3176) * :shirt: Apply `node-class-description-inputs-wrong-regular-node` (#3177) * :shirt: Apply `node-class-description-outputs-wrong` (#3178) * :shirt: Apply `node-execute-block-double-assertion-for-items` (#3179) * :shirt: Apply `node-param-default-wrong-for-collection` (#3180) * :shirt: Apply node-param-default-wrong-for-boolean (#3181) * Autofixed default missing * Autofixed booleans, worked well * :zap: Fix params * :rewind: Undo exempted autofixes * :package: Update package-lock.json * :shirt: Apply node-class-description-missing-subtitle (#3182) * :zap: Fix missing comma * :shirt: Apply `node-param-default-wrong-for-fixed-collection` (#3184) * :shirt: Add exception for `node-class-description-missing-subtitle` * :shirt: Apply `node-param-default-wrong-for-multi-options` (#3185) * :shirt: Apply `node-param-collection-type-unsorted-items` (#3186) * Missing coma * :shirt: Apply `node-param-default-wrong-for-simplify` (#3187) * :shirt: Apply `node-param-description-comma-separated-hyphen` (#3190) * :shirt: Apply `node-param-description-empty-string` (#3189) * :shirt: Apply `node-param-description-excess-inner-whitespace` (#3191) * Rule looks good * Add whitespace rule in eslint config * :zao: fix * :shirt: Apply `node-param-description-identical-to-display-name` (#3193) * :shirt: Apply `node-param-description-missing-for-ignore-ssl-issues` (#3195) * :rewind: Revert ":zao: fix" This reverts commit ef8a76f3dfedffd1bdccf3178af8a8d90cf5a55c. * :shirt: Apply `node-param-description-missing-for-simplify` (#3196) * :shirt: Apply `node-param-description-missing-final-period` (#3194) * Rule working as intended * Add rule to eslint * :shirt: Apply node-param-description-missing-for-return-all (#3197) * :zap: Restore `lintfix` command Co-authored-by: agobrech <45268029+agobrech@users.noreply.github.com> Co-authored-by: Omar Ajoue <krynble@gmail.com> Co-authored-by: agobrech <ael.gobrecht@gmail.com> Co-authored-by: Michael Kret <michael.k@radency.com>
2022-04-22 09:29:51 -07:00
const length = items.length;
const timezone = this.getTimezone();
const qs: IDataObject = {};
let responseData;
const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0);
if (resource === 'event') {
if (operation === 'track') {
// https://api.iterable.com/api/docs#events_trackBulk
const events = [];
for (let i = 0; i < length; i++) {
const name = this.getNodeParameter('name', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i);
if (!additionalFields.email && !additionalFields.id) {
throw new NodeOperationError(
this.getNode(),
'Either email or userId must be passed in to identify the user. Please add one of both via "Additional Fields". If both are passed in, email takes precedence.',
{ itemIndex: i },
);
}
const body: IDataObject = {
eventName: name,
};
Object.assign(body, additionalFields);
if (body.dataFieldsUi) {
const dataFields = (body.dataFieldsUi as IDataObject).dataFieldValues as IDataObject[];
const data: IDataObject = {};
for (const dataField of dataFields) {
data[dataField.key as string] = dataField.value;
}
body.dataFields = data;
delete body.dataFieldsUi;
}
if (body.createdAt) {
body.createdAt = moment.tz(body.createdAt, timezone).unix();
}
events.push(body);
}
responseData = await iterableApiRequest.call(this, 'POST', '/events/trackBulk', { events });
returnData.push(responseData);
}
}
if (resource === 'user') {
if (operation === 'upsert') {
// https://api.iterable.com/api/docs#users_updateUser
for (let i = 0; i < length; i++) {
const identifier = this.getNodeParameter('identifier', i) as string;
const value = this.getNodeParameter('value', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i);
const body: IDataObject = {};
if (identifier === 'email') {
body.email = value;
} else {
body.preferUserId = this.getNodeParameter('preferUserId', i) as boolean;
body.userId = value;
}
Object.assign(body, additionalFields);
if (body.dataFieldsUi) {
const dataFields = (body.dataFieldsUi as IDataObject).dataFieldValues as IDataObject[];
const data: IDataObject = {};
for (const dataField of dataFields) {
data[dataField.key as string] = dataField.value;
}
body.dataFields = data;
delete body.dataFieldsUi;
}
responseData = await iterableApiRequest.call(this, 'POST', '/users/update', body);
if (!this.continueOnFail()) {
if (responseData.code !== 'Success') {
throw new NodeOperationError(
this.getNode(),
`Iterable error response [400]: ${responseData.msg}`,
{ itemIndex: i },
);
}
}
returnData.push(responseData);
}
}
if (operation === 'delete') {
// https://api.iterable.com/api/docs#users_delete
// https://api.iterable.com/api/docs#users_delete_0
for (let i = 0; i < length; i++) {
const by = this.getNodeParameter('by', i) as string;
let endpoint;
if (by === 'email') {
const email = this.getNodeParameter('email', i) as string;
endpoint = `/users/${email}`;
} else {
const userId = this.getNodeParameter('userId', i) as string;
endpoint = `/users/byUserId/${userId}`;
}
responseData = await iterableApiRequest.call(this, 'DELETE', endpoint);
if (!this.continueOnFail()) {
if (responseData.code !== 'Success') {
:sparkles: Improve node error handling (#1309) * Add path mapping and response error interfaces * Add error handling and throwing functionality * Refactor error handling into a single function * Re-implement error handling in Hacker News node * Fix linting details * Re-implement error handling in Spotify node * Re-implement error handling in G Suite Admin node * :construction: create basic setup NodeError * :construction: add httpCodes * :construction: add path priolist * :construction: handle statusCode in error, adjust interfaces * :construction: fixing type issues w/Ivan * :construction: add error exploration * 👔 fix linter issues * :wrench: improve object check * :construction: remove path passing from NodeApiError * :construction: add multi error + refactor findProperty method * 👔 allow any * :wrench: handle multi error message callback * :zap: change return type of callback * :zap: add customCallback to MultiError * :construction: refactor to use INode * :hammer: handle arrays, continue search after first null property found * 🚫 refactor method access * :construction: setup NodeErrorView * :zap: change timestamp to Date.now * :books: Add documentation for methods and constants * :construction: change message setting * 🚚 move NodeErrors to workflow * :sparkles: add new ErrorView for Nodes * :art: improve error notification * :art: refactor interfaces * :zap: add WorkflowOperationError, refactor error throwing * 👕 fix linter issues * :art: rename param * :bug: fix handling normal errors * :zap: add usage of NodeApiError * :art: fix throw new error instead of constructor * :art: remove unnecessary code/comments * :art: adjusted spacing + updated status messages * :art: fix tab indentation * ✨ Replace current errors with custom errors (#1576) * :zap: Introduce NodeApiError in catch blocks * :zap: Introduce NodeOperationError in nodes * :zap: Add missing errors and remove incompatible * :zap: Fix NodeOperationError in incompatible nodes * :wrench: Adjust error handling in missed nodes PayPal, FileMaker, Reddit, Taiga and Facebook Graph API nodes * :hammer: Adjust Strava Trigger node error handling * :hammer: Adjust AWS nodes error handling * :hammer: Remove duplicate instantiation of NodeApiError * :bug: fix strava trigger node error handling * Add XML parsing to NodeApiError constructor (#1633) * :bug: Remove type annotation from catch variable * :sparkles: Add XML parsing to NodeApiError * :zap: Simplify error handling in Rekognition node * :zap: Pass in XML flag in generic functions * :fire: Remove try/catch wrappers at call sites * :hammer: Refactor setting description from XML * :hammer: Refactor let to const in resource loaders * :zap: Find property in parsed XML * :zap: Change let to const * :fire: Remove unneeded try/catch block * :shirt: Fix linting issues * :bug: Fix errors from merge conflict resolution * :zap: Add custom errors to latest contributions * :shirt: Fix linting issues * :zap: Refactor MongoDB helpers for custom errors * :bug: Correct custom error type * :zap: Apply feedback to A nodes * :zap: Apply feedback to missed A node * :zap: Apply feedback to B-D nodes * :zap: Apply feedback to E-F nodes * :zap: Apply feedback to G nodes * :zap: Apply feedback to H-L nodes * :zap: Apply feedback to M nodes * :zap: Apply feedback to P nodes * :zap: Apply feedback to R nodes * :zap: Apply feedback to S nodes * :zap: Apply feedback to T nodes * :zap: Apply feedback to V-Z nodes * :zap: Add HTTP code to iterable node error * :hammer: Standardize e as error * :hammer: Standardize err as error * :zap: Fix error handling for non-standard nodes Co-authored-by: Ben Hesseldieck <b.hesseldieck@gmail.com> Co-authored-by: Ben Hesseldieck <b.hesseldieck@gmail.com> Co-authored-by: Ben Hesseldieck <1849459+BHesseldieck@users.noreply.github.com>
2021-04-16 09:33:36 -07:00
throw new NodeApiError(this.getNode(), responseData);
}
}
returnData.push(responseData);
}
}
if (operation === 'get') {
// https://api.iterable.com/api/docs#users_getUser
// https://api.iterable.com/api/docs#users_getUserById
for (let i = 0; i < length; i++) {
const by = this.getNodeParameter('by', i) as string;
let endpoint;
if (by === 'email') {
const email = this.getNodeParameter('email', i) as string;
endpoint = `/users/getByEmail`;
qs.email = email;
} else {
const userId = this.getNodeParameter('userId', i) as string;
endpoint = `/users/byUserId/${userId}`;
}
responseData = await iterableApiRequest.call(this, 'GET', endpoint, {}, qs);
if (!this.continueOnFail()) {
if (Object.keys(responseData).length === 0) {
throw new NodeApiError(this.getNode(), responseData, {
message: `User not found`,
httpCode: '404',
});
}
}
responseData = responseData.user || {};
returnData.push(responseData);
}
}
}
if (resource === 'userList') {
if (operation === 'add') {
//https://api.iterable.com/api/docs#lists_subscribe
const listId = this.getNodeParameter('listId', 0) as string;
const identifier = this.getNodeParameter('identifier', 0) as string;
const body: IDataObject = {
listId: parseInt(listId, 10),
subscribers: [],
};
const subscribers: IDataObject[] = [];
for (let i = 0; i < length; i++) {
const value = this.getNodeParameter('value', i) as string;
if (identifier === 'email') {
subscribers.push({ email: value });
} else {
subscribers.push({ userId: value });
}
}
body.subscribers = subscribers;
responseData = await iterableApiRequest.call(this, 'POST', '/lists/subscribe', body);
returnData.push(responseData);
}
if (operation === 'remove') {
//https://api.iterable.com/api/docs#lists_unsubscribe
const listId = this.getNodeParameter('listId', 0) as string;
const identifier = this.getNodeParameter('identifier', 0) as string;
const additionalFields = this.getNodeParameter('additionalFields', 0);
const body: IDataObject = {
listId: parseInt(listId, 10),
subscribers: [],
campaignId: additionalFields.campaignId as number,
channelUnsubscribe: additionalFields.channelUnsubscribe as boolean,
};
const subscribers: IDataObject[] = [];
for (let i = 0; i < length; i++) {
const value = this.getNodeParameter('value', i) as string;
if (identifier === 'email') {
subscribers.push({ email: value });
} else {
subscribers.push({ userId: value });
}
}
body.subscribers = subscribers;
responseData = await iterableApiRequest.call(this, 'POST', '/lists/unsubscribe', body);
returnData.push(responseData);
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}