Skip to content

Commit b4ce031

Browse files
author
Vikhyath Mondreti
committed
fix(search): reset directly opened document caches
1 parent 77e91eb commit b4ce031

6 files changed

Lines changed: 131 additions & 32 deletions

‎apps/sim/hooks/queries/organization-accounts.test.tsx‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
55
import { createRoot, type Root } from 'react-dom/client'
66
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
77

8-
const mocks = vi.hoisted(() => ({ request: vi.fn() }))
8+
const mocks = vi.hoisted(() => ({ request: vi.fn(), refresh: vi.fn() }))
99
vi.mock('@/lib/api/client/request', () => ({ requestJson: mocks.request }))
10+
vi.mock('next/navigation', () => ({ useRouter: () => ({ refresh: mocks.refresh }) }))
1011

1112
import { ApiClientError } from '@/lib/api/client/errors'
1213
import {
@@ -27,10 +28,11 @@ import { searchSourceKeys } from '@/hooks/queries/utils/search-source-keys'
2728

2829
describe('personal account disconnect', () => {
2930
it.each([true, false])(
30-
'refreshes this organization only after success=%s, including after unmount',
31+
'clears content and refreshes the router only after success=%s, including after unmount',
3132
async (success) => {
3233
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
3334
mocks.request.mockReset()
35+
mocks.refresh.mockReset()
3436
const response = Promise.withResolvers<{ success: true }>()
3537
mocks.request.mockReturnValue(response.promise)
3638
const client = new QueryClient()
@@ -112,13 +114,22 @@ describe('personal account disconnect', () => {
112114
disconnectPersonalOrganizationAccountContract,
113115
{ params: { credentialId: 'own-credential' } }
114116
)
115-
for (const key of [own, catalog, results, ownDocument, resultOnlyDocument, chunks]) {
117+
for (const key of [
118+
own,
119+
catalog,
120+
results,
121+
ownDocument,
122+
resultOnlyDocument,
123+
chunks,
124+
otherDocument,
125+
]) {
116126
if (success) expect(client.getQueryData(key)).toBeUndefined()
117127
else expect(client.getQueryData(key)).toBeDefined()
118128
}
119129
expect(client.getQueryState(people)?.isInvalidated).toBe(success)
120130
expect(client.getQueryState(other)?.isInvalidated).toBe(false)
121-
for (const key of [otherResults, workspaceResults, otherDocument])
131+
expect(mocks.refresh).toHaveBeenCalledTimes(success ? 1 : 0)
132+
for (const key of [otherResults, workspaceResults])
122133
expect(client.getQueryData(key)).toEqual({ content: 'previously authorized content' })
123134
} finally {
124135
await act(async () => root.unmount())

‎apps/sim/hooks/queries/organization-accounts.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
useQuery,
88
useQueryClient,
99
} from '@tanstack/react-query'
10+
import { useRouter } from 'next/navigation'
1011
import { isApiClientError } from '@/lib/api/client/errors'
1112
import { requestJson } from '@/lib/api/client/request'
1213
import {
@@ -45,18 +46,21 @@ export const ORGANIZATION_ACCOUNTS_STALE_TIME = 30_000
4546
/** Disconnects an owned grant; indexing and source setup do not gate this operation. */
4647
export function useDisconnectPersonalOrganizationAccount(organizationId: string) {
4748
const queryClient = useQueryClient()
49+
const router = useRouter()
4850
return useMutation({
4951
mutationFn: (credentialId: string) =>
5052
requestJson(disconnectPersonalOrganizationAccountContract, {
5153
params: { credentialId },
5254
}),
53-
onSuccess: () =>
54-
Promise.all([
55+
onSuccess: async () => {
56+
await Promise.all([
5557
resetOrganizationSearchAccess(queryClient, organizationId),
5658
queryClient.invalidateQueries({
5759
queryKey: organizationAccountsKeys.detail(organizationId),
5860
}),
59-
]),
61+
])
62+
router.refresh()
63+
},
6064
})
6165
}
6266

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/** @vitest-environment jsdom */
2+
import { act } from 'react'
3+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
4+
import { createRoot } from 'react-dom/client'
5+
import { expect, it, vi } from 'vitest'
6+
7+
const mocks = vi.hoisted(() => ({ request: vi.fn(), refresh: vi.fn() }))
8+
vi.mock('@/lib/api/client/request', () => ({ requestJson: mocks.request }))
9+
vi.mock('next/navigation', () => ({ useRouter: () => ({ refresh: mocks.refresh }) }))
10+
11+
import { useUpdateSearchIntegration } from '@/hooks/queries/search-integrations'
12+
import { knowledgeKeys } from '@/hooks/queries/utils/knowledge-keys'
13+
14+
it.each([true, false])(
15+
'clears document content and refreshes server pages after integration update success=%s',
16+
async (success) => {
17+
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
18+
mocks.request.mockReset()
19+
mocks.refresh.mockReset()
20+
const response = Promise.withResolvers<{ data: { connectorType: string; approved: boolean } }>()
21+
mocks.request.mockReturnValue(response.promise)
22+
const client = new QueryClient()
23+
const root = createRoot(document.createElement('div'))
24+
const key = knowledgeKeys.document('kb-direct', 'document-direct')
25+
client.setQueryData(key, { content: 'previously authorized content' })
26+
let mutation: ReturnType<typeof useUpdateSearchIntegration>
27+
function Probe() {
28+
mutation = useUpdateSearchIntegration()
29+
return null
30+
}
31+
try {
32+
await act(async () =>
33+
root.render(
34+
<QueryClientProvider client={client}>
35+
<Probe />
36+
</QueryClientProvider>
37+
)
38+
)
39+
let pending: Promise<unknown>
40+
await act(async () => {
41+
pending = mutation.mutateAsync({
42+
organizationId: 'org-1',
43+
connectorType: 'github',
44+
approved: false,
45+
})
46+
})
47+
await act(async () =>
48+
root.render(<QueryClientProvider client={client}>{null}</QueryClientProvider>)
49+
)
50+
await act(async () => {
51+
if (success) {
52+
response.resolve({ data: { connectorType: 'github', approved: false } })
53+
await pending
54+
} else {
55+
const rejection = expect(pending).rejects.toThrow('Try again')
56+
response.reject(new Error('Try again'))
57+
await rejection
58+
}
59+
})
60+
expect(mocks.refresh).toHaveBeenCalledTimes(success ? 1 : 0)
61+
if (success) expect(client.getQueryData(key)).toBeUndefined()
62+
else expect(client.getQueryData(key)).toEqual({ content: 'previously authorized content' })
63+
} finally {
64+
await act(async () => root.unmount())
65+
client.clear()
66+
vi.unstubAllGlobals()
67+
}
68+
}
69+
)

‎apps/sim/hooks/queries/search-integrations.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
2+
import { useRouter } from 'next/navigation'
23
import { requestJson } from '@/lib/api/client/request'
34
import {
45
listSearchIntegrationsContract,
@@ -22,13 +23,16 @@ export function useSearchIntegrations(organizationId: string) {
2223

2324
export function useUpdateSearchIntegration() {
2425
const queryClient = useQueryClient()
26+
const router = useRouter()
2527
return useMutation({
2628
mutationFn: async (body: UpdateSearchIntegrationBody) =>
2729
(await requestJson(updateSearchIntegrationContract, { body })).data,
28-
onSuccess: (_data, { organizationId }) =>
29-
Promise.all([
30+
onSuccess: async (_data, { organizationId }) => {
31+
await Promise.all([
3032
resetOrganizationSearchAccess(queryClient, organizationId),
3133
queryClient.invalidateQueries({ queryKey: searchIntegrationKeys.list(organizationId) }),
32-
]),
34+
])
35+
router.refresh()
36+
},
3337
})
3438
}

‎apps/sim/hooks/queries/utils/reset-organization-search-access.test.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,35 @@ import { resourceScopeKey } from '@/lib/core/resource-scope'
66
import { knowledgeKeys } from '@/hooks/queries/utils/knowledge-keys'
77
import { resetOrganizationSearchAccess } from '@/hooks/queries/utils/reset-organization-search-access'
88

9+
it.each([
10+
{ name: 'document', key: knowledgeKeys.document('kb-direct', 'document-direct') },
11+
{ name: 'chunks', key: knowledgeKeys.chunks('kb-direct', 'document-direct', '') },
12+
])('clears and cancels directly loaded $name without source or Search caches', async ({ key }) => {
13+
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } })
14+
const response = Promise.withResolvers<{ content: string }>()
15+
const aborted = vi.fn()
16+
try {
17+
client.setQueryData(key, { content: 'cached private content' })
18+
const pending = client.fetchQuery({
19+
queryKey: key,
20+
queryFn: ({ signal }) => {
21+
signal.addEventListener('abort', aborted, { once: true })
22+
return response.promise
23+
},
24+
})
25+
const rejected = expect(pending).rejects.toThrow()
26+
await resetOrganizationSearchAccess(client, 'org-1')
27+
await rejected
28+
expect(aborted).toHaveBeenCalledOnce()
29+
expect(client.getQueryData(key)).toBeUndefined()
30+
response.resolve({ content: 'late private content' })
31+
await response.promise
32+
expect(client.getQueryData(key)).toBeUndefined()
33+
} finally {
34+
client.clear()
35+
}
36+
})
37+
938
it('cancels an in-flight search so its late result cannot restore disconnected content', async () => {
1039
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } })
1140
const key = knowledgeKeys.search(
Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import type { InfiniteData, QueryClient } from '@tanstack/react-query'
2-
import type { SearchSourcePage } from '@/lib/api/contracts/knowledge/connectors'
3-
import type { WorkspaceKnowledgeSearchResult } from '@/lib/api/contracts/knowledge/search'
1+
import type { QueryClient } from '@tanstack/react-query'
42
import { resourceScopeKey } from '@/lib/core/resource-scope'
53
import { knowledgeKeys } from '@/hooks/queries/utils/knowledge-keys'
64
import { searchSourceKeys } from '@/hooks/queries/utils/search-source-keys'
@@ -11,28 +9,12 @@ export async function resetOrganizationSearchAccess(
119
organizationId: string
1210
) {
1311
const scope = { kind: 'organization', organizationId } as const
14-
const pages = queryClient.getQueriesData<InfiniteData<SearchSourcePage>>({
15-
queryKey: searchSourceKeys.list(scope),
16-
predicate: (query) => query.queryKey[3] === 'pages',
17-
})
18-
const searchKey = [...knowledgeKeys.searches(), resourceScopeKey(scope)]
19-
const results = queryClient.getQueriesData<WorkspaceKnowledgeSearchResult[]>({
20-
queryKey: searchKey,
21-
})
22-
const knowledgeBaseIds = new Set([
23-
...pages.flatMap(
24-
([, data]) =>
25-
data?.pages.flatMap((page) => page.sources.map((source) => source.knowledgeBaseId)) ?? []
26-
),
27-
...results.flatMap(([, data]) => data?.map((result) => result.knowledgeBaseId) ?? []),
28-
])
2912
await Promise.all([
3013
queryClient.resetQueries({
31-
queryKey: searchKey,
14+
queryKey: [...knowledgeKeys.searches(), resourceScopeKey(scope)],
3215
}),
33-
...[...knowledgeBaseIds].map((id) =>
34-
queryClient.resetQueries({ queryKey: knowledgeKeys.detail(id) })
35-
),
16+
/** Document keys carry no resource scope and may exist without source or result caches. */
17+
queryClient.resetQueries({ queryKey: knowledgeKeys.details() }),
3618
queryClient.resetQueries({ queryKey: searchSourceKeys.list(scope) }),
3719
])
3820
}

0 commit comments

Comments
 (0)