From f5f1a702a721029109c366ed7912b625db23f17b Mon Sep 17 00:00:00 2001 From: Jorge Cortes Date: Wed, 23 Apr 2025 14:38:43 -0500 Subject: [PATCH] [Components] attio - added new components --- .../attio/actions/create-note/create-note.mjs | 31 +- .../actions/create-person/create-person.mjs | 199 ++++++++++++ .../attio/actions/create-task/create-task.mjs | 157 +++++++++ .../create-update-record.mjs | 42 ++- .../delete-list-entry/delete-list-entry.mjs | 18 +- .../actions/update-person/update-person.mjs | 219 +++++++++++++ components/attio/attio.app.mjs | 300 ++++++++++++------ components/attio/common/constants.mjs | 21 ++ components/attio/common/utils.mjs | 109 +++++-- components/attio/package.json | 2 +- components/attio/sources/common/base.mjs | 32 +- .../list-entry-deleted-instant.mjs | 28 +- .../list-entry-updated-instant.mjs | 28 +- .../new-activity-created-instant.mjs | 39 +++ .../test-event.mjs | 16 + .../new-list-entry-instant.mjs | 28 +- .../new-note-instant/new-note-instant.mjs | 11 +- .../new-object-attribute-instant.mjs | 11 +- .../new-record-created-instant.mjs | 28 +- .../note-updated-instant.mjs | 16 +- .../object-attribute-updated-instant.mjs | 11 +- .../record-updated-instant.mjs | 28 +- pnpm-lock.yaml | 2 - 23 files changed, 1139 insertions(+), 237 deletions(-) create mode 100644 components/attio/actions/create-person/create-person.mjs create mode 100644 components/attio/actions/create-task/create-task.mjs create mode 100644 components/attio/actions/update-person/update-person.mjs create mode 100644 components/attio/common/constants.mjs create mode 100644 components/attio/sources/new-activity-created-instant/new-activity-created-instant.mjs create mode 100644 components/attio/sources/new-activity-created-instant/test-event.mjs diff --git a/components/attio/actions/create-note/create-note.mjs b/components/attio/actions/create-note/create-note.mjs index 923c548133fde..3c4134a8b5766 100644 --- a/components/attio/actions/create-note/create-note.mjs +++ b/components/attio/actions/create-note/create-note.mjs @@ -4,7 +4,7 @@ export default { key: "attio-create-note", name: "Create Note", description: "Creates a new note for a given record. The note will be linked to the specified record. [See the documentation](https://developers.attio.com/reference/post_v2-notes)", - version: "0.0.2", + version: "0.0.3", type: "action", props: { attio, @@ -12,16 +12,25 @@ export default { propDefinition: [ attio, "objectId", + () => ({ + mapper: ({ + api_slug: value, + singular_noun: label, + }) => ({ + value, + label, + }), + }), ], - label: "Parent Object ID", - description: "The ID of the parent object the note belongs to", + label: "Parent Object", + description: "The parent object the note belongs to", }, parentRecordId: { propDefinition: [ attio, "recordId", - (c) => ({ - objectId: c.parentObject, + ({ parentObject }) => ({ + targetObject: parentObject, }), ], label: "Parent Record ID", @@ -38,8 +47,16 @@ export default { description: "The content of the note", }, }, + methods: { + createNote(args = {}) { + return this.attio.post({ + path: "/notes", + ...args, + }); + }, + }, async run({ $ }) { - const response = await this.attio.createNote({ + const response = await this.createNote({ $, data: { data: { @@ -51,7 +68,7 @@ export default { }, }, }); - $.export("$summary", `Successfully created note with ID: ${response.data.id.note_id}`); + $.export("$summary", `Successfully created note with ID \`${response.data.id.note_id}\`.`); return response; }, }; diff --git a/components/attio/actions/create-person/create-person.mjs b/components/attio/actions/create-person/create-person.mjs new file mode 100644 index 0000000000000..587820c637f86 --- /dev/null +++ b/components/attio/actions/create-person/create-person.mjs @@ -0,0 +1,199 @@ +import attio from "../../attio.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "attio-create-person", + name: "Create Person", + description: "Creates a new person. [See the documentation](https://developers.attio.com/reference/post_v2-objects-people-records).", + version: "0.0.1", + type: "action", + props: { + attio, + firstName: { + propDefinition: [ + attio, + "firstName", + ], + }, + lastName: { + propDefinition: [ + attio, + "lastName", + ], + }, + emailAddress: { + propDefinition: [ + attio, + "emailAddress", + ], + }, + description: { + propDefinition: [ + attio, + "description", + ], + }, + jobTitle: { + propDefinition: [ + attio, + "jobTitle", + ], + }, + phoneNumber: { + propDefinition: [ + attio, + "phoneNumber", + ], + }, + phoneNumberCountryCode: { + propDefinition: [ + attio, + "phoneNumberCountryCode", + ], + }, + linkedin: { + propDefinition: [ + attio, + "linkedin", + ], + }, + twitter: { + propDefinition: [ + attio, + "twitter", + ], + }, + facebook: { + propDefinition: [ + attio, + "facebook", + ], + }, + instagram: { + propDefinition: [ + attio, + "instagram", + ], + }, + companyId: { + label: "Company ID", + description: "The ID of the company to associate with the person.", + optional: true, + propDefinition: [ + attio, + "recordId", + () => ({ + targetObject: constants.TARGET_OBJECT.COMPANIES, + }), + ], + }, + }, + async run({ $ }) { + const { + attio, + firstName, + lastName, + emailAddress, + description, + jobTitle, + phoneNumber, + phoneNumberCountryCode, + linkedin, + twitter, + facebook, + instagram, + companyId, + } = this; + + const response = await attio.createRecord({ + $, + targetObject: constants.TARGET_OBJECT.PEOPLE, + data: { + data: { + values: { + ...(emailAddress && { + email_addresses: [ + { + email_address: emailAddress, + }, + ], + }), + ...(firstName || lastName) && { + name: [ + { + first_name: firstName || "", + last_name: lastName || "", + full_name: `${firstName || ""} ${lastName || ""}`.trim(), + }, + ], + }, + ...(description && { + description: [ + { + value: description, + }, + ], + }), + ...(jobTitle && { + job_title: [ + { + value: jobTitle, + }, + ], + }), + ...(phoneNumber && { + phone_numbers: [ + { + original_phone_number: phoneNumber, + ...(phoneNumberCountryCode && { + country_code: phoneNumberCountryCode, + }), + }, + ], + }), + ...(linkedin && { + linkedin: [ + { + value: linkedin, + }, + ], + }), + ...(twitter && { + twitter: [ + { + value: twitter, + }, + ], + }), + ...(facebook && { + facebook: [ + { + value: facebook, + }, + ], + }), + ...(instagram && { + instagram: [ + { + value: instagram, + }, + ], + }), + ...(companyId && { + company: [ + { + target_object: constants.TARGET_OBJECT.COMPANIES, + target_record_id: companyId, + }, + ], + }), + }, + }, + }, + }); + + $.export("$summary", `Successfully created person with ID \`${response.data.id.record_id}\`.`); + + return response; + }, +}; diff --git a/components/attio/actions/create-task/create-task.mjs b/components/attio/actions/create-task/create-task.mjs new file mode 100644 index 0000000000000..7dab9b23f8357 --- /dev/null +++ b/components/attio/actions/create-task/create-task.mjs @@ -0,0 +1,157 @@ +import attio from "../../attio.app.mjs"; +import constants from "../../common/constants.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "attio-create-task", + name: "Create Task", + description: "Creates a new task. [See the documentation](https://docs.attio.com/rest-api/endpoint-reference/tasks/create-a-task)", + version: "0.0.1", + type: "action", + props: { + attio, + content: { + type: "string", + label: "Content", + description: "The text content of the task", + }, + deadlineAt: { + type: "string", + label: "Deadline", + description: "The deadline of the task in ISO 8601 format (e.g. `2025-04-22T10:00:00Z`)", + }, + isCompleted: { + type: "boolean", + label: "Is Completed", + description: "Whether the task has been completed", + }, + assigneeIds: { + type: "string[]", + label: "Assignees", + description: "The id of the members to assign the task to", + propDefinition: [ + attio, + "workspaceMemberId", + ], + }, + numberOfLinkedRecords: { + type: "integer", + label: "Number Of Linked Records", + description: "The number of linked records to generate. Defaults to 1.", + default: 1, + reloadProps: true, + }, + }, + methods: { + linkedRecordPropsMapper(prefix) { + const { + [`${prefix}targetObject`]: targetObject, + [`${prefix}targetRecordId`]: targetRecordId, + } = this; + + return { + target_object: targetObject, + target_record_id: targetRecordId, + }; + }, + async getLinkedRecordsPropDefinitions({ + prefix, label, + } = {}) { + const targetObjectPropName = `${prefix}targetObject`; + const targetObject = this[targetObjectPropName]; + let targetRecordOptions = []; + + if (targetObject) { + const { data } = await this.attio.listRecords({ + targetObject, + data: { + limit: 100, + sorts: [ + { + direction: "desc", + attribute: "created_at", + field: "value", + }, + ], + }, + }); + + targetRecordOptions = data.map(({ + id: { record_id: value }, + values: { name }, + }) => ({ + value, + label: name[0]?.value || name[0]?.full_name || "unknown", + })); + } + + return { + [targetObjectPropName]: { + type: "string", + label: `${label} - Target Object`, + description: "The type of the record to link to. Eg. `companies`, `contacts`, `deals`", + options: Object.values(constants.TARGET_OBJECT), + reloadProps: true, + }, + [`${prefix}targetRecordId`]: { + type: "string", + label: `${label} - Target Record ID`, + description: "The ID of the record to link to", + options: targetRecordOptions, + }, + }; + }, + createTask(args = {}) { + return this.attio.post({ + path: "/tasks", + ...args, + }); + }, + }, + async additionalProps() { + const { + numberOfLinkedRecords, + getLinkedRecordsPropDefinitions, + } = this; + + return utils.getAdditionalProps({ + numberOfFields: numberOfLinkedRecords, + fieldName: "linked record", + getPropDefinitions: getLinkedRecordsPropDefinitions, + }); + }, + async run({ $ }) { + const { + content, + deadlineAt, + isCompleted, + assigneeIds, + numberOfLinkedRecords, + linkedRecordPropsMapper, + } = this; + + const response = await this.createTask({ + $, + data: { + data: { + format: "plaintext", + content, + deadline_at: deadlineAt, + is_completed: isCompleted, + assignees: assigneeIds?.map((id) => ({ + referenced_actor_type: "workspace-member", + referenced_actor_id: id, + })) || [], + linked_records: utils.getFieldsProps({ + numberOfFields: numberOfLinkedRecords, + fieldName: "linked record", + propsMapper: linkedRecordPropsMapper, + }), + }, + }, + }); + + $.export("$summary", `Successfully created task with ID \`${response.data.id.task_id}\`.`); + return response; + }, +}; diff --git a/components/attio/actions/create-update-record/create-update-record.mjs b/components/attio/actions/create-update-record/create-update-record.mjs index e0abe3c586fd1..5bc915548c902 100644 --- a/components/attio/actions/create-update-record/create-update-record.mjs +++ b/components/attio/actions/create-update-record/create-update-record.mjs @@ -1,44 +1,58 @@ import attio from "../../attio.app.mjs"; +import constants from "../../common/constants.mjs"; import utils from "../../common/utils.mjs"; export default { key: "attio-create-update-record", name: "Create or Update Record", description: "Creates or updates a specific record such as a person or a deal. If the record already exists, it's updated. Otherwise, a new record is created. [See the documentation](https://developers.attio.com/reference/put_v2-objects-object-records)", - version: "0.0.2", + version: "0.0.3", type: "action", props: { attio, objectId: { + reloadProps: true, propDefinition: [ attio, "objectId", + () => ({ + filter: (o) => o.api_slug !== constants.TARGET_OBJECT.DEALS, + }), ], }, - attributeId: { + matchingAttribute: { + reloadProps: true, propDefinition: [ attio, - "attributeId", + "matchingAttribute", (c) => ({ objectId: c.objectId, }), ], - reloadProps: true, }, }, async additionalProps() { const props = {}; - if (!this.attributeId) { + if (!this.matchingAttribute) { return props; } const attributes = await this.getRelevantAttributes(); + + const matchingAttribute = attributes.find( + (a) => a.api_slug === this.matchingAttribute, + ); + if (matchingAttribute) { + attributes.splice(attributes.indexOf(matchingAttribute), 1); + attributes.unshift(matchingAttribute); + } + for (const attribute of attributes) { - props[attribute.id.attribute_id] = { + props[attribute.api_slug] = { type: attribute.is_multiselect ? "string[]" : "string", label: attribute.title, - optional: attribute.id.attribute_id !== this.attributeId && !attribute.is_required, + optional: !attribute.is_required, }; } return props; @@ -52,29 +66,29 @@ export default { }, }); const attributes = await utils.streamIterator(stream); - return attributes.filter((a) => a.is_writable || a.id.attribute_id === this.attributeId); + + return attributes + .filter((a) => a.is_writable) + .sort((a, b) => b.is_required - a.is_required); }, }, async run({ $ }) { const { attio, - getRelevantAttributes, objectId, - attributeId, + matchingAttribute, ...values } = this; - const attributes = await getRelevantAttributes(); - const response = await attio.upsertRecord({ $, objectId, params: { - matching_attribute: attributeId, + matching_attribute: matchingAttribute, }, data: { data: { - values: utils.parseValues(attributes, values), + values, }, }, }); diff --git a/components/attio/actions/delete-list-entry/delete-list-entry.mjs b/components/attio/actions/delete-list-entry/delete-list-entry.mjs index 3b98149d16d05..9b675fd990876 100644 --- a/components/attio/actions/delete-list-entry/delete-list-entry.mjs +++ b/components/attio/actions/delete-list-entry/delete-list-entry.mjs @@ -4,7 +4,7 @@ export default { key: "attio-delete-list-entry", name: "Delete List Entry", description: "Deletes an existing entry from a specific list. [See the documentation](https://developers.attio.com/reference/delete_v2-lists-list-entries-entry-id)", - version: "0.0.2", + version: "0.0.3", type: "action", props: { attio, @@ -18,14 +18,24 @@ export default { propDefinition: [ attio, "entryId", - (c) => ({ - listId: c.listId, + ({ listId }) => ({ + listId, }), ], }, }, + methods: { + deleteListEntry({ + listId, entryId, ...opts + }) { + return this.attio.delete({ + path: `/lists/${listId}/entries/${entryId}`, + ...opts, + }); + }, + }, async run({ $ }) { - const response = await this.attio.deleteListEntry({ + const response = await this.deleteListEntry({ $, listId: this.listId, entryId: this.entryId, diff --git a/components/attio/actions/update-person/update-person.mjs b/components/attio/actions/update-person/update-person.mjs new file mode 100644 index 0000000000000..bf0e40bc311bc --- /dev/null +++ b/components/attio/actions/update-person/update-person.mjs @@ -0,0 +1,219 @@ +import attio from "../../attio.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "attio-update-person", + name: "Update Person", + description: "Update an existing person. [See the documentation](https://developers.attio.com/reference/patch_v2-objects-people-records-record-id).", + version: "0.0.1", + type: "action", + props: { + attio, + recordId: { + label: "Person ID", + description: "The identifier of the contact to update.", + propDefinition: [ + attio, + "recordId", + () => ({ + targetObject: constants.TARGET_OBJECT.PEOPLE, + mapper: ({ + id: { record_id: value }, + values: { name }, + }) => ({ + value, + label: name[0]?.full_name, + }), + }), + ], + }, + firstName: { + optional: true, + propDefinition: [ + attio, + "firstName", + ], + }, + lastName: { + optional: true, + propDefinition: [ + attio, + "lastName", + ], + }, + emailAddress: { + propDefinition: [ + attio, + "emailAddress", + ], + }, + description: { + propDefinition: [ + attio, + "description", + ], + }, + jobTitle: { + propDefinition: [ + attio, + "jobTitle", + ], + }, + phoneNumber: { + propDefinition: [ + attio, + "phoneNumber", + ], + }, + phoneNumberCountryCode: { + propDefinition: [ + attio, + "phoneNumberCountryCode", + ], + }, + linkedin: { + propDefinition: [ + attio, + "linkedin", + ], + }, + twitter: { + propDefinition: [ + attio, + "twitter", + ], + }, + facebook: { + propDefinition: [ + attio, + "facebook", + ], + }, + instagram: { + propDefinition: [ + attio, + "instagram", + ], + }, + companyId: { + label: "Company ID", + description: "The ID of the company to associate with the person.", + optional: true, + propDefinition: [ + attio, + "recordId", + () => ({ + targetObject: constants.TARGET_OBJECT.COMPANIES, + }), + ], + }, + }, + async run({ $ }) { + const { + attio, + recordId, + firstName, + lastName, + emailAddress, + description, + jobTitle, + phoneNumber, + phoneNumberCountryCode, + linkedin, + twitter, + facebook, + instagram, + companyId, + } = this; + + const response = await attio.updateRecord({ + recordId, + targetObject: constants.TARGET_OBJECT.PEOPLE, + data: { + data: { + values: { + ...(emailAddress && { + email_addresses: [ + { + email_address: emailAddress, + }, + ], + }), + ...(firstName || lastName) && { + name: [ + { + first_name: firstName || "", + last_name: lastName || "", + full_name: `${firstName || ""} ${lastName || ""}`.trim(), + }, + ], + }, + ...(description && { + description: [ + { + value: description, + }, + ], + }), + ...(jobTitle && { + job_title: [ + { + value: jobTitle, + }, + ], + }), + ...(phoneNumber && { + phone_numbers: [ + { + original_phone_number: phoneNumber, + ...(phoneNumberCountryCode && { + country_code: phoneNumberCountryCode, + }), + }, + ], + }), + ...(linkedin && { + linkedin: [ + { + value: linkedin, + }, + ], + }), + ...(twitter && { + twitter: [ + { + value: twitter, + }, + ], + }), + ...(facebook && { + facebook: [ + { + value: facebook, + }, + ], + }), + ...(instagram && { + instagram: [ + { + value: instagram, + }, + ], + }), + ...(companyId && { + company: [ + { + target_object: constants.TARGET_OBJECT.COMPANIES, + target_record_id: companyId, + }, + ], + }), + }, + }, + }, + }); + + $.export("$summary", `Successfully updated person with ID \`${response.data.id.record_id}\`.`); + return response; + }, +}; diff --git a/components/attio/attio.app.mjs b/components/attio/attio.app.mjs index 27f2fb1027f24..686f78700e902 100644 --- a/components/attio/attio.app.mjs +++ b/components/attio/attio.app.mjs @@ -1,10 +1,125 @@ import { axios } from "@pipedream/platform"; -const DEFAULT_LIMIT = 20; +import constants from "./common/constants.mjs"; export default { type: "app", app: "attio", propDefinitions: { + firstName: { + type: "string", + label: "First Name", + description: "The person's first name.", + }, + lastName: { + type: "string", + label: "Last Name", + description: "The person's last name.", + }, + emailAddress: { + type: "string", + label: "Email", + description: "The contact's email address.", + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "A description of the person.", + optional: true, + }, + jobTitle: { + type: "string", + label: "Job Title", + description: "The person's job title.", + optional: true, + }, + phoneNumber: { + type: "string", + label: "Phone Number", + description: "The person's phone number which is either a) prefixed with a country code (e.g. `+44....`) or b) a local number, where **Phone Number - Country Code** is specified in addition.", + optional: true, + }, + phoneNumberCountryCode: { + type: "string", + label: "Phone Number - Country Code", + description: "The country code for the phone number.", + optional: true, + }, + linkedin: { + type: "string", + label: "LinkedIn", + description: "The person's LinkedIn profile URL.", + optional: true, + }, + twitter: { + type: "string", + label: "Twitter", + description: "The person's Twitter handle.", + optional: true, + }, + facebook: { + type: "string", + label: "Facebook", + description: "The person's Facebook profile URL.", + optional: true, + }, + instagram: { + type: "string", + label: "Instagram", + description: "The person's Instagram profile URL.", + optional: true, + }, + recordId: { + type: "string", + label: "Record ID", + description: "Identifier of a record", + async options({ + page, + targetObject = constants.TARGET_OBJECT.COMPANIES, + sorts = [ + { + direction: "desc", + attribute: "created_at", + field: "value", + }, + ], + mapper = ({ + id: { record_id: value }, + values: { name }, + }) => ({ + value, + label: name[0]?.value || name[0]?.full_name, + }), + }) { + const { data } = await this.listRecords({ + targetObject, + data: { + limit: constants.DEFAULT_LIMIT, + offset: page * constants.DEFAULT_LIMIT, + sorts, + }, + }); + return data?.map(mapper); + }, + }, + workspaceMemberId: { + type: "string", + label: "Workspace Member ID", + description: "The identifier of a workspace member", + async options({ + mapper = ({ + id: { workspace_member_id: value }, + first_name: firstName, + last_name: lastName, + }) => ({ + value, + label: `${firstName || ""} ${lastName || ""}`.trim(), + }), + }) { + const { data } = await this.listWorkspaceMembers(); + return data?.map(mapper) || []; + }, + }, listId: { type: "string", label: "List ID", @@ -12,9 +127,9 @@ export default { async options() { const { data } = await this.listLists(); return data?.map(({ - id, name: label, + id: { list_id: value }, name: label, }) => ({ - value: id.list_id, + value, label, })) || []; }, @@ -29,55 +144,33 @@ export default { const { data } = await this.listEntries({ listId, params: { - limit: DEFAULT_LIMIT, - offset: page * DEFAULT_LIMIT, + limit: constants.DEFAULT_LIMIT, + offset: page * constants.DEFAULT_LIMIT, }, }); - return data?.map(({ id }) => id.entry_id) || []; + return data?.map(({ id: { entry_id: value } }) => value) || []; }, }, objectId: { type: "string", label: "Object ID", description: "The identifier of an object", - async options() { - const { data } = await this.listObjects(); - return data?.map(({ - id, singular_noun: label, + async options({ + filter = () => true, + mapper = ({ + id: { object_id: value }, singular_noun: label, }) => ({ - value: id.object_id, + value, label, - })) || []; - }, - }, - recordId: { - type: "string", - label: "Record ID", - description: "Identifier of a record", - async options({ - objectId, page, + }), }) { - const { data } = await this.listRecords({ - objectId, - params: { - limit: DEFAULT_LIMIT, - offset: page * DEFAULT_LIMIT, - }, - }); - return data?.map(({ - id, values, - }) => ({ - value: id.record_id, - label: (values?.name?.length && (values.name[0].value || values.name[0].full_name)) - ?? (values?.domains?.length && values.domains[0].domain) - ?? (values?.email_addresses?.length && values.email_addresses[0].email_address) - ?? values?.id?.record_id, - })) || []; + const { data } = await this.listObjects(); + return data?.filter(filter)?.map(mapper) || []; }, }, - attributeId: { + matchingAttribute: { type: "string", - label: "Attribute ID", + label: "Matching Attribute", description: "The ID or slug of the attribute to use to check if a record already exists. The attribute must be unique.", async options({ objectId, page, @@ -85,115 +178,128 @@ export default { const { data } = await this.listAttributes({ objectId, params: { - limit: DEFAULT_LIMIT, - offset: page * DEFAULT_LIMIT, + limit: constants.DEFAULT_LIMIT, + offset: page * constants.DEFAULT_LIMIT, }, }); return data - ?.filter((attribute) => attribute.is_unique) + ?.filter((attribute) => attribute.is_unique && attribute.api_slug !== "record_id") ?.map(({ - id, title: label, + api_slug: value, title: label, }) => ({ - value: id.attribute_id, + value, label, })) || []; }, }, }, methods: { - _baseUrl() { - return "https://api.attio.com/v2"; + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + }; }, _makeRequest({ - $ = this, - path, - ...opts + $ = this, path, headers, ...args }) { return axios($, { - url: `${this._baseUrl()}${path}`, - headers: { - Authorization: `Bearer ${this.$auth.oauth_access_token}`, - }, - ...opts, + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), }); }, - createWebhook(opts = {}) { + post(args = {}) { return this._makeRequest({ method: "POST", - path: "/webhooks", - ...opts, + ...args, }); }, - deleteWebhook({ - hookId, ...opts - }) { + patch(args = {}) { return this._makeRequest({ - method: "DELETE", - path: `/webhooks/${hookId}`, - ...opts, + method: "PATCH", + ...args, }); }, - listLists(opts = {}) { + put(args = {}) { return this._makeRequest({ - path: "/lists", - ...opts, + method: "PUT", + ...args, }); }, - listEntries({ - listId, ...opts - }) { + delete(args = {}) { return this._makeRequest({ - method: "POST", - path: `/lists/${listId}/entries/query`, - ...opts, + method: "DELETE", + ...args, }); }, - listObjects(opts = {}) { + listWorkspaceMembers(args = {}) { return this._makeRequest({ - path: "/objects", - ...opts, + path: "/workspace_members", + ...args, }); }, listRecords({ + targetObject, ...args + } = {}) { + return this.post({ + path: `/objects/${targetObject}/records/query`, + ...args, + }); + }, + createRecord({ + targetObject, ...args + } = {}) { + return this.post({ + path: `/objects/${targetObject}/records`, + ...args, + }); + }, + updateRecord({ + targetObject, recordId, ...args + } = {}) { + return this.patch({ + path: `/objects/${targetObject}/records/${recordId}`, + ...args, + }); + }, + upsertRecord({ objectId, ...opts }) { - return this._makeRequest({ - method: "POST", - path: `/objects/${objectId}/records/query`, + return this.put({ + path: `/objects/${objectId}/records`, ...opts, }); }, listAttributes({ - objectId, ...opts - }) { + objectId, ...args + } = {}) { return this._makeRequest({ path: `/objects/${objectId}/attributes`, - ...opts, + ...args, }); }, - createNote(opts = {}) { + listLists(args = {}) { return this._makeRequest({ - method: "POST", - path: "/notes", - ...opts, + path: "/lists", + ...args, }); }, - upsertRecord({ - objectId, ...opts - }) { - return this._makeRequest({ - method: "PUT", - path: `/objects/${objectId}/records`, - ...opts, + listEntries({ + listId, ...args + } = {}) { + return this.post({ + path: `/lists/${listId}/entries/query`, + ...args, }); }, - deleteListEntry({ - listId, entryId, ...opts - }) { + listObjects(args = {}) { return this._makeRequest({ - method: "DELETE", - path: `/lists/${listId}/entries/${entryId}`, - ...opts, + path: "/objects", + ...args, }); }, }, diff --git a/components/attio/common/constants.mjs b/components/attio/common/constants.mjs new file mode 100644 index 0000000000000..6907f9310bc37 --- /dev/null +++ b/components/attio/common/constants.mjs @@ -0,0 +1,21 @@ +const BASE_URL = "https://api.attio.com"; +const VERSION_PATH = "/v2"; +const DEFAULT_LIMIT = 100; + +const TARGET_OBJECT = { + PEOPLE: "people", + COMPANIES: "companies", + USERS: "users", + DEALS: "deals", + WORKSPACES: "workspaces", +}; + +const SEP = "_"; + +export default { + BASE_URL, + VERSION_PATH, + DEFAULT_LIMIT, + TARGET_OBJECT, + SEP, +}; diff --git a/components/attio/common/utils.mjs b/components/attio/common/utils.mjs index 240f1978403cd..dcc4911de8f6a 100644 --- a/components/attio/common/utils.mjs +++ b/components/attio/common/utils.mjs @@ -1,3 +1,5 @@ +import constants from "./constants.mjs"; + async function streamIterator(stream) { let resources = []; for await (const resource of stream) { @@ -33,30 +35,97 @@ async function *paginate({ } while (total === args.params.limit); } -function parseValues(attributes, values) { - for (const [ - key, - value, - ] of Object.entries(values)) { +function toPascalCase(str) { + return str.replace(/(\w)(\w*)/g, (_, group1, group2) => + group1.toUpperCase() + group2.toLowerCase()); +} + +function getMetadataProp({ + index, fieldName, prefix, label, +} = {}) { + const fieldIdx = index + 1; + const key = `${fieldName}${fieldIdx}`; + return { + prefix: prefix + ? `${prefix}${key}${constants.SEP}` + : `${key}${constants.SEP}`, + label: label + ? `${label} - ${toPascalCase(fieldName)} ${fieldIdx}` + : `${toPascalCase(fieldName)} ${fieldIdx}`, + }; +} + +function getFieldProps({ + index, fieldName, prefix, + propsMapper = function propsMapper(prefix) { + const { [`${prefix}name`]: name } = this; + return { + name, + }; + }, +} = {}) { + const { prefix: metaPrefix } = getMetadataProp({ + index, + fieldName, + prefix, + }); + return propsMapper(metaPrefix); +} + +function getFieldsProps({ + numberOfFields, fieldName, propsMapper, prefix, +} = {}) { + return Array.from({ + length: numberOfFields, + }).map((_, index) => getFieldProps({ + index, + fieldName, + prefix, + propsMapper, + })); +} + +async function getAdditionalProps({ + numberOfFields, fieldName, prefix, label, + getPropDefinitions = async ({ + prefix, label, + }) => ({ + [`${prefix}name`]: { + type: "string", + label, + description: "The name of the field.", + optional: true, + }, + }), +} = {}) { + return Array.from({ + length: numberOfFields, + }).reduce(async (reduction, _, index) => { const { - type, is_multiselect: isMultiselect, - } = attributes.find(({ id }) => id.attribute_id === key); - if (type === "checkbox") { - values[key] = isMultiselect - ? value.map((v) => !(v === "false" || v === "0")) - : !(value === "false" || value === "0"); - } - if (type === "number" || type === "rating") { - values[key] = isMultiselect - ? value.map((v) => +v) - : +value; - } - } - return values; + prefix: metaPrefix, + label: metaLabel, + } = getMetadataProp({ + index, + fieldName, + prefix, + label, + }); + + return { + ...await reduction, + ...await getPropDefinitions({ + prefix: metaPrefix, + label: metaLabel, + }), + }; + }, {}); } export default { streamIterator, paginate, - parseValues, + getAdditionalProps, + getFieldProps, + getFieldsProps, + getMetadataProp, }; diff --git a/components/attio/package.json b/components/attio/package.json index e74624a9a7544..b2bd785c60b4b 100644 --- a/components/attio/package.json +++ b/components/attio/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/attio", - "version": "0.2.0", + "version": "0.3.0", "description": "Pipedream Attio Components", "main": "attio.app.mjs", "keywords": [ diff --git a/components/attio/sources/common/base.mjs b/components/attio/sources/common/base.mjs index 967d7b8428bce..b223f7ea12a01 100644 --- a/components/attio/sources/common/base.mjs +++ b/components/attio/sources/common/base.mjs @@ -8,16 +8,11 @@ export default { }, hooks: { async activate() { - const { data: { id: { webhook_id: hookId } } } = await this.attio.createWebhook({ + const { data: { id: { webhook_id: hookId } } } = await this.createWebhook({ data: { data: { target_url: this.http.endpoint, - subscriptions: [ - { - event_type: this.getEventType(), - filter: this.getFilter(), - }, - ], + subscriptions: this.getSubscriptions(), }, }, }); @@ -26,7 +21,7 @@ export default { async deactivate() { const hookId = this._getHookId(); if (hookId) { - await this.attio.deleteWebhook({ + await this.deleteWebhook({ hookId, }); } @@ -39,15 +34,26 @@ export default { _setHookId(hookId) { this.db.set("hookId", hookId); }, - getFilter() { - return null; - }, - getEventType() { - throw new Error("getEventType is not implemented"); + getSubscriptions() { + throw new Error("getSubscriptions is not implemented"); }, generateMeta() { throw new Error("generateMeta is not implemented"); }, + createWebhook(args = {}) { + return this.attio.post({ + path: "/webhooks", + ...args, + }); + }, + deleteWebhook({ + hookId, ...opts + }) { + return this.attio.delete({ + path: `/webhooks/${hookId}`, + ...opts, + }); + }, }, async run(event) { const { body: { events } } = event; diff --git a/components/attio/sources/list-entry-deleted-instant/list-entry-deleted-instant.mjs b/components/attio/sources/list-entry-deleted-instant/list-entry-deleted-instant.mjs index 121a90a04d387..0bf0ead7dd03b 100644 --- a/components/attio/sources/list-entry-deleted-instant/list-entry-deleted-instant.mjs +++ b/components/attio/sources/list-entry-deleted-instant/list-entry-deleted-instant.mjs @@ -6,7 +6,7 @@ export default { key: "attio-list-entry-deleted-instant", name: "List Entry Deleted (Instant)", description: "Emit new event when a list entry is deleted (i.e. when a record is removed from a list).", - version: "0.0.1", + version: "0.0.2", type: "source", dedupe: "unique", props: { @@ -20,19 +20,21 @@ export default { }, methods: { ...common.methods, - getEventType() { - return "list-entry.deleted"; - }, - getFilter() { - return { - "$and": [ - { - field: "id.list_id", - operator: "equals", - value: this.listId, + getSubscriptions() { + return [ + { + event_type: "list-entry.deleted", + filter: { + "$and": [ + { + field: "id.list_id", + operator: "equals", + value: this.listId, + }, + ], }, - ], - }; + }, + ]; }, generateMeta(entry) { return { diff --git a/components/attio/sources/list-entry-updated-instant/list-entry-updated-instant.mjs b/components/attio/sources/list-entry-updated-instant/list-entry-updated-instant.mjs index 97970cdab82ef..5c595593b9d06 100644 --- a/components/attio/sources/list-entry-updated-instant/list-entry-updated-instant.mjs +++ b/components/attio/sources/list-entry-updated-instant/list-entry-updated-instant.mjs @@ -6,7 +6,7 @@ export default { key: "attio-list-entry-updated-instant", name: "List Entry Updated (Instant)", description: "Emit new event when an existing list entry is updated (i.e. when a list attribute is changed for a specific list entry, e.g. when setting \"Owner\")", - version: "0.0.1", + version: "0.0.2", type: "source", dedupe: "unique", props: { @@ -20,19 +20,21 @@ export default { }, methods: { ...common.methods, - getEventType() { - return "list-entry.updated"; - }, - getFilter() { - return { - "$and": [ - { - field: "id.list_id", - operator: "equals", - value: this.listId, + getSubscriptions() { + return [ + { + event_type: "list-entry.updated", + filter: { + "$and": [ + { + field: "id.list_id", + operator: "equals", + value: this.listId, + }, + ], }, - ], - }; + }, + ]; }, generateMeta(entry) { const ts = Date.now(); diff --git a/components/attio/sources/new-activity-created-instant/new-activity-created-instant.mjs b/components/attio/sources/new-activity-created-instant/new-activity-created-instant.mjs new file mode 100644 index 0000000000000..8dd13d4d8fcac --- /dev/null +++ b/components/attio/sources/new-activity-created-instant/new-activity-created-instant.mjs @@ -0,0 +1,39 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "attio-new-activity-created-instant", + name: "New Activity Created (Instant)", + description: "Emit new event when a note, task, or comment is created, useful for tracking engagement in real time.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getSubscriptions() { + return [ + { + event_type: "comment.created", + filter: null, + }, + { + event_type: "note.created", + filter: null, + }, + { + event_type: "task.created", + filter: null, + }, + ]; + }, + generateMeta(record) { + return { + id: record.id.task_id || record.id.note_id || record.id.comment_id, + summary: `New Activity with ID: ${record.id.task_id || record.id.note_id || record.id.comment_id}`, + ts: Date.now(), + }; + }, + }, + sampleEmit, +}; diff --git a/components/attio/sources/new-activity-created-instant/test-event.mjs b/components/attio/sources/new-activity-created-instant/test-event.mjs new file mode 100644 index 0000000000000..67deac88ae034 --- /dev/null +++ b/components/attio/sources/new-activity-created-instant/test-event.mjs @@ -0,0 +1,16 @@ +export default { + "webhook_id": "2f7629b9-ca46-4719-9281-fa3032bc0144", + "events": [ + { + "event_type": "task.created", + "id": { + "workspace_id": "530fca5b-2cac-4d0f-8845-46cbad16fa42", + "task_id": "85fb2ebd-57dc-4262-8cfb-4ff7f6c0b844" + }, + "actor": { + "type": "workspace-member", + "id": "10da1070-0695-484c-bd43-03720e993032" + } + } + ] +}; diff --git a/components/attio/sources/new-list-entry-instant/new-list-entry-instant.mjs b/components/attio/sources/new-list-entry-instant/new-list-entry-instant.mjs index 886af1869c311..6e30d45363b2a 100644 --- a/components/attio/sources/new-list-entry-instant/new-list-entry-instant.mjs +++ b/components/attio/sources/new-list-entry-instant/new-list-entry-instant.mjs @@ -6,7 +6,7 @@ export default { key: "attio-new-list-entry-instant", name: "New List Entry (Instant)", description: "Emit new event when a record, such as person, company, or deal, is added to a list", - version: "0.0.2", + version: "0.0.3", type: "source", dedupe: "unique", props: { @@ -20,19 +20,21 @@ export default { }, methods: { ...common.methods, - getEventType() { - return "list-entry.created"; - }, - getFilter() { - return { - "$and": [ - { - field: "id.list_id", - operator: "equals", - value: this.listId, + getSubscriptions() { + return [ + { + event_type: "list-entry.created", + filter: { + "$and": [ + { + field: "id.list_id", + operator: "equals", + value: this.listId, + }, + ], }, - ], - }; + }, + ]; }, generateMeta(entry) { return { diff --git a/components/attio/sources/new-note-instant/new-note-instant.mjs b/components/attio/sources/new-note-instant/new-note-instant.mjs index 96beab68b8b21..77065f6b97541 100644 --- a/components/attio/sources/new-note-instant/new-note-instant.mjs +++ b/components/attio/sources/new-note-instant/new-note-instant.mjs @@ -6,13 +6,18 @@ export default { key: "attio-new-note-instant", name: "New Note (Instant)", description: "Emit new event when a new note is created.", - version: "0.0.1", + version: "0.0.2", type: "source", dedupe: "unique", methods: { ...common.methods, - getEventType() { - return "note.created"; + getSubscriptions() { + return [ + { + event_type: "note.created", + filter: null, + }, + ]; }, generateMeta(note) { return { diff --git a/components/attio/sources/new-object-attribute-instant/new-object-attribute-instant.mjs b/components/attio/sources/new-object-attribute-instant/new-object-attribute-instant.mjs index f76c29e6268e4..379d8d24b6342 100644 --- a/components/attio/sources/new-object-attribute-instant/new-object-attribute-instant.mjs +++ b/components/attio/sources/new-object-attribute-instant/new-object-attribute-instant.mjs @@ -6,13 +6,18 @@ export default { key: "attio-new-object-attribute-instant", name: "New Object Attribute (Instant)", description: "Emit new event when an object attribute is created (e.g. when defining a new attribute \"Rating\" on the company object)", - version: "0.0.1", + version: "0.0.2", type: "source", dedupe: "unique", methods: { ...common.methods, - getEventType() { - return "object-attribute.created"; + getSubscriptions() { + return [ + { + event_type: "object-attribute.created", + filter: null, + }, + ]; }, generateMeta(attribute) { return { diff --git a/components/attio/sources/new-record-created-instant/new-record-created-instant.mjs b/components/attio/sources/new-record-created-instant/new-record-created-instant.mjs index 6bf50c00fb8eb..a367373500a35 100644 --- a/components/attio/sources/new-record-created-instant/new-record-created-instant.mjs +++ b/components/attio/sources/new-record-created-instant/new-record-created-instant.mjs @@ -6,7 +6,7 @@ export default { key: "attio-new-record-created-instant", name: "New Record Created (Instant)", description: "Emit new event when new record, such as person, company or deal gets created", - version: "0.0.2", + version: "0.0.3", type: "source", dedupe: "unique", props: { @@ -20,19 +20,21 @@ export default { }, methods: { ...common.methods, - getEventType() { - return "record.created"; - }, - getFilter() { - return { - "$and": [ - { - field: "id.object_id", - operator: "equals", - value: this.objectId, + getSubscriptions() { + return [ + { + event_type: "record.created", + filter: { + "$and": [ + { + field: "id.object_id", + operator: "equals", + value: this.objectId, + }, + ], }, - ], - }; + }, + ]; }, generateMeta(record) { return { diff --git a/components/attio/sources/note-updated-instant/note-updated-instant.mjs b/components/attio/sources/note-updated-instant/note-updated-instant.mjs index 64aaa4dc8fa5b..74fb89f21ebe9 100644 --- a/components/attio/sources/note-updated-instant/note-updated-instant.mjs +++ b/components/attio/sources/note-updated-instant/note-updated-instant.mjs @@ -6,19 +6,25 @@ export default { key: "attio-note-updated-instant", name: "Note Updated (Instant)", description: "Emit new event when the title of a note is modified. Body updates do not currently trigger webhooks.", - version: "0.0.1", + version: "0.0.2", type: "source", dedupe: "unique", methods: { ...common.methods, - getEventType() { - return "note.updated"; + getSubscriptions() { + return [ + { + event_type: "note.updated", + filter: null, + }, + ]; }, generateMeta(note) { + const ts = Date.now(); return { - id: note.id.note_id, + id: `${note.id.note_id}-${ts}`, summary: `Updated Note with ID: ${note.id.note_id}`, - ts: Date.now(), + ts, }; }, }, diff --git a/components/attio/sources/object-attribute-updated-instant/object-attribute-updated-instant.mjs b/components/attio/sources/object-attribute-updated-instant/object-attribute-updated-instant.mjs index 4ccb8c0112607..6e339f4998d30 100644 --- a/components/attio/sources/object-attribute-updated-instant/object-attribute-updated-instant.mjs +++ b/components/attio/sources/object-attribute-updated-instant/object-attribute-updated-instant.mjs @@ -6,13 +6,18 @@ export default { key: "attio-object-attribute-updated-instant", name: "Object Attribute Updated (Instant)", description: "Emit new event when an object attribute is updated (e.g. when renaming the \"Rating\" attribute to \"Score\" on the company object)", - version: "0.0.1", + version: "0.0.2", type: "source", dedupe: "unique", methods: { ...common.methods, - getEventType() { - return "object-attribute.updated"; + getSubscriptions() { + return [ + { + event_type: "object-attribute.updated", + filter: null, + }, + ]; }, generateMeta(attribute) { const ts = Date.now(); diff --git a/components/attio/sources/record-updated-instant/record-updated-instant.mjs b/components/attio/sources/record-updated-instant/record-updated-instant.mjs index cfe60ad7bdeef..340f79d3f5fad 100644 --- a/components/attio/sources/record-updated-instant/record-updated-instant.mjs +++ b/components/attio/sources/record-updated-instant/record-updated-instant.mjs @@ -6,7 +6,7 @@ export default { key: "attio-record-updated-instant", name: "Record Updated (Instant)", description: "Emit new event when values on a record, such as person, company or deal, are updated", - version: "0.0.2", + version: "0.0.3", type: "source", dedupe: "unique", props: { @@ -20,19 +20,21 @@ export default { }, methods: { ...common.methods, - getEventType() { - return "record.updated"; - }, - getFilter() { - return { - "$and": [ - { - field: "id.object_id", - operator: "equals", - value: this.objectId, + getSubscriptions() { + return [ + { + event_type: "record.updated", + filter: { + "$and": [ + { + field: "id.object_id", + operator: "equals", + value: this.objectId, + }, + ], }, - ], - }; + }, + ]; }, generateMeta(record) { const ts = Date.now(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e44a9fc727c88..6a66847759498 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34773,8 +34773,6 @@ snapshots: '@putout/operator-filesystem': 5.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3)) '@putout/operator-json': 2.2.0 putout: 36.13.1(eslint@8.57.1)(typescript@5.6.3) - transitivePeerDependencies: - - supports-color '@putout/operator-regexp@1.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3))': dependencies: