Skip to content

Commit 84c8dd2

Browse files
committed
fix(integrations): preserve defaults and refresh Search access
1 parent a4ea29c commit 84c8dd2

10 files changed

Lines changed: 274 additions & 95 deletions

File tree

‎apps/sim/blocks/blocks/flow.ts‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -888,10 +888,15 @@ export const FlowBlock: BlockConfig = {
888888
tool: (params) => `flow_${params.operation || 'search_entities'}`,
889889
params: (params) => {
890890
const result: Record<string, unknown> = {}
891-
if (params.limit != null && params.limit !== '') result.limit = Number(params.limit)
891+
result.limit =
892+
params.limit == null || (typeof params.limit === 'string' && !params.limit.trim())
893+
? undefined
894+
: Number(params.limit)
892895
for (const key of ['includeResolved', 'resolved', 'isVariant']) {
893-
if (params[key] != null && params[key] !== '')
894-
result[key] = params[key] === true || params[key] === 'true'
896+
result[key] =
897+
params[key] == null || params[key] === ''
898+
? undefined
899+
: params[key] === true || params[key] === 'true'
895900
}
896901
return result
897902
},

‎apps/sim/blocks/blocks/jama.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -708,12 +708,12 @@ export const JamaBlock: BlockConfig = {
708708
items: {
709709
type: 'json',
710710
description:
711-
'List Items, Search Items, or List Baseline Items results (id, documentKey, globalId, itemType, project, createdDate, modifiedDate, lastActivityDate, createdBy, modifiedBy, fields, version)',
711+
'Item list and search results (id, documentKey, globalId, itemType, project, createdDate, modifiedDate, lastActivityDate, createdBy, modifiedBy, fields, version)',
712712
},
713713
item: {
714714
type: 'json',
715715
description:
716-
'Get Item Version result (id, documentKey, globalId, itemType, project, createdDate, modifiedDate, lastActivityDate, createdBy, modifiedBy, fields, version)',
716+
'Get Item or Get Item Version result (id, documentKey, globalId, itemType, project, createdDate, modifiedDate, lastActivityDate, createdBy, modifiedBy, fields, version)',
717717
},
718718
id: { type: 'number', description: 'Created resource ID' },
719719
location: { type: 'string', description: 'Created resource API URL' },

‎apps/sim/executor/handlers/generic/handler.test.ts‎

Lines changed: 148 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,18 @@ import bufferPostFixture from '@/tools/buffer/__fixtures__/post.json'
1616
import { bufferCreatePostTool } from '@/tools/buffer/create_post'
1717
import { bufferEditPostTool } from '@/tools/buffer/edit_post'
1818
import type { BufferCreatePostParams, BufferEditPostParams } from '@/tools/buffer/types'
19+
import { flowCreateBranchTool } from '@/tools/flow/create-branch'
20+
import { flowListCommentsTool } from '@/tools/flow/list-comments'
21+
import { flowListEntitiesTool } from '@/tools/flow/list-entities'
1922
import { flowSetValueTool } from '@/tools/flow/set-value'
20-
import type { FlowSetValueParams } from '@/tools/flow/types'
23+
import type {
24+
FlowCreateBranchParams,
25+
FlowListCommentsParams,
26+
FlowListEntitiesParams,
27+
FlowSetValueParams,
28+
FlowUpdateCommentParams,
29+
} from '@/tools/flow/types'
30+
import { flowUpdateCommentTool } from '@/tools/flow/update-comment'
2131
import { mcpRunOperationTool } from '@/tools/mcp/run-operation'
2232
import type { ToolConfig } from '@/tools/types'
2333
import { getTool } from '@/tools/utils'
@@ -92,6 +102,8 @@ describe('GenericBlockHandler', () => {
92102
writes.length = 0
93103
mockGetBlock.mockReturnValue(FlowBlock)
94104
mockGetTool.mockReturnValue(flowSetValueTool)
105+
mockBlock.metadata = { id: 'flow', name: 'Flow' }
106+
mockBlock.config = { tool: 'flow_set_value', params: {} }
95107
const { validateRequiredParametersAfterMerge } =
96108
await vi.importActual<typeof import('@/tools/utils')>('@/tools/utils')
97109
mockExecuteTool.mockImplementation(async (toolId: string, params: FlowSetValueParams) => {
@@ -105,21 +117,13 @@ describe('GenericBlockHandler', () => {
105117
it.each(values.map((value) => ({ value, encoded: JSON.stringify(value) })))(
106118
'preserves the JSON value $encoded on the provider wire',
107119
async ({ value, encoded }) => {
108-
await handler.execute(
109-
mockContext,
110-
{
111-
...mockBlock,
112-
metadata: { id: 'flow', name: 'Flow' },
113-
config: { tool: 'flow_set_value', params: {} },
114-
},
115-
{
116-
operation: 'set_value',
117-
projectId: 'project',
118-
entityId: 'entity',
119-
fieldKey: 'name',
120-
value: encoded,
121-
}
122-
)
120+
await handler.execute(mockContext, mockBlock, {
121+
operation: 'set_value',
122+
projectId: 'project',
123+
entityId: 'entity',
124+
fieldKey: 'name',
125+
value: encoded,
126+
})
123127
expect(writes).toEqual([{ key: 'name', value }])
124128
}
125129
)
@@ -128,27 +132,140 @@ describe('GenericBlockHandler', () => {
128132
'rejects omitted or malformed JSON before sending a field write (%s)',
129133
async (value) => {
130134
await expect(
131-
handler.execute(
132-
mockContext,
133-
{
134-
...mockBlock,
135-
metadata: { id: 'flow', name: 'Flow' },
136-
config: { tool: 'flow_set_value', params: {} },
137-
},
138-
{
139-
operation: 'set_value',
140-
projectId: 'project',
141-
entityId: 'entity',
142-
fieldKey: 'name',
143-
value,
144-
}
145-
)
135+
handler.execute(mockContext, mockBlock, {
136+
operation: 'set_value',
137+
projectId: 'project',
138+
entityId: 'entity',
139+
fieldKey: 'name',
140+
value,
141+
})
146142
).rejects.toThrow()
147143
expect(writes).toEqual([])
148144
}
149145
)
146+
147+
it('rejects a whitespace-only field key before sending a write', async () => {
148+
await expect(
149+
handler.execute(mockContext, mockBlock, {
150+
operation: 'set_value',
151+
projectId: 'project',
152+
entityId: 'entity',
153+
fieldKey: ' \t ',
154+
value: '"updated"',
155+
})
156+
).rejects.toThrow('Field key is required')
157+
expect(writes).toEqual([])
158+
})
159+
})
160+
161+
describe('Flow list limits', () => {
162+
const requests: string[] = []
163+
164+
beforeEach(() => {
165+
requests.length = 0
166+
mockGetBlock.mockReturnValue(FlowBlock)
167+
mockGetTool.mockReturnValue(flowListEntitiesTool)
168+
mockExecuteTool.mockImplementation(
169+
async (_toolId: string, params: FlowListEntitiesParams) => {
170+
const url = flowListEntitiesTool.request.url
171+
requests.push(typeof url === 'function' ? url(params) : url)
172+
return { success: true, output: {} }
173+
}
174+
)
175+
mockBlock.metadata = { id: 'flow', name: 'Flow' }
176+
mockBlock.config = { tool: 'flow_list_entities', params: {} }
177+
})
178+
179+
it.each([
180+
{ limit: null, expected: '50' },
181+
{ limit: '', expected: '50' },
182+
{ limit: ' ', expected: '50' },
183+
{ limit: '25', expected: '25' },
184+
])('sends page size $expected when the Limit input is $limit', async ({ limit, expected }) => {
185+
await handler.execute(mockContext, mockBlock, {
186+
operation: 'list_entities',
187+
domain: 'https://api.example.com',
188+
projectId: 'project',
189+
limit,
190+
})
191+
expect(requests.map((url) => new URL(url).searchParams.get('limit'))).toEqual([expected])
192+
})
193+
194+
it('rejects an explicit zero instead of silently replacing it with the default', async () => {
195+
await expect(
196+
handler.execute(mockContext, mockBlock, {
197+
operation: 'list_entities',
198+
domain: 'https://api.example.com',
199+
projectId: 'project',
200+
limit: '0',
201+
})
202+
).rejects.toThrow('Limit must be an integer between 1 and 1000')
203+
expect(requests).toEqual([])
204+
})
150205
})
151206

207+
it.each([
208+
{ selection: 'untouched', value: null, expected: undefined },
209+
{ selection: 'cleared', value: '', expected: undefined },
210+
{ selection: 'No', value: 'false', expected: false },
211+
{ selection: 'Yes', value: true, expected: true },
212+
])(
213+
'sends optional Flow booleans correctly for $selection dropdowns',
214+
async ({ value, expected }) => {
215+
const requests: { includeResolved: string | null; body: unknown }[] = []
216+
mockGetBlock.mockReturnValue(FlowBlock)
217+
mockBlock.metadata = { id: 'flow', name: 'Flow' }
218+
219+
for (const tool of [flowListCommentsTool, flowUpdateCommentTool, flowCreateBranchTool]) {
220+
mockGetTool.mockReturnValue(tool)
221+
mockBlock.config = { tool: tool.id, params: {} }
222+
mockExecuteTool.mockImplementation(
223+
async (
224+
_toolId: string,
225+
params: FlowListCommentsParams & FlowUpdateCommentParams & FlowCreateBranchParams
226+
) => {
227+
const url = tool.request.url
228+
requests.push({
229+
includeResolved: new URL(
230+
typeof url === 'function' ? url(params) : url
231+
).searchParams.get('include_resolved'),
232+
body: await Response.json(tool.request.body?.(params) ?? null).json(),
233+
})
234+
return { success: true, output: {} }
235+
}
236+
)
237+
await handler.execute(mockContext, mockBlock, {
238+
operation: tool.id.replace('flow_', ''),
239+
domain: 'https://api.example.com',
240+
projectId: 'project',
241+
entityId: 'entity',
242+
commentId: 'comment',
243+
text: 'Updated comment',
244+
branchName: 'Pilot',
245+
includeResolved: value,
246+
resolved: value,
247+
isVariant: value,
248+
})
249+
}
250+
251+
expect(requests).toEqual([
252+
{ includeResolved: expected === undefined ? null : String(expected), body: null },
253+
{
254+
includeResolved: null,
255+
body:
256+
expected === undefined
257+
? { text: 'Updated comment' }
258+
: { text: 'Updated comment', resolved: expected },
259+
},
260+
{
261+
includeResolved: null,
262+
body:
263+
expected === undefined ? { name: 'Pilot' } : { name: 'Pilot', is_variant: expected },
264+
},
265+
])
266+
}
267+
)
268+
152269
it.each([
153270
{
154271
selection: 'keep-current scheduling',

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

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ import { ApiClientError } from '@/lib/api/client/errors'
1818
import { listOrganizationAccountPeopleContract } from '@/lib/api/contracts/organization-accounts'
1919
import { resourceScopeKey } from '@/lib/core/resource-scope'
2020
import { useOrganizationAccountPeople } from '@/hooks/queries/organization-accounts'
21-
import { useRemovePersonalSearchCredential } from '@/hooks/queries/personal-search-integrations'
21+
import {
22+
useRemovePersonalSearchCredential,
23+
useSavePersonalSearchCredential,
24+
} from '@/hooks/queries/personal-search-integrations'
2225
import { knowledgeKeys } from '@/hooks/queries/utils/knowledge-keys'
2326

2427
const mockRequestJson = apiClientRequestMockFns.mockRequestJson
@@ -29,9 +32,11 @@ describe('organization accounts', () => {
2932
let client: QueryClient
3033
let result: ReturnType<typeof useOrganizationAccountPeople>
3134
let removal: ReturnType<typeof useRemovePersonalSearchCredential>
35+
let save: ReturnType<typeof useSavePersonalSearchCredential>
3236

33-
function RemovalProbe() {
37+
function CredentialMutationProbe() {
3438
removal = useRemovePersonalSearchCredential()
39+
save = useSavePersonalSearchCredential()
3540
return null
3641
}
3742

@@ -139,36 +144,50 @@ describe('organization accounts', () => {
139144
expect(result.data?.pages).toHaveLength(1)
140145
})
141146

142-
it('clears disconnected Search content while preserving another organization’s results', async () => {
143-
const search = knowledgeKeys.search(
144-
resourceScopeKey({ kind: 'organization', organizationId: 'org-1' }),
145-
'private content'
146-
)
147-
const otherSearch = knowledgeKeys.search(
148-
resourceScopeKey({ kind: 'organization', organizationId: 'org-2' }),
149-
'private content'
150-
)
151-
const document = knowledgeKeys.document('knowledge-1', 'document-1')
152-
client.setQueryData(search, [{ content: 'disconnected source content' }])
153-
client.setQueryData(otherSearch, [{ content: 'other organization content' }])
154-
client.setQueryData(document, { content: 'disconnected document content' })
155-
mockRequestJson.mockResolvedValueOnce({ success: true })
147+
it.each(['remove', 'reconnect'] as const)(
148+
'clears cached Search content after %s while preserving another organization’s results',
149+
async (action) => {
150+
const search = knowledgeKeys.search(
151+
resourceScopeKey({ kind: 'organization', organizationId: 'org-1' }),
152+
'private content'
153+
)
154+
const otherSearch = knowledgeKeys.search(
155+
resourceScopeKey({ kind: 'organization', organizationId: 'org-2' }),
156+
'private content'
157+
)
158+
const document = knowledgeKeys.document('knowledge-1', 'document-1')
159+
client.setQueryData(search, [{ content: 'previous credential content' }])
160+
client.setQueryData(otherSearch, [{ content: 'other organization content' }])
161+
client.setQueryData(document, { content: 'previous credential document' })
162+
mockRequestJson.mockResolvedValueOnce(
163+
action === 'remove' ? { success: true } : { credentialId: 'credential-1' }
164+
)
156165

157-
await act(async () =>
158-
root.render(
159-
<QueryClientProvider client={client}>
160-
<RemovalProbe />
161-
</QueryClientProvider>
166+
await act(async () =>
167+
root.render(
168+
<QueryClientProvider client={client}>
169+
<CredentialMutationProbe />
170+
</QueryClientProvider>
171+
)
162172
)
163-
)
164-
await act(async () => {
165-
await removal.mutateAsync({ organizationId: 'org-1', credentialId: 'credential-1' })
166-
})
173+
await act(async () => {
174+
const body = { organizationId: 'org-1', credentialId: 'credential-1' }
175+
if (action === 'remove') await removal.mutateAsync(body)
176+
else
177+
await save.mutateAsync({
178+
...body,
179+
providerId: 'flow-service-account',
180+
apiToken: 'rotated-token',
181+
domain: 'https://api.flowengineering.com',
182+
providerWorkspaceId: 'workspace-1',
183+
})
184+
})
167185

168-
expect(client.getQueryData(search)).toBeUndefined()
169-
expect(client.getQueryData(document)).toBeUndefined()
170-
expect(client.getQueryData(otherSearch)).toEqual([{ content: 'other organization content' }])
171-
})
186+
expect(client.getQueryData(search)).toBeUndefined()
187+
expect(client.getQueryData(document)).toBeUndefined()
188+
expect(client.getQueryData(otherSearch)).toEqual([{ content: 'other organization content' }])
189+
}
190+
)
172191

173192
it.each([400, 401, 403, 404, 409, 422])(
174193
'does not retry a non-retryable %s response',

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

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export function useConnectPersonalSearchIntegration() {
7070
})
7171
}
7272

73-
/** Saves a member-owned provider credential and refreshes both Search connection inventories. */
73+
/** Saves a member-owned provider credential and clears Search content fetched with prior access. */
7474
export function useSavePersonalSearchCredential() {
7575
const client = useQueryClient()
7676
return useMutation({
@@ -82,12 +82,7 @@ export function useSavePersonalSearchCredential() {
8282
queryKey: organizationAccountsKeys.detail(body.organizationId),
8383
}),
8484
client.invalidateQueries({ queryKey: personalSearchIntegrationKeys.lists() }),
85-
client.invalidateQueries({
86-
queryKey: searchSourceKeys.list({
87-
kind: 'organization',
88-
organizationId: body.organizationId,
89-
}),
90-
}),
85+
resetOrganizationSearchAccess(client, body.organizationId),
9186
]),
9287
})
9388
}

0 commit comments

Comments
 (0)