Skip to content

Commit 46df316

Browse files
committed
New contact added to list trigger
1 parent 5403da4 commit 46df316

File tree

3 files changed

+259
-0
lines changed

3 files changed

+259
-0
lines changed

components/hubspot/hubspot.app.mjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,6 +1068,15 @@ export default {
10681068
...opts,
10691069
});
10701070
},
1071+
getListMembershipsByJoinOrder({
1072+
listId, ...opts
1073+
}) {
1074+
return this.makeRequest({
1075+
api: API_PATH.CRMV3,
1076+
endpoint: `/lists/${listId}/memberships/join-order`,
1077+
...opts,
1078+
});
1079+
},
10711080
batchGetObjects({
10721081
objectType, ...opts
10731082
}) {
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import common from "../common/common.mjs";
2+
import {
3+
DEFAULT_LIMIT,
4+
DEFAULT_CONTACT_PROPERTIES,
5+
} from "../../common/constants.mjs";
6+
import sampleEmit from "./test-event.mjs";
7+
8+
export default {
9+
...common,
10+
key: "hubspot-new-contact-added-to-list",
11+
name: "New Contact Added to List",
12+
description:
13+
"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)",
14+
version: "0.0.1",
15+
type: "source",
16+
dedupe: "unique",
17+
props: {
18+
...common.props,
19+
info: {
20+
type: "alert",
21+
alertType: "info",
22+
content: `Properties:\n\`${DEFAULT_CONTACT_PROPERTIES.join(", ")}\``,
23+
},
24+
lists: {
25+
propDefinition: [
26+
common.props.hubspot,
27+
"lists",
28+
],
29+
description: "Select the lists to watch for new contacts.",
30+
optional: false,
31+
},
32+
properties: {
33+
propDefinition: [
34+
common.props.hubspot,
35+
"contactProperties",
36+
() => ({
37+
excludeDefaultProperties: true,
38+
}),
39+
],
40+
label: "Additional contact properties to retrieve",
41+
optional: true,
42+
},
43+
},
44+
methods: {
45+
...common.methods,
46+
_getLastProcessedRecord(listId) {
47+
const key = `list_${listId}_last_record`;
48+
return this.db.get(key);
49+
},
50+
_setLastProcessedRecord(listId, recordId) {
51+
const key = `list_${listId}_last_record`;
52+
this.db.set(key, recordId);
53+
},
54+
getTs() {
55+
return Date.now();
56+
},
57+
generateMeta(membership, listInfo) {
58+
const { recordId } = membership;
59+
const ts = this.getTs();
60+
61+
return {
62+
id: `${listInfo.listId}-${recordId}`,
63+
summary: `Contact ${recordId} added to list: ${listInfo.name}`,
64+
ts,
65+
};
66+
},
67+
async getContactDetails(contactIds) {
68+
if (!contactIds.length) return {};
69+
70+
const { properties = [] } = this;
71+
const allProperties = [
72+
...DEFAULT_CONTACT_PROPERTIES,
73+
...properties,
74+
];
75+
76+
try {
77+
const { results } = await this.hubspot.batchGetObjects({
78+
objectType: "contacts",
79+
data: {
80+
inputs: contactIds.map((id) => ({
81+
id,
82+
})),
83+
properties: allProperties,
84+
},
85+
});
86+
87+
const contactMap = {};
88+
results.forEach((contact) => {
89+
contactMap[contact.id] = contact;
90+
});
91+
return contactMap;
92+
} catch (error) {
93+
console.warn("Error fetching contact details:", error);
94+
return {};
95+
}
96+
},
97+
async processListMemberships(listId, listInfo) {
98+
const lastProcessedRecord = this._getLastProcessedRecord(listId);
99+
const newMemberships = [];
100+
101+
let params = {
102+
limit: DEFAULT_LIMIT,
103+
};
104+
105+
if (lastProcessedRecord) {
106+
params.after = lastProcessedRecord;
107+
}
108+
109+
try {
110+
let hasMore = true;
111+
let latestRecordId = lastProcessedRecord;
112+
113+
while (hasMore) {
114+
const {
115+
results, paging,
116+
} =
117+
await this.hubspot.getListMembershipsByJoinOrder({
118+
listId,
119+
params,
120+
});
121+
122+
if (!results || results.length === 0) {
123+
break;
124+
}
125+
126+
for (const membership of results) {
127+
newMemberships.push({
128+
membership,
129+
listInfo,
130+
});
131+
latestRecordId = membership.recordId;
132+
}
133+
134+
if (paging?.next?.after) {
135+
params.after = paging.next.after;
136+
} else {
137+
hasMore = false;
138+
}
139+
}
140+
141+
if (latestRecordId) {
142+
this._setLastProcessedRecord(listId, latestRecordId);
143+
}
144+
} catch (error) {
145+
console.error(`Error processing list ${listId}:`, error);
146+
}
147+
148+
return newMemberships;
149+
},
150+
async processResults() {
151+
const { lists } = this;
152+
153+
if (!lists || lists.length === 0) {
154+
console.warn("No lists selected to monitor");
155+
return;
156+
}
157+
158+
const allNewMemberships = [];
159+
160+
for (const listId of lists) {
161+
try {
162+
const listInfo = {
163+
listId,
164+
name: `List ${listId}`,
165+
};
166+
167+
const newMemberships = await this.processListMemberships(
168+
listId,
169+
listInfo,
170+
);
171+
allNewMemberships.push(...newMemberships);
172+
} catch (error) {
173+
console.error(`Error processing list ${listId}:`, error);
174+
}
175+
}
176+
177+
if (allNewMemberships.length > 0) {
178+
const contactIds = allNewMemberships.map(
179+
({ membership }) => membership.recordId,
180+
);
181+
const contactDetails = await this.getContactDetails(contactIds);
182+
183+
for (const {
184+
membership, listInfo,
185+
} of allNewMemberships) {
186+
const contactDetail = contactDetails[membership.recordId] || {};
187+
188+
const eventData = {
189+
listId: listInfo.listId,
190+
listName: listInfo.name,
191+
contactId: membership.recordId,
192+
contact: contactDetail,
193+
membership,
194+
addedAt: new Date().toISOString(),
195+
};
196+
197+
const meta = this.generateMeta(membership, listInfo);
198+
this.$emit(eventData, meta);
199+
}
200+
}
201+
},
202+
getParams() {
203+
return {};
204+
},
205+
},
206+
sampleEmit,
207+
};
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
export default {
2+
listId: "123456789",
3+
listName: "List 123456789",
4+
contactId: "31612976545",
5+
contact: {
6+
id: "31612976545",
7+
properties: {
8+
address: null,
9+
annualrevenue: null,
10+
city: "San Francisco",
11+
company: "Acme Corp",
12+
country: "United States",
13+
createdate: "2024-08-26T15:30:45.123Z",
14+
15+
fax: null,
16+
firstname: "John",
17+
hs_createdate: "2024-08-26T15:30:45.123Z",
18+
hs_email_domain: "example.com",
19+
hs_language: null,
20+
hs_object_id: "31612976545",
21+
hs_persona: null,
22+
industry: "Technology",
23+
jobtitle: "Software Engineer",
24+
lastmodifieddate: "2024-08-26T15:32:15.456Z",
25+
lastname: "Doe",
26+
lifecyclestage: "lead",
27+
mobilephone: null,
28+
numemployees: null,
29+
phone: "+1-555-123-4567",
30+
salutation: null,
31+
state: "California",
32+
website: "https://example.com",
33+
zip: "94102",
34+
},
35+
createdAt: "2024-08-26T15:30:45.123Z",
36+
updatedAt: "2024-08-26T15:32:15.456Z",
37+
archived: false,
38+
},
39+
membership: {
40+
recordId: "31612976545",
41+
},
42+
addedAt: "2024-08-26T15:35:20.789Z",
43+
};

0 commit comments

Comments
 (0)