Skip to content

Commit e7cfef9

Browse files
BillLeoutsakosvl346Bill Leoutsakos
andauthored
fix(selectors): paginate HubSpot owner options (#7359)
* fix(selectors): paginate HubSpot owner options * fix(selectors): preserve hydrated HubSpot owner IDs --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
1 parent 83e5f5f commit e7cfef9

3 files changed

Lines changed: 110 additions & 30 deletions

File tree

apps/sim/lib/selectors/manifest.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ export const selectorManifest = {
162162
}),
163163
'harmonic.savedSearches': providerSelector([], { detail: true, unknownDetail: true }),
164164
'hubspot.lists': providerSelector([], { listMode: 'paginated', search: true, detail: true }),
165-
'hubspot.owners': providerSelector(),
165+
'hubspot.owners': providerSelector([], { listMode: 'paginated', detail: true }),
166166
'hubspot.pipelines': providerSelector(['objectType', 'customObjectTypeId']),
167167
'hubspot.pipelineStages': providerSelector(['objectType', 'customObjectTypeId', 'pipelineId'], {
168168
readiness: { all: ['oauthCredential', 'pipelineId'] },

apps/sim/lib/selectors/server/providers/hubspot.test.ts

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@ import { createSelectorProtectedValues } from '@/lib/selectors/server/protected-
1616
import { hubspotSelectorAttachments } from '@/lib/selectors/server/providers/hubspot'
1717
import type { ExecuteServerSelectorArgs } from '@/lib/selectors/server/types'
1818

19-
function args(request: ExecuteServerSelectorArgs['request']): ExecuteServerSelectorArgs {
19+
function args(
20+
request: ExecuteServerSelectorArgs['request'],
21+
selectorKey: ExecuteServerSelectorArgs['selectorKey'] = 'hubspot.lists'
22+
): ExecuteServerSelectorArgs {
2023
return {
21-
selectorKey: 'hubspot.lists',
24+
selectorKey,
2225
context: { oauthCredential: 'credential-1' },
2326
request,
2427
scope: { kind: 'workspace', workspaceId: 'workspace-1' },
@@ -113,4 +116,67 @@ describe('HubSpot server selector adapter', () => {
113116
expect(String(mockFetch.mock.calls[0]?.[0])).toBe('https://api.hubapi.com/crm/v3/lists/123')
114117
expect(mockFetch).toHaveBeenCalledTimes(1)
115118
})
119+
120+
it('paginates active owners through the HubSpot continuation cursor on demand', async () => {
121+
mockFetch
122+
.mockResolvedValueOnce(
123+
new Response(
124+
JSON.stringify({
125+
results: [
126+
{ id: '100', firstName: 'Former', lastName: 'Owner', archived: true },
127+
{ id: '101', firstName: 'Ada', lastName: 'Lovelace', archived: false },
128+
],
129+
paging: { next: { after: 'owner-page-2' } },
130+
}),
131+
{ status: 200 }
132+
)
133+
)
134+
.mockResolvedValueOnce(
135+
new Response(JSON.stringify({ results: [{ id: '102', email: 'grace@example.com' }] }), {
136+
status: 200,
137+
})
138+
)
139+
140+
const first = await hubspotSelectorAttachments['hubspot.owners'].execute(
141+
args({ kind: 'list' }, 'hubspot.owners')
142+
)
143+
const second = await hubspotSelectorAttachments['hubspot.owners'].execute(
144+
args({ kind: 'list', cursor: 'owner-page-2' }, 'hubspot.owners')
145+
)
146+
147+
expect(first).toEqual({
148+
kind: 'list',
149+
items: [{ id: '101', label: 'Ada Lovelace' }],
150+
nextCursor: 'owner-page-2',
151+
})
152+
expect(second).toEqual({
153+
kind: 'list',
154+
items: [{ id: '102', label: 'grace@example.com' }],
155+
})
156+
const firstUrl = new URL(String(mockFetch.mock.calls[0]?.[0]))
157+
const secondUrl = new URL(String(mockFetch.mock.calls[1]?.[0]))
158+
expect(firstUrl.searchParams.get('limit')).toBe('100')
159+
expect(firstUrl.searchParams.has('after')).toBe(false)
160+
expect(secondUrl.searchParams.get('after')).toBe('owner-page-2')
161+
expect(mockFetch).toHaveBeenCalledTimes(2)
162+
})
163+
164+
it('hydrates a selected owner directly by id', async () => {
165+
mockFetch.mockResolvedValueOnce(
166+
new Response(JSON.stringify({ id: '777', firstName: 'Katherine', lastName: 'Johnson' }), {
167+
status: 200,
168+
})
169+
)
170+
171+
await expect(
172+
hubspotSelectorAttachments['hubspot.owners'].execute(
173+
args({ kind: 'detail', id: '000777' }, 'hubspot.owners')
174+
)
175+
).resolves.toEqual({
176+
kind: 'detail',
177+
item: { id: '000777', label: 'Katherine Johnson' },
178+
})
179+
expect(String(mockFetch.mock.calls[0]?.[0])).toBe('https://api.hubapi.com/crm/v3/owners/000777')
180+
expect(mockFetch).toHaveBeenCalledTimes(1)
181+
})
116182
})

apps/sim/lib/selectors/server/providers/hubspot.ts

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,21 @@ interface HubSpotPipeline {
158158
archived?: boolean
159159
}
160160

161+
interface HubSpotOwner {
162+
id: string
163+
email?: string
164+
firstName?: string
165+
lastName?: string
166+
archived?: boolean
167+
}
168+
169+
function hubspotOwnerOption(owner: HubSpotOwner) {
170+
return {
171+
id: owner.id,
172+
label: [owner.firstName, owner.lastName].filter(Boolean).join(' ') || owner.email || owner.id,
173+
}
174+
}
175+
161176
async function loadPipelines(args: ExecuteServerSelectorArgs): Promise<HubSpotPipeline[]> {
162177
const objectType = resolveObjectType(args)
163178
if (!objectType) return []
@@ -194,37 +209,36 @@ async function executePipelineStages(args: ExecuteServerSelectorArgs) {
194209
}
195210

196211
async function executeOwners(args: ExecuteServerSelectorArgs) {
197-
requireListRequest(args.selectorKey, args.request)
198212
const accessToken = await hubspotToken(args)
199-
const owners: Array<{
200-
id: string
201-
email?: string
202-
firstName?: string
203-
lastName?: string
204-
archived?: boolean
205-
}> = []
206-
let after: string | undefined
207-
for (let page = 0; page < 10; page++) {
208-
const url = new URL('https://api.hubapi.com/crm/v3/owners')
209-
url.searchParams.set('limit', '100')
210-
if (after) url.searchParams.set('after', after)
211-
const data = await fetchProviderJson<{
212-
results?: typeof owners
213-
paging?: { next?: { after?: string } }
214-
}>(url, { headers: { Authorization: `Bearer ${accessToken}` }, signal: args.signal })
215-
owners.push(...(data.results ?? []))
216-
after = data.paging?.next?.after
217-
if (!after) break
213+
if (args.request.kind === 'detail') {
214+
const ownerId = args.request.id.trim()
215+
if (!ownerId || ownerId.length > 100) throw new SelectorContextUnavailableError()
216+
const owner = await fetchProviderJson<HubSpotOwner>(
217+
`https://api.hubapi.com/crm/v3/owners/${encodeURIComponent(ownerId)}`,
218+
{
219+
headers: { Authorization: `Bearer ${accessToken}` },
220+
signal: args.signal,
221+
}
222+
)
223+
return detailSelectorResult(
224+
owner.archived || !owner.id ? null : { ...hubspotOwnerOption(owner), id: ownerId }
225+
)
218226
}
227+
228+
requireListRequest(args.selectorKey, args.request)
229+
const url = new URL('https://api.hubapi.com/crm/v3/owners')
230+
url.searchParams.set('limit', '100')
231+
if (args.request.cursor) url.searchParams.set('after', args.request.cursor)
232+
const data = await fetchProviderJson<{
233+
results?: HubSpotOwner[]
234+
paging?: { next?: { after?: string } }
235+
}>(url, { headers: { Authorization: `Bearer ${accessToken}` }, signal: args.signal })
219236
return listSelectorResult(
220-
owners
237+
(data.results ?? [])
221238
.filter((owner) => !owner.archived && owner.id)
222-
.map((owner) => ({
223-
id: owner.id,
224-
label:
225-
[owner.firstName, owner.lastName].filter(Boolean).join(' ') || owner.email || owner.id,
226-
}))
227-
.sort((left, right) => left.label.localeCompare(right.label))
239+
.map(hubspotOwnerOption)
240+
.sort((left, right) => left.label.localeCompare(right.label)),
241+
data.paging?.next?.after
228242
)
229243
}
230244

0 commit comments

Comments
 (0)