Skip to content

Commit e187f86

Browse files
committed
fix(agiloft): cap search results against unverified alrest pagination
Whether alrest honours page/limit in the request body is unverified — those names carry over from the legacy EWSearch query string, and neither the customer's working tool nor any known client sends them. If the server ignores them a broad query returns the whole table at roughly 184KB per contract record, so the result is bounded here instead of trusting the server.
1 parent 307492d commit e187f86

4 files changed

Lines changed: 45 additions & 3 deletions

File tree

apps/sim/app/api/tools/agiloft/create_record/route.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,3 +204,26 @@ describe('optional inputs arriving as null', () => {
204204
expect(JSON.parse(init.body as string)).toEqual({ query: "status='Active'" })
205205
})
206206
})
207+
208+
describe('search result ceiling', () => {
209+
it('caps the returned records, since alrest honouring limit is unverified', async () => {
210+
arrange(
211+
res({
212+
json: {
213+
success: true,
214+
result: Array.from({ length: 250 }, (_, i) => ({ id: i })),
215+
},
216+
})
217+
)
218+
219+
const response = await SEARCH(
220+
createMockRequest('POST', { ...baseBody, query: "status='Active'" })
221+
)
222+
const data = (await response.json()) as {
223+
output: { records: unknown[]; totalCount: number }
224+
}
225+
226+
expect(data.output.records).toHaveLength(200)
227+
expect(data.output.totalCount).toBe(200)
228+
})
229+
})

apps/sim/app/api/tools/agiloft/search_records/route.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { checkInternalAuth } from '@/lib/auth/hybrid'
88
import { generateRequestId } from '@/lib/core/utils/request'
99
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1010
import type { AgiloftSearchResponse } from '@/tools/agiloft/types'
11-
import { alrestSearchUrl, parseFieldList } from '@/tools/agiloft/utils'
11+
import { AGILOFT_MAX_SEARCH_RECORDS, alrestSearchUrl, parseFieldList } from '@/tools/agiloft/utils'
1212
import { executeAlrestRequest, readAlrestJson } from '@/tools/agiloft/utils.server'
1313

1414
export const dynamic = 'force-dynamic'
@@ -70,7 +70,15 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
7070
),
7171
}),
7272
async (response) => {
73-
const records = (await readAlrestJson<Record<string, unknown>[]>(response)) ?? []
73+
const returned = (await readAlrestJson<Record<string, unknown>[]>(response)) ?? []
74+
const records = returned.slice(0, AGILOFT_MAX_SEARCH_RECORDS)
75+
76+
if (returned.length > records.length) {
77+
logger.warn(
78+
`[${requestId}] Agiloft search returned ${returned.length} records; truncated to ${AGILOFT_MAX_SEARCH_RECORDS}`,
79+
{ table: params.table }
80+
)
81+
}
7482

7583
return {
7684
success: true,

apps/sim/blocks/blocks/agiloft.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -497,7 +497,7 @@ export const AgiloftBlock: BlockConfig = {
497497
totalCount: {
498498
type: 'number',
499499
description:
500-
'Number of matching results. For a paginated search this counts the current page only',
500+
'Number of records returned by this call, capped at 200 — not a total match count',
501501
condition: {
502502
field: 'operation',
503503
value: ['search_records', 'select_records', 'attachment_info'],

apps/sim/tools/agiloft/utils.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,17 @@ export function alrestRetrieveAttachUrl(base: string, table: string): string {
5656
return `${base}/${tableSegment(table)}/retrieveAttach?lang=${AGILOFT_LANG}`
5757
}
5858

59+
/**
60+
* Hard ceiling on records returned from a search.
61+
*
62+
* Whether alrest honours `page`/`limit` in the request body is unverified — the
63+
* names carry over from the legacy EWSearch query string. If it ignores them a
64+
* broad query returns the whole table, and an unfiltered contract record runs
65+
* to roughly 184 KB, so the result is capped here rather than trusting the
66+
* server to bound it.
67+
*/
68+
export const AGILOFT_MAX_SEARCH_RECORDS = 200
69+
5970
/**
6071
* Splits a comma-separated field list into the `field` array alrest search
6172
* accepts. Field selection is the only way to keep a response small — a single

0 commit comments

Comments
 (0)