n8n/packages/nodes-base/nodes/ActionNetwork/GenericFunctions.ts
Omar Ajoue 7ce7285f7a
Load credentials from the database (#1741)
* Changes to types so that credentials can be always loaded from DB

This first commit changes all return types from the execute functions
and calls to get credentials to be async so we can use await.

This is a first step as previously credentials were loaded in memory and
always available. We will now be loading them from the DB which requires
turning the whole call chain async.

* Fix updated files

* Removed unnecessary credential loading to improve performance

* Fix typo

*  Fix issue

* Updated new nodes to load credentials async

*  Remove not needed comment

Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
2021-08-20 18:57:30 +02:00

349 lines
7.9 KiB
TypeScript

import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow';
import {
OptionsWithUri,
} from 'request';
import {
flow,
omit,
} from 'lodash';
import {
AllFieldsUi,
FieldWithPrimaryField,
LinksFieldContainer,
PersonResponse,
PetitionResponse,
Resource,
Response,
} from './types';
export async function actionNetworkApiRequest(
this: IExecuteFunctions | ILoadOptionsFunctions,
method: string,
endpoint: string,
body: IDataObject = {},
qs: IDataObject = {},
) {
const credentials = await this.getCredentials('actionNetworkApi') as { apiKey: string } | undefined;
if (credentials === undefined) {
throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
}
const options: OptionsWithUri = {
headers: {
'OSDI-API-Token': credentials.apiKey,
},
method,
body,
qs,
uri: `https://actionnetwork.org/api/v2${endpoint}`,
json: true,
};
if (!Object.keys(body).length) {
delete options.body;
}
if (!Object.keys(qs).length) {
delete options.qs;
}
try {
return await this.helpers.request!(options);
} catch (error) {
throw new NodeApiError(this.getNode(), error);
}
}
export async function handleListing(
this: IExecuteFunctions | ILoadOptionsFunctions,
method: string,
endpoint: string,
body: IDataObject = {},
qs: IDataObject = {},
options?: { returnAll: true },
) {
const returnData: IDataObject[] = [];
let responseData;
qs.perPage = 25; // max
qs.page = 1;
const returnAll = options?.returnAll ?? this.getNodeParameter('returnAll', 0, false) as boolean;
const limit = this.getNodeParameter('limit', 0, 0) as number;
const itemsKey = toItemsKey(endpoint);
do {
responseData = await actionNetworkApiRequest.call(this, method, endpoint, body, qs);
const items = responseData._embedded[itemsKey];
returnData.push(...items);
if (!returnAll && returnData.length >= limit) {
return returnData.slice(0, limit);
}
qs.page = responseData.page as number;
} while (responseData.total_pages && qs.page < responseData.total_pages);
return returnData;
}
// ----------------------------------------
// helpers
// ----------------------------------------
/**
* Convert an endpoint to the key needed to access data in the response.
*/
const toItemsKey = (endpoint: string) => {
// handle two-resource endpoint
if (
endpoint.includes('/signatures') ||
endpoint.includes('/attendances') ||
endpoint.includes('/taggings')
) {
endpoint = endpoint.split('/').pop()!;
}
return `osdi:${endpoint.replace(/\//g, '')}`;
};
export const extractId = (response: LinksFieldContainer) => {
return response._links.self.href.split('/').pop() ?? 'No ID';
};
export const makeOsdiLink = (personId: string) => {
return {
_links: {
'osdi:person': {
href: `https://actionnetwork.org/api/v2/people/${personId}`,
},
},
};
};
export const isPrimary = (field: FieldWithPrimaryField) => field.primary;
// ----------------------------------------
// field adjusters
// ----------------------------------------
function adjustLanguagesSpoken(allFields: AllFieldsUi) {
if (!allFields.languages_spoken) return allFields;
return {
...omit(allFields, ['languages_spoken']),
languages_spoken: [allFields.languages_spoken],
};
}
function adjustPhoneNumbers(allFields: AllFieldsUi) {
if (!allFields.phone_numbers) return allFields;
return {
...omit(allFields, ['phone_numbers']),
phone_numbers: [
allFields.phone_numbers.phone_numbers_fields,
],
};
}
function adjustPostalAddresses(allFields: AllFieldsUi) {
if (!allFields.postal_addresses) return allFields;
if (allFields.postal_addresses.postal_addresses_fields.length) {
const adjusted = allFields.postal_addresses.postal_addresses_fields.map((field) => {
const copy: IDataObject = {
...omit(field, ['address_lines', 'location']),
};
if (field.address_lines) {
copy.address_lines = [field.address_lines];
}
if (field.location) {
copy.location = field.location.location_fields;
}
return copy;
});
return {
...omit(allFields, ['postal_addresses']),
postal_addresses: adjusted,
};
}
}
function adjustLocation(allFields: AllFieldsUi) {
if (!allFields.location) return allFields;
const locationFields = allFields.location.postal_addresses_fields;
const adjusted: IDataObject = {
...omit(locationFields, ['address_lines', 'location']),
};
if (locationFields.address_lines) {
adjusted.address_lines = [locationFields.address_lines];
}
if (locationFields.location) {
adjusted.location = locationFields.location.location_fields;
}
return {
...omit(allFields, ['location']),
location: adjusted,
};
}
function adjustTargets(allFields: AllFieldsUi) {
if (!allFields.target) return allFields;
const adjusted = allFields.target.split(',').map(value => ({ name: value }));
return {
...omit(allFields, ['target']),
target: adjusted,
};
}
// ----------------------------------------
// payload adjusters
// ----------------------------------------
export const adjustPersonPayload = flow(
adjustLanguagesSpoken,
adjustPhoneNumbers,
adjustPostalAddresses,
);
export const adjustPetitionPayload = adjustTargets;
export const adjustEventPayload = adjustLocation;
// ----------------------------------------
// resource loaders
// ----------------------------------------
async function loadResource(this: ILoadOptionsFunctions, resource: string) {
return await handleListing.call(this, 'GET', `/${resource}`, {}, {}, { returnAll: true });
}
export const resourceLoaders = {
async getTags(this: ILoadOptionsFunctions) {
const tags = await loadResource.call(this, 'tags') as Array<{ name: string } & LinksFieldContainer>;
return tags.map((tag) => ({ name: tag.name, value: extractId(tag) }));
},
async getTaggings(this: ILoadOptionsFunctions) {
const tagId = this.getNodeParameter('tagId', 0);
const endpoint = `/tags/${tagId}/taggings`;
// two-resource endpoint, so direct call
const taggings = await handleListing.call(
this, 'GET', endpoint, {}, {}, { returnAll: true },
) as LinksFieldContainer[];
return taggings.map((tagging) => {
const taggingId = extractId(tagging);
return {
name: taggingId,
value: taggingId,
};
});
},
};
// ----------------------------------------
// response simplifiers
// ----------------------------------------
export const simplifyResponse = (response: Response, resource: Resource) => {
if (resource === 'person') {
return simplifyPersonResponse(response as PersonResponse);
} else if (resource === 'petition') {
return simplifyPetitionResponse(response as PetitionResponse);
}
const fieldsToSimplify = [
'identifiers',
'_links',
'action_network:sponsor',
'reminders',
];
return {
id: extractId(response),
...omit(response, fieldsToSimplify),
};
};
const simplifyPetitionResponse = (response: PetitionResponse) => {
const fieldsToSimplify = [
'identifiers',
'_links',
'action_network:hidden',
'_embedded',
];
return {
id: extractId(response),
...omit(response, fieldsToSimplify),
creator: simplifyPersonResponse(response._embedded['osdi:creator']),
};
};
const simplifyPersonResponse = (response: PersonResponse) => {
const emailAddress = response.email_addresses.filter(isPrimary);
const phoneNumber = response.phone_numbers.filter(isPrimary);
const postalAddress = response.postal_addresses.filter(isPrimary);
const fieldsToSimplify = [
'identifiers',
'email_addresses',
'phone_numbers',
'postal_addresses',
'languages_spoken',
'_links',
];
return {
id: extractId(response),
...omit(response, fieldsToSimplify),
...{ email_address: emailAddress[0].address || '' },
...{ phone_number: phoneNumber[0].number || '' },
...{
postal_address: {
...postalAddress && omit(postalAddress[0], 'address_lines'),
address_lines: postalAddress[0].address_lines ?? '',
},
},
language_spoken: response.languages_spoken[0],
};
};