mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-26 13:14:07 -08:00
7ce7285f7a
* 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>
349 lines
7.9 KiB
TypeScript
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],
|
|
};
|
|
};
|