n8n/packages/nodes-base/nodes/SendInBlue/GenericFunctions.ts
brianinoa 74cedd94a8
feat(SendInBlue Node): Add SendInBlue Regular + Trigger Node (#3746)
* add sendinblue svg icon

* Add code and required files for new sendinblue node

* Add node to package.json

* Update credentials to display API Key instead of Access Token

* Use new svg found in brandfetch

*  Improvements

* ♻️ Moved descriptions for email to it's own file

*  Added support for contact get

*  moved email descriptions to it's own file

*  Add logic to conditionally remove/format sms,email

*  Improvements

*  Refactor Sender descriptions to it's own file

*  Fix urls

*  Improvements attempt

*  Refactor remove inline descriptions

*  Minor improvement

* 🎨 Learn a nice way to send options as key-value

*  Improvements

* ♻️ Fix Create Operation structure

* ♻️ Refactor create functionality for attribute

♻️ Introduce override for createAttribute selectedCategory

♻️ Add delete functionality

* 🔥 Remove preSend from delete

*  Implement override for body types

*  Cleanup node file

*  Update response for contact update
 Update request url for contact delete

*  Add presend check for optional properties that are empty
 Add Model file and TransactionalEmail interface

*  formatting

* ♻️ Remove requestOperations from Node Description level

* ♻️ Cleanup routing for Get All
♻️ Make Identifier required

*  Formatting

* ♻️ Add Options Collection

* ♻️ Add Filters area

* ♻️ Formatting

* ♻️ Handle empty return

* ♻️ Remove unused code

* ♻️ Fix pagination
♻️ Fix empty return for delete

*  Add pagination

*  Fix Modified Since

* ♻️ Reorder send operation ui

*  Remove no longer needed presend
 Add send html template operation

* ♻️ Make Contact Attribute name and type required

* ♻️ Rename Attribute to Contact Attribute

* ♻️ Rename Identifier to Contact Identifier

* ♻️ Remove SMS from root level because it can exist in Contact Attributes

* ♻️ Fix Array type using 'Array<T>'
♻️ Fix double quotes should be single quotes

* 👕 Lint Fix

*  Add email attachment functionality
 Add attachment data validation

*  Add dynamic loading of Email Template IDs

* ♻️ Cleanup validation method

*  Introduce workaround and use binary data for attachments

* feat: Migrated to npm release of riot-tmpl fork.

* 👕 Lint fix rules

* 👕 Lint fix rules

* fix: Updated imports to use @n8n_io/riot-tmpl

* fix: Fixed Logger.ts types.

*  Fix mixmatch of filename and package.json credentials list

*  fix mixmatch in nodes list

* feat(core): Give access to getBinaryDataBuffer in preSend method

*  clean up mixmatches in node naming

* ♻️ Refactor code to use newly exposed getBinaryDataBuffer method

*  Improvements

* 🔥 Remove unnecessary lines

* 👕 Fix linting issues

*  Fix issues with up to date APIs and improve readability

*  update naming of files

* ♻️ Move sendHtml boolean above subject
♻️ Update naming from Parameters to Fields

* ♻️ Move sendHtml boolean above subject
♻️ Update naming from Parameters to Fields

* ♻️ Add attribute name url encoding
♻️ Change limit's default to 50

*  Fix default for templateId

*  Fix display name for attribute list

* ♻️ Add clarity to attribute value display name

* ♻️ Add tags and attachments for emails

* ♻️ Add use of item's binary data fileName

* 👕 Fix action lint rule

* 👕 Remove deprecated lint rule

* ⬆️ Update eslint-plugin-n8n-nodes-base

* 👕 Fix lint rule for file name

*  Fix update attribute

* ♻️ Add upsert capabilites

* 🔥 Remove create or update operation

* ♻️ Add sendInBlueWebhookApi namespace

* ♻️ Add Webhook API functionality

*  Add SendInBlue Trigger

*  Return correct webhookId data

*  Add placeholder for receiving data

* 👕 Fixing existing linting issues

* 🚨 Enable namespacing in tslint file

* 👕 Fix linting issues

*  Rename exported WebhookApi

* 🔥 Remove unused Model.ts file

* ♻️ Update node to use SendInBlue namespace

*  Revert back to allowing upsert functionality

* ♻️ Fix options to better describe events

* Remove update flag for create operation

* ♻️ Fix discrepancies for contact resource

* remove no-namespace lint rule

* 👕 Fix linting issues

* ♻️ Add sendInBlueWebhookApi namespace

* ♻️ Add Webhook API functionality

*  Add SendInBlue Trigger

*  Return correct webhookId data

*  Add placeholder for receiving data

* 👕 Fix linting issues

*  Rename exported WebhookApi

* ♻️ Fix options to better describe events

* Add optionswithuri import that was lost

*  Fix details from janober's review

*  Fix order of displayName and name properties

*  Fix default value and improve loadOptions

*  Introduce support for comma separated attribute values

*  Introduce support for comma separated attribute values

* 👕 Fix linting issues

* Update defaults and required props

*  Fix copy paste issue Upsert was not using correct endpoint

*  Fix upsert email field display name

*  Last update, upsert email description

*  Add PostReceived type limit

Co-authored-by: ricardo <ricardoespinoza105@gmail.com>
Co-authored-by: Alex Grozav <alex@grozav.com>
Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
2022-08-03 18:08:51 +02:00

389 lines
9.8 KiB
TypeScript

import {
IExecuteSingleFunctions,
IHookFunctions,
IHttpRequestOptions,
IWebhookFunctions,
JsonObject,
NodeOperationError,
} from 'n8n-workflow';
import { OptionsWithUri } from 'request';
import MailComposer from 'nodemailer/lib/mail-composer';
export namespace SendInBlueNode {
type ValidEmailFields = { to: string } | { sender: string } | { cc: string } | { bcc: string };
type Address = { address: string; name?: string };
type Email = { email: string; name?: string };
type ToEmail = { to: Email[] };
type SenderEmail = { sender: Email };
type CCEmail = { cc: Email[] };
type BBCEmail = { bbc: Email[] };
type ValidatedEmail = ToEmail | SenderEmail | CCEmail | BBCEmail;
enum OVERRIDE_MAP_VALUES {
'CATEGORY' = 'category',
'NORMAL' = 'boolean',
'TRANSACTIONAL' = 'id',
}
enum OVERRIDE_MAP_TYPE {
'CATEGORY' = 'category',
'NORMAL' = 'normal',
'TRANSACTIONAL' = 'transactional',
}
export const INTERCEPTORS = new Map<string, (body: JsonObject) => void>([
[
OVERRIDE_MAP_TYPE.CATEGORY,
(body: JsonObject) => {
body!.type = OVERRIDE_MAP_VALUES.CATEGORY;
},
],
[
OVERRIDE_MAP_TYPE.NORMAL,
(body: JsonObject) => {
body!.type = OVERRIDE_MAP_VALUES.NORMAL;
},
],
[
OVERRIDE_MAP_TYPE.TRANSACTIONAL,
(body: JsonObject) => {
body!.type = OVERRIDE_MAP_VALUES.TRANSACTIONAL;
},
],
]);
export namespace Validators {
export async function validateAndCompileAttachmentsData(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const dataPropertyList = this.getNodeParameter(
'additionalFields.emailAttachments.attachment',
) as JsonObject;
const { body } = requestOptions;
const { attachment = [] } = body as { attachment: Array<{ content: string; name: string }> };
try {
const { binaryPropertyName } = dataPropertyList;
const dataMappingList = (binaryPropertyName as string).split(',');
for (const attachmentDataName of dataMappingList) {
const binaryPropertyName = attachmentDataName;
const item = this.getInputData();
if (item.binary![binaryPropertyName as string] === undefined) {
throw new NodeOperationError(
this.getNode(),
`No binary data property “${binaryPropertyName}” exists on item!`,
);
}
const bufferFromIncomingData = (await this.helpers.getBinaryDataBuffer(
binaryPropertyName,
)) as Buffer;
const {
data: content,
mimeType,
fileName,
fileExtension,
} = await this.helpers.prepareBinaryData(bufferFromIncomingData);
const itemIndex = this.getItemIndex();
const name = getFileName(
itemIndex,
mimeType,
fileExtension,
fileName || item.binary!.data.fileName,
);
attachment.push({ content, name });
}
Object.assign(body!, { attachment });
return requestOptions;
} catch (err) {
throw new NodeOperationError(this.getNode(), `${err}`);
}
}
export async function validateAndCompileTags(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const { tag } = this.getNodeParameter('additionalFields.emailTags.tags') as JsonObject;
const tags = (tag as string)
.split(',')
.map((tag) => tag.trim())
.filter((tag) => {
return tag !== '';
});
const { body } = requestOptions;
Object.assign(body!, { tags });
return requestOptions;
}
export async function validateAndCompileCCEmails(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const ccData = this.getNodeParameter(
'additionalFields.receipientsCC.receipientCc',
) as JsonObject;
const { cc } = ccData;
const { body } = requestOptions;
const data = validateEmailStrings({ cc: cc as string });
Object.assign(body!, data);
return requestOptions;
}
export async function validateAndCompileBCCEmails(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const bccData = this.getNodeParameter(
'additionalFields.receipientsBCC.receipientBcc',
) as JsonObject;
const { bcc } = bccData;
const { body } = requestOptions;
const data = validateEmailStrings({ bcc: bcc as string });
Object.assign(body!, data);
return requestOptions;
}
export async function validateAndCompileReceipientEmails(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const to = this.getNodeParameter('receipients') as string;
const { body } = requestOptions;
const data = validateEmailStrings({ to });
Object.assign(body!, data);
return requestOptions;
}
export async function validateAndCompileSenderEmail(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const sender = this.getNodeParameter('sender') as string;
const { body } = requestOptions;
const data = validateEmailStrings({ sender });
Object.assign(body!, data);
return requestOptions;
}
export async function validateAndCompileTemplateParameters(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const parameterData = this.getNodeParameter(
'additionalFields.templateParameters.parameterValues',
);
const { body } = requestOptions;
const { parmeters } = parameterData as JsonObject;
const params = (parmeters as string)
.split(',')
.filter((parameter) => {
return parameter.split('=').length === 2;
})
.map((parameter) => {
const [key, value] = parameter.split('=');
return {
[key]: value,
};
})
.reduce((obj, cObj) => {
Object.assign(obj, cObj);
return obj;
}, {});
Object.assign(body!, { params });
return requestOptions;
}
function validateEmailStrings(input: ValidEmailFields): ValidatedEmail {
const composer = new MailComposer({ ...input });
const addressFields = composer.compile().getAddresses();
const fieldFetcher = new Map<string, () => Email[] | Email>([
[
'bcc',
() => {
return (addressFields.bcc as unknown as Address[])?.map(formatToEmailName);
},
],
[
'cc',
() => {
return (addressFields.cc as unknown as Address[])?.map(formatToEmailName);
},
],
[
'from',
() => {
return (addressFields.from as unknown as Address[])?.map(formatToEmailName);
},
],
[
'reply-to',
() => {
return (addressFields['reply-to'] as unknown as Address[])?.map(formatToEmailName);
},
],
[
'sender',
() => {
return (addressFields.sender as unknown as Address[])?.map(formatToEmailName)[0];
},
],
[
'to',
() => {
return (addressFields.to as unknown as Address[])?.map(formatToEmailName);
},
],
]);
const result: { [key in keyof ValidatedEmail]: Email[] | Email } = {} as ValidatedEmail;
Object.keys(input).reduce((obj: { [key: string]: Email[] | Email }, key: string) => {
const getter = fieldFetcher.get(key);
const value = getter!();
obj[key] = value;
return obj;
}, result);
return result as ValidatedEmail;
}
}
function getFileName(
itemIndex: number,
mimeType: string,
fileExt: string,
fileName: string,
): string {
let ext = fileExt;
if (fileExt === undefined) {
ext = mimeType.split('/')[1];
}
let name = `${fileName}.${ext}`;
if (fileName === undefined) {
name = `file-${itemIndex}.${ext}`;
}
return name;
}
function formatToEmailName(data: Address): Email {
const { address: email, name } = data;
const result = { email };
if (name !== undefined && name !== '') {
Object.assign(result, { name });
}
return { ...result };
}
}
export namespace SendInBlueWebhookApi {
interface WebhookDetails {
url: string;
id: number;
description: string;
events: string[];
type: string;
createdAt: string;
modifiedAt: string;
}
interface WebhookId {
id: string;
}
interface Webhooks {
webhooks: WebhookDetails[];
}
const credentialsName = 'sendinblueApi';
const baseURL = 'https://api.sendinblue.com/v3';
export const supportedAuthMap = new Map<string, (ref: IWebhookFunctions) => Promise<string>>([
[
'apiKey',
async (ref: IWebhookFunctions): Promise<string> => {
const credentials = await ref.getCredentials(credentialsName);
return credentials.sharedSecret as string;
},
],
]);
export const fetchWebhooks = async (ref: IHookFunctions, type: string): Promise<Webhooks> => {
const endpoint = `${baseURL}/webhooks?type=${type}`;
const options: OptionsWithUri = {
method: 'GET',
headers: {
Accept: 'application/json',
},
uri: endpoint,
};
const webhooks = (await ref.helpers.requestWithAuthentication.call(
ref,
credentialsName,
options,
)) as string;
return JSON.parse(webhooks) as Webhooks;
};
export const createWebHook = async (
ref: IHookFunctions,
type: string,
events: string[],
url: string,
): Promise<WebhookId> => {
const endpoint = `${baseURL}/webhooks`;
const options: OptionsWithUri = {
method: 'POST',
headers: {
Accept: 'application/json',
},
uri: endpoint,
body: {
events,
type,
url,
},
};
const webhookId = await ref.helpers.requestWithAuthentication.call(
ref,
credentialsName,
options,
);
return JSON.parse(webhookId) as WebhookId;
};
export const deleteWebhook = async (ref: IHookFunctions, webhookId: string) => {
const endpoint = `${baseURL}/webhooks/${webhookId}`;
const body = {};
const options: OptionsWithUri = {
method: 'DELETE',
headers: {
Accept: 'application/json',
},
uri: endpoint,
body,
};
return await ref.helpers.requestWithAuthentication.call(ref, credentialsName, options);
};
}