Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/sim/lib/selectors/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export const selectorManifest = {
}),
'harmonic.savedSearches': providerSelector([], { detail: true, unknownDetail: true }),
'hubspot.lists': providerSelector([], { listMode: 'paginated', search: true, detail: true }),
'hubspot.owners': providerSelector(),
'hubspot.owners': providerSelector([], { listMode: 'paginated', detail: true }),
'hubspot.pipelines': providerSelector(['objectType', 'customObjectTypeId']),
'hubspot.pipelineStages': providerSelector(['objectType', 'customObjectTypeId', 'pipelineId'], {
readiness: { all: ['oauthCredential', 'pipelineId'] },
Expand Down
70 changes: 68 additions & 2 deletions apps/sim/lib/selectors/server/providers/hubspot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ import { createSelectorProtectedValues } from '@/lib/selectors/server/protected-
import { hubspotSelectorAttachments } from '@/lib/selectors/server/providers/hubspot'
import type { ExecuteServerSelectorArgs } from '@/lib/selectors/server/types'

function args(request: ExecuteServerSelectorArgs['request']): ExecuteServerSelectorArgs {
function args(
request: ExecuteServerSelectorArgs['request'],
selectorKey: ExecuteServerSelectorArgs['selectorKey'] = 'hubspot.lists'
): ExecuteServerSelectorArgs {
return {
selectorKey: 'hubspot.lists',
selectorKey,
context: { oauthCredential: 'credential-1' },
request,
scope: { kind: 'workspace', workspaceId: 'workspace-1' },
Expand Down Expand Up @@ -113,4 +116,67 @@ describe('HubSpot server selector adapter', () => {
expect(String(mockFetch.mock.calls[0]?.[0])).toBe('https://api.hubapi.com/crm/v3/lists/123')
expect(mockFetch).toHaveBeenCalledTimes(1)
})

it('paginates active owners through the HubSpot continuation cursor on demand', async () => {
mockFetch
.mockResolvedValueOnce(
new Response(
JSON.stringify({
results: [
{ id: '100', firstName: 'Former', lastName: 'Owner', archived: true },
{ id: '101', firstName: 'Ada', lastName: 'Lovelace', archived: false },
],
paging: { next: { after: 'owner-page-2' } },
}),
{ status: 200 }
)
)
.mockResolvedValueOnce(
new Response(JSON.stringify({ results: [{ id: '102', email: 'grace@example.com' }] }), {
status: 200,
})
)

const first = await hubspotSelectorAttachments['hubspot.owners'].execute(
args({ kind: 'list' }, 'hubspot.owners')
)
const second = await hubspotSelectorAttachments['hubspot.owners'].execute(
args({ kind: 'list', cursor: 'owner-page-2' }, 'hubspot.owners')
)

expect(first).toEqual({
kind: 'list',
items: [{ id: '101', label: 'Ada Lovelace' }],
nextCursor: 'owner-page-2',
})
expect(second).toEqual({
kind: 'list',
items: [{ id: '102', label: 'grace@example.com' }],
})
const firstUrl = new URL(String(mockFetch.mock.calls[0]?.[0]))
const secondUrl = new URL(String(mockFetch.mock.calls[1]?.[0]))
expect(firstUrl.searchParams.get('limit')).toBe('100')
expect(firstUrl.searchParams.has('after')).toBe(false)
expect(secondUrl.searchParams.get('after')).toBe('owner-page-2')
expect(mockFetch).toHaveBeenCalledTimes(2)
})

it('hydrates a selected owner directly by id', async () => {
mockFetch.mockResolvedValueOnce(
new Response(JSON.stringify({ id: '777', firstName: 'Katherine', lastName: 'Johnson' }), {
status: 200,
})
)

await expect(
hubspotSelectorAttachments['hubspot.owners'].execute(
args({ kind: 'detail', id: '000777' }, 'hubspot.owners')
)
).resolves.toEqual({
kind: 'detail',
item: { id: '000777', label: 'Katherine Johnson' },
})
expect(String(mockFetch.mock.calls[0]?.[0])).toBe('https://api.hubapi.com/crm/v3/owners/000777')
expect(mockFetch).toHaveBeenCalledTimes(1)
})
})
68 changes: 41 additions & 27 deletions apps/sim/lib/selectors/server/providers/hubspot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,21 @@ interface HubSpotPipeline {
archived?: boolean
}

interface HubSpotOwner {
id: string
email?: string
firstName?: string
lastName?: string
archived?: boolean
}

function hubspotOwnerOption(owner: HubSpotOwner) {
return {
id: owner.id,
label: [owner.firstName, owner.lastName].filter(Boolean).join(' ') || owner.email || owner.id,
}
}

async function loadPipelines(args: ExecuteServerSelectorArgs): Promise<HubSpotPipeline[]> {
const objectType = resolveObjectType(args)
if (!objectType) return []
Expand Down Expand Up @@ -194,37 +209,36 @@ async function executePipelineStages(args: ExecuteServerSelectorArgs) {
}

async function executeOwners(args: ExecuteServerSelectorArgs) {
requireListRequest(args.selectorKey, args.request)
const accessToken = await hubspotToken(args)
const owners: Array<{
id: string
email?: string
firstName?: string
lastName?: string
archived?: boolean
}> = []
let after: string | undefined
for (let page = 0; page < 10; page++) {
const url = new URL('https://api.hubapi.com/crm/v3/owners')
url.searchParams.set('limit', '100')
if (after) url.searchParams.set('after', after)
const data = await fetchProviderJson<{
results?: typeof owners
paging?: { next?: { after?: string } }
}>(url, { headers: { Authorization: `Bearer ${accessToken}` }, signal: args.signal })
owners.push(...(data.results ?? []))
after = data.paging?.next?.after
if (!after) break
if (args.request.kind === 'detail') {
const ownerId = args.request.id.trim()
if (!ownerId || ownerId.length > 100) throw new SelectorContextUnavailableError()
const owner = await fetchProviderJson<HubSpotOwner>(
`https://api.hubapi.com/crm/v3/owners/${encodeURIComponent(ownerId)}`,
{
headers: { Authorization: `Bearer ${accessToken}` },
signal: args.signal,
}
)
return detailSelectorResult(
owner.archived || !owner.id ? null : { ...hubspotOwnerOption(owner), id: ownerId }
)
}

requireListRequest(args.selectorKey, args.request)
const url = new URL('https://api.hubapi.com/crm/v3/owners')
url.searchParams.set('limit', '100')
if (args.request.cursor) url.searchParams.set('after', args.request.cursor)
const data = await fetchProviderJson<{
results?: HubSpotOwner[]
paging?: { next?: { after?: string } }
}>(url, { headers: { Authorization: `Bearer ${accessToken}` }, signal: args.signal })
return listSelectorResult(
owners
(data.results ?? [])
.filter((owner) => !owner.archived && owner.id)
.map((owner) => ({
id: owner.id,
label:
[owner.firstName, owner.lastName].filter(Boolean).join(' ') || owner.email || owner.id,
}))
.sort((left, right) => left.label.localeCompare(right.label))
.map(hubspotOwnerOption)
.sort((left, right) => left.label.localeCompare(right.label)),
Comment thread
BillLeoutsakosvl346 marked this conversation as resolved.
Comment thread
BillLeoutsakosvl346 marked this conversation as resolved.
data.paging?.next?.after
)
}

Expand Down
Loading