n8n/packages/nodes-base/nodes/ActionNetwork/GenericFunctions.ts
Elias Meire 100d9bc087
refactor: Add IRequestOptions type to helpers.request for more type safety (no-changelog) (#8563)
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
2024-02-14 16:29:09 +01:00

324 lines
7.8 KiB
TypeScript

import type {
IDataObject,
IExecuteFunctions,
IHttpRequestMethods,
ILoadOptionsFunctions,
IRequestOptions,
} from 'n8n-workflow';
import flow from 'lodash/flow';
import omit from 'lodash/omit';
import type {
AllFieldsUi,
FieldWithPrimaryField,
LinksFieldContainer,
PersonResponse,
PetitionResponse,
Resource,
Response,
} from './types';
export async function actionNetworkApiRequest(
this: IExecuteFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
endpoint: string,
body: IDataObject = {},
qs: IDataObject = {},
) {
const options: IRequestOptions = {
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;
}
return await this.helpers.requestWithAuthentication.call(this, 'actionNetworkApi', options);
}
/**
* 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 async function handleListing(
this: IExecuteFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
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 as IHttpRequestMethods,
endpoint,
body,
qs,
);
const items = responseData._embedded[itemsKey];
returnData.push(...(items as IDataObject[]));
if (!returnAll && returnData.length >= limit) {
return returnData.slice(0, limit);
}
if (responseData._links?.next?.href) {
const queryString = new URLSearchParams(
responseData._links.next.href.split('?')[1] as string,
);
qs.page = queryString.get('page') as string;
}
} while (responseData._links?.next);
return returnData;
}
// ----------------------------------------
// helpers
// ----------------------------------------
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
// ----------------------------------------
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],
};
};
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']),
};
};
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),
};
};