@eugeneboondock/hubspot-mcp-server 0.2.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +22 -0
- package/LICENSE +21 -0
- package/NOTES_TASKS_FIX_SUMMARY.md +0 -0
- package/README.md +217 -0
- package/dist/auth-client.d.ts +29 -0
- package/dist/auth-client.js +76 -0
- package/dist/auth-client.js.map +1 -0
- package/dist/hubspot-client.d.ts +88 -0
- package/dist/hubspot-client.js +1730 -0
- package/dist/hubspot-client.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1249 -0
- package/dist/index.js.map +1 -0
- package/dist/token-manager.d.ts +54 -0
- package/dist/token-manager.js +163 -0
- package/dist/token-manager.js.map +1 -0
- package/package.json +42 -0
- package/src/auth-client.ts +105 -0
- package/src/hubspot-client.ts +2120 -0
- package/src/index.ts +1378 -0
- package/src/token-manager.ts +207 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,2120 @@
|
|
|
1
|
+
import { Client } from '@hubspot/api-client';
|
|
2
|
+
import dotenv from 'dotenv';
|
|
3
|
+
|
|
4
|
+
dotenv.config();
|
|
5
|
+
|
|
6
|
+
// Convert any datetime objects to ISO strings
|
|
7
|
+
export function convertDatetimeFields(obj: any): any {
|
|
8
|
+
if (obj === null || obj === undefined) {
|
|
9
|
+
return obj;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (typeof obj === 'object') {
|
|
13
|
+
if (obj instanceof Date) {
|
|
14
|
+
return obj.toISOString();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (Array.isArray(obj)) {
|
|
18
|
+
return obj.map(item => convertDatetimeFields(item));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const result: Record<string, any> = {};
|
|
22
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
23
|
+
result[key] = convertDatetimeFields(value);
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return obj;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function filterRecordsByUpdatedAfter(
|
|
32
|
+
records: any[] | undefined,
|
|
33
|
+
updatedAfter?: string,
|
|
34
|
+
extractor?: (record: any) => string | number | null,
|
|
35
|
+
debugLabel: string = 'records'
|
|
36
|
+
): any[] {
|
|
37
|
+
if (!Array.isArray(records) || !updatedAfter) {
|
|
38
|
+
return records || [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const cutoff = Date.parse(updatedAfter);
|
|
42
|
+
if (Number.isNaN(cutoff)) {
|
|
43
|
+
return records;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let missingTimestampCount = 0;
|
|
47
|
+
let invalidTimestampCount = 0;
|
|
48
|
+
|
|
49
|
+
const filtered = records.filter((record) => {
|
|
50
|
+
const rawValue = extractor
|
|
51
|
+
? extractor(record)
|
|
52
|
+
: record?.updatedAt ??
|
|
53
|
+
record?.updated_at ??
|
|
54
|
+
record?.updated ??
|
|
55
|
+
record?.lastUpdated ??
|
|
56
|
+
record?.lastUpdatedAt ??
|
|
57
|
+
record?.properties?.hs_lastmodifieddate ??
|
|
58
|
+
record?.properties?.lastmodifieddate ??
|
|
59
|
+
record?.metadata?.lastUpdated ??
|
|
60
|
+
record?.metadata?.updatedAt ??
|
|
61
|
+
record?.stats?.lastUpdated ??
|
|
62
|
+
null;
|
|
63
|
+
|
|
64
|
+
if (!rawValue) {
|
|
65
|
+
missingTimestampCount++;
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const timestamp =
|
|
70
|
+
typeof rawValue === 'number' ? rawValue : Date.parse(rawValue as string);
|
|
71
|
+
|
|
72
|
+
if (Number.isNaN(timestamp)) {
|
|
73
|
+
invalidTimestampCount++;
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return timestamp > cutoff;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const removed = records.length - filtered.length;
|
|
81
|
+
console.log(
|
|
82
|
+
`đ Incremental filter for ${debugLabel}: kept ${filtered.length}/${records.length} records updated after ${new Date(
|
|
83
|
+
cutoff
|
|
84
|
+
).toISOString()}`
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
if (removed === 0) {
|
|
88
|
+
console.log(
|
|
89
|
+
`âšī¸ No ${debugLabel} records newer than ${new Date(cutoff).toISOString()}`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (missingTimestampCount > 0) {
|
|
94
|
+
console.warn(
|
|
95
|
+
`[Incremental] ${debugLabel}: ${missingTimestampCount} record(s) missing updated timestamp (included by default).`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (invalidTimestampCount > 0) {
|
|
100
|
+
console.warn(
|
|
101
|
+
`[Incremental] ${debugLabel}: ${invalidTimestampCount} record(s) had invalid timestamp values (included by default).`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return filtered;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export class HubSpotScopeError extends Error {
|
|
109
|
+
public scope: string;
|
|
110
|
+
public code: string;
|
|
111
|
+
|
|
112
|
+
constructor(scopeName: string, cause?: any) {
|
|
113
|
+
super(`HubSpot scope "${scopeName}" is not granted for this account.`);
|
|
114
|
+
this.name = 'HubSpotScopeError';
|
|
115
|
+
this.scope = scopeName;
|
|
116
|
+
this.code = 'HUBSPOT_SCOPE_MISSING';
|
|
117
|
+
if (cause) {
|
|
118
|
+
(this as any).cause = cause;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export class HubSpotClient {
|
|
124
|
+
private client: Client;
|
|
125
|
+
private accessToken: string;
|
|
126
|
+
private lastRequestTime: number = 0;
|
|
127
|
+
private requestCount: number = 0;
|
|
128
|
+
private rateLimitWindow: number = 1000; // 1 second window
|
|
129
|
+
|
|
130
|
+
constructor(accessToken?: string) {
|
|
131
|
+
const token = accessToken ||
|
|
132
|
+
process.env.HUBSPOT_ACCESS_TOKEN ||
|
|
133
|
+
process.env.HUBSPOT_DEVELOPER_API_KEY ||
|
|
134
|
+
process.env.HUBSPOT_PERSONAL_ACCESS_KEY;
|
|
135
|
+
|
|
136
|
+
if (!token) {
|
|
137
|
+
throw new Error('HubSpot access token is required. Set HUBSPOT_ACCESS_TOKEN, HUBSPOT_DEVELOPER_API_KEY, or HUBSPOT_PERSONAL_ACCESS_KEY');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this.accessToken = token;
|
|
141
|
+
this.client = new Client({ accessToken: token });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Update the access token and reinitialize the client
|
|
146
|
+
*/
|
|
147
|
+
updateAccessToken(newToken: string): void {
|
|
148
|
+
this.accessToken = newToken;
|
|
149
|
+
this.client = new Client({ accessToken: newToken });
|
|
150
|
+
console.log('đ HubSpot client: Access token updated');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Wrapper for API calls with automatic token refresh on 401 errors
|
|
155
|
+
*/
|
|
156
|
+
private async callWithRetry<T>(apiCall: () => Promise<T>, retryOnAuth: boolean = true): Promise<T> {
|
|
157
|
+
try {
|
|
158
|
+
return await apiCall();
|
|
159
|
+
} catch (error: any) {
|
|
160
|
+
// Check if it's a 401 authentication error
|
|
161
|
+
if (error?.code === 401 &&
|
|
162
|
+
error?.body?.category === 'EXPIRED_AUTHENTICATION' &&
|
|
163
|
+
retryOnAuth) {
|
|
164
|
+
|
|
165
|
+
console.log('đ HubSpot client: Detected 401 error, attempting token refresh...');
|
|
166
|
+
|
|
167
|
+
// Try to get a fresh token from environment (in case it was updated)
|
|
168
|
+
const refreshedToken = process.env.HUBSPOT_ACCESS_TOKEN ||
|
|
169
|
+
process.env.HUBSPOT_DEVELOPER_API_KEY ||
|
|
170
|
+
process.env.HUBSPOT_PERSONAL_ACCESS_KEY;
|
|
171
|
+
|
|
172
|
+
if (refreshedToken && refreshedToken !== this.accessToken) {
|
|
173
|
+
console.log('đ HubSpot client: Found updated token, retrying...');
|
|
174
|
+
this.updateAccessToken(refreshedToken);
|
|
175
|
+
|
|
176
|
+
// Retry the API call once with the new token
|
|
177
|
+
return await apiCall();
|
|
178
|
+
} else {
|
|
179
|
+
console.log('â HubSpot client: No updated token available for refresh');
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Rate limiting to prevent 429 errors
|
|
190
|
+
*/
|
|
191
|
+
private async rateLimitDelay(): Promise<void> {
|
|
192
|
+
const now = Date.now();
|
|
193
|
+
const timeSinceLastRequest = now - this.lastRequestTime;
|
|
194
|
+
|
|
195
|
+
// Reset counter if window has passed
|
|
196
|
+
if (timeSinceLastRequest > this.rateLimitWindow) {
|
|
197
|
+
this.requestCount = 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// If we've made too many requests, wait
|
|
201
|
+
if (this.requestCount >= 5) {
|
|
202
|
+
const waitTime = this.rateLimitWindow - timeSinceLastRequest;
|
|
203
|
+
if (waitTime > 0) {
|
|
204
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
205
|
+
}
|
|
206
|
+
this.requestCount = 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
this.requestCount++;
|
|
210
|
+
this.lastRequestTime = Date.now();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Helper method to paginate through all results using HubSpot's after cursor
|
|
215
|
+
*/
|
|
216
|
+
private async paginateSearch(
|
|
217
|
+
searchFn: (searchRequest: any) => Promise<any>,
|
|
218
|
+
baseRequest: any,
|
|
219
|
+
limit: number = 100
|
|
220
|
+
): Promise<any[]> {
|
|
221
|
+
// Ensure limit doesn't exceed HubSpot's maximum
|
|
222
|
+
const safeLimit = Math.min(Math.max(limit, 1), 200);
|
|
223
|
+
|
|
224
|
+
const allResults: any[] = [];
|
|
225
|
+
let after: string | undefined = undefined;
|
|
226
|
+
let hasMore = true;
|
|
227
|
+
|
|
228
|
+
while (hasMore) {
|
|
229
|
+
await this.rateLimitDelay();
|
|
230
|
+
|
|
231
|
+
const searchRequest = {
|
|
232
|
+
...baseRequest,
|
|
233
|
+
limit: safeLimit,
|
|
234
|
+
...(after ? { after } : {})
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const response = await searchFn(searchRequest);
|
|
238
|
+
|
|
239
|
+
if (response.results && response.results.length > 0) {
|
|
240
|
+
allResults.push(...response.results);
|
|
241
|
+
console.log(`đ Fetched ${response.results.length} records (total: ${allResults.length})`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Check for pagination cursor
|
|
245
|
+
if (response.paging?.next?.after) {
|
|
246
|
+
after = response.paging.next.after;
|
|
247
|
+
} else {
|
|
248
|
+
hasMore = false;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return allResults;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private normalizeDateInput(value?: string | number | Date): string | null {
|
|
256
|
+
if (!value) return null;
|
|
257
|
+
const dateValue = value instanceof Date ? value : new Date(value);
|
|
258
|
+
if (Number.isNaN(dateValue.getTime())) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
return dateValue.toISOString();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private applyUpdatedAfterFilter(
|
|
265
|
+
baseRequest: any,
|
|
266
|
+
updatedAfter?: string,
|
|
267
|
+
propertyName: string = 'hs_lastmodifieddate'
|
|
268
|
+
) {
|
|
269
|
+
if (!baseRequest || !updatedAfter) {
|
|
270
|
+
return baseRequest;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const normalized = this.normalizeDateInput(updatedAfter);
|
|
274
|
+
if (!normalized) {
|
|
275
|
+
return baseRequest;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const filter = {
|
|
279
|
+
propertyName,
|
|
280
|
+
operator: 'GT',
|
|
281
|
+
value: normalized
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
if (Array.isArray(baseRequest.filterGroups) && baseRequest.filterGroups.length > 0) {
|
|
285
|
+
baseRequest.filterGroups = baseRequest.filterGroups.map((group: any) => ({
|
|
286
|
+
...group,
|
|
287
|
+
filters: Array.isArray(group.filters) ? [...group.filters, filter] : [filter]
|
|
288
|
+
}));
|
|
289
|
+
} else {
|
|
290
|
+
baseRequest.filterGroups = [{ filters: [filter] }];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return baseRequest;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Helper method to paginate through all results using HubSpot's after cursor (for List/GET APIs)
|
|
298
|
+
*/
|
|
299
|
+
private async paginateList(
|
|
300
|
+
listFn: (params: any) => Promise<any>,
|
|
301
|
+
limit: number = 100
|
|
302
|
+
): Promise<any[]> {
|
|
303
|
+
// Ensure limit doesn't exceed HubSpot's maximum
|
|
304
|
+
const safeLimit = Math.min(Math.max(limit, 1), 200);
|
|
305
|
+
|
|
306
|
+
const allResults: any[] = [];
|
|
307
|
+
let after: string | undefined = undefined;
|
|
308
|
+
let hasMore = true;
|
|
309
|
+
|
|
310
|
+
while (hasMore) {
|
|
311
|
+
await this.rateLimitDelay();
|
|
312
|
+
|
|
313
|
+
const params: any = { limit: safeLimit, after };
|
|
314
|
+
const response = await listFn(params);
|
|
315
|
+
|
|
316
|
+
// Standard v3 SDK responses have a 'results' array
|
|
317
|
+
const results = response.results;
|
|
318
|
+
|
|
319
|
+
if (results && Array.isArray(results) && results.length > 0) {
|
|
320
|
+
allResults.push(...results);
|
|
321
|
+
console.log(`đ Fetched ${results.length} records (total: ${allResults.length})`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Standard v3 SDK pagination cursor, guard against stuck cursors
|
|
325
|
+
const nextAfter = response.paging?.next?.after;
|
|
326
|
+
if (!nextAfter || nextAfter === after || !results || results.length === 0) {
|
|
327
|
+
hasMore = false;
|
|
328
|
+
} else {
|
|
329
|
+
after = nextAfter;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return allResults;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async getRecentCompanies(limit: number = 100, updatedAfter?: string): Promise<any> {
|
|
336
|
+
return this.callWithRetry(async () => {
|
|
337
|
+
console.log('đĸ Fetching ALL companies with pagination...');
|
|
338
|
+
|
|
339
|
+
// Create search request with sort by lastmodifieddate
|
|
340
|
+
const baseRequest = {
|
|
341
|
+
sorts: ['lastmodifieddate:desc'],
|
|
342
|
+
properties: [
|
|
343
|
+
// Basic company info - enhanced
|
|
344
|
+
'name', 'domain', 'website', 'phone', 'industry', 'company_type',
|
|
345
|
+
// Address info - comprehensive
|
|
346
|
+
'address', 'address2', 'city', 'state', 'zip', 'country', 'country_code',
|
|
347
|
+
// Company details - expanded
|
|
348
|
+
'description', 'numberofemployees', 'annualrevenue', 'type',
|
|
349
|
+
'founded_year', 'business_type', 'revenue_range', 'employee_range',
|
|
350
|
+
'target_account', 'ideal_customer_profile', 'account_tier',
|
|
351
|
+
// Contact info
|
|
352
|
+
'owneremail', 'ownername', 'phone', 'owner_phone',
|
|
353
|
+
// Website and online presence - comprehensive
|
|
354
|
+
'website', 'domain', 'blog_url', 'facebook_url', 'twitter_url',
|
|
355
|
+
'linkedin_url', 'youtube_url', 'instagram_url', 'pinterest_url',
|
|
356
|
+
'googleplus_url', 'klout_score', 'alexa_ranking', 'semrush_ranking',
|
|
357
|
+
// Company associations and details
|
|
358
|
+
'hs_parent_company_id', 'hs_child_company_ids', 'hs_analytics_source',
|
|
359
|
+
'hs_analytics_source_data_1', 'hs_analytics_source_data_2', 'hs_analytics_source_data_3',
|
|
360
|
+
'hs_analytics_source_data_4', 'hs_analytics_source_raw',
|
|
361
|
+
// Custom properties - expanded
|
|
362
|
+
'custom_property_1', 'custom_property_2', 'custom_property_3', 'custom_property_4', 'custom_property_5',
|
|
363
|
+
'custom_property_6', 'custom_property_7', 'custom_property_8', 'custom_property_9', 'custom_property_10',
|
|
364
|
+
// Dates and timestamps - comprehensive
|
|
365
|
+
'createdate', 'hs_lastmodifieddate', 'lastmodifieddate', 'hs_createdate',
|
|
366
|
+
'first_conversion_date', 'last_conversion_date', 'hs_first_engagement_date',
|
|
367
|
+
'hs_last_contacted', 'hs_last_activity_date', 'hs_first_contact_createdate',
|
|
368
|
+
// Additional fields - enhanced
|
|
369
|
+
'hs_object_id', 'hs_unique_creation_key', 'hs_company_id', 'id',
|
|
370
|
+
'is_deleted', 'hs_time_to_first_engagement', 'hs_time_to_close',
|
|
371
|
+
// Lead status and scoring
|
|
372
|
+
'hs_lead_status', 'hs_lead_scoring', 'hs_predictivecontactscore_v2',
|
|
373
|
+
'hs_predictivecontactscoring_v2', 'hs_ideal_customer_profile'
|
|
374
|
+
]
|
|
375
|
+
};
|
|
376
|
+
this.applyUpdatedAfterFilter(baseRequest, updatedAfter, 'hs_lastmodifieddate');
|
|
377
|
+
|
|
378
|
+
// Use pagination to fetch ALL companies
|
|
379
|
+
const allCompanies = await this.paginateSearch(
|
|
380
|
+
(req) => this.client.crm.companies.searchApi.doSearch(req),
|
|
381
|
+
baseRequest,
|
|
382
|
+
limit
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
console.log(`â
Total companies fetched: ${allCompanies.length}`);
|
|
386
|
+
return convertDatetimeFields(allCompanies);
|
|
387
|
+
}).catch((error: any) => {
|
|
388
|
+
console.error('Error getting recent companies:', error);
|
|
389
|
+
return { error: error.message };
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async getRecentContacts(limit: number = 100, updatedAfter?: string): Promise<any> {
|
|
394
|
+
try {
|
|
395
|
+
console.log('đ Fetching ALL contacts with pagination...');
|
|
396
|
+
|
|
397
|
+
// Create search request with sort by lastmodifieddate
|
|
398
|
+
const baseRequest = {
|
|
399
|
+
sorts: ['lastmodifieddate:desc'],
|
|
400
|
+
properties: [
|
|
401
|
+
// Basic info - enhanced
|
|
402
|
+
'firstname', 'lastname', 'email', 'phone', 'mobilephone', 'jobtitle',
|
|
403
|
+
'salutation', 'middle_name', 'prefix', 'suffix',
|
|
404
|
+
// Company info - comprehensive
|
|
405
|
+
'company', 'website', 'company_name', 'company_website',
|
|
406
|
+
'company_domain', 'company_industry', 'company_size',
|
|
407
|
+
// Address info - comprehensive
|
|
408
|
+
'address', 'address2', 'city', 'state', 'zip', 'country', 'country_code',
|
|
409
|
+
// Contact properties - expanded
|
|
410
|
+
'lifecyclestage', 'lead_status', 'hs_lead_status', 'contact_owner',
|
|
411
|
+
'hs_contact_stage', 'hs_contact_status', 'hs_contact_source',
|
|
412
|
+
// Company association and details - enhanced
|
|
413
|
+
'associatedcompanyid', 'associatedcompany_name', 'associatedcompany_domain',
|
|
414
|
+
'associatedcompany_website', 'associatedcompany_industry', 'associatedcompany_size',
|
|
415
|
+
// Social media and online presence - comprehensive
|
|
416
|
+
'linkedin_profile', 'twitter_profile', 'facebook_profile',
|
|
417
|
+
'linkedin_url', 'twitter_url', 'facebook_url', 'instagram_url', 'youtube_url',
|
|
418
|
+
// Professional details - expanded
|
|
419
|
+
'seniority', 'department', 'industry', 'job_function', 'job_role',
|
|
420
|
+
'job_level', 'years_experience', 'education_level', 'school',
|
|
421
|
+
// Lead scoring and qualification - comprehensive
|
|
422
|
+
'hs_lead_scoring', 'hs_lead_status', 'hs_analytics_source',
|
|
423
|
+
'hs_analytics_source_data_1', 'hs_analytics_source_data_2', 'hs_analytics_source_data_3',
|
|
424
|
+
'hs_analytics_source_data_4', 'hs_analytics_source_raw',
|
|
425
|
+
'hs_predictivecontactscore_v2', 'hs_predictivecontactscoring_v2',
|
|
426
|
+
// Custom properties - expanded
|
|
427
|
+
'custom_property_1', 'custom_property_2', 'custom_property_3', 'custom_property_4', 'custom_property_5',
|
|
428
|
+
'custom_property_6', 'custom_property_7', 'custom_property_8', 'custom_property_9', 'custom_property_10',
|
|
429
|
+
// Dates and timestamps - comprehensive
|
|
430
|
+
'createdate', 'hs_lastmodifieddate', 'lastmodifieddate', 'hs_createdate',
|
|
431
|
+
'first_conversion_date', 'last_conversion_date', 'hs_first_engagement_date',
|
|
432
|
+
'hs_last_contacted', 'hs_last_activity_date', 'hs_first_contact_createdate',
|
|
433
|
+
// Additional fields - enhanced
|
|
434
|
+
'hs_object_id', 'hs_unique_creation_key', 'hs_contact_id', 'id',
|
|
435
|
+
'is_deleted', 'hs_time_to_first_engagement', 'hs_contact_stage_probability'
|
|
436
|
+
]
|
|
437
|
+
};
|
|
438
|
+
this.applyUpdatedAfterFilter(baseRequest, updatedAfter, 'hs_lastmodifieddate');
|
|
439
|
+
|
|
440
|
+
// Use pagination to fetch ALL contacts
|
|
441
|
+
const allContacts = await this.paginateSearch(
|
|
442
|
+
(req) => this.client.crm.contacts.searchApi.doSearch(req),
|
|
443
|
+
baseRequest,
|
|
444
|
+
limit
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
console.log(`â
Total contacts fetched: ${allContacts.length}`);
|
|
448
|
+
return convertDatetimeFields(allContacts);
|
|
449
|
+
} catch (error: any) {
|
|
450
|
+
console.error('Error getting recent contacts:', error);
|
|
451
|
+
return { error: error.message };
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async getAssociations(fromObjectType: string, fromObjectId: string, toObjectType: string): Promise<any[]> {
|
|
456
|
+
try {
|
|
457
|
+
const allAssociations: any[] = [];
|
|
458
|
+
let after: string | undefined = undefined;
|
|
459
|
+
|
|
460
|
+
while (true) {
|
|
461
|
+
await this.rateLimitDelay();
|
|
462
|
+
const response = await this.client.crm.associations.v4.basicApi.getPage(
|
|
463
|
+
fromObjectType,
|
|
464
|
+
fromObjectId,
|
|
465
|
+
toObjectType,
|
|
466
|
+
after,
|
|
467
|
+
500
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
const results = response.results || [];
|
|
471
|
+
if (results.length > 0) {
|
|
472
|
+
allAssociations.push(...results);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const nextAfter = response.paging?.next?.after;
|
|
476
|
+
if (!nextAfter || nextAfter === after || results.length === 0) {
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
after = nextAfter;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return allAssociations;
|
|
483
|
+
} catch (error: any) {
|
|
484
|
+
console.error(`Error fetching associations for ${fromObjectType}:${fromObjectId} -> ${toObjectType}:`, error);
|
|
485
|
+
throw new Error(`HubSpot API error: ${error.message}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async searchTasks(minutes: number = 30, limit: number = 50): Promise<any> {
|
|
490
|
+
try {
|
|
491
|
+
// Calculate the date range (past N minutes)
|
|
492
|
+
const endTime = new Date();
|
|
493
|
+
const startTime = new Date(endTime);
|
|
494
|
+
startTime.setMinutes(startTime.getMinutes() - minutes);
|
|
495
|
+
const startTimestamp = startTime.getTime();
|
|
496
|
+
|
|
497
|
+
console.log(`[MCP Debug] Searching tasks from ${startTime.toISOString()} to ${endTime.toISOString()}`);
|
|
498
|
+
console.log(`[MCP Debug] Timestamp filter: ${startTimestamp}`);
|
|
499
|
+
|
|
500
|
+
const baseRequest = {
|
|
501
|
+
filterGroups: [{
|
|
502
|
+
filters: [{
|
|
503
|
+
propertyName: 'hs_lastmodifieddate',
|
|
504
|
+
operator: 'GTE' as any,
|
|
505
|
+
value: startTimestamp.toString()
|
|
506
|
+
}]
|
|
507
|
+
}],
|
|
508
|
+
properties: ['hs_task_subject', 'hs_task_body', 'hs_task_status', 'hs_lastmodifieddate', 'hs_createdate'],
|
|
509
|
+
sorts: ['hs_lastmodifieddate:desc']
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const allTasks = await this.paginateSearch(
|
|
513
|
+
(req) => this.client.crm.objects.tasks.searchApi.doSearch(req),
|
|
514
|
+
baseRequest,
|
|
515
|
+
limit
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
console.log(`[MCP Debug] Tasks found: ${allTasks.length}`);
|
|
519
|
+
|
|
520
|
+
return convertDatetimeFields(allTasks);
|
|
521
|
+
|
|
522
|
+
} catch (error: any) {
|
|
523
|
+
console.error('Error searching tasks:', error);
|
|
524
|
+
return { error: error.message };
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async searchNotes(minutes: number = 30, limit: number = 50): Promise<any> {
|
|
529
|
+
try {
|
|
530
|
+
// Calculate the date range (past N minutes)
|
|
531
|
+
const endTime = new Date();
|
|
532
|
+
const startTime = new Date(endTime);
|
|
533
|
+
startTime.setMinutes(startTime.getMinutes() - minutes);
|
|
534
|
+
const startTimestamp = startTime.getTime();
|
|
535
|
+
|
|
536
|
+
console.log(`[MCP Debug] Searching notes from ${startTime.toISOString()} to ${endTime.toISOString()}`);
|
|
537
|
+
console.log(`[MCP Debug] Timestamp filter: ${startTimestamp}`);
|
|
538
|
+
|
|
539
|
+
const baseRequest = {
|
|
540
|
+
filterGroups: [{
|
|
541
|
+
filters: [{
|
|
542
|
+
propertyName: 'hs_lastmodifieddate',
|
|
543
|
+
operator: 'GTE' as any,
|
|
544
|
+
value: startTimestamp.toString()
|
|
545
|
+
}]
|
|
546
|
+
}],
|
|
547
|
+
properties: ['hs_note_body', 'hs_lastmodifieddate', 'hs_createdate'],
|
|
548
|
+
sorts: ['hs_lastmodifieddate:desc']
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
const allNotes = await this.paginateSearch(
|
|
552
|
+
(req) => this.client.crm.objects.notes.searchApi.doSearch(req),
|
|
553
|
+
baseRequest,
|
|
554
|
+
limit
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
console.log(`[MCP Debug] Notes found: ${allNotes.length}`);
|
|
558
|
+
return convertDatetimeFields(allNotes);
|
|
559
|
+
|
|
560
|
+
} catch (error: any) {
|
|
561
|
+
console.error('Error searching notes:', error);
|
|
562
|
+
return { error: error.message };
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async searchCalls(minutes: number = 30, limit: number = 50): Promise<any> {
|
|
567
|
+
try {
|
|
568
|
+
// Calculate the date range (past N minutes)
|
|
569
|
+
const endTime = new Date();
|
|
570
|
+
const startTime = new Date(endTime);
|
|
571
|
+
startTime.setMinutes(startTime.getMinutes() - minutes);
|
|
572
|
+
const startTimestamp = startTime.getTime();
|
|
573
|
+
|
|
574
|
+
console.log(`[MCP Debug] Searching calls from ${startTime.toISOString()} to ${endTime.toISOString()}`);
|
|
575
|
+
console.log(`[MCP Debug] Timestamp filter: ${startTimestamp}`);
|
|
576
|
+
|
|
577
|
+
const baseRequest = {
|
|
578
|
+
filterGroups: [{
|
|
579
|
+
filters: [{
|
|
580
|
+
propertyName: 'hs_lastmodifieddate',
|
|
581
|
+
operator: 'GTE' as any,
|
|
582
|
+
value: startTimestamp.toString()
|
|
583
|
+
}]
|
|
584
|
+
}],
|
|
585
|
+
properties: ['hs_call_title', 'hs_call_body', 'hs_call_duration', 'hs_call_status', 'hs_lastmodifieddate', 'hs_createdate'],
|
|
586
|
+
sorts: ['hs_lastmodifieddate:desc']
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
const allCalls = await this.paginateSearch(
|
|
590
|
+
(req) => this.client.crm.objects.calls.searchApi.doSearch(req),
|
|
591
|
+
baseRequest,
|
|
592
|
+
limit
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
console.log(`[MCP Debug] Calls found: ${allCalls.length}`);
|
|
596
|
+
return convertDatetimeFields(allCalls);
|
|
597
|
+
|
|
598
|
+
} catch (error: any) {
|
|
599
|
+
console.error('Error searching calls:', error);
|
|
600
|
+
return { error: error.message };
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async searchEmails(minutes: number = 30, limit: number = 50): Promise<any> {
|
|
605
|
+
try {
|
|
606
|
+
// Calculate the date range (past N minutes)
|
|
607
|
+
const endTime = new Date();
|
|
608
|
+
const startTime = new Date(endTime);
|
|
609
|
+
startTime.setMinutes(startTime.getMinutes() - minutes);
|
|
610
|
+
const startTimestamp = startTime.getTime();
|
|
611
|
+
|
|
612
|
+
console.log(`[MCP Debug] Searching emails from ${startTime.toISOString()} to ${endTime.toISOString()}`);
|
|
613
|
+
console.log(`[MCP Debug] Timestamp filter: ${startTimestamp}`);
|
|
614
|
+
|
|
615
|
+
const baseRequest = {
|
|
616
|
+
filterGroups: [{
|
|
617
|
+
filters: [{
|
|
618
|
+
propertyName: 'hs_lastmodifieddate',
|
|
619
|
+
operator: 'GTE' as any,
|
|
620
|
+
value: startTimestamp.toString()
|
|
621
|
+
}]
|
|
622
|
+
}],
|
|
623
|
+
properties: ['hs_email_subject', 'hs_email_text', 'hs_email_status', 'hs_lastmodifieddate', 'hs_createdate'],
|
|
624
|
+
sorts: ['hs_lastmodifieddate:desc']
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
const allEmails = await this.paginateSearch(
|
|
628
|
+
(req) => this.client.crm.objects.emails.searchApi.doSearch(req),
|
|
629
|
+
baseRequest,
|
|
630
|
+
limit
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
console.log(`[MCP Debug] Emails found: ${allEmails.length}`);
|
|
634
|
+
return convertDatetimeFields(allEmails);
|
|
635
|
+
|
|
636
|
+
} catch (error: any) {
|
|
637
|
+
console.error('Error searching emails:', error);
|
|
638
|
+
return { error: error.message };
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
async searchMeetings(minutes: number = 30, limit: number = 50): Promise<any> {
|
|
643
|
+
try {
|
|
644
|
+
// Calculate the date range (past N minutes)
|
|
645
|
+
const endTime = new Date();
|
|
646
|
+
const startTime = new Date(endTime);
|
|
647
|
+
startTime.setMinutes(startTime.getMinutes() - minutes);
|
|
648
|
+
const startTimestamp = startTime.getTime();
|
|
649
|
+
|
|
650
|
+
console.log(`[MCP Debug] Searching meetings from ${startTime.toISOString()} to ${endTime.toISOString()}`);
|
|
651
|
+
console.log(`[MCP Debug] Timestamp filter: ${startTimestamp}`);
|
|
652
|
+
|
|
653
|
+
const baseRequest = {
|
|
654
|
+
filterGroups: [{
|
|
655
|
+
filters: [{
|
|
656
|
+
propertyName: 'hs_lastmodifieddate',
|
|
657
|
+
operator: 'GTE' as any,
|
|
658
|
+
value: startTimestamp.toString()
|
|
659
|
+
}]
|
|
660
|
+
}],
|
|
661
|
+
properties: ['hs_meeting_title', 'hs_meeting_body', 'hs_meeting_start_time', 'hs_meeting_end_time', 'hs_lastmodifieddate', 'hs_createdate'],
|
|
662
|
+
sorts: ['hs_lastmodifieddate:desc']
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
const allMeetings = await this.paginateSearch(
|
|
666
|
+
(req) => this.client.crm.objects.meetings.searchApi.doSearch(req),
|
|
667
|
+
baseRequest,
|
|
668
|
+
limit
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
console.log(`[MCP Debug] Meetings found: ${allMeetings.length}`);
|
|
672
|
+
return convertDatetimeFields(allMeetings);
|
|
673
|
+
|
|
674
|
+
} catch (error: any) {
|
|
675
|
+
console.error('Error searching meetings:', error);
|
|
676
|
+
return { error: error.message };
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
async getRecentEngagements(limit: number = 50): Promise<any> {
|
|
681
|
+
try {
|
|
682
|
+
await this.rateLimitDelay();
|
|
683
|
+
|
|
684
|
+
console.log(`[MCP Debug] Searching for all engagements`);
|
|
685
|
+
|
|
686
|
+
// Ensure limit doesn't exceed HubSpot's maximum per search
|
|
687
|
+
const safeLimit = Math.min(Math.max(limit, 1), 200);
|
|
688
|
+
|
|
689
|
+
// Get all notes and tasks using CRM v3 API (no time filtering) with safe limits
|
|
690
|
+
const [notesResponse, tasksResponse] = await Promise.allSettled([
|
|
691
|
+
// Get all notes
|
|
692
|
+
this.client.crm.objects.notes.searchApi.doSearch({
|
|
693
|
+
properties: ['hs_note_body', 'hs_lastmodifieddate', 'hs_createdate'],
|
|
694
|
+
sorts: ['hs_lastmodifieddate:desc'],
|
|
695
|
+
limit: Math.floor(safeLimit / 2)
|
|
696
|
+
}),
|
|
697
|
+
// Get all tasks
|
|
698
|
+
this.client.crm.objects.tasks.searchApi.doSearch({
|
|
699
|
+
properties: ['hs_task_subject', 'hs_task_body', 'hs_task_status', 'hs_lastmodifieddate', 'hs_createdate'],
|
|
700
|
+
sorts: ['hs_lastmodifieddate:desc'],
|
|
701
|
+
limit: Math.floor(safeLimit / 2)
|
|
702
|
+
})
|
|
703
|
+
]);
|
|
704
|
+
|
|
705
|
+
console.log(`[MCP Debug] Notes response status: ${notesResponse.status}`);
|
|
706
|
+
console.log(`[MCP Debug] Tasks response status: ${tasksResponse.status}`);
|
|
707
|
+
|
|
708
|
+
if (notesResponse.status === 'rejected') {
|
|
709
|
+
console.error(`[MCP Debug] Notes search failed:`, notesResponse.reason);
|
|
710
|
+
} else {
|
|
711
|
+
console.log(`[MCP Debug] Notes found: ${notesResponse.value?.results?.length || 0}`);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if (tasksResponse.status === 'rejected') {
|
|
715
|
+
console.error(`[MCP Debug] Tasks search failed:`, tasksResponse.reason);
|
|
716
|
+
} else {
|
|
717
|
+
console.log(`[MCP Debug] Tasks found: ${tasksResponse.value?.results?.length || 0}`);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Format the engagements from CRM v3 responses
|
|
721
|
+
const formattedEngagements = [];
|
|
722
|
+
|
|
723
|
+
// Process notes
|
|
724
|
+
if (notesResponse.status === 'fulfilled' && notesResponse.value?.results) {
|
|
725
|
+
for (const note of notesResponse.value.results) {
|
|
726
|
+
formattedEngagements.push({
|
|
727
|
+
id: note.id,
|
|
728
|
+
type: 'NOTE',
|
|
729
|
+
engagement: {
|
|
730
|
+
id: note.id,
|
|
731
|
+
type: 'NOTE',
|
|
732
|
+
createdAt: note.properties?.hs_createdate,
|
|
733
|
+
lastUpdated: note.properties?.hs_lastmodifieddate
|
|
734
|
+
},
|
|
735
|
+
metadata: {
|
|
736
|
+
body: note.properties?.hs_note_body
|
|
737
|
+
},
|
|
738
|
+
properties: note.properties,
|
|
739
|
+
created_at: note.properties?.hs_createdate,
|
|
740
|
+
last_updated: note.properties?.hs_lastmodifieddate
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Process tasks
|
|
746
|
+
if (tasksResponse.status === 'fulfilled' && tasksResponse.value?.results) {
|
|
747
|
+
for (const task of tasksResponse.value.results) {
|
|
748
|
+
formattedEngagements.push({
|
|
749
|
+
id: task.id,
|
|
750
|
+
type: 'TASK',
|
|
751
|
+
engagement: {
|
|
752
|
+
id: task.id,
|
|
753
|
+
type: 'TASK',
|
|
754
|
+
createdAt: task.properties?.hs_createdate,
|
|
755
|
+
lastUpdated: task.properties?.hs_lastmodifieddate
|
|
756
|
+
},
|
|
757
|
+
metadata: {
|
|
758
|
+
subject: task.properties?.hs_task_subject,
|
|
759
|
+
body: task.properties?.hs_task_body,
|
|
760
|
+
status: task.properties?.hs_task_status
|
|
761
|
+
},
|
|
762
|
+
properties: task.properties,
|
|
763
|
+
created_at: task.properties?.hs_createdate,
|
|
764
|
+
last_updated: task.properties?.hs_lastmodifieddate
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Sort by last modified date (most recent first)
|
|
770
|
+
formattedEngagements.sort((a, b) => {
|
|
771
|
+
const aTime = new Date(a.last_updated || a.created_at || 0).getTime();
|
|
772
|
+
const bTime = new Date(b.last_updated || b.created_at || 0).getTime();
|
|
773
|
+
return bTime - aTime;
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
console.log(`[MCP Debug] Returning ${formattedEngagements.length} formatted engagements`);
|
|
777
|
+
|
|
778
|
+
return convertDatetimeFields(formattedEngagements);
|
|
779
|
+
|
|
780
|
+
} catch (error: any) {
|
|
781
|
+
console.error('Error getting recent engagements:', error);
|
|
782
|
+
return { error: error.message };
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
async createContact(
|
|
788
|
+
firstname: string,
|
|
789
|
+
lastname: string,
|
|
790
|
+
email?: string,
|
|
791
|
+
properties?: Record<string, any>
|
|
792
|
+
): Promise<any> {
|
|
793
|
+
try {
|
|
794
|
+
// Search for existing contacts with same name and company
|
|
795
|
+
const company = properties?.company;
|
|
796
|
+
|
|
797
|
+
// Use type assertion to satisfy the HubSpot API client types
|
|
798
|
+
const searchRequest = {
|
|
799
|
+
filterGroups: [{
|
|
800
|
+
filters: [
|
|
801
|
+
{
|
|
802
|
+
propertyName: 'firstname',
|
|
803
|
+
operator: 'EQ',
|
|
804
|
+
value: firstname
|
|
805
|
+
} as any,
|
|
806
|
+
{
|
|
807
|
+
propertyName: 'lastname',
|
|
808
|
+
operator: 'EQ',
|
|
809
|
+
value: lastname
|
|
810
|
+
} as any
|
|
811
|
+
]
|
|
812
|
+
}]
|
|
813
|
+
} as any;
|
|
814
|
+
|
|
815
|
+
// Add company filter if provided
|
|
816
|
+
if (company) {
|
|
817
|
+
searchRequest.filterGroups[0].filters.push({
|
|
818
|
+
propertyName: 'company',
|
|
819
|
+
operator: 'EQ',
|
|
820
|
+
value: company
|
|
821
|
+
} as any);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const searchResponse = await this.client.crm.contacts.searchApi.doSearch(searchRequest);
|
|
825
|
+
|
|
826
|
+
if (searchResponse.total > 0) {
|
|
827
|
+
// Contact already exists
|
|
828
|
+
return {
|
|
829
|
+
message: 'Contact already exists',
|
|
830
|
+
contact: searchResponse.results[0]
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// If no existing contact found, proceed with creation
|
|
835
|
+
const contactProperties: Record<string, any> = {
|
|
836
|
+
firstname,
|
|
837
|
+
lastname
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
// Add email if provided
|
|
841
|
+
if (email) {
|
|
842
|
+
contactProperties.email = email;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Add any additional properties
|
|
846
|
+
if (properties) {
|
|
847
|
+
Object.assign(contactProperties, properties);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Create contact
|
|
851
|
+
const apiResponse = await this.client.crm.contacts.basicApi.create({
|
|
852
|
+
properties: contactProperties
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
return apiResponse;
|
|
856
|
+
} catch (error: any) {
|
|
857
|
+
console.error('Error creating contact:', error);
|
|
858
|
+
throw new Error(`HubSpot API error: ${error.message}`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
async createCompany(name: string, properties?: Record<string, any>): Promise<any> {
|
|
863
|
+
try {
|
|
864
|
+
await this.rateLimitDelay();
|
|
865
|
+
// Search for existing companies with same name
|
|
866
|
+
// Use type assertion to satisfy the HubSpot API client types
|
|
867
|
+
const searchRequest = {
|
|
868
|
+
filterGroups: [{
|
|
869
|
+
filters: [
|
|
870
|
+
{
|
|
871
|
+
propertyName: 'name',
|
|
872
|
+
operator: 'EQ',
|
|
873
|
+
value: name
|
|
874
|
+
} as any
|
|
875
|
+
]
|
|
876
|
+
}]
|
|
877
|
+
} as any;
|
|
878
|
+
|
|
879
|
+
const searchResponse = await this.client.crm.companies.searchApi.doSearch(searchRequest);
|
|
880
|
+
|
|
881
|
+
if (searchResponse.total > 0) {
|
|
882
|
+
// Company already exists
|
|
883
|
+
return {
|
|
884
|
+
message: 'Company already exists',
|
|
885
|
+
company: searchResponse.results[0]
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// If no existing company found, proceed with creation
|
|
890
|
+
const companyProperties: Record<string, any> = {
|
|
891
|
+
name
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
// Add any additional properties
|
|
895
|
+
if (properties) {
|
|
896
|
+
Object.assign(companyProperties, properties);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Create company
|
|
900
|
+
const apiResponse = await this.client.crm.companies.basicApi.create({
|
|
901
|
+
properties: companyProperties
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
return apiResponse;
|
|
905
|
+
} catch (error: any) {
|
|
906
|
+
console.error('Error creating company:', error);
|
|
907
|
+
throw new Error(`HubSpot API error: ${error.message}`);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
async createNote(body: string, associations?: any[]): Promise<any> {
|
|
912
|
+
try {
|
|
913
|
+
await this.rateLimitDelay();
|
|
914
|
+
|
|
915
|
+
// Create note properties
|
|
916
|
+
const noteProperties: Record<string, any> = {
|
|
917
|
+
hs_note_body: body
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
// Create the note using CRM v3 API
|
|
921
|
+
const noteResponse = await this.client.crm.objects.notes.basicApi.create({
|
|
922
|
+
properties: noteProperties,
|
|
923
|
+
associations: []
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
// If associations are provided, create them
|
|
927
|
+
if (associations && associations.length > 0) {
|
|
928
|
+
for (const association of associations) {
|
|
929
|
+
try {
|
|
930
|
+
await this.client.crm.associations.v4.basicApi.create(
|
|
931
|
+
'notes',
|
|
932
|
+
noteResponse.id,
|
|
933
|
+
association.toObjectType,
|
|
934
|
+
association.toObjectId,
|
|
935
|
+
[{ associationCategory: association.category || 'USER_DEFINED', associationTypeId: association.typeId || 1 }]
|
|
936
|
+
);
|
|
937
|
+
} catch (associationError) {
|
|
938
|
+
console.warn('Failed to create association for note:', associationError);
|
|
939
|
+
// Continue with other associations
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
return {
|
|
945
|
+
id: noteResponse.id,
|
|
946
|
+
properties: noteResponse.properties,
|
|
947
|
+
associations: associations || []
|
|
948
|
+
};
|
|
949
|
+
} catch (error: any) {
|
|
950
|
+
console.error('Error creating note:', error);
|
|
951
|
+
throw new Error(`HubSpot API error: ${error.message}`);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
async updateContact(
|
|
956
|
+
contactId: string,
|
|
957
|
+
properties: Record<string, any>
|
|
958
|
+
): Promise<any> {
|
|
959
|
+
try {
|
|
960
|
+
// Check if contact exists
|
|
961
|
+
try {
|
|
962
|
+
await this.client.crm.contacts.basicApi.getById(contactId);
|
|
963
|
+
} catch (error: any) {
|
|
964
|
+
// If contact doesn't exist, return a message
|
|
965
|
+
if (error.statusCode === 404) {
|
|
966
|
+
return {
|
|
967
|
+
message: 'Contact not found, no update performed',
|
|
968
|
+
contactId
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
// For other errors, throw them to be caught by the outer try/catch
|
|
972
|
+
throw error;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Update the contact
|
|
976
|
+
const apiResponse = await this.client.crm.contacts.basicApi.update(contactId, {
|
|
977
|
+
properties
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
return {
|
|
981
|
+
message: 'Contact updated successfully',
|
|
982
|
+
contactId,
|
|
983
|
+
properties
|
|
984
|
+
};
|
|
985
|
+
} catch (error: any) {
|
|
986
|
+
console.error('Error updating contact:', error);
|
|
987
|
+
throw new Error(`HubSpot API error: ${error.message}`);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
async updateCompany(
|
|
992
|
+
companyId: string,
|
|
993
|
+
properties: Record<string, any>
|
|
994
|
+
): Promise<any> {
|
|
995
|
+
try {
|
|
996
|
+
// Check if company exists
|
|
997
|
+
try {
|
|
998
|
+
await this.client.crm.companies.basicApi.getById(companyId);
|
|
999
|
+
} catch (error: any) {
|
|
1000
|
+
// If company doesn't exist, return a message
|
|
1001
|
+
if (error.statusCode === 404) {
|
|
1002
|
+
return {
|
|
1003
|
+
message: 'Company not found, no update performed',
|
|
1004
|
+
companyId
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
// For other errors, throw them to be caught by the outer try/catch
|
|
1008
|
+
throw error;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// Update the company
|
|
1012
|
+
const apiResponse = await this.client.crm.companies.basicApi.update(companyId, {
|
|
1013
|
+
properties
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
return {
|
|
1017
|
+
message: 'Company updated successfully',
|
|
1018
|
+
companyId,
|
|
1019
|
+
properties
|
|
1020
|
+
};
|
|
1021
|
+
} catch (error: any) {
|
|
1022
|
+
console.error('Error updating company:', error);
|
|
1023
|
+
throw new Error(`HubSpot API error: ${error.message}`);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
async getRecentDeals(limit: number = 100, updatedAfter?: string): Promise<any> {
|
|
1028
|
+
try {
|
|
1029
|
+
console.log('Fetching ALL deals with pagination...');
|
|
1030
|
+
|
|
1031
|
+
const baseRequest = {
|
|
1032
|
+
sorts: ['hs_lastmodifieddate:desc'],
|
|
1033
|
+
properties: [
|
|
1034
|
+
'dealname', 'amount', 'dealstage', 'createdate', 'closedate',
|
|
1035
|
+
'hubspot_owner_id', 'dealtype', 'pipeline', 'hs_analytics_source',
|
|
1036
|
+
'hs_deal_stage_probability', 'days_to_close', 'num_contacted_notes'
|
|
1037
|
+
]
|
|
1038
|
+
};
|
|
1039
|
+
|
|
1040
|
+
this.applyUpdatedAfterFilter(baseRequest, updatedAfter, 'hs_lastmodifieddate');
|
|
1041
|
+
|
|
1042
|
+
const allDeals = await this.paginateSearch(
|
|
1043
|
+
(req) => this.client.crm.deals.searchApi.doSearch(req),
|
|
1044
|
+
baseRequest,
|
|
1045
|
+
limit
|
|
1046
|
+
);
|
|
1047
|
+
|
|
1048
|
+
console.log(`?o. Total deals fetched: ${allDeals.length}`);
|
|
1049
|
+
return convertDatetimeFields(allDeals);
|
|
1050
|
+
} catch (error: any) {
|
|
1051
|
+
console.error('Error fetching deals:', error);
|
|
1052
|
+
throw new Error(`HubSpot API error: ${error.message}`);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
async getCurrentUser(): Promise<any> {
|
|
1057
|
+
try {
|
|
1058
|
+
await this.rateLimitDelay();
|
|
1059
|
+
// Use the OAuth v1 access-tokens introspection endpoint (no Authorization header required)
|
|
1060
|
+
// https://api.hubapi.com/oauth/v1/access-tokens/{token}
|
|
1061
|
+
const resp = await fetch(`https://api.hubapi.com/oauth/v1/access-tokens/${this.accessToken}`);
|
|
1062
|
+
|
|
1063
|
+
if (!resp.ok) {
|
|
1064
|
+
// Return a minimal object instead of throwing to avoid noisy errors upstream
|
|
1065
|
+
return {
|
|
1066
|
+
userId: 'unknown',
|
|
1067
|
+
userEmail: 'unknown',
|
|
1068
|
+
hubId: null,
|
|
1069
|
+
scopes: [],
|
|
1070
|
+
error: `HTTP ${resp.status}: ${resp.statusText}`
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
const data = await resp.json();
|
|
1075
|
+
return {
|
|
1076
|
+
userId: data.user_id || data.user || 'unknown',
|
|
1077
|
+
userEmail: data.user_email || data.email || 'unknown',
|
|
1078
|
+
hubId: data.hub_id || data.hubId || null,
|
|
1079
|
+
scopes: data.scopes || []
|
|
1080
|
+
};
|
|
1081
|
+
} catch (error: any) {
|
|
1082
|
+
console.error('Error fetching current user:', error);
|
|
1083
|
+
// Graceful fallback to avoid breaking downstream consumers
|
|
1084
|
+
return {
|
|
1085
|
+
userId: 'unknown',
|
|
1086
|
+
userEmail: 'unknown',
|
|
1087
|
+
hubId: null,
|
|
1088
|
+
scopes: [],
|
|
1089
|
+
error: error?.message || 'unknown'
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
async getRecentTickets(limit: number = 100): Promise<any> {
|
|
1095
|
+
try {
|
|
1096
|
+
console.log('?YZ? Fetching ALL tickets with pagination...');
|
|
1097
|
+
|
|
1098
|
+
const ticketProperties = [
|
|
1099
|
+
'subject', 'content', 'hs_ticket_priority', 'hs_pipeline', 'hs_pipeline_stage',
|
|
1100
|
+
'createdate', 'hs_lastmodifieddate', 'hubspot_owner_id',
|
|
1101
|
+
'hs_ticket_category', 'hs_ticket_id', 'source_type',
|
|
1102
|
+
'closed_date', 'first_agent_reply_date', 'time_to_close', 'time_to_first_agent_reply',
|
|
1103
|
+
'hs_resolution', 'hs_custom_inbox', 'hs_was_imported',
|
|
1104
|
+
'num_associated_conversations', 'num_notes', 'num_contacted_notes'
|
|
1105
|
+
];
|
|
1106
|
+
|
|
1107
|
+
const baseRequest = {
|
|
1108
|
+
sorts: ['hs_lastmodifieddate:desc'],
|
|
1109
|
+
properties: ticketProperties
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
const allTickets = await this.paginateSearch(
|
|
1113
|
+
(req) => this.client.crm.tickets.searchApi.doSearch(req),
|
|
1114
|
+
baseRequest,
|
|
1115
|
+
limit
|
|
1116
|
+
);
|
|
1117
|
+
|
|
1118
|
+
console.log(`?o. Total tickets fetched: ${allTickets.length}`);
|
|
1119
|
+
return convertDatetimeFields(allTickets);
|
|
1120
|
+
} catch (error: any) {
|
|
1121
|
+
console.error('Error fetching tickets:', error);
|
|
1122
|
+
throw new Error(`HubSpot API error: ${error.message}`);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
async getRecentOrders(limit: number = 100, updatedAfter?: string): Promise<any[]> {
|
|
1127
|
+
try {
|
|
1128
|
+
const orderProperties = [
|
|
1129
|
+
'hs_order_name', 'hs_source_store', 'hs_fulfillment_status',
|
|
1130
|
+
'hs_currency_code', 'hs_total_price', 'hs_shipping_price',
|
|
1131
|
+
'hs_tax_price', 'hs_discount_price', 'hs_createdate', 'hs_lastmodifieddate',
|
|
1132
|
+
'hs_external_order_id', 'hs_order_status', 'hs_shipping_address_city',
|
|
1133
|
+
'hubspot_owner_id', 'hs_payment_status', 'hs_order_currency_code'
|
|
1134
|
+
];
|
|
1135
|
+
const searchRequest = {
|
|
1136
|
+
sorts: ['hs_lastmodifieddate:desc'],
|
|
1137
|
+
properties: orderProperties
|
|
1138
|
+
};
|
|
1139
|
+
|
|
1140
|
+
if (updatedAfter) {
|
|
1141
|
+
this.applyUpdatedAfterFilter(searchRequest, updatedAfter, 'hs_lastmodifieddate');
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// Note: Orders might not be available in all HubSpot portals
|
|
1145
|
+
// This depends on having Commerce Hub or ecommerce integrations
|
|
1146
|
+
const allOrders = await this.paginateSearch(
|
|
1147
|
+
(req) => this.client.crm.objects.searchApi.doSearch('orders', req),
|
|
1148
|
+
searchRequest,
|
|
1149
|
+
limit
|
|
1150
|
+
);
|
|
1151
|
+
return convertDatetimeFields(allOrders);
|
|
1152
|
+
} catch (error: any) {
|
|
1153
|
+
console.error('Error fetching orders:', error);
|
|
1154
|
+
// Return empty array if orders are not available rather than throwing
|
|
1155
|
+
return [];
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
async getOrders(limit: number = 100, updatedAfter?: string): Promise<any[]> {
|
|
1160
|
+
return this.getRecentOrders(limit, updatedAfter);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
|
|
1164
|
+
async getContactById(contactId: string): Promise<any> {
|
|
1165
|
+
try {
|
|
1166
|
+
await this.rateLimitDelay();
|
|
1167
|
+
const contact = await this.client.crm.contacts.basicApi.getById(contactId, [
|
|
1168
|
+
// Basic info - enhanced
|
|
1169
|
+
'firstname', 'lastname', 'email', 'phone', 'mobilephone', 'jobtitle',
|
|
1170
|
+
'salutation', 'middle_name', 'prefix', 'suffix',
|
|
1171
|
+
// Company info - comprehensive
|
|
1172
|
+
'company', 'website', 'company_name', 'company_website',
|
|
1173
|
+
'company_domain', 'company_industry', 'company_size',
|
|
1174
|
+
// Address info - comprehensive
|
|
1175
|
+
'address', 'address2', 'city', 'state', 'zip', 'country', 'country_code',
|
|
1176
|
+
// Contact properties - expanded
|
|
1177
|
+
'lifecyclestage', 'lead_status', 'hs_lead_status', 'contact_owner',
|
|
1178
|
+
'hs_contact_stage', 'hs_contact_status', 'hs_contact_source',
|
|
1179
|
+
// Company association and details - enhanced
|
|
1180
|
+
'associatedcompanyid', 'associatedcompany_name', 'associatedcompany_domain',
|
|
1181
|
+
'associatedcompany_website', 'associatedcompany_industry', 'associatedcompany_size',
|
|
1182
|
+
// Social media and online presence - comprehensive
|
|
1183
|
+
'linkedin_profile', 'twitter_profile', 'facebook_profile',
|
|
1184
|
+
'linkedin_url', 'twitter_url', 'facebook_url', 'instagram_url', 'youtube_url',
|
|
1185
|
+
// Professional details - expanded
|
|
1186
|
+
'seniority', 'department', 'industry', 'job_function', 'job_role',
|
|
1187
|
+
'job_level', 'years_experience', 'education_level', 'school',
|
|
1188
|
+
// Lead scoring and qualification - comprehensive
|
|
1189
|
+
'hs_lead_scoring', 'hs_lead_status', 'hs_analytics_source',
|
|
1190
|
+
'hs_analytics_source_data_1', 'hs_analytics_source_data_2', 'hs_analytics_source_data_3',
|
|
1191
|
+
'hs_analytics_source_data_4', 'hs_analytics_source_raw',
|
|
1192
|
+
'hs_predictivecontactscore_v2', 'hs_predictivecontactscoring_v2',
|
|
1193
|
+
// Custom properties - expanded
|
|
1194
|
+
'custom_property_1', 'custom_property_2', 'custom_property_3', 'custom_property_4', 'custom_property_5',
|
|
1195
|
+
'custom_property_6', 'custom_property_7', 'custom_property_8', 'custom_property_9', 'custom_property_10',
|
|
1196
|
+
// Dates and timestamps - comprehensive
|
|
1197
|
+
'createdate', 'hs_lastmodifieddate', 'lastmodifieddate', 'hs_createdate',
|
|
1198
|
+
'first_conversion_date', 'last_conversion_date', 'hs_first_engagement_date',
|
|
1199
|
+
'hs_last_contacted', 'hs_last_activity_date', 'hs_first_contact_createdate',
|
|
1200
|
+
// Additional fields - enhanced
|
|
1201
|
+
'hs_object_id', 'hs_unique_creation_key', 'hs_contact_id', 'id',
|
|
1202
|
+
'is_deleted', 'hs_time_to_first_engagement', 'hs_contact_stage_probability'
|
|
1203
|
+
]);
|
|
1204
|
+
return convertDatetimeFields(contact);
|
|
1205
|
+
} catch (error: any) {
|
|
1206
|
+
console.error('Error fetching contact by ID:', error);
|
|
1207
|
+
throw new Error(`HubSpot API error: ${error.message}`);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
async getCompanyById(companyId: string): Promise<any> {
|
|
1212
|
+
try {
|
|
1213
|
+
await this.rateLimitDelay();
|
|
1214
|
+
const company = await this.client.crm.companies.basicApi.getById(companyId, [
|
|
1215
|
+
// Basic company info - enhanced
|
|
1216
|
+
'name', 'domain', 'website', 'phone', 'industry', 'company_type',
|
|
1217
|
+
// Address info - comprehensive
|
|
1218
|
+
'address', 'address2', 'city', 'state', 'zip', 'country', 'country_code',
|
|
1219
|
+
// Company details - expanded
|
|
1220
|
+
'description', 'numberofemployees', 'annualrevenue', 'type',
|
|
1221
|
+
'founded_year', 'business_type', 'revenue_range', 'employee_range',
|
|
1222
|
+
'target_account', 'ideal_customer_profile', 'account_tier',
|
|
1223
|
+
// Contact info
|
|
1224
|
+
'owneremail', 'ownername', 'phone', 'owner_phone',
|
|
1225
|
+
// Website and online presence - comprehensive
|
|
1226
|
+
'website', 'domain', 'blog_url', 'facebook_url', 'twitter_url',
|
|
1227
|
+
'linkedin_url', 'youtube_url', 'instagram_url', 'pinterest_url',
|
|
1228
|
+
'googleplus_url', 'klout_score', 'alexa_ranking', 'semrush_ranking',
|
|
1229
|
+
// Company associations and details
|
|
1230
|
+
'hs_parent_company_id', 'hs_child_company_ids', 'hs_analytics_source',
|
|
1231
|
+
'hs_analytics_source_data_1', 'hs_analytics_source_data_2', 'hs_analytics_source_data_3',
|
|
1232
|
+
'hs_analytics_source_data_4', 'hs_analytics_source_raw',
|
|
1233
|
+
// Custom properties - expanded
|
|
1234
|
+
'custom_property_1', 'custom_property_2', 'custom_property_3', 'custom_property_4', 'custom_property_5',
|
|
1235
|
+
'custom_property_6', 'custom_property_7', 'custom_property_8', 'custom_property_9', 'custom_property_10',
|
|
1236
|
+
// Dates and timestamps - comprehensive
|
|
1237
|
+
'createdate', 'hs_lastmodifieddate', 'lastmodifieddate', 'hs_createdate',
|
|
1238
|
+
'first_conversion_date', 'last_conversion_date', 'hs_first_engagement_date',
|
|
1239
|
+
'hs_last_contacted', 'hs_last_activity_date', 'hs_first_contact_createdate',
|
|
1240
|
+
// Additional fields - enhanced
|
|
1241
|
+
'hs_object_id', 'hs_unique_creation_key', 'hs_company_id', 'id',
|
|
1242
|
+
'is_deleted', 'hs_time_to_first_engagement', 'hs_time_to_close',
|
|
1243
|
+
// Lead status and scoring
|
|
1244
|
+
'hs_lead_status', 'hs_lead_scoring', 'hs_predictivecontactscore_v2',
|
|
1245
|
+
'hs_predictivecontactscoring_v2', 'hs_ideal_customer_profile'
|
|
1246
|
+
]);
|
|
1247
|
+
return convertDatetimeFields(company);
|
|
1248
|
+
} catch (error: any) {
|
|
1249
|
+
console.error('Error fetching company by ID:', error);
|
|
1250
|
+
throw new Error(`HubSpot API error: ${error.message}`);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
async getDealById(dealId: string): Promise<any> {
|
|
1255
|
+
try {
|
|
1256
|
+
await this.rateLimitDelay();
|
|
1257
|
+
const deal = await this.client.crm.deals.basicApi.getById(dealId, [
|
|
1258
|
+
'dealname', 'amount', 'dealstage', 'pipeline', 'closedate', 'hs_lastmodifieddate', 'createdate'
|
|
1259
|
+
]);
|
|
1260
|
+
return convertDatetimeFields(deal);
|
|
1261
|
+
} catch (error: any) {
|
|
1262
|
+
console.error('Error fetching deal by ID:', error);
|
|
1263
|
+
throw new Error(`HubSpot API error: ${error.message}`);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
async getTicketById(ticketId: string): Promise<any> {
|
|
1268
|
+
try {
|
|
1269
|
+
await this.rateLimitDelay();
|
|
1270
|
+
const ticket = await this.client.crm.objects.basicApi.getById('tickets', ticketId, [
|
|
1271
|
+
'subject', 'content', 'hs_ticket_priority', 'hs_ticket_category', 'hs_ticket_status', 'hs_lastmodifieddate', 'createdate'
|
|
1272
|
+
]);
|
|
1273
|
+
return convertDatetimeFields(ticket);
|
|
1274
|
+
} catch (error: any) {
|
|
1275
|
+
console.error('Error fetching ticket by ID:', error);
|
|
1276
|
+
throw new Error(`HubSpot API error: ${error.message}`);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
// Helper method for graceful error handling of optional scopes
|
|
1282
|
+
private handleOptionalScopeError(error: any, scopeName: string, fallbackValue: any = []): any {
|
|
1283
|
+
const errorMessage = error.message?.toLowerCase() || '';
|
|
1284
|
+
const isPermissionError = errorMessage.includes('scope') ||
|
|
1285
|
+
errorMessage.includes('permission') ||
|
|
1286
|
+
errorMessage.includes('unauthorized') ||
|
|
1287
|
+
errorMessage.includes('forbidden') ||
|
|
1288
|
+
error.status === 403 ||
|
|
1289
|
+
error.status === 401;
|
|
1290
|
+
|
|
1291
|
+
if (isPermissionError) {
|
|
1292
|
+
console.error(`HubSpot scope missing: ${scopeName}. Unable to complete API call.`, {
|
|
1293
|
+
scope: scopeName,
|
|
1294
|
+
status: error?.status,
|
|
1295
|
+
category: error?.body?.category,
|
|
1296
|
+
message: error?.message
|
|
1297
|
+
});
|
|
1298
|
+
throw new HubSpotScopeError(scopeName, error);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// Re-throw non-permission errors
|
|
1302
|
+
throw error;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// Marketing Hub Methods
|
|
1306
|
+
async getForms(limit: number = 100): Promise<any[]> {
|
|
1307
|
+
try {
|
|
1308
|
+
console.log('đ Fetching ALL forms with pagination...');
|
|
1309
|
+
const allForms = await this.paginateList(
|
|
1310
|
+
// Use the dedicated v3 marketing forms API client
|
|
1311
|
+
(params) => this.client.marketing.forms.formsApi.getPage(
|
|
1312
|
+
params.after,
|
|
1313
|
+
params.limit
|
|
1314
|
+
),
|
|
1315
|
+
limit
|
|
1316
|
+
);
|
|
1317
|
+
console.log(`â
Total forms fetched: ${allForms.length}`);
|
|
1318
|
+
return allForms;
|
|
1319
|
+
} catch (error: any) {
|
|
1320
|
+
console.error('Error fetching forms:', error);
|
|
1321
|
+
return this.handleOptionalScopeError(error, 'forms', []);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
async getFormSubmissions(formId: string, limit: number = 100): Promise<any[]> {
|
|
1326
|
+
try {
|
|
1327
|
+
await this.rateLimitDelay();
|
|
1328
|
+
const response = await this.client.apiRequest({
|
|
1329
|
+
method: 'GET',
|
|
1330
|
+
path: `/forms/v2/submissions/forms/${formId}?limit=${limit}`
|
|
1331
|
+
});
|
|
1332
|
+
const data = await response.json();
|
|
1333
|
+
return data?.results || [];
|
|
1334
|
+
} catch (error: any) {
|
|
1335
|
+
console.error('Error fetching form submissions:', error);
|
|
1336
|
+
return this.handleOptionalScopeError(error, 'forms', []);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
async getMarketingEmails(limit: number = 100, updatedAfter?: string): Promise<any[]> {
|
|
1341
|
+
try {
|
|
1342
|
+
console.log('đ§ Fetching ALL marketing emails with pagination (v3)...');
|
|
1343
|
+
|
|
1344
|
+
const normalizedFilter = this.normalizeDateInput(updatedAfter);
|
|
1345
|
+
const allEmails = await this.paginateList(
|
|
1346
|
+
async (params) => {
|
|
1347
|
+
const query = new URLSearchParams();
|
|
1348
|
+
query.append('limit', params.limit.toString());
|
|
1349
|
+
if (params.after) {
|
|
1350
|
+
query.append('after', params.after);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
const path = `/marketing/v3/emails?${query.toString()}`;
|
|
1354
|
+
const response = await this.client.apiRequest({ method: 'GET', path });
|
|
1355
|
+
return await response.json();
|
|
1356
|
+
},
|
|
1357
|
+
limit
|
|
1358
|
+
);
|
|
1359
|
+
|
|
1360
|
+
const normalizedEmails = convertDatetimeFields(allEmails);
|
|
1361
|
+
const filteredEmails = filterRecordsByUpdatedAfter(
|
|
1362
|
+
normalizedEmails,
|
|
1363
|
+
normalizedFilter || undefined,
|
|
1364
|
+
(email) => email?.updatedAt,
|
|
1365
|
+
'marketing emails'
|
|
1366
|
+
);
|
|
1367
|
+
|
|
1368
|
+
console.log(`â
Total marketing emails fetched: ${filteredEmails.length}`);
|
|
1369
|
+
return filteredEmails;
|
|
1370
|
+
} catch (error: any) {
|
|
1371
|
+
console.error('Error fetching marketing emails:', error);
|
|
1372
|
+
return this.handleOptionalScopeError(error, 'marketing-email', []);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
async getEmailEvents(limit: number = 100): Promise<any[]> {
|
|
1377
|
+
try {
|
|
1378
|
+
console.log('đ Fetching marketing email events...');
|
|
1379
|
+
const allEvents = await this.paginateList(
|
|
1380
|
+
async (params) => {
|
|
1381
|
+
const path = `/marketing/v3/marketing-events/events?limit=${params.limit}${params.after ? `&after=${params.after}` : ''}`;
|
|
1382
|
+
const response = await this.client.apiRequest({ method: 'GET', path });
|
|
1383
|
+
return await response.json();
|
|
1384
|
+
},
|
|
1385
|
+
limit
|
|
1386
|
+
);
|
|
1387
|
+
console.log(`â
Total email events fetched: ${allEvents.length}`);
|
|
1388
|
+
return allEvents;
|
|
1389
|
+
} catch (error: any) {
|
|
1390
|
+
console.error('Error fetching email events:', error);
|
|
1391
|
+
return this.handleOptionalScopeError(error, 'crm.objects.marketing_events.read', []);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
async getCampaigns(limit: number = 100): Promise<any[]> {
|
|
1396
|
+
try {
|
|
1397
|
+
console.log('đĸ Fetching ALL campaigns with pagination...');
|
|
1398
|
+
const allCampaigns = await this.paginateList(
|
|
1399
|
+
async (params) => {
|
|
1400
|
+
const path = `/marketing/v3/campaigns?limit=${params.limit}${params.after ? `&after=${params.after}` : ''}`;
|
|
1401
|
+
const response = await this.client.apiRequest({ method: 'GET', path });
|
|
1402
|
+
return await response.json();
|
|
1403
|
+
},
|
|
1404
|
+
limit
|
|
1405
|
+
);
|
|
1406
|
+
console.log(`â
Total campaigns fetched: ${allCampaigns.length}`);
|
|
1407
|
+
return allCampaigns;
|
|
1408
|
+
} catch (error: any) {
|
|
1409
|
+
console.error('Error fetching campaigns:', error);
|
|
1410
|
+
return this.handleOptionalScopeError(error, 'marketing.campaigns.read', []);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
async getAnalytics(startDate: string, endDate: string): Promise<any> {
|
|
1415
|
+
try {
|
|
1416
|
+
await this.rateLimitDelay();
|
|
1417
|
+
const path = `/analytics/v3/reports/domains/total-traffic?start_date=${startDate}&end_date=${endDate}`;
|
|
1418
|
+
|
|
1419
|
+
const response = await this.client.apiRequest({
|
|
1420
|
+
method: 'GET',
|
|
1421
|
+
path: path
|
|
1422
|
+
});
|
|
1423
|
+
return await response.json();
|
|
1424
|
+
} catch (error: any) {
|
|
1425
|
+
console.error('Error fetching analytics:', error);
|
|
1426
|
+
return this.handleOptionalScopeError(error, 'analytics.behavioral_events.send', {});
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
async getBlogPosts(limit: number = 100): Promise<any[]> {
|
|
1431
|
+
try {
|
|
1432
|
+
console.log('đ Fetching ALL blog posts with pagination...');
|
|
1433
|
+
const allPosts = await this.paginateList(
|
|
1434
|
+
async (params) => {
|
|
1435
|
+
const path = `/cms/v3/blogs/posts?limit=${params.limit}${params.after ? `&after=${params.after}` : ''}`;
|
|
1436
|
+
const response = await this.client.apiRequest({ method: 'GET', path });
|
|
1437
|
+
return await response.json();
|
|
1438
|
+
},
|
|
1439
|
+
limit
|
|
1440
|
+
);
|
|
1441
|
+
console.log(`â
Total blog posts fetched: ${allPosts.length}`);
|
|
1442
|
+
return allPosts;
|
|
1443
|
+
} catch (error: any) {
|
|
1444
|
+
console.error('Error fetching blog posts:', error);
|
|
1445
|
+
return this.handleOptionalScopeError(error, 'content', []);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
async getLandingPages(limit: number = 100): Promise<any[]> {
|
|
1450
|
+
try {
|
|
1451
|
+
console.log('đ Fetching ALL landing pages with pagination...');
|
|
1452
|
+
const allPages = await this.paginateList(
|
|
1453
|
+
async (params) => {
|
|
1454
|
+
const path = `/cms/v3/pages/landing-pages?limit=${params.limit}${params.after ? `&after=${params.after}` : ''}`;
|
|
1455
|
+
const response = await this.client.apiRequest({ method: 'GET', path });
|
|
1456
|
+
return await response.json();
|
|
1457
|
+
},
|
|
1458
|
+
limit
|
|
1459
|
+
);
|
|
1460
|
+
console.log(`â
Total landing pages fetched: ${allPages.length}`);
|
|
1461
|
+
return allPages;
|
|
1462
|
+
} catch (error: any) {
|
|
1463
|
+
console.error('Error fetching landing pages:', error);
|
|
1464
|
+
return this.handleOptionalScopeError(error, 'content', []);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
async getKnowledgeBase(limit: number = 100): Promise<any[]> {
|
|
1468
|
+
try {
|
|
1469
|
+
console.log('đ Fetching ALL knowledge base articles with pagination...');
|
|
1470
|
+
const allArticles = await this.paginateList(
|
|
1471
|
+
async (params) => {
|
|
1472
|
+
const path = `/cms/v3/knowledge-base/articles?limit=${params.limit}${params.after ? `&after=${params.after}` : ''}`;
|
|
1473
|
+
const response = await this.client.apiRequest({ method: 'GET', path });
|
|
1474
|
+
return await response.json();
|
|
1475
|
+
},
|
|
1476
|
+
limit
|
|
1477
|
+
);
|
|
1478
|
+
console.log(`â
Total knowledge base articles fetched: ${allArticles.length}`);
|
|
1479
|
+
return allArticles;
|
|
1480
|
+
} catch (error: any) {
|
|
1481
|
+
console.error('Error fetching knowledge base:', error);
|
|
1482
|
+
return this.handleOptionalScopeError(error, 'cms.knowledge_base.articles.read', []);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// Sales Hub Methods
|
|
1487
|
+
async getPipelines(objectType: string = 'deals'): Promise<any[]> {
|
|
1488
|
+
try {
|
|
1489
|
+
await this.rateLimitDelay();
|
|
1490
|
+
// Use the dedicated SDK method for pipelines
|
|
1491
|
+
const response = await this.client.crm.pipelines.pipelinesApi.getAll(objectType);
|
|
1492
|
+
return response.results || [];
|
|
1493
|
+
} catch (error: any) {
|
|
1494
|
+
console.error(`Error fetching pipelines for ${objectType}:`, error);
|
|
1495
|
+
// Handle cases where the object type might not support pipelines
|
|
1496
|
+
if (error.code === 404) {
|
|
1497
|
+
console.warn(`Pipelines for object type '${objectType}' not found.`);
|
|
1498
|
+
return [];
|
|
1499
|
+
}
|
|
1500
|
+
return this.handleOptionalScopeError(error, 'crm.pipelines.read', []);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
async getProperties(objectType: string): Promise<any[]> {
|
|
1505
|
+
if (!objectType) {
|
|
1506
|
+
console.error('Error in getProperties: objectType was not provided.');
|
|
1507
|
+
return [];
|
|
1508
|
+
}
|
|
1509
|
+
try {
|
|
1510
|
+
await this.rateLimitDelay();
|
|
1511
|
+
// Use the dedicated SDK method for properties
|
|
1512
|
+
const response = await this.client.crm.properties.coreApi.getAll(objectType);
|
|
1513
|
+
return response.results || [];
|
|
1514
|
+
} catch (error: any) {
|
|
1515
|
+
console.error(`Error fetching properties for ${objectType}:`, error);
|
|
1516
|
+
return this.handleOptionalScopeError(error, 'crm.properties.read', []);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
async getQuotes(limit: number = 100, updatedAfter?: string): Promise<any[]> {
|
|
1521
|
+
try {
|
|
1522
|
+
console.log('đ° Fetching ALL quotes with pagination...');
|
|
1523
|
+
|
|
1524
|
+
const quoteProperties = [
|
|
1525
|
+
'hs_title', 'hs_expiration_date', 'hs_status', 'hs_quote_amount',
|
|
1526
|
+
'hs_sender_firstname', 'hs_sender_lastname', 'hs_sender_email',
|
|
1527
|
+
'hs_esign_enabled', 'hs_payment_enabled', 'hs_template_id',
|
|
1528
|
+
'createdate', 'hs_lastmodifieddate', 'hubspot_owner_id',
|
|
1529
|
+
'hs_quote_link', 'hs_pdf_download_link', 'hs_domain'
|
|
1530
|
+
];
|
|
1531
|
+
|
|
1532
|
+
if (updatedAfter) {
|
|
1533
|
+
const searchRequest = {
|
|
1534
|
+
sorts: ['hs_lastmodifieddate:desc'],
|
|
1535
|
+
properties: quoteProperties
|
|
1536
|
+
};
|
|
1537
|
+
this.applyUpdatedAfterFilter(searchRequest, updatedAfter, 'hs_lastmodifieddate');
|
|
1538
|
+
const incrementalQuotes = await this.paginateSearch(
|
|
1539
|
+
(req) => this.client.crm.quotes.searchApi.doSearch(req),
|
|
1540
|
+
searchRequest,
|
|
1541
|
+
limit
|
|
1542
|
+
);
|
|
1543
|
+
console.log(`â
Total quotes fetched (incremental): ${incrementalQuotes.length}`);
|
|
1544
|
+
return convertDatetimeFields(incrementalQuotes);
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
const allQuotes = await this.paginateList(
|
|
1548
|
+
async (params) => {
|
|
1549
|
+
const props = quoteProperties.join(',');
|
|
1550
|
+
const path = `/crm/v3/objects/quotes?limit=${params.limit}&properties=${props}${params.after ? `&after=${params.after}` : ''}`;
|
|
1551
|
+
const response = await this.client.apiRequest({ method: 'GET', path });
|
|
1552
|
+
return await response.json();
|
|
1553
|
+
},
|
|
1554
|
+
limit
|
|
1555
|
+
);
|
|
1556
|
+
console.log(`â
Total quotes fetched: ${allQuotes.length}`);
|
|
1557
|
+
return allQuotes;
|
|
1558
|
+
} catch (error: any) {
|
|
1559
|
+
console.error('Error fetching quotes:', error);
|
|
1560
|
+
return this.handleOptionalScopeError(error, 'crm.objects.quotes.read', []);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
async getUsers(limit: number = 100): Promise<any[]> {
|
|
1565
|
+
try {
|
|
1566
|
+
console.log('đĨ Fetching ALL users with pagination...');
|
|
1567
|
+
const allUsers = await this.paginateList(
|
|
1568
|
+
// Use the dedicated SDK method for users
|
|
1569
|
+
(params) => this.client.settings.users.usersApi.getPage(
|
|
1570
|
+
params.limit,
|
|
1571
|
+
params.after
|
|
1572
|
+
),
|
|
1573
|
+
limit
|
|
1574
|
+
);
|
|
1575
|
+
console.log(`â
Total users fetched: ${allUsers.length}`);
|
|
1576
|
+
return allUsers;
|
|
1577
|
+
} catch (error: any) {
|
|
1578
|
+
console.error('Error fetching users:', error);
|
|
1579
|
+
// This scope is usually available, but we can handle errors just in case.
|
|
1580
|
+
return this.handleOptionalScopeError(error, 'settings.users.read', []);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
async getOwners(limit: number = 100): Promise<any[]> {
|
|
1585
|
+
try {
|
|
1586
|
+
console.log('đ§âđŧ Fetching ALL owners with pagination...');
|
|
1587
|
+
const allOwners = await this.paginateList(
|
|
1588
|
+
(params) => this.client.crm.owners.ownersApi.getPage(
|
|
1589
|
+
undefined,
|
|
1590
|
+
params.after,
|
|
1591
|
+
params.limit,
|
|
1592
|
+
false
|
|
1593
|
+
),
|
|
1594
|
+
limit
|
|
1595
|
+
);
|
|
1596
|
+
console.log(`â
Total owners fetched: ${allOwners.length}`);
|
|
1597
|
+
return allOwners;
|
|
1598
|
+
} catch (error: any) {
|
|
1599
|
+
console.error('Error fetching owners:', error);
|
|
1600
|
+
return this.handleOptionalScopeError(error, 'crm.objects.owners.read', []);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// Service Hub Methods
|
|
1605
|
+
async getConversations(limit: number = 100): Promise<any[]> {
|
|
1606
|
+
try {
|
|
1607
|
+
console.log('đŦ Fetching ALL conversations with pagination...');
|
|
1608
|
+
const allConversations = await this.paginateList(
|
|
1609
|
+
async (params) => {
|
|
1610
|
+
const path = `/conversations/v3/conversations/threads?limit=${params.limit}${params.after ? `&after=${params.after}` : ''}`;
|
|
1611
|
+
const response = await this.client.apiRequest({ method: 'GET', path });
|
|
1612
|
+
return await response.json();
|
|
1613
|
+
},
|
|
1614
|
+
limit
|
|
1615
|
+
);
|
|
1616
|
+
console.log(`â
Total conversations fetched: ${allConversations.length}`);
|
|
1617
|
+
return allConversations;
|
|
1618
|
+
} catch (error: any) {
|
|
1619
|
+
console.error('Error fetching conversations:', error);
|
|
1620
|
+
return this.handleOptionalScopeError(error, 'conversations.read', []);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
|
|
1625
|
+
async getCalls(limit: number = 100, updatedAfter?: string): Promise<any[]> {
|
|
1626
|
+
try {
|
|
1627
|
+
await this.rateLimitDelay();
|
|
1628
|
+
const callProperties = [
|
|
1629
|
+
'hs_call_title', 'hs_call_body', 'hs_call_status', 'hs_call_direction',
|
|
1630
|
+
'hs_call_disposition', 'hs_call_duration', 'hs_timestamp',
|
|
1631
|
+
'hs_call_from_number', 'hs_call_to_number', 'hs_call_recording_url',
|
|
1632
|
+
'createdate', 'hs_lastmodifieddate', 'hubspot_owner_id',
|
|
1633
|
+
'hs_call_callee_object_id', 'hs_call_callee_object_type'
|
|
1634
|
+
];
|
|
1635
|
+
const searchRequest = {
|
|
1636
|
+
sorts: ['hs_timestamp:desc'],
|
|
1637
|
+
limit,
|
|
1638
|
+
properties: callProperties
|
|
1639
|
+
};
|
|
1640
|
+
if (updatedAfter) {
|
|
1641
|
+
this.applyUpdatedAfterFilter(searchRequest, updatedAfter, 'hs_lastmodifieddate');
|
|
1642
|
+
}
|
|
1643
|
+
const response = await this.client.crm.objects.searchApi.doSearch('calls', searchRequest);
|
|
1644
|
+
const results = convertDatetimeFields(response.results || []);
|
|
1645
|
+
console.log(`Total calls fetched: ${results.length}`);
|
|
1646
|
+
return results;
|
|
1647
|
+
} catch (error: any) {
|
|
1648
|
+
console.error('Error fetching calls:', error);
|
|
1649
|
+
return this.handleOptionalScopeError(error, 'crm.objects.calls.read', []);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
async getMeetings(limit: number = 100, updatedAfter?: string): Promise<any[]> {
|
|
1654
|
+
try {
|
|
1655
|
+
await this.rateLimitDelay();
|
|
1656
|
+
const meetingProperties = [
|
|
1657
|
+
'hs_meeting_title', 'hs_meeting_body', 'hs_meeting_outcome',
|
|
1658
|
+
'hs_meeting_start_time', 'hs_meeting_end_time', 'hs_timestamp',
|
|
1659
|
+
'hs_meeting_external_url', 'hs_meeting_location', 'hs_meeting_source',
|
|
1660
|
+
'createdate', 'hs_lastmodifieddate', 'hubspot_owner_id',
|
|
1661
|
+
'hs_internal_meeting_notes', 'hs_meeting_ms_teams_payload'
|
|
1662
|
+
];
|
|
1663
|
+
const searchRequest = {
|
|
1664
|
+
sorts: ['hs_meeting_start_time:desc'],
|
|
1665
|
+
limit,
|
|
1666
|
+
properties: meetingProperties
|
|
1667
|
+
};
|
|
1668
|
+
if (updatedAfter) {
|
|
1669
|
+
this.applyUpdatedAfterFilter(searchRequest, updatedAfter, 'hs_lastmodifieddate');
|
|
1670
|
+
}
|
|
1671
|
+
const response = await this.client.crm.objects.searchApi.doSearch('meetings', searchRequest);
|
|
1672
|
+
const results = convertDatetimeFields(response.results || []);
|
|
1673
|
+
console.log(`Total meetings fetched: ${results.length}`);
|
|
1674
|
+
return results;
|
|
1675
|
+
} catch (error: any) {
|
|
1676
|
+
console.error('Error fetching meetings:', error);
|
|
1677
|
+
return this.handleOptionalScopeError(error, 'crm.objects.meetings.read', []);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
async getCustomObjects(objectType?: string, limit: number = 100): Promise<any[]> {
|
|
1682
|
+
const resolvedTypes: string[] = [];
|
|
1683
|
+
const schemaPropertiesByType = new Map<string, string[]>();
|
|
1684
|
+
|
|
1685
|
+
if (objectType && objectType !== 'custom_objects') {
|
|
1686
|
+
resolvedTypes.push(objectType);
|
|
1687
|
+
} else {
|
|
1688
|
+
try {
|
|
1689
|
+
console.log('Discovering custom object schemas...');
|
|
1690
|
+
await this.rateLimitDelay();
|
|
1691
|
+
const schemaResponse = await this.client.crm.schemas.coreApi.getAll();
|
|
1692
|
+
const customSchemas =
|
|
1693
|
+
schemaResponse?.results?.filter(schema =>
|
|
1694
|
+
typeof schema?.objectTypeId === 'string' &&
|
|
1695
|
+
schema.objectTypeId.startsWith('2-')
|
|
1696
|
+
) || [];
|
|
1697
|
+
|
|
1698
|
+
for (const schema of customSchemas) {
|
|
1699
|
+
const typeId = schema.objectTypeId || schema.name;
|
|
1700
|
+
if (typeId) {
|
|
1701
|
+
resolvedTypes.push(typeId);
|
|
1702
|
+
const propertyNames = Array.isArray(schema.properties)
|
|
1703
|
+
? schema.properties
|
|
1704
|
+
.map((property) => property?.name)
|
|
1705
|
+
.filter((name): name is string => Boolean(name))
|
|
1706
|
+
: [];
|
|
1707
|
+
if (propertyNames.length) {
|
|
1708
|
+
schemaPropertiesByType.set(typeId, propertyNames);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
if (resolvedTypes.length === 0) {
|
|
1714
|
+
console.log('No custom object schemas found for this portal.');
|
|
1715
|
+
return [];
|
|
1716
|
+
}
|
|
1717
|
+
} catch (error: any) {
|
|
1718
|
+
return this.handleOptionalScopeError(error, 'crm.schemas.custom.read', []);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
try {
|
|
1723
|
+
const aggregated: any[] = [];
|
|
1724
|
+
|
|
1725
|
+
for (const typeId of resolvedTypes) {
|
|
1726
|
+
console.log(`Fetching custom objects for type ${typeId}...`);
|
|
1727
|
+
try {
|
|
1728
|
+
const objectsForType = await this.paginateList(
|
|
1729
|
+
(params) =>
|
|
1730
|
+
this.client.crm.objects.basicApi.getPage(
|
|
1731
|
+
typeId,
|
|
1732
|
+
params.limit,
|
|
1733
|
+
params.after,
|
|
1734
|
+
schemaPropertiesByType.get(typeId)
|
|
1735
|
+
),
|
|
1736
|
+
limit
|
|
1737
|
+
);
|
|
1738
|
+
|
|
1739
|
+
const tagged = objectsForType.map(obj => ({
|
|
1740
|
+
...obj,
|
|
1741
|
+
objectTypeId: typeId
|
|
1742
|
+
}));
|
|
1743
|
+
|
|
1744
|
+
aggregated.push(...tagged);
|
|
1745
|
+
} catch (error: any) {
|
|
1746
|
+
if (error?.status === 404 || error?.code === 404) {
|
|
1747
|
+
console.warn(`Custom object type '${typeId}' not found.`);
|
|
1748
|
+
continue;
|
|
1749
|
+
}
|
|
1750
|
+
throw error;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
console.log(
|
|
1755
|
+
`Fetched ${aggregated.length} custom objects across ${resolvedTypes.length} type(s)`
|
|
1756
|
+
);
|
|
1757
|
+
return aggregated;
|
|
1758
|
+
} catch (error: any) {
|
|
1759
|
+
if (error?.status === 404 || error?.code === 404) {
|
|
1760
|
+
console.warn('Custom objects endpoint not found for this portal.');
|
|
1761
|
+
return [];
|
|
1762
|
+
}
|
|
1763
|
+
return this.handleOptionalScopeError(error, 'crm.objects.custom.read', []);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
// CMS Hub Methods
|
|
1768
|
+
async getSitePages(limit: number = 100): Promise<any[]> {
|
|
1769
|
+
try {
|
|
1770
|
+
console.log('đ Fetching ALL site pages with pagination...');
|
|
1771
|
+
const allPages = await this.paginateList(
|
|
1772
|
+
async (params) => {
|
|
1773
|
+
const path = `/cms/v3/pages/site-pages?limit=${params.limit}${params.after ? `&after=${params.after}` : ''}`;
|
|
1774
|
+
const response = await this.client.apiRequest({ method: 'GET', path });
|
|
1775
|
+
return await response.json();
|
|
1776
|
+
},
|
|
1777
|
+
limit
|
|
1778
|
+
);
|
|
1779
|
+
console.log(`â
Total site pages fetched: ${allPages.length}`);
|
|
1780
|
+
return allPages;
|
|
1781
|
+
} catch (error: any) {
|
|
1782
|
+
console.error('Error fetching site pages:', error);
|
|
1783
|
+
return this.handleOptionalScopeError(error, 'content', []);
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
async getFiles(limit: number = 100): Promise<any[]> {
|
|
1788
|
+
try {
|
|
1789
|
+
console.log('đ Fetching ALL files with pagination...');
|
|
1790
|
+
const pageSize = Math.min(Math.max(limit, 1), 200);
|
|
1791
|
+
const chunkThreshold = 9000; // stay well below HubSpot's 10k search cap
|
|
1792
|
+
const seenIds = new Set<string>();
|
|
1793
|
+
const allFiles: any[] = [];
|
|
1794
|
+
let createdAtCursor: string | undefined;
|
|
1795
|
+
let keepFetching = true;
|
|
1796
|
+
|
|
1797
|
+
while (keepFetching) {
|
|
1798
|
+
const segment = await this.fetchFilesSegment(pageSize, chunkThreshold, createdAtCursor);
|
|
1799
|
+
|
|
1800
|
+
if (segment.results.length === 0) {
|
|
1801
|
+
break;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
for (const file of segment.results) {
|
|
1805
|
+
if (file?.id && !seenIds.has(file.id)) {
|
|
1806
|
+
seenIds.add(file.id);
|
|
1807
|
+
allFiles.push(file);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
if (!segment.reachedThreshold) {
|
|
1812
|
+
keepFetching = false;
|
|
1813
|
+
} else if (segment.lastCreatedAt) {
|
|
1814
|
+
const nextStart = new Date(segment.lastCreatedAt);
|
|
1815
|
+
nextStart.setMilliseconds(nextStart.getMilliseconds() + 1);
|
|
1816
|
+
createdAtCursor = nextStart.toISOString();
|
|
1817
|
+
} else {
|
|
1818
|
+
keepFetching = false;
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
console.log(`â
Total files fetched: ${allFiles.length}`);
|
|
1823
|
+
return allFiles;
|
|
1824
|
+
} catch (error: any) {
|
|
1825
|
+
console.error('Error fetching files:', error);
|
|
1826
|
+
return this.handleOptionalScopeError(error, 'files', []);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
private async fetchFilesSegment(
|
|
1831
|
+
pageSize: number,
|
|
1832
|
+
chunkThreshold: number,
|
|
1833
|
+
createdAtGte?: string
|
|
1834
|
+
): Promise<{ results: any[]; reachedThreshold: boolean; lastCreatedAt?: string }> {
|
|
1835
|
+
const collected: any[] = [];
|
|
1836
|
+
let reachedThreshold = false;
|
|
1837
|
+
let lastCreatedAt: string | undefined;
|
|
1838
|
+
let after: string | undefined;
|
|
1839
|
+
let hasMore = true;
|
|
1840
|
+
|
|
1841
|
+
while (hasMore) {
|
|
1842
|
+
await this.rateLimitDelay();
|
|
1843
|
+
const createdAtDate = createdAtGte ? new Date(createdAtGte) : undefined;
|
|
1844
|
+
const response = await this.client.files.filesApi.doSearch(
|
|
1845
|
+
undefined,
|
|
1846
|
+
after,
|
|
1847
|
+
undefined,
|
|
1848
|
+
pageSize,
|
|
1849
|
+
['createdAt'],
|
|
1850
|
+
undefined,
|
|
1851
|
+
undefined,
|
|
1852
|
+
undefined,
|
|
1853
|
+
createdAtDate
|
|
1854
|
+
);
|
|
1855
|
+
|
|
1856
|
+
const results = response.results || [];
|
|
1857
|
+
if (results.length === 0) {
|
|
1858
|
+
break;
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
collected.push(...results);
|
|
1862
|
+
lastCreatedAt = results[results.length - 1]?.createdAt;
|
|
1863
|
+
|
|
1864
|
+
if (!response.paging?.next?.after) {
|
|
1865
|
+
hasMore = false;
|
|
1866
|
+
} else {
|
|
1867
|
+
after = response.paging.next.after;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
if (collected.length >= chunkThreshold) {
|
|
1871
|
+
reachedThreshold = true;
|
|
1872
|
+
hasMore = false;
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
return { results: collected, reachedThreshold, lastCreatedAt };
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
async getHubDbTables(limit: number = 100): Promise<any[]> {
|
|
1880
|
+
try {
|
|
1881
|
+
console.log('đ Fetching ALL HubDB tables with pagination...');
|
|
1882
|
+
const allTables = await this.paginateList(
|
|
1883
|
+
(params) => this.client.cms.hubdb.tablesApi.getAllTables(undefined, params.after, params.limit),
|
|
1884
|
+
limit
|
|
1885
|
+
);
|
|
1886
|
+
console.log(`â
Total HubDB tables fetched: ${allTables.length}`);
|
|
1887
|
+
return allTables;
|
|
1888
|
+
} catch (error: any) {
|
|
1889
|
+
console.error('Error fetching HubDB tables:', error);
|
|
1890
|
+
return this.handleOptionalScopeError(error, 'hubdb', []);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
// Commerce Hub Methods
|
|
1895
|
+
async getProducts(limit: number = 100, updatedAfter?: string): Promise<any[]> {
|
|
1896
|
+
try {
|
|
1897
|
+
console.log('đĻ Fetching ALL products with pagination...');
|
|
1898
|
+
|
|
1899
|
+
const productProperties = [
|
|
1900
|
+
'name', 'description', 'price', 'hs_sku', 'hs_cost_of_goods_sold',
|
|
1901
|
+
'hs_recurring_billing_period', 'hs_product_type', 'hs_url',
|
|
1902
|
+
'createdate', 'hs_lastmodifieddate', 'hs_folder_id',
|
|
1903
|
+
'tax', 'hs_images', 'hs_avatar_filemanager_key'
|
|
1904
|
+
];
|
|
1905
|
+
|
|
1906
|
+
if (updatedAfter) {
|
|
1907
|
+
const searchRequest = {
|
|
1908
|
+
sorts: ['hs_lastmodifieddate:desc'],
|
|
1909
|
+
properties: productProperties
|
|
1910
|
+
};
|
|
1911
|
+
this.applyUpdatedAfterFilter(searchRequest, updatedAfter, 'hs_lastmodifieddate');
|
|
1912
|
+
const incrementalProducts = await this.paginateSearch(
|
|
1913
|
+
(req) => this.client.crm.products.searchApi.doSearch(req),
|
|
1914
|
+
searchRequest,
|
|
1915
|
+
limit
|
|
1916
|
+
);
|
|
1917
|
+
const normalizedProducts = convertDatetimeFields(incrementalProducts);
|
|
1918
|
+
return filterRecordsByUpdatedAfter(
|
|
1919
|
+
normalizedProducts,
|
|
1920
|
+
updatedAfter,
|
|
1921
|
+
(record) => record?.updatedAt,
|
|
1922
|
+
'products'
|
|
1923
|
+
);
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
const allProducts = await this.paginateList(
|
|
1927
|
+
(params) =>
|
|
1928
|
+
this.client.crm.products.basicApi.getPage(
|
|
1929
|
+
params.limit,
|
|
1930
|
+
params.after,
|
|
1931
|
+
productProperties
|
|
1932
|
+
),
|
|
1933
|
+
limit
|
|
1934
|
+
);
|
|
1935
|
+
const normalizedProducts = convertDatetimeFields(allProducts);
|
|
1936
|
+
return normalizedProducts;
|
|
1937
|
+
} catch (error: any) {
|
|
1938
|
+
console.error('Error fetching products:', error);
|
|
1939
|
+
return this.handleOptionalScopeError(error, 'crm.objects.products.read', []);
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
async getLineItems(limit: number = 100, updatedAfter?: string): Promise<any[]> {
|
|
1944
|
+
try {
|
|
1945
|
+
console.log('đ Fetching ALL line items with pagination...');
|
|
1946
|
+
|
|
1947
|
+
const lineItemProperties = [
|
|
1948
|
+
'name', 'quantity', 'price', 'amount', 'discount',
|
|
1949
|
+
'hs_sku', 'hs_product_id', 'hs_line_item_currency_code',
|
|
1950
|
+
'createdate', 'hs_lastmodifieddate',
|
|
1951
|
+
'hs_recurring_billing_period', 'hs_term_in_months', 'tax'
|
|
1952
|
+
];
|
|
1953
|
+
|
|
1954
|
+
if (updatedAfter) {
|
|
1955
|
+
const searchRequest = {
|
|
1956
|
+
sorts: ['hs_lastmodifieddate:desc'],
|
|
1957
|
+
properties: lineItemProperties
|
|
1958
|
+
};
|
|
1959
|
+
this.applyUpdatedAfterFilter(searchRequest, updatedAfter, 'hs_lastmodifieddate');
|
|
1960
|
+
const incrementalLineItems = await this.paginateSearch(
|
|
1961
|
+
(req) => this.client.crm.lineItems.searchApi.doSearch(req),
|
|
1962
|
+
searchRequest,
|
|
1963
|
+
limit
|
|
1964
|
+
);
|
|
1965
|
+
console.log(`â
Total line items fetched (incremental): ${incrementalLineItems.length}`);
|
|
1966
|
+
return convertDatetimeFields(incrementalLineItems);
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
const allLineItems = await this.paginateList(
|
|
1970
|
+
(params) => this.client.crm.lineItems.basicApi.getPage(
|
|
1971
|
+
params.limit,
|
|
1972
|
+
params.after,
|
|
1973
|
+
lineItemProperties
|
|
1974
|
+
),
|
|
1975
|
+
limit
|
|
1976
|
+
);
|
|
1977
|
+
console.log(`â
Total line items fetched: ${allLineItems.length}`);
|
|
1978
|
+
return allLineItems;
|
|
1979
|
+
} catch (error: any) {
|
|
1980
|
+
console.error('Error fetching line items:', error);
|
|
1981
|
+
return this.handleOptionalScopeError(error, 'crm.objects.line_items.read', []);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
// Enterprise Methods
|
|
1986
|
+
async searchObjects(objectType: string, query: string, limit: number = 100): Promise<any[]> {
|
|
1987
|
+
try {
|
|
1988
|
+
await this.rateLimitDelay();
|
|
1989
|
+
const response = await this.client.apiRequest({
|
|
1990
|
+
method: 'POST',
|
|
1991
|
+
path: `/crm/v3/objects/${objectType}/search`,
|
|
1992
|
+
body: {
|
|
1993
|
+
query: query,
|
|
1994
|
+
limit: limit,
|
|
1995
|
+
sorts: [{ propertyName: "createdate", direction: "DESCENDING" }]
|
|
1996
|
+
}
|
|
1997
|
+
});
|
|
1998
|
+
const data = await response.json();
|
|
1999
|
+
return data?.results || [];
|
|
2000
|
+
} catch (error: any) {
|
|
2001
|
+
console.error('Error searching objects:', error);
|
|
2002
|
+
throw new Error(`HubSpot API error: ${error.message}`);
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
async getLists(limit: number = 100): Promise<any[]> {
|
|
2007
|
+
try {
|
|
2008
|
+
console.log('đ Fetching ALL lists with search pagination...');
|
|
2009
|
+
const pageSize = Math.min(Math.max(limit, 1), 100);
|
|
2010
|
+
const allLists: any[] = [];
|
|
2011
|
+
let offset: number | undefined = undefined;
|
|
2012
|
+
let hasMore = true;
|
|
2013
|
+
|
|
2014
|
+
while (hasMore) {
|
|
2015
|
+
await this.rateLimitDelay();
|
|
2016
|
+
|
|
2017
|
+
const body: any = { limit: pageSize };
|
|
2018
|
+
if (typeof offset === 'number') {
|
|
2019
|
+
body.offset = offset;
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
const response = await this.client.apiRequest({
|
|
2023
|
+
method: 'POST',
|
|
2024
|
+
path: `/crm/v3/lists/search`,
|
|
2025
|
+
body
|
|
2026
|
+
});
|
|
2027
|
+
|
|
2028
|
+
const data = await response.json();
|
|
2029
|
+
const pageLists = Array.isArray(data?.lists) ? data.lists : [];
|
|
2030
|
+
if (pageLists.length > 0) {
|
|
2031
|
+
allLists.push(...pageLists);
|
|
2032
|
+
console.log(`đ Fetched ${pageLists.length} lists (total: ${allLists.length})`);
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
hasMore = Boolean(data?.hasMore);
|
|
2036
|
+
offset = data?.offset;
|
|
2037
|
+
|
|
2038
|
+
if (!hasMore || typeof offset !== 'number') {
|
|
2039
|
+
break;
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
console.log(`â
Total lists fetched: ${allLists.length}`);
|
|
2044
|
+
return allLists;
|
|
2045
|
+
} catch (error: any) {
|
|
2046
|
+
console.error('Error fetching lists:', error);
|
|
2047
|
+
throw new Error(`HubSpot API error: ${error.message}`);
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
async getWorkflows(limit: number = 100): Promise<any[]> {
|
|
2052
|
+
try {
|
|
2053
|
+
console.log('?sT??? Fetching workflows from automation API with pagination...');
|
|
2054
|
+
|
|
2055
|
+
const allWorkflows = await this.paginateList(
|
|
2056
|
+
async (params) => {
|
|
2057
|
+
const query = new URLSearchParams();
|
|
2058
|
+
query.append('limit', params.limit.toString());
|
|
2059
|
+
if (params.after) {
|
|
2060
|
+
query.append('after', params.after);
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
const response = await this.client.apiRequest({
|
|
2064
|
+
method: 'GET',
|
|
2065
|
+
path: `/automation/v3/workflows?${query.toString()}`
|
|
2066
|
+
});
|
|
2067
|
+
|
|
2068
|
+
const data = await response.json();
|
|
2069
|
+
const results = Array.isArray(data?.workflows)
|
|
2070
|
+
? data.workflows
|
|
2071
|
+
: Array.isArray(data?.results)
|
|
2072
|
+
? data.results
|
|
2073
|
+
: [];
|
|
2074
|
+
|
|
2075
|
+
return {
|
|
2076
|
+
results,
|
|
2077
|
+
paging: data?.paging
|
|
2078
|
+
};
|
|
2079
|
+
},
|
|
2080
|
+
limit
|
|
2081
|
+
);
|
|
2082
|
+
|
|
2083
|
+
console.log(`?o. Total workflows fetched: ${allWorkflows.length}`);
|
|
2084
|
+
return allWorkflows;
|
|
2085
|
+
} catch (error: any) {
|
|
2086
|
+
console.error('Error fetching workflows:', error);
|
|
2087
|
+
return this.handleOptionalScopeError(error, 'automation', []);
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
async getDomains(limit: number = 10): Promise<any[]> {
|
|
2092
|
+
try {
|
|
2093
|
+
console.log('?Y"? Fetching ALL domains with pagination...');
|
|
2094
|
+
|
|
2095
|
+
const allDomains = await this.paginateList(
|
|
2096
|
+
async (params) => {
|
|
2097
|
+
const query = new URLSearchParams();
|
|
2098
|
+
query.append('limit', params.limit.toString());
|
|
2099
|
+
if (params.after) {
|
|
2100
|
+
query.append('after', params.after);
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
const response = await this.client.apiRequest({
|
|
2104
|
+
method: 'GET',
|
|
2105
|
+
path: `/cms/v3/domains?${query.toString()}`
|
|
2106
|
+
});
|
|
2107
|
+
|
|
2108
|
+
return await response.json();
|
|
2109
|
+
},
|
|
2110
|
+
limit
|
|
2111
|
+
);
|
|
2112
|
+
|
|
2113
|
+
console.log(`?o. Total domains fetched: ${allDomains.length}`);
|
|
2114
|
+
return allDomains;
|
|
2115
|
+
} catch (error: any) {
|
|
2116
|
+
console.error('Error fetching domains:', error);
|
|
2117
|
+
return this.handleOptionalScopeError(error, 'content', []);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
}
|