improvement(search): bounded Jira fallback, organization search through integrations page connections by waleedlatif1 · Pull Request #8985 · simstudioai/sim · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/sim/lib/sim-search/live/account-session.ts
19 changes: 19 additions & 0 deletions apps/sim/lib/sim-search/live/accounts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,25 @@ describe('live account discovery boundaries', () => {
})
expect(mocks.resolve).not.toHaveBeenCalled()
})
it('searches an organization only through its integrations page enrollments', async () => {
mockListOrganizationSearchApprovals.mockResolvedValue(new Map([['jira', true]]))
mocks.managed.mockResolvedValue([
{ id: 'enrolled', providerId: 'jira', displayName: 'Enrolled Jira' },
])
const legacy = { id: 'legacy', providerId: 'jira', displayName: 'Legacy Jira' }
const metadata = ['enrolled', 'legacy'].map((id) => ({
id,
scope: 'read:jira-work',
grantedScopes: null,
revokedAt: null,
}))
queueTableRows(schemaMock.credential, [legacy])
queueTableRows(schemaMock.credential, metadata)
queueTableRows(schemaMock.credential, metadata)
expect(
(await listLiveAccounts({ organizationId: 'org' }, 'reader')).map((row) => row.id)
).toEqual(['enrolled'])
})
it('does not resolve another account even when the caller knows its ID', async () => {
metadata()
await expect(resolveLiveAccount(owner, 'reader', 'someone-else')).rejects.toMatchObject({
Expand Down
37 changes: 10 additions & 27 deletions apps/sim/lib/sim-search/live/accounts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { db } from '@sim/db'
import { account, credential, user } from '@sim/db/schema'
import { and, eq, inArray, isNull, ne } from 'drizzle-orm'
import { and, eq, inArray, isNull } from 'drizzle-orm'
import { liveSearchProviderSchema } from '@/lib/api/contracts/mothership-assistant-tools'
import {
type ResourceOwner,
Expand Down Expand Up @@ -28,34 +28,17 @@ import {
} from '@/lib/sim-search/live/provider-catalog'
import type { LiveAccount } from '@/lib/sim-search/live/types'

/**
* A workspace searches the person's workspace credentials; an organization searches only the
* accounts its members connect on the integrations page (credential-group enrollments).
*/
async function listOAuthRows(scope: ResourceScope, userId: string) {
if (scope.kind === 'workspace') return getPersonalOAuthCredentials(scope.workspaceId, userId)
const [managed, personal] = await Promise.all([
getOwnOrganizationManagedOAuthCredentials({ organizationId: scope.organizationId, userId }),
db
.select({
id: credential.id,
providerId: credential.providerId,
displayName: credential.displayName,
})
.from(credential)
.innerJoin(account, eq(account.id, credential.accountId))
.where(
and(
resourceScopeCondition(credential, scope),
eq(credential.type, 'oauth'),
eq(account.userId, userId),
eq(account.providerId, credential.providerId),
ne(credential.providerId, 'slack')
)
),
])
return [
...managed.map((row) => ({ ...row, type: 'managed_oauth' as const })),
...personal.flatMap((row) =>
row.providerId ? [{ ...row, providerId: row.providerId, type: 'oauth' as const }] : []
),
]
const managed = await getOwnOrganizationManagedOAuthCredentials({
organizationId: scope.organizationId,
userId,
})
return managed.map((row) => ({ ...row, type: 'managed_oauth' as const }))
}

async function listCodaTokenRows(scope: ResourceScope, userId: string) {
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/lib/sim-search/live/atlassian.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ export async function searchAtlassian(
fields: ['summary', 'description', 'updated', 'creator', 'status'],
...(input.native?.cursor && single ? { nextPageToken: input.native.cursor } : {}),
},
signal: input.signal,
})
)
return {
Expand All @@ -190,6 +191,7 @@ export async function searchAtlassian(
expand: 'content.version,content.space',
...(input.native?.cursor && single ? { cursor: input.native.cursor } : {}),
},
signal: input.signal,
})
)
const next = string(object(data._links).next)
Expand Down
6 changes: 4 additions & 2 deletions apps/sim/lib/sim-search/live/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@ export function createNativeClient(input: {
query?: Record<string, string | string[]>
body?: unknown
googleService?: 'sheets'
signal?: AbortSignal
}
) {
input.signal.throwIfAborted()
const signal = options?.signal ? AbortSignal.any([input.signal, options.signal]) : input.signal
signal.throwIfAborted()
if (++requests > (input.requestBudget ?? NATIVE_SEARCH_REQUEST_BUDGET))
throw new NativeSearchError('unavailable', 'Request budget reached. Narrow the query.')
const url = new URL(input.origin)
Expand All @@ -68,7 +70,7 @@ export function createNativeClient(input: {
},
method: options?.body ? 'POST' : 'GET',
...(options?.body ? { body: JSON.stringify(options.body) } : {}),
signal: input.signal,
signal,
timeout: 10_000,
maxResponseBytes: NATIVE_RESPONSE_MAX_BYTES,
maxRedirects: 0,
Expand Down
127 changes: 112 additions & 15 deletions apps/sim/lib/sim-search/live/term-fallback.test.ts
Loading
Loading