From 224a26c9220239ddc88fe07e1eacc12043c50166 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Sun, 27 Jun 2021 13:07:25 +0200 Subject: [PATCH] :sparkles: Add Action Network node (#1897) * :sparkles: Create Action Network node * :fire: Remove comments * :fire: Remove status in attendance * :fire: Remove loaders per feedback Loaders removed for person, event, signature and petition * :truck: Rename tagging to person tag * :hammer: Convert address_lines param to string * :zap: Simplify responses for person resource * :zap: Add simplify to all operations * :pencil2: Add documentation links * :zap: Improvements * :pencil2: Fix positioning of doc links * :hammer: Refactor updateFields in signature:update * :zap: Address minor comments * :zap: Improvements * :zap: Add continue on fail * :zap: Minor improvements Co-authored-by: ricardo Co-authored-by: Jan Oberhauser --- .../ActionNetworkApi.credentials.ts | 17 + .../nodes/ActionNetwork/ActionNetwork.node.ts | 551 ++++++++++++++++++ .../nodes/ActionNetwork/GenericFunctions.ts | 348 +++++++++++ .../nodes/ActionNetwork/actionNetwork.svg | 1 + .../descriptions/AttendanceDescription.ts | 185 ++++++ .../descriptions/EventDescription.ts | 168 ++++++ .../descriptions/PersonDescription.ts | 250 ++++++++ .../descriptions/PersonTagDescription.ts | 122 ++++ .../descriptions/PetitionDescription.ts | 213 +++++++ .../descriptions/SharedFields.ts | 376 ++++++++++++ .../descriptions/SignatureDescription.ts | 282 +++++++++ .../descriptions/TagDescription.ts | 131 +++++ .../nodes/ActionNetwork/descriptions/index.ts | 7 + .../nodes-base/nodes/ActionNetwork/types.d.ts | 99 ++++ packages/nodes-base/package.json | 2 + 15 files changed, 2752 insertions(+) create mode 100644 packages/nodes-base/credentials/ActionNetworkApi.credentials.ts create mode 100644 packages/nodes-base/nodes/ActionNetwork/ActionNetwork.node.ts create mode 100644 packages/nodes-base/nodes/ActionNetwork/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/ActionNetwork/actionNetwork.svg create mode 100644 packages/nodes-base/nodes/ActionNetwork/descriptions/AttendanceDescription.ts create mode 100644 packages/nodes-base/nodes/ActionNetwork/descriptions/EventDescription.ts create mode 100644 packages/nodes-base/nodes/ActionNetwork/descriptions/PersonDescription.ts create mode 100644 packages/nodes-base/nodes/ActionNetwork/descriptions/PersonTagDescription.ts create mode 100644 packages/nodes-base/nodes/ActionNetwork/descriptions/PetitionDescription.ts create mode 100644 packages/nodes-base/nodes/ActionNetwork/descriptions/SharedFields.ts create mode 100644 packages/nodes-base/nodes/ActionNetwork/descriptions/SignatureDescription.ts create mode 100644 packages/nodes-base/nodes/ActionNetwork/descriptions/TagDescription.ts create mode 100644 packages/nodes-base/nodes/ActionNetwork/descriptions/index.ts create mode 100644 packages/nodes-base/nodes/ActionNetwork/types.d.ts diff --git a/packages/nodes-base/credentials/ActionNetworkApi.credentials.ts b/packages/nodes-base/credentials/ActionNetworkApi.credentials.ts new file mode 100644 index 0000000000..e8817b35fe --- /dev/null +++ b/packages/nodes-base/credentials/ActionNetworkApi.credentials.ts @@ -0,0 +1,17 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class ActionNetworkApi implements ICredentialType { + name = 'actionNetworkApi'; + displayName = 'Action Network API'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/ActionNetwork/ActionNetwork.node.ts b/packages/nodes-base/nodes/ActionNetwork/ActionNetwork.node.ts new file mode 100644 index 0000000000..13f6157268 --- /dev/null +++ b/packages/nodes-base/nodes/ActionNetwork/ActionNetwork.node.ts @@ -0,0 +1,551 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, + NodeOperationError, +} from 'n8n-workflow'; + +import { + actionNetworkApiRequest, + adjustEventPayload, + adjustPersonPayload, + adjustPetitionPayload, + handleListing, + makeOsdiLink, + resourceLoaders, + simplifyResponse, +} from './GenericFunctions'; + +import { + attendanceFields, + attendanceOperations, + eventFields, + eventOperations, + personFields, + personOperations, + personTagFields, + personTagOperations, + petitionFields, + petitionOperations, + signatureFields, + signatureOperations, + tagFields, + tagOperations, +} from './descriptions'; + +import { + AllFieldsUi, + EmailAddressUi, + Operation, + PersonResponse, + Resource, + Response, +} from './types'; + +export class ActionNetwork implements INodeType { + description: INodeTypeDescription = { + displayName: 'Action Network', + name: 'actionNetwork', + icon: 'file:actionNetwork.svg', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}', + description: 'Consume the Action Network API', + defaults: { + name: 'Action Network', + color: '#9dd3ed', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'actionNetworkApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Attendance', + value: 'attendance', + }, + { + name: 'Event', + value: 'event', + }, + { + name: 'Person', + value: 'person', + }, + { + name: 'Person Tag', + value: 'personTag', + }, + { + name: 'Petition', + value: 'petition', + }, + { + name: 'Signature', + value: 'signature', + }, + { + name: 'Tag', + value: 'tag', + }, + ], + default: 'attendance', + description: 'Resource to consume', + }, + ...attendanceOperations, + ...attendanceFields, + ...eventOperations, + ...eventFields, + ...personOperations, + ...personFields, + ...petitionOperations, + ...petitionFields, + ...signatureOperations, + ...signatureFields, + ...tagOperations, + ...tagFields, + ...personTagOperations, + ...personTagFields, + ], + }; + + methods = { + loadOptions: resourceLoaders, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + + const resource = this.getNodeParameter('resource', 0) as Resource; + const operation = this.getNodeParameter('operation', 0) as Operation; + + let response; + + for (let i = 0; i < items.length; i++) { + try { + if (resource === 'attendance') { + + // ********************************************************************** + // attendance + // ********************************************************************** + + // https://actionnetwork.org/docs/v2/attendances + + if (operation === 'create') { + + // ---------------------------------------- + // attendance: create + // ---------------------------------------- + + const personId = this.getNodeParameter('personId', i) as string; + const eventId = this.getNodeParameter('eventId', i); + + const body = makeOsdiLink(personId) as IDataObject; + + const endpoint = `/events/${eventId}/attendances`; + response = await actionNetworkApiRequest.call(this, 'POST', endpoint, body); + + } else if (operation === 'get') { + + // ---------------------------------------- + // attendance: get + // ---------------------------------------- + + const eventId = this.getNodeParameter('eventId', i); + const attendanceId = this.getNodeParameter('attendanceId', i); + + const endpoint = `/events/${eventId}/attendances/${attendanceId}`; + response = await actionNetworkApiRequest.call(this, 'GET', endpoint); + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // attendance: getAll + // ---------------------------------------- + + const eventId = this.getNodeParameter('eventId', i); + + const endpoint = `/events/${eventId}/attendances`; + response = await handleListing.call(this, 'GET', endpoint); + + } + + } else if (resource === 'event') { + + // ********************************************************************** + // event + // ********************************************************************** + + // https://actionnetwork.org/docs/v2/events + + if (operation === 'create') { + + // ---------------------------------------- + // event: create + // ---------------------------------------- + + const body = { + origin_system: this.getNodeParameter('originSystem', i), + title: this.getNodeParameter('title', i), + } as IDataObject; + + const additionalFields = this.getNodeParameter('additionalFields', i) as AllFieldsUi; + + if (Object.keys(additionalFields).length) { + Object.assign(body, adjustEventPayload(additionalFields)); + } + + response = await actionNetworkApiRequest.call(this, 'POST', '/events', body); + + } else if (operation === 'get') { + + // ---------------------------------------- + // event: get + // ---------------------------------------- + + const eventId = this.getNodeParameter('eventId', i); + + response = await actionNetworkApiRequest.call(this, 'GET', `/events/${eventId}`); + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // event: getAll + // ---------------------------------------- + + response = await handleListing.call(this, 'GET', '/events'); + + } + + } else if (resource === 'person') { + + // ********************************************************************** + // person + // ********************************************************************** + + // https://actionnetwork.org/docs/v2/people + + if (operation === 'create') { + + // ---------------------------------------- + // person: create + // ---------------------------------------- + + const emailAddresses = this.getNodeParameter('email_addresses', i) as EmailAddressUi; + + const body = { + person: { + email_addresses: [emailAddresses.email_addresses_fields], // only one accepted by API + }, + } as IDataObject; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(body.person, adjustPersonPayload(additionalFields)); + } + + response = await actionNetworkApiRequest.call(this, 'POST', '/people', body); + + } else if (operation === 'get') { + + // ---------------------------------------- + // person: get + // ---------------------------------------- + + const personId = this.getNodeParameter('personId', i); + + response = await actionNetworkApiRequest.call(this, 'GET', `/people/${personId}`) as PersonResponse; + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // person: getAll + // ---------------------------------------- + + response = await handleListing.call(this, 'GET', '/people') as PersonResponse[]; + + } else if (operation === 'update') { + + // ---------------------------------------- + // person: update + // ---------------------------------------- + + const personId = this.getNodeParameter('personId', i); + const body = {} as IDataObject; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (Object.keys(updateFields).length) { + Object.assign(body, adjustPersonPayload(updateFields)); + } else { + throw new NodeOperationError( + this.getNode(), + `Please enter at least one field to update for the ${resource}.`, + ); + } + + response = await actionNetworkApiRequest.call(this, 'PUT', `/people/${personId}`, body); + + } + + } else if (resource === 'petition') { + + // ********************************************************************** + // petition + // ********************************************************************** + + // https://actionnetwork.org/docs/v2/petitions + + if (operation === 'create') { + + // ---------------------------------------- + // petition: create + // ---------------------------------------- + + const body = { + origin_system: this.getNodeParameter('originSystem', i), + title: this.getNodeParameter('title', i), + } as IDataObject; + + const additionalFields = this.getNodeParameter('additionalFields', i) as AllFieldsUi; + + if (Object.keys(additionalFields).length) { + Object.assign(body, adjustPetitionPayload(additionalFields)); + } + + response = await actionNetworkApiRequest.call(this, 'POST', '/petitions', body); + + } else if (operation === 'get') { + + // ---------------------------------------- + // petition: get + // ---------------------------------------- + + const petitionId = this.getNodeParameter('petitionId', i); + + const endpoint = `/petitions/${petitionId}`; + response = await actionNetworkApiRequest.call(this, 'GET', endpoint); + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // petition: getAll + // ---------------------------------------- + + response = await handleListing.call(this, 'GET', '/petitions'); + + } else if (operation === 'update') { + + // ---------------------------------------- + // petition: update + // ---------------------------------------- + + const petitionId = this.getNodeParameter('petitionId', i); + const body = {} as IDataObject; + const updateFields = this.getNodeParameter('updateFields', i) as AllFieldsUi; + + if (Object.keys(updateFields).length) { + Object.assign(body, adjustPetitionPayload(updateFields)); + } else { + throw new NodeOperationError( + this.getNode(), + `Please enter at least one field to update for the ${resource}.`, + ); + } + + response = await actionNetworkApiRequest.call(this, 'PUT', `/petitions/${petitionId}`, body); + + } + + } else if (resource === 'signature') { + + // ********************************************************************** + // signature + // ********************************************************************** + + // https://actionnetwork.org/docs/v2/signatures + + if (operation === 'create') { + + // ---------------------------------------- + // signature: create + // ---------------------------------------- + + const personId = this.getNodeParameter('personId', i) as string; + const petitionId = this.getNodeParameter('petitionId', i); + + const body = makeOsdiLink(personId) as IDataObject; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(body, additionalFields); + } + + const endpoint = `/petitions/${petitionId}/signatures`; + response = await actionNetworkApiRequest.call(this, 'POST', endpoint, body); + + } else if (operation === 'get') { + + // ---------------------------------------- + // signature: get + // ---------------------------------------- + + const petitionId = this.getNodeParameter('petitionId', i); + const signatureId = this.getNodeParameter('signatureId', i); + + const endpoint = `/petitions/${petitionId}/signatures/${signatureId}`; + response = await actionNetworkApiRequest.call(this, 'GET', endpoint); + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // signature: getAll + // ---------------------------------------- + + const petitionId = this.getNodeParameter('petitionId', i); + + const endpoint = `/petitions/${petitionId}/signatures`; + response = await handleListing.call(this, 'GET', endpoint); + + } else if (operation === 'update') { + + // ---------------------------------------- + // signature: update + // ---------------------------------------- + + const petitionId = this.getNodeParameter('petitionId', i); + const signatureId = this.getNodeParameter('signatureId', i); + const body = {}; + const updateFields = this.getNodeParameter('updateFields', i) as AllFieldsUi; + + if (Object.keys(updateFields).length) { + Object.assign(body, updateFields); + } else { + throw new NodeOperationError( + this.getNode(), + `Please enter at least one field to update for the ${resource}.`, + ); + } + + const endpoint = `/petitions/${petitionId}/signatures/${signatureId}`; + response = await actionNetworkApiRequest.call(this, 'PUT', endpoint, body); + + } + + } else if (resource === 'tag') { + + // ********************************************************************** + // tag + // ********************************************************************** + + // https://actionnetwork.org/docs/v2/tags + + if (operation === 'create') { + + // ---------------------------------------- + // tag: create + // ---------------------------------------- + + const body = { + name: this.getNodeParameter('name', i), + } as IDataObject; + + response = await actionNetworkApiRequest.call(this, 'POST', '/tags', body); + + } else if (operation === 'get') { + + // ---------------------------------------- + // tag: get + // ---------------------------------------- + + const tagId = this.getNodeParameter('tagId', i); + + response = await actionNetworkApiRequest.call(this, 'GET', `/tags/${tagId}`); + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // tag: getAll + // ---------------------------------------- + + response = await handleListing.call(this, 'GET', '/tags'); + + } + + } else if (resource === 'personTag') { + + // ********************************************************************** + // personTag + // ********************************************************************** + + // https://actionnetwork.org/docs/v2/taggings + + if (operation === 'add') { + + // ---------------------------------------- + // personTag: add + // ---------------------------------------- + + const personId = this.getNodeParameter('personId', i) as string; + const tagId = this.getNodeParameter('tagId', i); + + const body = makeOsdiLink(personId) as IDataObject; + + const endpoint = `/tags/${tagId}/taggings`; + response = await actionNetworkApiRequest.call(this, 'POST', endpoint, body); + + } else if (operation === 'remove') { + + // ---------------------------------------- + // personTag: remove + // ---------------------------------------- + + const tagId = this.getNodeParameter('tagId', i); + const taggingId = this.getNodeParameter('taggingId', i); + + const endpoint = `/tags/${tagId}/taggings/${taggingId}`; + response = await actionNetworkApiRequest.call(this, 'DELETE', endpoint); + + } + + } + + const simplify = this.getNodeParameter('simple', i, false) as boolean; + + if (simplify) { + response = operation === 'getAll' + ? response.map((i: Response) => simplifyResponse(i, resource)) + : simplifyResponse(response, resource); + } + + Array.isArray(response) + ? returnData.push(...response) + : returnData.push(response); + + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + throw error; + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/ActionNetwork/GenericFunctions.ts b/packages/nodes-base/nodes/ActionNetwork/GenericFunctions.ts new file mode 100644 index 0000000000..32bb775feb --- /dev/null +++ b/packages/nodes-base/nodes/ActionNetwork/GenericFunctions.ts @@ -0,0 +1,348 @@ +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 = 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], + }; +}; diff --git a/packages/nodes-base/nodes/ActionNetwork/actionNetwork.svg b/packages/nodes-base/nodes/ActionNetwork/actionNetwork.svg new file mode 100644 index 0000000000..acf96bb6eb --- /dev/null +++ b/packages/nodes-base/nodes/ActionNetwork/actionNetwork.svg @@ -0,0 +1 @@ +actionnetwork diff --git a/packages/nodes-base/nodes/ActionNetwork/descriptions/AttendanceDescription.ts b/packages/nodes-base/nodes/ActionNetwork/descriptions/AttendanceDescription.ts new file mode 100644 index 0000000000..0eacc615b8 --- /dev/null +++ b/packages/nodes-base/nodes/ActionNetwork/descriptions/AttendanceDescription.ts @@ -0,0 +1,185 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + makeSimpleField, +} from './SharedFields'; + +export const attendanceOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'attendance', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + ], + default: 'create', + description: 'Operation to perform', + }, +] as INodeProperties[]; + +export const attendanceFields = [ + // ---------------------------------------- + // attendance: create + // ---------------------------------------- + { + displayName: 'Person ID', + name: 'personId', + description: 'ID of the person to create an attendance for.', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'attendance', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Event ID', + name: 'eventId', + description: 'ID of the event to create an attendance for.', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'attendance', + ], + operation: [ + 'create', + ], + }, + }, + }, + makeSimpleField('attendance', 'create'), + + // ---------------------------------------- + // attendance: get + // ---------------------------------------- + { + displayName: 'Event ID', + name: 'eventId', + description: 'ID of the event whose attendance to retrieve.', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'attendance', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Attendance ID', + name: 'attendanceId', + description: 'ID of the attendance to retrieve.', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'attendance', + ], + operation: [ + 'get', + ], + }, + }, + }, + makeSimpleField('attendance', 'get'), + + // ---------------------------------------- + // attendance: getAll + // ---------------------------------------- + { + displayName: 'Event ID', + name: 'eventId', + description: 'ID of the event to create an attendance for.', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'attendance', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'attendance', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + resource: [ + 'attendance', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + makeSimpleField('attendance', 'getAll'), +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/ActionNetwork/descriptions/EventDescription.ts b/packages/nodes-base/nodes/ActionNetwork/descriptions/EventDescription.ts new file mode 100644 index 0000000000..1e30d2b10a --- /dev/null +++ b/packages/nodes-base/nodes/ActionNetwork/descriptions/EventDescription.ts @@ -0,0 +1,168 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + eventAdditionalFieldsOptions, + makeSimpleField, +} from './SharedFields'; + +export const eventOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'event', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + ], + default: 'create', + description: 'Operation to perform', + }, +] as INodeProperties[]; + +export const eventFields = [ + // ---------------------------------------- + // event: create + // ---------------------------------------- + { + displayName: 'Origin System', + name: 'originSystem', + description: 'Source where the event originated.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Title', + name: 'title', + description: 'Title of the event to create.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'create', + ], + }, + }, + }, + makeSimpleField('event', 'create'), + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'create', + ], + }, + }, + options: eventAdditionalFieldsOptions, + }, + + // ---------------------------------------- + // event: get + // ---------------------------------------- + { + displayName: 'Event ID', + name: 'eventId', + description: 'ID of the event to retrieve.', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'get', + ], + }, + }, + }, + makeSimpleField('event', 'get'), + + // ---------------------------------------- + // event: getAll + // ---------------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + makeSimpleField('event', 'getAll'), +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/ActionNetwork/descriptions/PersonDescription.ts b/packages/nodes-base/nodes/ActionNetwork/descriptions/PersonDescription.ts new file mode 100644 index 0000000000..0e78c679da --- /dev/null +++ b/packages/nodes-base/nodes/ActionNetwork/descriptions/PersonDescription.ts @@ -0,0 +1,250 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + makeSimpleField, + personAdditionalFieldsOptions, +} from './SharedFields'; + +export const personOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'person', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Update', + value: 'update', + }, + ], + default: 'create', + description: 'Operation to perform', + }, +] as INodeProperties[]; + +export const personFields = [ + // ---------------------------------------- + // person: create + // ---------------------------------------- + makeSimpleField('person', 'create'), + { + displayName: 'Email Address', // on create, only _one_ must be passed in + name: 'email_addresses', + type: 'fixedCollection', + default: {}, + placeholder: 'Add Email Address Field', + description: 'Person’s email addresses.', + displayOptions: { + show: { + resource: [ + 'person', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Email Addresses Fields', + name: 'email_addresses_fields', + values: [ + { + displayName: 'Address', + name: 'address', + type: 'string', + default: '', + description: 'Person\'s email address.', + }, + { + displayName: 'Primary', + name: 'primary', + type: 'hidden', + default: true, + description: 'Whether this is the person\'s primary email address.', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + default: 'subscribed', + description: 'Subscription status of this email address.', + options: [ + { + name: 'Bouncing', + value: 'bouncing', + }, + { + name: 'Previous Bounce', + value: 'previous bounce', + }, + { + name: 'Previous Spam Complaint', + value: 'previous spam complaint', + }, + { + name: 'Spam Complaint', + value: 'spam complaint', + }, + { + name: 'Subscribed', + value: 'subscribed', + }, + { + name: 'Unsubscribed', + value: 'unsubscribed', + }, + ], + }, + ], + }, + ], + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'person', + ], + operation: [ + 'create', + ], + }, + }, + options: personAdditionalFieldsOptions, + }, + + // ---------------------------------------- + // person: get + // ---------------------------------------- + { + displayName: 'Person ID', + name: 'personId', + description: 'ID of the person to retrieve.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'person', + ], + operation: [ + 'get', + ], + }, + }, + }, + makeSimpleField('person', 'get'), + + // ---------------------------------------- + // person: getAll + // ---------------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'person', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + resource: [ + 'person', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + makeSimpleField('person', 'getAll'), + + // ---------------------------------------- + // person: update + // ---------------------------------------- + { + displayName: 'Person ID', + name: 'personId', + description: 'ID of the person to update.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'person', + ], + operation: [ + 'update', + ], + }, + }, + }, + makeSimpleField('person', 'update'), + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'person', + ], + operation: [ + 'update', + ], + }, + }, + options: personAdditionalFieldsOptions, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/ActionNetwork/descriptions/PersonTagDescription.ts b/packages/nodes-base/nodes/ActionNetwork/descriptions/PersonTagDescription.ts new file mode 100644 index 0000000000..82bbd64b2e --- /dev/null +++ b/packages/nodes-base/nodes/ActionNetwork/descriptions/PersonTagDescription.ts @@ -0,0 +1,122 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const personTagOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'personTag', + ], + }, + }, + options: [ + { + name: 'Add', + value: 'add', + }, + { + name: 'Remove', + value: 'remove', + }, + ], + default: 'add', + description: 'Operation to perform', + }, +] as INodeProperties[]; + +export const personTagFields = [ + // ---------------------------------------- + // personTag: add + // ---------------------------------------- + { + displayName: 'Tag ID', + name: 'tagId', + description: 'ID of the tag to add.', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + required: true, + default: [], + displayOptions: { + show: { + resource: [ + 'personTag', + ], + operation: [ + 'add', + ], + }, + }, + }, + { + displayName: 'Person ID', + name: 'personId', + description: 'ID of the person to add the tag to.', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'personTag', + ], + operation: [ + 'add', + ], + }, + }, + }, + + // ---------------------------------------- + // personTag: remove + // ---------------------------------------- + { + displayName: 'Tag ID', + name: 'tagId', + description: 'ID of the tag whose tagging to delete.', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + default: [], + required: true, + displayOptions: { + show: { + resource: [ + 'personTag', + ], + operation: [ + 'remove', + ], + }, + }, + }, + { + displayName: 'Tagging ID', + name: 'taggingId', + description: 'ID of the tagging to remove.', + type: 'options', + typeOptions: { + loadOptionsDependsOn: 'tagId', + loadOptionsMethod: 'getTaggings', + }, + required: true, + default: [], + displayOptions: { + show: { + resource: [ + 'personTag', + ], + operation: [ + 'remove', + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/ActionNetwork/descriptions/PetitionDescription.ts b/packages/nodes-base/nodes/ActionNetwork/descriptions/PetitionDescription.ts new file mode 100644 index 0000000000..717a661dad --- /dev/null +++ b/packages/nodes-base/nodes/ActionNetwork/descriptions/PetitionDescription.ts @@ -0,0 +1,213 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + makeSimpleField, + petitionAdditionalFieldsOptions, +} from './SharedFields'; + +export const petitionOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'petition', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Update', + value: 'update', + }, + ], + default: 'create', + description: 'Operation to perform', + }, +] as INodeProperties[]; + +export const petitionFields = [ + // ---------------------------------------- + // petition: create + // ---------------------------------------- + { + displayName: 'Origin System', + name: 'originSystem', + description: 'Source where the petition originated.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'petition', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Title', + name: 'title', + description: 'Title of the petition to create.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'petition', + ], + operation: [ + 'create', + ], + }, + }, + }, + makeSimpleField('petition', 'create'), + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'petition', + ], + operation: [ + 'create', + ], + }, + }, + options: petitionAdditionalFieldsOptions, + }, + + // ---------------------------------------- + // petition: get + // ---------------------------------------- + { + displayName: 'Petition ID', + name: 'petitionId', + description: 'ID of the petition to retrieve.', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'petition', + ], + operation: [ + 'get', + ], + }, + }, + }, + makeSimpleField('petition', 'get'), + + // ---------------------------------------- + // petition: getAll + // ---------------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'petition', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + resource: [ + 'petition', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + makeSimpleField('petition', 'getAll'), + + // ---------------------------------------- + // petition: update + // ---------------------------------------- + { + displayName: 'Petition ID', + name: 'petitionId', + description: 'ID of the petition to update.', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'petition', + ], + operation: [ + 'update', + ], + }, + }, + }, + makeSimpleField('petition', 'update'), + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'petition', + ], + operation: [ + 'update', + ], + }, + }, + options: petitionAdditionalFieldsOptions, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/ActionNetwork/descriptions/SharedFields.ts b/packages/nodes-base/nodes/ActionNetwork/descriptions/SharedFields.ts new file mode 100644 index 0000000000..321eaa1d97 --- /dev/null +++ b/packages/nodes-base/nodes/ActionNetwork/descriptions/SharedFields.ts @@ -0,0 +1,376 @@ +import { + Operation, + Resource, +} from '../types'; + +export const languageOptions = [ + { + name: 'Danish', + value: 'da', + }, + { + name: 'Dutch', + value: 'nl', + }, + { + name: 'English', + value: 'en', + }, + { + name: 'Finnish', + value: 'fi', + }, + { + name: 'French', + value: 'fr', + }, + { + name: 'German', + value: 'de', + }, + { + name: 'Hungarian', + value: 'hu', + }, + { + name: 'Indonesian', + value: 'id', + }, + { + name: 'Japanese', + value: 'ja', + }, + { + name: 'Portuguese - Portugal', + value: 'pt', + }, + { + name: 'Portuguese - Brazil', + value: 'br', + }, + { + name: 'Rumanian', + value: 'ru', + }, + { + name: 'Spanish', + value: 'es', + }, + { + name: 'Swedish', + value: 'sv', + }, + { + name: 'Turkish', + value: 'tr', + }, + { + name: 'Welsh', + value: 'cy', + }, +] as const; + +const postalAddressesFields = [ + { + displayName: 'Primary', + name: 'primary', + type: 'boolean', + default: false, + description: 'Whether this is the person\'s primary address.', + }, + { + displayName: 'Address Line', + name: 'address_lines', + type: 'string', // The Action Network API expects a string array but ignores any string beyond the first, so this input field is simplified to string. + default: '', + description: 'Line for a person\'s address.', + }, + { + displayName: 'Locality', + name: 'locality', + type: 'string', + default: '', + description: 'City or other local administrative area. If blank, this will be filled in based on Action Network\'s geocoding.', + }, + { + displayName: 'Region', + name: 'region', + type: 'string', + default: '', + description: 'State or subdivision code per ISO 3166-2.', + }, + { + displayName: 'Postal Code', + name: 'postal_code', + type: 'string', + default: '', + description: 'Region specific postal code, such as ZIP code.', + }, + { + displayName: 'Country', + name: 'country', + type: 'string', + default: '', + description: 'Country code according to ISO 3166-1 Alpha-2. Defaults to US.', + }, + { + displayName: 'Language', + name: 'language', + type: 'string', + default: '', + description: 'Language in which the address is recorded, per ISO 639.', + }, + { + displayName: 'Location', + name: 'location', + type: 'fixedCollection', + default: '', + options: [ + { + displayName: 'Location Fields', + name: 'location_fields', + values: [ + { + displayName: 'Latitude', + name: 'latitude', + type: 'string', + default: '', + description: 'Latitude of the location of the address.', + }, + { + displayName: 'Longitude', + name: 'longitude', + type: 'string', + default: '', + description: 'Longitude of the location of the address.', + }, + ], + }, + ], + }, +]; + +export const eventAdditionalFieldsOptions = [ + { + displayName: 'Browser URL', + name: 'browser_url', + type: 'string', + default: '', + description: 'URL to this event’s page on the Action Network or a third party.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'Description of the event. HTML supported.', + }, + { + displayName: 'End Date', + name: 'end_date', + type: 'dateTime', + default: '', + description: 'End date and time of the event.', + }, + { + displayName: 'Featured Image URL', + name: 'featured_image_url', + type: 'string', + default: '', + description: 'URL to this event’s featured image on the Action Network.', + }, + { + displayName: 'Instructions', + name: 'instructions', + type: 'string', + default: '', + description: 'Event\'s instructions for activists, visible after they RSVP. HTML supported.', + }, + { + displayName: 'Location', + name: 'location', + type: 'fixedCollection', + default: {}, + placeholder: 'Add Location Field', + typeOptions: { + multipleValues: false, + }, + options: [ + // different name, identical structure + { + displayName: 'Postal Addresses Fields', + name: 'postal_addresses_fields', + placeholder: 'Add Postal Address Field', + values: postalAddressesFields, + }, + ], + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'Internal (not public) title of the event.', + }, + { + displayName: 'Start Date', + name: 'start_date', + type: 'dateTime', + default: '', + description: 'Start date and time of the event.', + }, +]; + +export const personAdditionalFieldsOptions = [ + { + displayName: 'Family Name', + name: 'family_name', + type: 'string', + default: '', + description: 'Person’s last name.', + }, + { + displayName: 'Given Name', + name: 'given_name', + type: 'string', + default: '', + description: 'Person’s first name.', + }, + { + displayName: 'Language Spoken', + name: 'languages_spoken', + type: 'options', // Action Network accepts a `string[]` of language codes, but supports only one language per person - sending an array of 2+ languages will result in the first valid language being set as the preferred language for the person. Therefore, the user may select only one option in the n8n UI. + default: [], + description: 'Language spoken by the person', + options: languageOptions, + }, + { + displayName: 'Phone Number', // on create, only _one_ must be passed in + name: 'phone_numbers', + type: 'fixedCollection', + default: {}, + placeholder: 'Add Phone Numbers Field', + options: [ + { + displayName: 'Phone Numbers Fields', + name: 'phone_numbers_fields', + placeholder: 'Add Phone Number Field', + values: [ + { + displayName: 'Number', + name: 'number', + type: 'string', + default: '', + description: 'Person\'s mobile number, in international format without the plus sign.', + }, + { + displayName: 'Primary', + name: 'primary', + type: 'hidden', + default: true, + description: 'Whether this is the person\'s primary phone number.', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + default: 'subscribed', + description: 'Subscription status of this number.', + options: [ + { + name: 'Bouncing', + value: 'bouncing', + }, + { + name: 'Previous Bounce', + value: 'previous bounce', + }, + { + name: 'Subscribed', + value: 'subscribed', + }, + { + name: 'Unsubscribed', + value: 'unsubscribed', + }, + ], + }, + ], + }, + ], + }, + { + displayName: 'Postal Addresses', + name: 'postal_addresses', + type: 'fixedCollection', + default: {}, + placeholder: 'Add Postal Addresses Field', + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Postal Addresses Fields', + name: 'postal_addresses_fields', + placeholder: 'Add Postal Address Field', + values: postalAddressesFields, + }, + ], + }, +]; + +export const petitionAdditionalFieldsOptions = [ + { + displayName: 'Browser URL', + name: 'browser_url', + type: 'string', + default: '', + description: 'URL to this petition’s page on the Action Network or a third party.', + }, + { + displayName: 'Featured Image URL', + name: 'featured_image_url', + type: 'string', + default: '', + description: 'URL to this action’s featured image on the Action Network.', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'Internal (not public) title of the petition.', + }, + { + displayName: 'Petition Text', + name: 'petition_text', + type: 'string', + default: '', + description: 'Text of the letter to the petition’s target.', + }, + { + displayName: 'Targets', + name: 'target', + type: 'string', + default: '', + description: 'Comma-separated names of targets for this petition.', + }, +]; + +export const makeSimpleField = (resource: Resource, operation: Operation) => ({ + displayName: 'Simple', + name: 'simple', + type: 'boolean', + displayOptions: { + show: { + resource: [ + resource, + ], + operation: [ + operation, + ], + }, + }, + default: true, + description: 'Return a simplified version of the response instead of the raw data.', +}); diff --git a/packages/nodes-base/nodes/ActionNetwork/descriptions/SignatureDescription.ts b/packages/nodes-base/nodes/ActionNetwork/descriptions/SignatureDescription.ts new file mode 100644 index 0000000000..013e4ae200 --- /dev/null +++ b/packages/nodes-base/nodes/ActionNetwork/descriptions/SignatureDescription.ts @@ -0,0 +1,282 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + makeSimpleField, +} from './SharedFields'; + +export const signatureOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'signature', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Update', + value: 'update', + }, + ], + default: 'create', + description: 'Operation to perform', + }, +] as INodeProperties[]; + +export const signatureFields = [ + // ---------------------------------------- + // signature: create + // ---------------------------------------- + { + displayName: 'Petition ID', + name: 'petitionId', + description: 'ID of the petition to sign.', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'signature', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Person ID', + name: 'personId', + description: 'ID of the person whose signature to create.', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'signature', + ], + operation: [ + 'create', + ], + }, + }, + }, + makeSimpleField('signature', 'create'), + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'signature', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Comments', + name: 'comments', + type: 'string', + default: '', + description: 'Comments to leave when signing this petition.', + }, + ], + }, + + // ---------------------------------------- + // signature: get + // ---------------------------------------- + { + displayName: 'Petition ID', + name: 'petitionId', + description: 'ID of the petition whose signature to retrieve.', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'signature', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Signature ID', + name: 'signatureId', + description: 'ID of the signature to retrieve.', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'signature', + ], + operation: [ + 'get', + ], + }, + }, + }, + makeSimpleField('signature', 'get'), + + // ---------------------------------------- + // signature: getAll + // ---------------------------------------- + { + displayName: 'Petition ID', + name: 'petitionId', + description: 'ID of the petition whose signatures to retrieve.', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'signature', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'signature', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + resource: [ + 'signature', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + makeSimpleField('signature', 'getAll'), + + // ---------------------------------------- + // signature: update + // ---------------------------------------- + { + displayName: 'Petition ID', + name: 'petitionId', + description: 'ID of the petition whose signature to update.', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'signature', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Signature ID', + name: 'signatureId', + description: 'ID of the signature to update.', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'signature', + ], + operation: [ + 'update', + ], + }, + }, + }, + makeSimpleField('signature', 'update'), + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'signature', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Comments', + name: 'comments', + type: 'string', + default: '', + description: 'Comments to leave when signing this petition.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/ActionNetwork/descriptions/TagDescription.ts b/packages/nodes-base/nodes/ActionNetwork/descriptions/TagDescription.ts new file mode 100644 index 0000000000..521c2df08b --- /dev/null +++ b/packages/nodes-base/nodes/ActionNetwork/descriptions/TagDescription.ts @@ -0,0 +1,131 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + makeSimpleField, +} from './SharedFields'; + +export const tagOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'tag', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + ], + default: 'create', + description: 'Operation to perform', + }, +] as INodeProperties[]; + +export const tagFields = [ + // ---------------------------------------- + // tag: create + // ---------------------------------------- + { + displayName: 'Name', + name: 'name', + description: 'Name of the tag to create.', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'tag', + ], + operation: [ + 'create', + ], + }, + }, + }, + makeSimpleField('tag', 'create'), + + // ---------------------------------------- + // tag: get + // ---------------------------------------- + { + displayName: 'Tag ID', + name: 'tagId', + description: 'ID of the tag to retrieve.', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'tag', + ], + operation: [ + 'get', + ], + }, + }, + }, + makeSimpleField('tag', 'get'), + + // ---------------------------------------- + // tag: getAll + // ---------------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'tag', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + resource: [ + 'tag', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + makeSimpleField('tag', 'getAll'), +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/ActionNetwork/descriptions/index.ts b/packages/nodes-base/nodes/ActionNetwork/descriptions/index.ts new file mode 100644 index 0000000000..98f5c74ad2 --- /dev/null +++ b/packages/nodes-base/nodes/ActionNetwork/descriptions/index.ts @@ -0,0 +1,7 @@ +export * from './AttendanceDescription'; +export * from './EventDescription'; +export * from './PersonDescription'; +export * from './PersonTagDescription'; +export * from './PetitionDescription'; +export * from './SignatureDescription'; +export * from './TagDescription'; diff --git a/packages/nodes-base/nodes/ActionNetwork/types.d.ts b/packages/nodes-base/nodes/ActionNetwork/types.d.ts new file mode 100644 index 0000000000..b2ce726aa7 --- /dev/null +++ b/packages/nodes-base/nodes/ActionNetwork/types.d.ts @@ -0,0 +1,99 @@ +import { languageOptions } from './descriptions/SharedFields'; + +export type Resource = 'attendance' | 'event' | 'person' | 'personTag' | 'petition' | 'signature' | 'tag'; + +export type Operation = 'create' | 'delete' | 'get' | 'getAll' | 'update' | 'add' | 'remove'; + +export type LanguageCodes = typeof languageOptions[number]['value'] + + +// ---------------------------------------- +// UI fields +// ---------------------------------------- + +export type AllFieldsUi = { + email_addresses: EmailAddressUi; + postal_addresses: PostalAddressesUi; + phone_numbers: PhoneNumberUi; + languages_spoken: LanguageCodes; + target: string; + location: LocationUi; +} + +export type EmailAddressUi = { + email_addresses_fields: EmailAddressField, +} + +export type EmailAddressField = { + primary: boolean; + address: string; + status: EmailStatus; +} + +type BaseStatus = 'subscribed' | 'unsubscribed' | 'bouncing' | 'previous bounce'; + +type EmailStatus = BaseStatus | 'spam complaint' | 'previous spam complaint'; + +type PhoneNumberUi = { + phone_numbers_fields: PhoneNumberField[], +} + +export type PhoneNumberField = { + primary: boolean; + number: string; + status: BaseStatus; +}; + +type PostalAddressesUi = { + postal_addresses_fields: PostalAddressField[], +} + +type LocationUi = { + postal_addresses_fields: PostalAddressField, +} + +export type PostalAddressField = { + primary: boolean; + address_lines: string; + locality: string; + region: string; + postal_code: string; + country: string; + language: LanguageCodes; + location: { location_fields: LatitudeLongitude } +} + +type LatitudeLongitude = { + latitude: string; + longitude: string; +} + +export type FieldWithPrimaryField = EmailAddressField | PhoneNumberField | PostalAddressField; + + +// ---------------------------------------- +// responses +// ---------------------------------------- + +export type LinksFieldContainer = { _links: { self: { href: string } } }; + +export type Response = JsonObject & LinksFieldContainer; + +export type PersonResponse = Response & { + identifiers: string[]; + email_addresses: EmailAddressField[]; + phone_numbers: PhoneNumberField[]; + postal_addresses: PostalAddressField[]; + languages_spoken: LanguageCodes[]; +}; + +export type PetitionResponse = Response & { _embedded: { 'osdi:creator': PersonResponse } }; + + +// ---------------------------------------- +// utils +// ---------------------------------------- + +export type JsonValue = string | number | boolean | null | JsonObject | JsonValue[]; + +export type JsonObject = { [key: string]: JsonValue }; diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 5b1fbf7fe7..a695b58406 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -27,6 +27,7 @@ ], "n8n": { "credentials": [ + "dist/credentials/ActionNetworkApi.credentials.js", "dist/credentials/ActiveCampaignApi.credentials.js", "dist/credentials/AgileCrmApi.credentials.js", "dist/credentials/AcuitySchedulingApi.credentials.js", @@ -282,6 +283,7 @@ "dist/credentials/ZulipApi.credentials.js" ], "nodes": [ + "dist/nodes/ActionNetwork/ActionNetwork.node.js", "dist/nodes/ActiveCampaign/ActiveCampaign.node.js", "dist/nodes/ActiveCampaign/ActiveCampaignTrigger.node.js", "dist/nodes/AgileCrm/AgileCrm.node.js",