sources/new-contact-added-to-list/new-contact-added-to-list.mjs
import common from "../common/common.mjs"; import { DEFAULT_LIMIT, DEFAULT_CONTACT_PROPERTIES, API_PATH, } from "../../common/constants.mjs"; import sampleEmit from "./test-event.mjs"; export default { ...common, key: "hubspot-new-contact-added-to-list", name: "New Contact Added to List", description: "Emit new event when a contact is added to a HubSpot list. [See the documentation](https://developers.hubspot.com/docs/reference/api/crm/lists#get-%2Fcrm%2Fv3%2Flists%2F%7Blistid%7D%2Fmemberships%2Fjoin-order)", version: "0.0.8", type: "source", dedupe: "unique", props: { ...common.props, info: { type: "alert", alertType: "info", content: `Properties:\n\`${DEFAULT_CONTACT_PROPERTIES.join(", ")}\``, }, listId: { type: "string", label: "List ID", description: "Select the list to watch for new contacts", withLabel: true, async options({ page }) { const { lists } = await this.searchLists({ data: { count: DEFAULT_LIMIT, offset: page * DEFAULT_LIMIT, }, }); return ( lists?.map(({ listId, name, }) => ({ value: listId, label: name, })) || [] ); }, }, properties: { propDefinition: [ common.props.hubspot, "contactProperties", () => ({ excludeDefaultProperties: true, }), ], label: "Additional contact properties to retrieve", optional: true, }, fetchAllProperties: { type: "boolean", label: "Fetch all contact properties", description: "When enabled, fetches all available contact properties instead of just default and selected properties. This provides complete contact data but may increase API usage.", optional: true, default: false, }, }, methods: { ...common.methods, _getKey(listId) { return `list_${listId}_last_timestamp`; }, _getLastMembershipTimestamp(listId) { return this.db.get(this._getKey(listId)); }, _setLastMembershipTimestamp(listId, timestamp) { this.db.set(this._getKey(listId), timestamp); }, searchLists(opts = {}) { return this.hubspot.makeRequest({ api: API_PATH.CRMV3, method: "POST", endpoint: "/lists/search", ...opts, }); }, getTs() { return Date.now(); }, generateMeta(membership, listInfo) { const { recordId, membershipTimestamp, } = membership; const ts = membershipTimestamp ? new Date(membershipTimestamp).getTime() : this.getTs(); return { id: `${listInfo.listId}-${recordId}-${ts}`, summary: `Contact ${recordId} added to list: ${listInfo.name}`, ts, }; }, async getContactDetails(contactIds) { if (!contactIds.length) return {}; if (this.fetchAllProperties) { const chunks = []; const chunkSize = 100; for (let i = 0; i < contactIds.length; i += chunkSize) { chunks.push(contactIds.slice(i, i + chunkSize)); } const contactMap = {}; try { for (const chunk of chunks) { try { const { results } = await this.hubspot.batchGetContactsWithAllProperties({ contactIds: chunk, }); results.forEach((contact) => { contactMap[contact.id] = contact; }); } catch (error) { console.warn( `Error fetching contact details for chunk of ${chunk.length} contacts:`, error, ); } } return contactMap; } catch (error) { console.warn("Error processing contact details:", error); return {}; } } const { properties = [] } = this; const allProperties = [ ...DEFAULT_CONTACT_PROPERTIES, ...properties, ]; const chunks = []; const chunkSize = 100; for (let i = 0; i < contactIds.length; i += chunkSize) { chunks.push(contactIds.slice(i, i + chunkSize)); } const contactMap = {}; try { for (const chunk of chunks) { try { const { results } = await this.hubspot.batchGetObjects({ objectType: "contacts", data: { inputs: chunk.map((id) => ({ id, })), properties: allProperties, }, }); results.forEach((contact) => { contactMap[contact.id] = contact; }); } catch (error) { console.warn( `Error fetching contact details for chunk of ${chunk.length} contacts:`, error, ); } } return contactMap; } catch (error) { console.warn("Error processing contact details:", error); return {}; } }, async processListMemberships(listId, listInfo) { const lastMembershipTimestamp = this._getLastMembershipTimestamp(listId); const newMemberships = []; let params = { limit: DEFAULT_LIMIT, }; try { let hasMore = true; let latestMembershipTimestamp = lastMembershipTimestamp; if (!lastMembershipTimestamp) { const baselineTimestamp = new Date().toISOString(); this._setLastMembershipTimestamp(listId, baselineTimestamp); return newMemberships; } while (hasMore) { const { results, paging, } = await this.hubspot.getListMembershipsByJoinOrder({ listId, params, }); if (!results) { console.warn( `No results returned from API for list ${listId} - possible API issue`, ); break; } if (results.length === 0) { break; } for (const membership of results) { const { membershipTimestamp } = membership; if (membershipTimestamp > lastMembershipTimestamp) { newMemberships.push({ membership, listInfo, }); if ( !latestMembershipTimestamp || membershipTimestamp > latestMembershipTimestamp ) { latestMembershipTimestamp = membershipTimestamp; } } } if (paging?.next?.after) { params.after = paging.next.after; } else { hasMore = false; } } if (latestMembershipTimestamp !== lastMembershipTimestamp) { this._setLastMembershipTimestamp(listId, latestMembershipTimestamp); } } catch (error) { console.error(`Error processing list ${listId}:`, error); } return newMemberships; }, async processResults() { if (!this.listId) { console.warn("No list selected to monitor"); return; } const listId = this.listId?.value || this.listId; const listInfo = { listId, name: `List ${this.listId?.label || listId}`, }; try { const newMemberships = await this.processListMemberships( listId, listInfo, ); if (newMemberships.length > 0) { const contactIds = newMemberships.map( ({ membership }) => membership.recordId, ); const contactDetails = await this.getContactDetails(contactIds); for (const { membership, listInfo, } of newMemberships) { const contactDetail = contactDetails[membership.recordId] || {}; const eventData = { listId: listInfo.listId, listName: listInfo.name, contactId: membership.recordId, contact: contactDetail, membership, addedAt: membership.membershipTimestamp, }; const meta = this.generateMeta(membership, listInfo); this.$emit(eventData, meta); } } } catch (error) { console.error(`Error processing list ${listId}:`, error); } }, getParams() { return {}; }, }, sampleEmit, };