1+ import { z } from 'zod'
12import { getScopesForService } from '@/lib/oauth/utils'
3+ import { MAX_SELECTOR_OPTIONS } from '@/lib/selectors/limits'
24import type { ServerSelectorKey } from '@/lib/selectors/manifest'
35import { resolveSelectorOAuthAccessToken } from '@/lib/selectors/server/credentials'
46import {
@@ -8,6 +10,7 @@ import {
810} from '@/lib/selectors/server/errors'
911import { fetchProviderJson } from '@/lib/selectors/server/providers/provider-http'
1012import {
13+ detailSelectorResult ,
1114 type ExecuteServerSelectorArgs ,
1215 listSelectorResult ,
1316 requireListRequest ,
@@ -30,6 +33,24 @@ const BUILT_IN_PATH: Record<string, string> = {
3033 ticket : 'tickets' ,
3134}
3235
36+ const HUBSPOT_LISTS_PAGE_SIZE = 500
37+
38+ const hubspotListSchema = z . object ( {
39+ listId : z . string ( ) . min ( 1 ) . max ( 100 ) ,
40+ name : z . string ( ) . min ( 1 ) . max ( 1_000 ) ,
41+ deletedAt : z . string ( ) . nullable ( ) . optional ( ) ,
42+ } )
43+
44+ const hubspotListsPageSchema = z . object ( {
45+ hasMore : z . boolean ( ) ,
46+ lists : z . array ( hubspotListSchema ) . max ( HUBSPOT_LISTS_PAGE_SIZE ) ,
47+ offset : z . number ( ) . int ( ) . nonnegative ( ) ,
48+ } )
49+
50+ const hubspotListDetailSchema = z . object ( {
51+ list : hubspotListSchema ,
52+ } )
53+
3354function resolveObjectType ( args : ExecuteServerSelectorArgs ) : string | null {
3455 const selected = args . context . objectType ?? 'contact'
3556 if ( selected !== 'custom' ) return selected
@@ -78,27 +99,55 @@ async function executeProperties(args: ExecuteServerSelectorArgs) {
7899}
79100
80101async function executeLists ( args : ExecuteServerSelectorArgs ) {
81- requireListRequest ( args . selectorKey , args . request )
82102 const accessToken = await hubspotToken ( args )
83- const data = await fetchProviderJson < {
84- lists ?: Array < { listId : string ; name : string ; deletedAt ?: string | null } >
85- } > ( 'https://api.hubapi.com/crm/v3/lists/search?count=500' , {
103+ if ( args . request . kind === 'detail' ) {
104+ const listId = args . request . id . trim ( )
105+ if ( ! listId || listId . length > 100 ) throw new SelectorContextUnavailableError ( )
106+ const body = await fetchProviderJson < unknown > (
107+ `https://api.hubapi.com/crm/v3/lists/${ encodeURIComponent ( listId ) } ` ,
108+ {
109+ headers : { Authorization : `Bearer ${ accessToken } ` } ,
110+ signal : args . signal ,
111+ }
112+ )
113+ const parsed = hubspotListDetailSchema . safeParse ( body )
114+ if ( ! parsed . success ) throw new SelectorOptionsUnavailableError ( )
115+ const list = parsed . data . list
116+ return detailSelectorResult ( list . deletedAt ? null : { id : args . request . id , label : list . name } )
117+ }
118+
119+ requireListRequest ( args . selectorKey , args . request )
120+ const cursor = args . request . cursor
121+ if ( cursor && ! / ^ \d { 1 , 10 } $ / . test ( cursor ) ) throw new SelectorContextUnavailableError ( )
122+ const offset = cursor ? Number ( cursor ) : 0
123+ if ( ! Number . isSafeInteger ( offset ) || offset < 0 || offset > MAX_SELECTOR_OPTIONS ) {
124+ throw new SelectorContextUnavailableError ( )
125+ }
126+ const search = args . request . search ?. trim ( )
127+ const body = await fetchProviderJson < unknown > ( 'https://api.hubapi.com/crm/v3/lists/search' , {
86128 method : 'POST' ,
87129 headers : {
88130 Authorization : `Bearer ${ accessToken } ` ,
89131 'Content-Type' : 'application/json' ,
90132 } ,
91133 body : JSON . stringify ( {
92- query : '' ,
134+ count : HUBSPOT_LISTS_PAGE_SIZE ,
135+ offset,
136+ ...( search ? { query : search } : { } ) ,
93137 processingTypes : [ 'MANUAL' , 'DYNAMIC' , 'SNAPSHOT' ] ,
94138 } ) ,
95139 signal : args . signal ,
96140 } )
141+ const parsed = hubspotListsPageSchema . safeParse ( body )
142+ if ( ! parsed . success ) throw new SelectorOptionsUnavailableError ( )
143+ const data = parsed . data
144+ if ( data . hasMore && data . offset <= offset ) throw new SelectorOptionsUnavailableError ( )
97145 return listSelectorResult (
98- ( data . lists ?? [ ] )
146+ data . lists
99147 . filter ( ( list ) => ! list . deletedAt && list . listId && list . name )
100148 . map ( ( list ) => ( { id : list . listId , label : list . name } ) )
101- . sort ( ( left , right ) => left . label . localeCompare ( right . label ) )
149+ . sort ( ( left , right ) => left . label . localeCompare ( right . label ) ) ,
150+ data . hasMore ? String ( data . offset ) : undefined
102151 )
103152}
104153
0 commit comments