Skip to content

Commit 77e91eb

Browse files
author
Vikhyath Mondreti
committed
fix(search): clear cached content after disconnect
1 parent d4d11d9 commit 77e91eb

5 files changed

Lines changed: 138 additions & 34 deletions

File tree

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

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ import {
1414
listOrganizationAccountPeopleContract,
1515
updateOrganizationAccountsContract,
1616
} from '@/lib/api/contracts/organization-accounts'
17+
import { resourceScopeKey } from '@/lib/core/resource-scope'
1718
import {
1819
organizationAccountsKeys,
1920
useDisconnectPersonalOrganizationAccount,
2021
useOrganizationAccountPeople,
2122
useUpdateOrganizationAccounts,
2223
} from '@/hooks/queries/organization-accounts'
2324
import { slackSearchKeys } from '@/hooks/queries/slack-search'
25+
import { knowledgeKeys } from '@/hooks/queries/utils/knowledge-keys'
2426
import { searchSourceKeys } from '@/hooks/queries/utils/search-source-keys'
2527

2628
describe('personal account disconnect', () => {
@@ -48,7 +50,39 @@ describe('personal account disconnect', () => {
4850
)
4951
const people = organizationAccountsKeys.people('org-1')
5052
const other = searchSourceKeys.list({ kind: 'organization', organizationId: 'org-2' })
51-
for (const key of [own, catalog, people, other]) client.setQueryData(key, { existing: true })
53+
const results = knowledgeKeys.search(
54+
resourceScopeKey({ kind: 'organization', organizationId: 'org-1' }),
55+
'private content'
56+
)
57+
const otherResults = knowledgeKeys.search(
58+
resourceScopeKey({ kind: 'organization', organizationId: 'org-2' }),
59+
'private content'
60+
)
61+
const workspaceResults = knowledgeKeys.search('workspace-1', 'private content')
62+
const ownDocument = knowledgeKeys.document('kb-1', 'document-1')
63+
const chunks = knowledgeKeys.chunks('kb-2', 'document-2', '')
64+
const otherDocument = knowledgeKeys.document('kb-other', 'document-other')
65+
const resultOnlyDocument = knowledgeKeys.document('kb-result-only', 'document-result-only')
66+
const sourcePages = {
67+
pages: [
68+
{ sources: [{ knowledgeBaseId: 'kb-1' }], nextCursor: 'next' },
69+
{ sources: [{ knowledgeBaseId: 'kb-2' }], nextCursor: null },
70+
],
71+
pageParams: [undefined, 'next'],
72+
}
73+
for (const key of [own, catalog]) client.setQueryData(key, sourcePages)
74+
client.setQueryData(results, [{ knowledgeBaseId: 'kb-result-only' }])
75+
for (const key of [
76+
people,
77+
other,
78+
otherResults,
79+
workspaceResults,
80+
ownDocument,
81+
resultOnlyDocument,
82+
chunks,
83+
otherDocument,
84+
])
85+
client.setQueryData(key, { content: 'previously authorized content' })
5286
try {
5387
await act(async () =>
5488
root.render(
@@ -78,9 +112,14 @@ describe('personal account disconnect', () => {
78112
disconnectPersonalOrganizationAccountContract,
79113
{ params: { credentialId: 'own-credential' } }
80114
)
81-
for (const key of [own, catalog, people])
82-
expect(client.getQueryState(key)?.isInvalidated).toBe(success)
115+
for (const key of [own, catalog, results, ownDocument, resultOnlyDocument, chunks]) {
116+
if (success) expect(client.getQueryData(key)).toBeUndefined()
117+
else expect(client.getQueryData(key)).toBeDefined()
118+
}
119+
expect(client.getQueryState(people)?.isInvalidated).toBe(success)
83120
expect(client.getQueryState(other)?.isInvalidated).toBe(false)
121+
for (const key of [otherResults, workspaceResults, otherDocument])
122+
expect(client.getQueryData(key)).toEqual({ content: 'previously authorized content' })
84123
} finally {
85124
await act(async () => root.unmount())
86125
client.clear()

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
updateOrganizationAccountWorkspaceAccessContract,
3838
} from '@/lib/api/contracts/organization-accounts'
3939
import { slackSearchKeys } from '@/hooks/queries/slack-search'
40+
import { resetOrganizationSearchAccess } from '@/hooks/queries/utils/reset-organization-search-access'
4041
import { searchSourceKeys } from '@/hooks/queries/utils/search-source-keys'
4142

4243
export const ORGANIZATION_ACCOUNTS_STALE_TIME = 30_000
@@ -51,9 +52,7 @@ export function useDisconnectPersonalOrganizationAccount(organizationId: string)
5152
}),
5253
onSuccess: () =>
5354
Promise.all([
54-
queryClient.invalidateQueries({
55-
queryKey: searchSourceKeys.list({ kind: 'organization', organizationId }),
56-
}),
55+
resetOrganizationSearchAccess(queryClient, organizationId),
5756
queryClient.invalidateQueries({
5857
queryKey: organizationAccountsKeys.detail(organizationId),
5958
}),
Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
1-
import { type InfiniteData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
1+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
22
import { requestJson } from '@/lib/api/client/request'
3-
import type { SearchSourcePage } from '@/lib/api/contracts/knowledge/connectors'
43
import {
54
listSearchIntegrationsContract,
65
type UpdateSearchIntegrationBody,
76
updateSearchIntegrationContract,
87
} from '@/lib/api/contracts/knowledge/search-integrations'
9-
import { resourceScopeKey } from '@/lib/core/resource-scope'
10-
import { knowledgeKeys } from '@/hooks/queries/utils/knowledge-keys'
8+
import { resetOrganizationSearchAccess } from '@/hooks/queries/utils/reset-organization-search-access'
119
import { searchIntegrationKeys } from '@/hooks/queries/utils/search-integration-keys'
12-
import { searchSourceKeys } from '@/hooks/queries/utils/search-source-keys'
1310

1411
export const SEARCH_INTEGRATIONS_STALE_TIME = 30_000
1512

@@ -28,29 +25,10 @@ export function useUpdateSearchIntegration() {
2825
return useMutation({
2926
mutationFn: async (body: UpdateSearchIntegrationBody) =>
3027
(await requestJson(updateSearchIntegrationContract, { body })).data,
31-
onSuccess: async (_data, { organizationId }) => {
32-
const scope = { kind: 'organization', organizationId } as const
33-
const pages = queryClient.getQueriesData<InfiniteData<SearchSourcePage>>({
34-
queryKey: searchSourceKeys.list(scope),
35-
predicate: (query) => query.queryKey[3] === 'pages',
36-
})
37-
const knowledgeBaseIds = new Set(
38-
pages.flatMap(
39-
([, data]) =>
40-
data?.pages.flatMap((page) => page.sources.map((source) => source.knowledgeBaseId)) ??
41-
[]
42-
)
43-
)
44-
await Promise.all([
28+
onSuccess: (_data, { organizationId }) =>
29+
Promise.all([
30+
resetOrganizationSearchAccess(queryClient, organizationId),
4531
queryClient.invalidateQueries({ queryKey: searchIntegrationKeys.list(organizationId) }),
46-
queryClient.invalidateQueries({ queryKey: searchSourceKeys.list(scope) }),
47-
queryClient.resetQueries({
48-
queryKey: [...knowledgeKeys.searches(), resourceScopeKey(scope)],
49-
}),
50-
...[...knowledgeBaseIds].map((id) =>
51-
queryClient.resetQueries({ queryKey: knowledgeKeys.detail(id) })
52-
),
53-
])
54-
},
32+
]),
5533
})
5634
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/** @vitest-environment node */
2+
import { QueryClient } from '@tanstack/react-query'
3+
import { expect, it, vi } from 'vitest'
4+
import type { WorkspaceKnowledgeSearchResult } from '@/lib/api/contracts/knowledge/search'
5+
import { resourceScopeKey } from '@/lib/core/resource-scope'
6+
import { knowledgeKeys } from '@/hooks/queries/utils/knowledge-keys'
7+
import { resetOrganizationSearchAccess } from '@/hooks/queries/utils/reset-organization-search-access'
8+
9+
it('cancels an in-flight search so its late result cannot restore disconnected content', async () => {
10+
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } })
11+
const key = knowledgeKeys.search(
12+
resourceScopeKey({ kind: 'organization', organizationId: 'org-1' }),
13+
'private content'
14+
)
15+
const response = Promise.withResolvers<WorkspaceKnowledgeSearchResult[]>()
16+
const result: WorkspaceKnowledgeSearchResult = {
17+
documentId: 'document-1',
18+
knowledgeBaseId: 'kb-1',
19+
knowledgeBaseName: 'Knowledge',
20+
documentName: 'Private document',
21+
sourceUrl: null,
22+
connectorType: 'github',
23+
sourceModifiedAt: null,
24+
author: null,
25+
content: 'cached private content',
26+
chunkIndex: 0,
27+
similarity: 1,
28+
}
29+
const aborted = vi.fn()
30+
try {
31+
client.setQueryData(key, [result])
32+
const pending = client.fetchQuery({
33+
queryKey: key,
34+
queryFn: ({ signal }) => {
35+
signal.addEventListener('abort', aborted, { once: true })
36+
return response.promise
37+
},
38+
})
39+
const rejected = expect(pending).rejects.toThrow()
40+
await resetOrganizationSearchAccess(client, 'org-1')
41+
await rejected
42+
expect(aborted).toHaveBeenCalledOnce()
43+
expect(client.getQueryData(key)).toBeUndefined()
44+
response.resolve([{ ...result, content: 'late private content' }])
45+
await response.promise
46+
expect(client.getQueryData(key)).toBeUndefined()
47+
} finally {
48+
client.clear()
49+
}
50+
})
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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'
4+
import { resourceScopeKey } from '@/lib/core/resource-scope'
5+
import { knowledgeKeys } from '@/hooks/queries/utils/knowledge-keys'
6+
import { searchSourceKeys } from '@/hooks/queries/utils/search-source-keys'
7+
8+
/** Drops cached results and document content when organization Search access changes. */
9+
export async function resetOrganizationSearchAccess(
10+
queryClient: QueryClient,
11+
organizationId: string
12+
) {
13+
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+
])
29+
await Promise.all([
30+
queryClient.resetQueries({
31+
queryKey: searchKey,
32+
}),
33+
...[...knowledgeBaseIds].map((id) =>
34+
queryClient.resetQueries({ queryKey: knowledgeKeys.detail(id) })
35+
),
36+
queryClient.resetQueries({ queryKey: searchSourceKeys.list(scope) }),
37+
])
38+
}

0 commit comments

Comments
 (0)