From 8d2371917f80b9ee740c1ee32f0484a7d23635ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 10 Mar 2021 19:51:05 -0300 Subject: [PATCH] :sparkles: Add Wise node (#1496) * :tada: Register node and credentials * :art: Add SVG icon * :zap: Add node stub * :zap: Update credentials registration * :zap: Add API credentials * :zap: Add generic functions stub * :zap: Update node stub * :zap: Add stubs for resource descriptions * :art: Fix SVG icon size and positioning * :hammer: Fix credentials casing * :zap: Implement account operations * :zap: Add borderless accounts to account:get * :zap: Remove redundant option * :zap: Complete account:get with statement * :zap: Implement exchangeRate:get * :zap: Implement profile:get and profile:getAll * :zap: Implement quote:create and quote:get * :zap: Add findRequiredFields for recipient:create * :fire: Remove resource per feedback * :zap: Implement transfer:create * :zap: Implement transfer:delete and transfer:get * :books: Add documentation links * :zap: Implement transfer:getAll * :zap: Implement transfer:execute * :zap: Simulate transfer completion for PDF receipt * :zap: Remove logging * :zap: Add missing divider * :zap: Add Wise Trigger and improvements * :hammer: Refactor account operations * :zap: Small improvement Co-authored-by: ricardo Co-authored-by: Jan Oberhauser --- .../credentials/WiseApi.credentials.ts | 34 ++ .../nodes-base/nodes/Wise/GenericFunctions.ts | 164 ++++++ packages/nodes-base/nodes/Wise/Wise.node.ts | 514 ++++++++++++++++++ .../nodes-base/nodes/Wise/WiseTrigger.node.ts | 189 +++++++ .../Wise/descriptions/AccountDescription.ts | 195 +++++++ .../descriptions/ExchangeRateDescription.ts | 140 +++++ .../Wise/descriptions/ProfileDescription.ts | 57 ++ .../Wise/descriptions/QuoteDescription.ts | 181 ++++++ .../Wise/descriptions/RecipientDescription.ts | 73 +++ .../Wise/descriptions/TransferDescription.ts | 460 ++++++++++++++++ .../nodes/Wise/descriptions/index.ts | 6 + packages/nodes-base/nodes/Wise/wise.svg | 4 + packages/nodes-base/package.json | 3 + 13 files changed, 2020 insertions(+) create mode 100644 packages/nodes-base/credentials/WiseApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Wise/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Wise/Wise.node.ts create mode 100644 packages/nodes-base/nodes/Wise/WiseTrigger.node.ts create mode 100644 packages/nodes-base/nodes/Wise/descriptions/AccountDescription.ts create mode 100644 packages/nodes-base/nodes/Wise/descriptions/ExchangeRateDescription.ts create mode 100644 packages/nodes-base/nodes/Wise/descriptions/ProfileDescription.ts create mode 100644 packages/nodes-base/nodes/Wise/descriptions/QuoteDescription.ts create mode 100644 packages/nodes-base/nodes/Wise/descriptions/RecipientDescription.ts create mode 100644 packages/nodes-base/nodes/Wise/descriptions/TransferDescription.ts create mode 100644 packages/nodes-base/nodes/Wise/descriptions/index.ts create mode 100644 packages/nodes-base/nodes/Wise/wise.svg diff --git a/packages/nodes-base/credentials/WiseApi.credentials.ts b/packages/nodes-base/credentials/WiseApi.credentials.ts new file mode 100644 index 0000000000..7f9f871cbf --- /dev/null +++ b/packages/nodes-base/credentials/WiseApi.credentials.ts @@ -0,0 +1,34 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class WiseApi implements ICredentialType { + name = 'wiseApi'; + displayName = 'Wise API'; + documentationUrl = 'wise'; + properties = [ + { + displayName: 'API Token', + name: 'apiToken', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Environment', + name: 'environment', + type: 'options' as NodePropertyTypes, + default: 'live', + options: [ + { + name: 'Live', + value: 'live', + }, + { + name: 'Test', + value: 'test', + }, + ], + }, + ]; +} diff --git a/packages/nodes-base/nodes/Wise/GenericFunctions.ts b/packages/nodes-base/nodes/Wise/GenericFunctions.ts new file mode 100644 index 0000000000..a4aa44f430 --- /dev/null +++ b/packages/nodes-base/nodes/Wise/GenericFunctions.ts @@ -0,0 +1,164 @@ +import { + IExecuteFunctions, + IHookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, +} from 'n8n-workflow'; + +import { + OptionsWithUri, +} from 'request'; + +/** + * Make an authenticated API request to Wise. + */ +export async function wiseApiRequest( + this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, + method: string, + endpoint: string, + body: IDataObject = {}, + qs: IDataObject = {}, + option: IDataObject = {}, +) { + const { apiToken, environment } = this.getCredentials('wiseApi') as { + apiToken: string, + environment: 'live' | 'test', + }; + + const rootUrl = environment === 'live' + ? 'https://api.transferwise.com/' + : 'https://api.sandbox.transferwise.tech/'; + + const options: OptionsWithUri = { + headers: { + 'user-agent': 'n8n', + 'Authorization': `Bearer ${apiToken}`, + }, + method, + uri: `${rootUrl}${endpoint}`, + qs, + body, + json: true, + }; + + if (!Object.keys(body).length) { + delete options.body; + } + + if (!Object.keys(qs).length) { + delete options.qs; + } + + if (Object.keys(option)) { + Object.assign(options, option); + } + + try { + return await this.helpers.request!(options); + } catch (error) { + + const errors = error.error.errors; + + if (errors && Array.isArray(errors)) { + const errorMessage = errors.map((e) => e.message).join(' | '); + throw new Error(`Wise error response [${error.statusCode}]: ${errorMessage}`); + } + + throw new Error(`Wise error response [${error.statusCode}]: ${error}`); + } +} + +/** + * Populate the binary property of node items with binary data for a PDF file. + */ +export async function handleBinaryData( + this: IExecuteFunctions, + items: INodeExecutionData[], + i: number, + endpoint: string, +) { + const data = await wiseApiRequest.call(this, 'GET', endpoint, {}, {}, { encoding: null }); + const binaryProperty = this.getNodeParameter('binaryProperty', i) as string; + + items[i].binary = items[i].binary ?? {}; + items[i].binary![binaryProperty] = await this.helpers.prepareBinaryData(data); + items[i].binary![binaryProperty].fileName = this.getNodeParameter('fileName', i) as string; + items[i].binary![binaryProperty].fileExtension = 'pdf'; + + return items; +} + +export function getTriggerName(eventName: string) { + const events: IDataObject = { + 'tranferStateChange': 'transfers#state-change', + 'transferActiveCases': 'transfers#active-cases', + 'balanceCredit': 'balances#credit', + }; + return events[eventName]; +} + +export type BorderlessAccount = { + id: number, + balances: Array<{ currency: string }> +}; + +export type ExchangeRateAdditionalFields = { + interval: 'day' | 'hour' | 'minute', + range: { + rangeProperties: { from: string, to: string } + }, + time: string, +}; + +export type Profile = { + id: number, + type: 'business' | 'personal', +}; + +export type Recipient = { + id: number, + accountHolderName: string +}; + +export type StatementAdditionalFields = { + lineStyle: 'COMPACT' | 'FLAT', + range: { + rangeProperties: { intervalStart: string, intervalEnd: string } + }, +}; + +export type TransferFilters = { + [key: string]: string | IDataObject; + range: { + rangeProperties: { createdDateStart: string, createdDateEnd: string } + }, + sourceCurrency: string, + status: string, + targetCurrency: string, +}; + +export const livePublicKey = ` +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvO8vXV+JksBzZAY6GhSO +XdoTCfhXaaiZ+qAbtaDBiu2AGkGVpmEygFmWP4Li9m5+Ni85BhVvZOodM9epgW3F +bA5Q1SexvAF1PPjX4JpMstak/QhAgl1qMSqEevL8cmUeTgcMuVWCJmlge9h7B1CS +D4rtlimGZozG39rUBDg6Qt2K+P4wBfLblL0k4C4YUdLnpGYEDIth+i8XsRpFlogx +CAFyH9+knYsDbR43UJ9shtc42Ybd40Afihj8KnYKXzchyQ42aC8aZ/h5hyZ28yVy +Oj3Vos0VdBIs/gAyJ/4yyQFCXYte64I7ssrlbGRaco4nKF3HmaNhxwyKyJafz19e +HwIDAQAB +-----END PUBLIC KEY-----`; + +export const testPublicKey = ` +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwpb91cEYuyJNQepZAVfP +ZIlPZfNUefH+n6w9SW3fykqKu938cR7WadQv87oF2VuT+fDt7kqeRziTmPSUhqPU +ys/V2Q1rlfJuXbE+Gga37t7zwd0egQ+KyOEHQOpcTwKmtZ81ieGHynAQzsn1We3j +wt760MsCPJ7GMT141ByQM+yW1Bx+4SG3IGjXWyqOWrcXsxAvIXkpUD/jK/L958Cg +nZEgz0BSEh0QxYLITnW1lLokSx/dTianWPFEhMC9BgijempgNXHNfcVirg1lPSyg +z7KqoKUN0oHqWLr2U1A+7kqrl6O2nx3CKs1bj1hToT1+p4kcMoHXA7kA+VBLUpEs +VwIDAQAB +-----END PUBLIC KEY-----`; diff --git a/packages/nodes-base/nodes/Wise/Wise.node.ts b/packages/nodes-base/nodes/Wise/Wise.node.ts new file mode 100644 index 0000000000..dfdf1e7beb --- /dev/null +++ b/packages/nodes-base/nodes/Wise/Wise.node.ts @@ -0,0 +1,514 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + accountFields, + accountOperations, + exchangeRateFields, + exchangeRateOperations, + profileFields, + profileOperations, + quoteFields, + quoteOperations, + recipientFields, + recipientOperations, + transferFields, + transferOperations, +} from './descriptions'; + +import { + BorderlessAccount, + ExchangeRateAdditionalFields, + handleBinaryData, + Profile, + Recipient, + StatementAdditionalFields, + TransferFilters, + wiseApiRequest, +} from './GenericFunctions'; + +import { + omit, +} from 'lodash'; + +import * as moment from 'moment-timezone'; + +import * as uuid from 'uuid/v4'; + +export class Wise implements INodeType { + description: INodeTypeDescription = { + displayName: 'Wise', + name: 'wise', + icon: 'file:wise.svg', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume the Wise API', + defaults: { + name: 'Wise', + color: '#37517e', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'wiseApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Account', + value: 'account', + }, + { + name: 'Exchange Rate', + value: 'exchangeRate', + }, + { + name: 'Profile', + value: 'profile', + }, + { + name: 'Recipient', + value: 'recipient', + }, + { + name: 'Quote', + value: 'quote', + }, + { + name: 'Transfer', + value: 'transfer', + }, + ], + default: 'account', + description: 'Resource to consume', + }, + ...accountOperations, + ...accountFields, + ...exchangeRateOperations, + ...exchangeRateFields, + ...profileOperations, + ...profileFields, + ...quoteOperations, + ...quoteFields, + ...recipientOperations, + ...recipientFields, + ...transferOperations, + ...transferFields, + ], + }; + + methods = { + loadOptions: { + async getBorderlessAccounts(this: ILoadOptionsFunctions) { + const qs = { + profileId: this.getNodeParameter('profileId', 0), + }; + + const accounts = await wiseApiRequest.call(this, 'GET', 'v1/borderless-accounts', {}, qs); + + return accounts.map(({ id, balances }: BorderlessAccount) => ({ + name: balances.map(({ currency }) => currency).join(' - '), + value: id, + })); + }, + + async getProfiles(this: ILoadOptionsFunctions) { + const profiles = await wiseApiRequest.call(this, 'GET', 'v1/profiles'); + + return profiles.map(({ id, type }: Profile) => ({ + name: type.charAt(0).toUpperCase() + type.slice(1), + value: id, + })); + }, + + async getRecipients(this: ILoadOptionsFunctions) { + const qs = { + profileId: this.getNodeParameter('profileId', 0), + }; + + const recipients = await wiseApiRequest.call(this, 'GET', 'v1/accounts', {}, qs); + + return recipients.map(({ id, accountHolderName }: Recipient) => ({ + name: accountHolderName, + value: id, + })); + }, + }, + }; + + async execute(this: IExecuteFunctions) { + const items = this.getInputData(); + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + const timezone = this.getTimezone(); + + let responseData; + const returnData: IDataObject[] = []; + let downloadReceipt = false; + + for (let i = 0; i < items.length; i++) { + + try { + + if (resource === 'account') { + + // ********************************************************************* + // account + // ********************************************************************* + + if (operation === 'getBalances') { + + // ---------------------------------- + // account: getBalances + // ---------------------------------- + + // https://api-docs.transferwise.com/#borderless-accounts-get-account-balance + + const qs = { + profileId: this.getNodeParameter('profileId', i), + }; + + responseData = await wiseApiRequest.call(this, 'GET', 'v1/borderless-accounts', {}, qs); + + } else if (operation === 'getCurrencies') { + + // ---------------------------------- + // account: getCurrencies + // ---------------------------------- + + // https://api-docs.transferwise.com/#borderless-accounts-get-available-currencies + + responseData = await wiseApiRequest.call(this, 'GET', 'v1/borderless-accounts/balance-currencies'); + + } else if (operation === 'getStatement') { + + // ---------------------------------- + // account: getStatement + // ---------------------------------- + + // https://api-docs.transferwise.com/#borderless-accounts-get-account-statement + + const profileId = this.getNodeParameter('profileId', i); + const borderlessAccountId = this.getNodeParameter('borderlessAccountId', i); + const endpoint = `v3/profiles/${profileId}/borderless-accounts/${borderlessAccountId}/statement.json`; + + const qs = { + currency: this.getNodeParameter('currency', i), + } as IDataObject; + + const { lineStyle, range } = this.getNodeParameter('additionalFields', i) as StatementAdditionalFields; + + if (lineStyle !== undefined) { + qs.type = lineStyle; + } + + if (range !== undefined) { + qs.intervalStart = moment.tz(range.rangeProperties.intervalStart, timezone).utc().format(); + qs.intervalEnd = moment.tz(range.rangeProperties.intervalEnd, timezone).utc().format(); + } else { + qs.intervalStart = moment().subtract(1, 'months').utc().format(); + qs.intervalEnd = moment().utc().format(); + } + + responseData = await wiseApiRequest.call(this, 'GET', endpoint, {}, qs); + + } + + } else if (resource === 'exchangeRate') { + + // ********************************************************************* + // exchangeRate + // ********************************************************************* + + if (operation === 'get') { + + // ---------------------------------- + // exchangeRate: get + // ---------------------------------- + + // https://api-docs.transferwise.com/#exchange-rates-list + + const qs = { + source: this.getNodeParameter('source', i), + target: this.getNodeParameter('target', i), + } as IDataObject; + + const { + interval, + range, + time, + } = this.getNodeParameter('additionalFields', i) as ExchangeRateAdditionalFields; + + if (interval !== undefined) { + qs.group = interval; + } + + if (time !== undefined) { + qs.time = time; + } + + if (range !== undefined && time === undefined) { + qs.from = moment.tz(range.rangeProperties.from, timezone).utc().format(); + qs.to = moment.tz(range.rangeProperties.to, timezone).utc().format(); + } else { + qs.from = moment().subtract(1, 'months').utc().format(); + qs.to = moment().format(); + } + + responseData = await wiseApiRequest.call(this, 'GET', 'v1/rates', {}, qs); + } + + } else if (resource === 'profile') { + + // ********************************************************************* + // profile + // ********************************************************************* + + if (operation === 'get') { + + // ---------------------------------- + // profile: get + // ---------------------------------- + + // https://api-docs.transferwise.com/#user-profiles-get-by-id + + const profileId = this.getNodeParameter('profileId', i); + responseData = await wiseApiRequest.call(this, 'GET', `v1/profiles/${profileId}`); + + } else if (operation === 'getAll') { + + // ---------------------------------- + // profile: getAll + // ---------------------------------- + + // https://api-docs.transferwise.com/#user-profiles-list + + responseData = await wiseApiRequest.call(this, 'GET', 'v1/profiles'); + + } + + } else if (resource === 'recipient') { + + // ********************************************************************* + // recipient + // ********************************************************************* + + if (operation === 'getAll') { + + // ---------------------------------- + // recipient: getAll + // ---------------------------------- + + // https://api-docs.transferwise.com/#recipient-accounts-list + + responseData = await wiseApiRequest.call(this, 'GET', 'v1/accounts'); + + const returnAll = this.getNodeParameter('returnAll', i); + + if (!returnAll) { + const limit = this.getNodeParameter('limit', i); + responseData = responseData.slice(0, limit); + } + } + + } else if (resource === 'quote') { + + // ********************************************************************* + // quote + // ********************************************************************* + + if (operation === 'create') { + + // ---------------------------------- + // quote: create + // ---------------------------------- + + // https://api-docs.transferwise.com/#quotes-create + + const body = { + profile: this.getNodeParameter('profileId', i), + sourceCurrency: (this.getNodeParameter('sourceCurrency', i) as string).toUpperCase(), + targetCurrency: (this.getNodeParameter('targetCurrency', i) as string).toUpperCase(), + } as IDataObject; + + const amountType = this.getNodeParameter('amountType', i) as 'source' | 'target'; + + if (amountType === 'source') { + body.sourceAmount = this.getNodeParameter('amount', i); + } else if (amountType === 'target') { + body.targetAmount = this.getNodeParameter('amount', i); + } + + responseData = await wiseApiRequest.call(this, 'POST', 'v2/quotes', body, {}); + + } else if (operation === 'get') { + + // ---------------------------------- + // quote: get + // ---------------------------------- + + // https://api-docs.transferwise.com/#quotes-get-by-id + + const quoteId = this.getNodeParameter('quoteId', i); + responseData = await wiseApiRequest.call(this, 'GET', `v2/quotes/${quoteId}`); + } + + } else if (resource === 'transfer') { + + // ********************************************************************* + // transfer + // ********************************************************************* + + if (operation === 'create') { + + // ---------------------------------- + // transfer: create + // ---------------------------------- + + // https://api-docs.transferwise.com/#transfers-create + + const body = { + quoteUuid: this.getNodeParameter('quoteId', i), + targetAccount: this.getNodeParameter('targetAccountId', i), + customerTransactionId: uuid(), + } as IDataObject; + + const { reference } = this.getNodeParameter('additionalFields', i) as { reference: string }; + + if (reference !== undefined) { + body.details = { reference }; + } + + responseData = await wiseApiRequest.call(this, 'POST', 'v1/transfers', body, {}); + + } else if (operation === 'delete') { + + // ---------------------------------- + // transfer: delete + // ---------------------------------- + + // https://api-docs.transferwise.com/#transfers-cancel + + const transferId = this.getNodeParameter('transferId', i); + responseData = await wiseApiRequest.call(this, 'PUT', `v1/transfers/${transferId}/cancel`); + + } else if (operation === 'execute') { + + // ---------------------------------- + // transfer: execute + // ---------------------------------- + + // https://api-docs.transferwise.com/#transfers-fund + + const profileId = this.getNodeParameter('profileId', i); + const transferId = this.getNodeParameter('transferId', i) as string; + + const endpoint = `v3/profiles/${profileId}/transfers/${transferId}/payments`; + responseData = await wiseApiRequest.call(this, 'POST', endpoint, { type: 'BALANCE' }, {}); + + // in sandbox, simulate transfer completion so that PDF receipt can be downloaded + + const { environment } = this.getCredentials('wiseApi') as IDataObject; + + if (environment === 'test') { + for (const endpoint of ['processing', 'funds_converted', 'outgoing_payment_sent']) { + await wiseApiRequest.call(this, 'GET', `v1/simulation/transfers/${transferId}/${endpoint}`); + } + } + + } else if (operation === 'get') { + + // ---------------------------------- + // transfer: get + // ---------------------------------- + + const transferId = this.getNodeParameter('transferId', i); + downloadReceipt = this.getNodeParameter('downloadReceipt', i) as boolean; + + if (downloadReceipt) { + + // https://api-docs.transferwise.com/#transfers-get-receipt-pdf + + responseData = await handleBinaryData.call(this, items, i, `v1/transfers/${transferId}/receipt.pdf`); + + } else { + + // https://api-docs.transferwise.com/#transfers-get-by-id + + responseData = await wiseApiRequest.call(this, 'GET', `v1/transfers/${transferId}`); + } + + } else if (operation === 'getAll') { + + // ---------------------------------- + // transfer: getAll + // ---------------------------------- + + // https://api-docs.transferwise.com/#transfers-list + + const qs = { + profile: this.getNodeParameter('profileId', i), + } as IDataObject; + + const filters = this.getNodeParameter('filters', i) as TransferFilters; + + Object.keys(omit(filters, 'range')).forEach(key => { + qs[key] = filters[key]; + }); + + if (filters.range !== undefined) { + qs.createdDateStart = moment(filters.range.rangeProperties.createdDateStart).format(); + qs.createdDateEnd = moment(filters.range.rangeProperties.createdDateEnd).format(); + } else { + qs.createdDateStart = moment().subtract(1, 'months').format(); + qs.createdDateEnd = moment().format(); + } + + const returnAll = this.getNodeParameter('returnAll', i); + + if (!returnAll) { + qs.limit = this.getNodeParameter('limit', i); + } + + responseData = await wiseApiRequest.call(this, 'GET', 'v1/transfers', {}, qs); + } + } + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.toString() }); + continue; + } + + throw error; + } + + Array.isArray(responseData) + ? returnData.push(...responseData) + : returnData.push(responseData); + } + + if (downloadReceipt && responseData !== undefined) { + return this.prepareOutputData(responseData); + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Wise/WiseTrigger.node.ts b/packages/nodes-base/nodes/Wise/WiseTrigger.node.ts new file mode 100644 index 0000000000..6b458488ff --- /dev/null +++ b/packages/nodes-base/nodes/Wise/WiseTrigger.node.ts @@ -0,0 +1,189 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeType, + INodeTypeDescription, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + getTriggerName, + livePublicKey, + Profile, + testPublicKey, + wiseApiRequest, +} from './GenericFunctions'; + +import { + createVerify, +} from 'crypto'; + +export class WiseTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Wise Trigger', + name: 'wiseTrigger', + icon: 'file:wise.svg', + group: ['trigger'], + version: 1, + subtitle: '={{$parameter["event"]}}', + description: 'Handle Wise events via webhooks', + defaults: { + name: 'Wise Trigger', + color: '#37517e', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'wiseApi', + required: true, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Profile', + name: 'profileId', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getProfiles', + }, + default: '', + }, + { + displayName: 'Event', + name: 'event', + type: 'options', + required: true, + default: '', + options: [ + { + name: 'Balance Credit', + value: 'balanceCredit', + description: 'Triggered every time a balance account is credited.', + }, + { + name: 'Transfer Active Case', + value: 'transferActiveCases', + description: `Triggered every time a transfer's list of active cases is updated.`, + }, + { + name: 'Transfer State Changed', + value: 'tranferStateChange', + description: `Triggered every time a transfer's status is updated.`, + }, + ], + }, + ], + }; + + methods = { + loadOptions: { + async getProfiles(this: ILoadOptionsFunctions) { + const profiles = await wiseApiRequest.call(this, 'GET', 'v1/profiles'); + return profiles.map(({ id, type }: Profile) => ({ + name: type.charAt(0).toUpperCase() + type.slice(1), + value: id, + })); + }, + }, + }; + // @ts-ignore + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const webhookUrl = this.getNodeWebhookUrl('default'); + const profileId = this.getNodeParameter('profileId') as string; + const event = this.getNodeParameter('event') as string; + const webhooks = await wiseApiRequest.call(this, 'GET', `v3/profiles/${profileId}/subscriptions`); + const trigger = getTriggerName(event); + for (const webhook of webhooks) { + if (webhook.delivery.url === webhookUrl && webhook.scope.id === profileId && webhook.trigger_on === trigger) { + webhookData.webhookId = webhook.id; + return true; + } + } + return false; + }, + async create(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + const profileId = this.getNodeParameter('profileId') as string; + const event = this.getNodeParameter('event') as string; + const trigger = getTriggerName(event); + const body: IDataObject = { + name: `n8n Webhook`, + trigger_on: trigger, + delivery: { + version: '2.0.0', + url: webhookUrl, + }, + }; + const webhook = await wiseApiRequest.call(this, 'POST', `v3/profiles/${profileId}/subscriptions`, body); + webhookData.webhookId = webhook.id; + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const profileId = this.getNodeParameter('profileId') as string; + try { + await wiseApiRequest.call(this, 'DELETE', `v3/profiles/${profileId}/subscriptions/${webhookData.webhookId}`); + } catch (error) { + return false; + } + delete webhookData.webhookId; + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const req = this.getRequestObject(); + const headers = this.getHeaderData() as IDataObject; + const credentials = this.getCredentials('wiseApi') as IDataObject; + + if (headers['x-test-notification'] === 'true') { + const res = this.getResponseObject(); + res.status(200).end(); + return { + noWebhookResponse: true, + }; + } + + const signature = headers['x-signature'] as string; + + const publicKey = (credentials.environment === 'test') ? testPublicKey : livePublicKey as string; + + //@ts-ignore + const sig = createVerify('RSA-SHA1').update(req.rawBody); + const verified = sig.verify( + publicKey, + signature, + 'base64', + ); + + if (verified === false) { + return {}; + } + + return { + workflowData: [ + this.helpers.returnJsonArray(req.body), + ], + }; + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Wise/descriptions/AccountDescription.ts b/packages/nodes-base/nodes/Wise/descriptions/AccountDescription.ts new file mode 100644 index 0000000000..5b7fd3d5fa --- /dev/null +++ b/packages/nodes-base/nodes/Wise/descriptions/AccountDescription.ts @@ -0,0 +1,195 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const accountOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'getBalances', + description: 'Operation to perform', + options: [ + { + name: 'Get Balances', + value: 'getBalances', + description: 'Retrieve balances for all account currencies of this user.', + }, + { + name: 'Get Currencies', + value: 'getCurrencies', + description: 'Retrieve currencies in the borderless account of this user.', + }, + { + name: 'Get Statement', + value: 'getStatement', + description: 'Retrieve the statement for the borderless account of this user.', + }, + ], + displayOptions: { + show: { + resource: [ + 'account', + ], + }, + }, + }, +] as INodeProperties[]; + +export const accountFields = [ + // ---------------------------------- + // account: getBalances + // ---------------------------------- + { + displayName: 'Profile ID', + name: 'profileId', + type: 'options', + required: true, + default: [], + typeOptions: { + loadOptionsMethod: 'getProfiles', + }, + description: 'ID of the user profile to retrieve the balance of.', + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'getBalances', + ], + }, + }, + }, + + // ---------------------------------- + // account: getStatement + // ---------------------------------- + { + displayName: 'Profile ID', + name: 'profileId', + type: 'options', + default: [], + typeOptions: { + loadOptionsMethod: 'getProfiles', + }, + description: 'ID of the user profile whose account to retrieve the statement of.', + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'getStatement', + ], + }, + }, + }, + { + displayName: 'Borderless Account ID', + name: 'borderlessAccountId', + type: 'options', + default: [], + required: true, + typeOptions: { + loadOptionsMethod: 'getBorderlessAccounts', + loadOptionsDependsOn: [ + 'profileId', + ], + }, + description: 'ID of the borderless account to retrieve the statement of.', + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'getStatement', + ], + }, + }, + }, + { + displayName: 'Currency', + name: 'currency', + type: 'string', + default: '', + // TODO: preload + description: 'Code of the currency of the borderless account to retrieve the statement of.', + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'getStatement', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'getStatement', + ], + }, + }, + options: [ + { + displayName: 'Line Style', + name: 'lineStyle', + type: 'options', + default: 'COMPACT', + description: 'Line style to retrieve the statement in.', + options: [ + { + name: 'Compact', + value: 'COMPACT', + description: 'Single line per transaction.', + }, + { + name: 'Flat', + value: 'FLAT', + description: 'Separate lines for transaction fees.', + }, + ], + }, + { + displayName: 'Range', + name: 'range', + type: 'fixedCollection', + placeholder: 'Add Range', + default: {}, + options: [ + { + displayName: 'Range Properties', + name: 'rangeProperties', + values: [ + { + displayName: 'Range Start', + name: 'intervalStart', + type: 'dateTime', + default: '', + }, + { + displayName: 'Range End', + name: 'intervalEnd', + type: 'dateTime', + default: '', + }, + ], + }, + ], + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Wise/descriptions/ExchangeRateDescription.ts b/packages/nodes-base/nodes/Wise/descriptions/ExchangeRateDescription.ts new file mode 100644 index 0000000000..bee99e6fe4 --- /dev/null +++ b/packages/nodes-base/nodes/Wise/descriptions/ExchangeRateDescription.ts @@ -0,0 +1,140 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const exchangeRateOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Get', + value: 'get', + }, + ], + displayOptions: { + show: { + resource: [ + 'exchangeRate', + ], + }, + }, + }, +] as INodeProperties[]; + +export const exchangeRateFields = [ + // ---------------------------------- + // exchangeRate: get + // ---------------------------------- + { + displayName: 'Source Currency', + name: 'source', + type: 'string', + default: '', + description: 'Code of the source currency to retrieve the exchange rate for.', + displayOptions: { + show: { + resource: [ + 'exchangeRate', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Target Currency', + name: 'target', + type: 'string', + default: '', + description: 'Code of the target currency to retrieve the exchange rate for.', + displayOptions: { + show: { + resource: [ + 'exchangeRate', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'exchangeRate', + ], + operation: [ + 'get', + ], + }, + }, + options: [ + { + displayName: 'Interval', + name: 'interval', + type: 'options', + default: 'day', + options: [ + { + name: 'Day', + value: 'day', + }, + { + name: 'Hour', + value: 'hour', + }, + { + name: 'Minute', + value: 'minute', + }, + ], + }, + { + displayName: 'Range', + name: 'range', + type: 'fixedCollection', + placeholder: 'Add Range', + description: 'Range of time to retrieve the exchange rate for.', + default: {}, + options: [ + { + displayName: 'Range Properties', + name: 'rangeProperties', + values: [ + { + displayName: 'Range Start', + name: 'from', + type: 'dateTime', + default: '', + }, + { + displayName: 'Range End', + name: 'to', + type: 'dateTime', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Time', + name: 'time', + type: 'dateTime', + default: '', + description: 'Point in time to retrieve the exchange rate for.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Wise/descriptions/ProfileDescription.ts b/packages/nodes-base/nodes/Wise/descriptions/ProfileDescription.ts new file mode 100644 index 0000000000..dfa8e88c53 --- /dev/null +++ b/packages/nodes-base/nodes/Wise/descriptions/ProfileDescription.ts @@ -0,0 +1,57 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const profileOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + ], + displayOptions: { + show: { + resource: [ + 'profile', + ], + }, + }, + }, +] as INodeProperties[]; + +export const profileFields = [ + // ---------------------------------- + // profile: get + // ---------------------------------- + { + displayName: 'Profile ID', + name: 'profileId', + type: 'options', + required: true, + default: [], + typeOptions: { + loadOptionsMethod: 'getProfiles', + }, + description: 'ID of the user profile to retrieve.', + displayOptions: { + show: { + resource: [ + 'profile', + ], + operation: [ + 'get', + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Wise/descriptions/QuoteDescription.ts b/packages/nodes-base/nodes/Wise/descriptions/QuoteDescription.ts new file mode 100644 index 0000000000..6cb5446d87 --- /dev/null +++ b/packages/nodes-base/nodes/Wise/descriptions/QuoteDescription.ts @@ -0,0 +1,181 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const quoteOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Get', + value: 'get', + }, + ], + displayOptions: { + show: { + resource: [ + 'quote', + ], + }, + }, + }, +] as INodeProperties[]; + +export const quoteFields = [ + // ---------------------------------- + // quote: create + // ---------------------------------- + { + displayName: 'Profile ID', + name: 'profileId', + type: 'options', + required: true, + default: [], + typeOptions: { + loadOptionsMethod: 'getProfiles', + }, + description: 'ID of the user profile to create the quote under.', + displayOptions: { + show: { + resource: [ + 'quote', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Target Account ID', + name: 'targetAccountId', + type: 'options', + required: true, + default: [], + typeOptions: { + loadOptionsMethod: 'getRecipients', + }, + description: 'ID of the account that will receive the funds.', + displayOptions: { + show: { + resource: [ + 'quote', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Amount Type', + name: 'amountType', + type: 'options', + default: 'source', + options: [ + { + name: 'Source', + value: 'source', + }, + { + name: 'Target', + value: 'target', + }, + ], + description: 'Whether the amount is to be sent or received.', + displayOptions: { + show: { + resource: [ + 'quote', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Amount', + name: 'amount', + type: 'number', + default: 1, + typeOptions: { + minValue: 1, + }, + description: 'Amount of funds for the quote to create.', + displayOptions: { + show: { + resource: [ + 'quote', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Source Currency', + name: 'sourceCurrency', + type: 'string', + default: '', + description: 'Code of the currency to send for the quote to create.', + displayOptions: { + show: { + resource: [ + 'quote', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Target Currency', + name: 'targetCurrency', + type: 'string', + default: '', + description: 'Code of the currency to receive for the quote to create.', + displayOptions: { + show: { + resource: [ + 'quote', + ], + operation: [ + 'create', + ], + }, + }, + }, + + // ---------------------------------- + // quote: get + // ---------------------------------- + { + displayName: 'Quote ID', + name: 'quoteId', + type: 'string', + required: true, + default: '', + description: 'ID of the quote to retrieve.', + displayOptions: { + show: { + resource: [ + 'quote', + ], + operation: [ + 'get', + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Wise/descriptions/RecipientDescription.ts b/packages/nodes-base/nodes/Wise/descriptions/RecipientDescription.ts new file mode 100644 index 0000000000..9479a8808b --- /dev/null +++ b/packages/nodes-base/nodes/Wise/descriptions/RecipientDescription.ts @@ -0,0 +1,73 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const recipientOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'getAll', + description: 'Operation to perform', + options: [ + { + name: 'Get All', + value: 'getAll', + }, + ], + displayOptions: { + show: { + resource: [ + 'recipient', + ], + }, + }, + }, +] as INodeProperties[]; + +export const recipientFields = [ + // ---------------------------------- + // recipient: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'recipient', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 5, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + displayOptions: { + show: { + resource: [ + 'recipient', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Wise/descriptions/TransferDescription.ts b/packages/nodes-base/nodes/Wise/descriptions/TransferDescription.ts new file mode 100644 index 0000000000..2e81ad5b04 --- /dev/null +++ b/packages/nodes-base/nodes/Wise/descriptions/TransferDescription.ts @@ -0,0 +1,460 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const transferOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Delete', + value: 'delete', + }, + { + name: 'Execute', + value: 'execute', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + ], + displayOptions: { + show: { + resource: [ + 'transfer', + ], + }, + }, + }, +] as INodeProperties[]; + +export const transferFields = [ + // ---------------------------------- + // transfer: create + // ---------------------------------- + { + displayName: 'Profile ID', + name: 'profileId', + type: 'options', + required: true, + default: [], + typeOptions: { + loadOptionsMethod: 'getProfiles', + loadOptionsDependsOn: [ + 'profileId', + ], + }, + description: 'ID of the user profile to retrieve the balance of.', + displayOptions: { + show: { + resource: [ + 'transfer', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Quote ID', + name: 'quoteId', + type: 'string', + required: true, + default: '', + description: 'ID of the quote based on which to create the transfer.', + displayOptions: { + show: { + resource: [ + 'transfer', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Target Account ID', + name: 'targetAccountId', + type: 'options', + required: true, + default: [], + typeOptions: { + loadOptionsMethod: 'getRecipients', + }, + description: 'ID of the account that will receive the funds.', + displayOptions: { + show: { + resource: [ + 'transfer', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'transfer', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Reference', + name: 'reference', + type: 'string', + default: '', + description: 'Reference text to show in the recipient\'s bank statement', + }, + ], + }, + + // ---------------------------------- + // transfer: delete + // ---------------------------------- + { + displayName: 'Transfer ID', + name: 'transferId', + type: 'string', + required: true, + default: '', + description: 'ID of the transfer to delete.', + displayOptions: { + show: { + resource: [ + 'transfer', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------- + // transfer: execute + // ---------------------------------- + { + displayName: 'Profile ID', + name: 'profileId', + type: 'options', + required: true, + default: [], + typeOptions: { + loadOptionsMethod: 'getProfiles', + }, + description: 'ID of the user profile to execute the transfer under.', + displayOptions: { + show: { + resource: [ + 'transfer', + ], + operation: [ + 'execute', + ], + }, + }, + }, + { + displayName: 'Transfer ID', + name: 'transferId', + type: 'string', + required: true, + default: '', + description: 'ID of the transfer to execute.', + displayOptions: { + show: { + resource: [ + 'transfer', + ], + operation: [ + 'execute', + ], + }, + }, + }, + + // ---------------------------------- + // transfer: get + // ---------------------------------- + { + displayName: 'Transfer ID', + name: 'transferId', + type: 'string', + required: true, + default: '', + description: 'ID of the transfer to retrieve.', + displayOptions: { + show: { + resource: [ + 'transfer', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Download Receipt', + name: 'downloadReceipt', + type: 'boolean', + required: true, + default: false, + description: 'Download the transfer receipt as a PDF file.
Only for executed transfers, having status \'Outgoing Payment Sent\'.', + displayOptions: { + show: { + resource: [ + 'transfer', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Binary Property', + name: 'binaryProperty', + type: 'string', + required: true, + default: 'data', + description: 'Name of the binary property to which to write to.', + displayOptions: { + show: { + resource: [ + 'transfer', + ], + operation: [ + 'get', + ], + downloadReceipt: [ + true, + ], + }, + }, + }, + { + displayName: 'File Name', + name: 'fileName', + type: 'string', + required: true, + default: '', + placeholder: 'data.pdf', + description: 'Name of the file that will be downloaded.', + displayOptions: { + show: { + resource: [ + 'transfer', + ], + operation: [ + 'get', + ], + downloadReceipt: [ + true, + ], + }, + }, + }, + + // ---------------------------------- + // transfer: getAll + // ---------------------------------- + { + displayName: 'Profile ID', + name: 'profileId', + type: 'options', + required: true, + default: [], + typeOptions: { + loadOptionsMethod: 'getProfiles', + }, + description: 'ID of the user profile to retrieve.', + displayOptions: { + show: { + resource: [ + 'transfer', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'transfer', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 5, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + displayOptions: { + show: { + resource: [ + 'transfer', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: [ + 'transfer', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Range', + name: 'range', + type: 'fixedCollection', + placeholder: 'Add Range', + description: 'Range of time for filtering the transfers.', + default: {}, + options: [ + { + displayName: 'Range Properties', + name: 'rangeProperties', + values: [ + { + displayName: 'Created Date Start', + name: 'createdDateStart', + type: 'dateTime', + default: '', + }, + { + displayName: 'Created Date End', + name: 'createdDateEnd', + type: 'dateTime', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Source Currency', + name: 'sourceCurrency', + type: 'string', + default: '', + description: 'Code of the source currency for filtering the transfers.', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + default: 'processing', + options: [ + { + name: 'Bounced Back', + value: 'bounced_back', + }, + { + name: 'Cancelled', + value: 'cancelled', + }, + { + name: 'Charged Back', + value: 'charged_back', + }, + { + name: 'Outgoing Payment Sent', + value: 'outgoing_payment_sent', + }, + { + name: 'Funds Converted', + value: 'funds_converted', + }, + { + name: 'Funds Refunded', + value: 'funds_refunded', + }, + { + name: 'Incoming Payment Waiting', + value: 'incoming_payment_waiting', + }, + { + name: 'Processing', + value: 'processing', + }, + { + name: 'Unknown', + value: 'unknown', + }, + { + name: 'Waiting for Recipient Input to Proceed', + value: 'waiting_recipient_input_to_proceed', + }, + ], + }, + { + displayName: 'Target Currency', + name: 'targetCurrency', + type: 'string', + default: '', + description: 'Code of the target currency for filtering the transfers.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Wise/descriptions/index.ts b/packages/nodes-base/nodes/Wise/descriptions/index.ts new file mode 100644 index 0000000000..0e6fc50395 --- /dev/null +++ b/packages/nodes-base/nodes/Wise/descriptions/index.ts @@ -0,0 +1,6 @@ +export * from './AccountDescription'; +export * from './ExchangeRateDescription'; +export * from './ProfileDescription'; +export * from './QuoteDescription'; +export * from './RecipientDescription'; +export * from './TransferDescription'; diff --git a/packages/nodes-base/nodes/Wise/wise.svg b/packages/nodes-base/nodes/Wise/wise.svg new file mode 100644 index 0000000000..d0dfa43fad --- /dev/null +++ b/packages/nodes-base/nodes/Wise/wise.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index c10bde2596..eb8287839b 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -248,6 +248,7 @@ "dist/credentials/WebflowApi.credentials.js", "dist/credentials/WebflowOAuth2Api.credentials.js", "dist/credentials/WekanApi.credentials.js", + "dist/credentials/WiseApi.credentials.js", "dist/credentials/WooCommerceApi.credentials.js", "dist/credentials/WordpressApi.credentials.js", "dist/credentials/WufooApi.credentials.js", @@ -520,6 +521,8 @@ "dist/nodes/WooCommerce/WooCommerceTrigger.node.js", "dist/nodes/WriteBinaryFile.node.js", "dist/nodes/Wufoo/WufooTrigger.node.js", + "dist/nodes/Wise/Wise.node.js", + "dist/nodes/Wise/WiseTrigger.node.js", "dist/nodes/Xero/Xero.node.js", "dist/nodes/Xml.node.js", "dist/nodes/Yourls/Yourls.node.js",