Skip to content

Commit dd96bf7

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(selectors): paginate BigQuery resources
1 parent 8430f0f commit dd96bf7

3 files changed

Lines changed: 391 additions & 88 deletions

File tree

‎apps/sim/lib/selectors/manifest.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,12 @@ export const selectorManifest = {
9696
'attio.objects': providerSelector([], { detail: true }),
9797
'bigquery.datasets': providerSelector(['projectId', 'impersonateUserEmail'], {
9898
readiness: { all: ['oauthCredential', 'projectId'] },
99+
listMode: 'paginated',
99100
detail: true,
100101
}),
101102
'bigquery.tables': providerSelector(['projectId', 'datasetId', 'impersonateUserEmail'], {
102103
readiness: { all: ['oauthCredential', 'projectId', 'datasetId'] },
104+
listMode: 'paginated',
103105
detail: true,
104106
}),
105107
'bitbucket.workspaces': providerSelector([], { listMode: 'paginated', detail: true }),
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
5+
6+
const { mockFetch, mockResolveSelectorOAuthAccessToken } = vi.hoisted(() => ({
7+
mockFetch: vi.fn(),
8+
mockResolveSelectorOAuthAccessToken: vi.fn(),
9+
}))
10+
11+
vi.mock('@/lib/selectors/server/credentials', () => ({
12+
resolveSelectorOAuthAccessToken: mockResolveSelectorOAuthAccessToken,
13+
}))
14+
15+
import { createSelectorProtectedValues } from '@/lib/selectors/server/protected-values'
16+
import { bigQuerySelectorAttachments } from '@/lib/selectors/server/providers/bigquery'
17+
import type { ExecuteServerSelectorArgs } from '@/lib/selectors/server/types'
18+
19+
const PROJECT_ID = 'selector-test'
20+
const DATASET_ID = 'analytics'
21+
22+
function args(
23+
selectorKey: 'bigquery.datasets' | 'bigquery.tables',
24+
request: ExecuteServerSelectorArgs['request']
25+
): ExecuteServerSelectorArgs {
26+
return {
27+
selectorKey,
28+
context: {
29+
oauthCredential: 'credential-1',
30+
projectId: PROJECT_ID,
31+
...(selectorKey === 'bigquery.tables' ? { datasetId: DATASET_ID } : {}),
32+
},
33+
request,
34+
scope: { kind: 'workspace', workspaceId: 'workspace-1' },
35+
workspaceId: 'workspace-1',
36+
principal: { kind: 'session', userId: 'user-1', sessionId: 'session-1' },
37+
requesterUserId: 'user-1',
38+
credential: { suppliedId: 'credential-1' },
39+
references: new Map(),
40+
protectedValues: createSelectorProtectedValues(),
41+
}
42+
}
43+
44+
function dataset(datasetId: string, friendlyName: string) {
45+
return {
46+
datasetReference: { projectId: PROJECT_ID, datasetId },
47+
friendlyName,
48+
}
49+
}
50+
51+
function table(tableId: string, friendlyName: string) {
52+
return {
53+
tableReference: { projectId: PROJECT_ID, datasetId: DATASET_ID, tableId },
54+
friendlyName,
55+
}
56+
}
57+
58+
describe('BigQuery server selector adapters', () => {
59+
beforeEach(() => {
60+
vi.clearAllMocks()
61+
vi.stubGlobal('fetch', mockFetch)
62+
mockResolveSelectorOAuthAccessToken.mockResolvedValue('server-only-token')
63+
})
64+
65+
afterAll(() => vi.unstubAllGlobals())
66+
67+
it('returns one dataset page and forwards its continuation token on demand', async () => {
68+
mockFetch
69+
.mockResolvedValueOnce(
70+
new Response(
71+
JSON.stringify({
72+
datasets: [dataset('dataset_1', 'Dataset 1')],
73+
nextPageToken: 'dataset-page-2',
74+
}),
75+
{ status: 200 }
76+
)
77+
)
78+
.mockResolvedValueOnce(
79+
new Response(JSON.stringify({ datasets: [dataset('dataset_2', 'Dataset 2')] }), {
80+
status: 200,
81+
})
82+
)
83+
84+
await expect(
85+
bigQuerySelectorAttachments['bigquery.datasets'].execute(
86+
args('bigquery.datasets', { kind: 'list' })
87+
)
88+
).resolves.toEqual({
89+
kind: 'list',
90+
items: [{ id: 'dataset_1', label: 'Dataset 1' }],
91+
nextCursor: 'dataset-page-2',
92+
})
93+
await expect(
94+
bigQuerySelectorAttachments['bigquery.datasets'].execute(
95+
args('bigquery.datasets', { kind: 'list', cursor: 'dataset-page-2' })
96+
)
97+
).resolves.toEqual({
98+
kind: 'list',
99+
items: [{ id: 'dataset_2', label: 'Dataset 2' }],
100+
})
101+
102+
const firstUrl = new URL(String(mockFetch.mock.calls[0]?.[0]))
103+
const secondUrl = new URL(String(mockFetch.mock.calls[1]?.[0]))
104+
expect(firstUrl.searchParams.get('maxResults')).toBe('200')
105+
expect(firstUrl.searchParams.has('pageToken')).toBe(false)
106+
expect(secondUrl.searchParams.get('pageToken')).toBe('dataset-page-2')
107+
expect(mockFetch).toHaveBeenCalledTimes(2)
108+
})
109+
110+
it('returns one table page and forwards its continuation token on demand', async () => {
111+
mockFetch
112+
.mockResolvedValueOnce(
113+
new Response(
114+
JSON.stringify({
115+
tables: [table('table_1', 'Table 1')],
116+
nextPageToken: 'table-page-2',
117+
}),
118+
{ status: 200 }
119+
)
120+
)
121+
.mockResolvedValueOnce(
122+
new Response(JSON.stringify({ tables: [table('table_2', 'Table 2')] }), {
123+
status: 200,
124+
})
125+
)
126+
127+
await expect(
128+
bigQuerySelectorAttachments['bigquery.tables'].execute(
129+
args('bigquery.tables', { kind: 'list' })
130+
)
131+
).resolves.toEqual({
132+
kind: 'list',
133+
items: [{ id: 'table_1', label: 'Table 1' }],
134+
nextCursor: 'table-page-2',
135+
})
136+
await expect(
137+
bigQuerySelectorAttachments['bigquery.tables'].execute(
138+
args('bigquery.tables', { kind: 'list', cursor: 'table-page-2' })
139+
)
140+
).resolves.toEqual({
141+
kind: 'list',
142+
items: [{ id: 'table_2', label: 'Table 2' }],
143+
})
144+
145+
const firstUrl = new URL(String(mockFetch.mock.calls[0]?.[0]))
146+
const secondUrl = new URL(String(mockFetch.mock.calls[1]?.[0]))
147+
expect(firstUrl.searchParams.get('maxResults')).toBe('200')
148+
expect(firstUrl.searchParams.has('pageToken')).toBe(false)
149+
expect(secondUrl.searchParams.get('pageToken')).toBe('table-page-2')
150+
expect(mockFetch).toHaveBeenCalledTimes(2)
151+
})
152+
153+
it('hydrates a selected dataset directly by id', async () => {
154+
mockFetch.mockResolvedValueOnce(
155+
new Response(JSON.stringify(dataset('saved_dataset', 'Saved Dataset')), { status: 200 })
156+
)
157+
158+
await expect(
159+
bigQuerySelectorAttachments['bigquery.datasets'].execute(
160+
args('bigquery.datasets', { kind: 'detail', id: 'saved_dataset' })
161+
)
162+
).resolves.toEqual({
163+
kind: 'detail',
164+
item: { id: 'saved_dataset', label: 'Saved Dataset' },
165+
})
166+
167+
const url = new URL(String(mockFetch.mock.calls[0]?.[0]))
168+
expect(url.pathname).toBe(`/bigquery/v2/projects/${PROJECT_ID}/datasets/saved_dataset`)
169+
expect(url.searchParams.get('datasetView')).toBe('METADATA')
170+
expect(mockFetch).toHaveBeenCalledTimes(1)
171+
})
172+
173+
it('hydrates a selected table directly by its encoded id', async () => {
174+
const tableId = 'Sales Table-Δ'
175+
mockFetch.mockResolvedValueOnce(
176+
new Response(JSON.stringify(table(tableId, 'Saved Table')), { status: 200 })
177+
)
178+
179+
await expect(
180+
bigQuerySelectorAttachments['bigquery.tables'].execute(
181+
args('bigquery.tables', { kind: 'detail', id: tableId })
182+
)
183+
).resolves.toEqual({
184+
kind: 'detail',
185+
item: { id: tableId, label: 'Saved Table' },
186+
})
187+
188+
const url = new URL(String(mockFetch.mock.calls[0]?.[0]))
189+
expect(url.pathname).toBe(
190+
`/bigquery/v2/projects/${PROJECT_ID}/datasets/${DATASET_ID}/tables/Sales%20Table-%CE%94`
191+
)
192+
expect(url.searchParams.get('view')).toBe('BASIC')
193+
expect(mockFetch).toHaveBeenCalledTimes(1)
194+
})
195+
196+
it.each([
197+
['bigquery.datasets', { kind: 'detail', id: 'missing_dataset' }],
198+
['bigquery.tables', { kind: 'detail', id: 'missing_table' }],
199+
] as const)('returns null when %s detail is missing', async (selectorKey, request) => {
200+
mockFetch.mockResolvedValueOnce(new Response(null, { status: 404 }))
201+
202+
await expect(
203+
bigQuerySelectorAttachments[selectorKey].execute(args(selectorKey, request))
204+
).resolves.toEqual({ kind: 'detail', item: null })
205+
expect(mockFetch).toHaveBeenCalledTimes(1)
206+
})
207+
})

0 commit comments

Comments
 (0)