Skip to content

Commit 31df335

Browse files
committed
fix(sailpoint): reject empty aggregation definitions
1 parent 29ba351 commit 31df335

4 files changed

Lines changed: 65 additions & 4 deletions

File tree

‎apps/sim/lib/internal/sailpoint/execute-tool.test.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,20 @@ describe('SailPoint internal tool handler', () => {
412412
expect(mockFetch).not.toHaveBeenCalled()
413413
})
414414

415+
it.each([
416+
['aggregationsDsl', {}],
417+
['aggregationsDsl', '{}'],
418+
['aggregations', {}],
419+
['aggregations', '{}'],
420+
])('rejects an empty %s aggregate definition', async (field, value) => {
421+
const response = await request('sailpoint_search_aggregate', { [field]: value })
422+
expect(response.status).toBe(400)
423+
expect(await response.json()).toMatchObject({
424+
error: expect.stringContaining('must be a non-empty object'),
425+
})
426+
expect(mockFetch).not.toHaveBeenCalled()
427+
})
428+
415429
it.each([
416430
[
417431
'sailpoint_list_entitlements',

‎apps/sim/lib/internal/sailpoint/schema.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { isRecordLike } from '@sim/utils/object'
12
import { z } from 'zod'
23
import { FileInputSchema } from '@/lib/uploads/utils/file-schemas'
34

@@ -198,11 +199,15 @@ const schemas = {
198199
...pagination(),
199200
}).superRefine((value, ctx) => {
200201
validateSearchQuerySelection(value, ctx, false)
201-
if (!value.aggregationsDsl && !value.aggregations) {
202+
const hasAggregationsDsl =
203+
isRecordLike(value.aggregationsDsl) && Object.keys(value.aggregationsDsl).length > 0
204+
const hasAggregations =
205+
isRecordLike(value.aggregations) && Object.keys(value.aggregations).length > 0
206+
if (!hasAggregationsDsl && !hasAggregations) {
202207
ctx.addIssue({
203208
code: z.ZodIssueCode.custom,
204209
path: ['aggregationsDsl'],
205-
message: 'aggregationsDsl or aggregations is required',
210+
message: 'aggregationsDsl or aggregations must be a non-empty object',
206211
})
207212
}
208213
}),
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { sailpointSearchAggregateTool } from '@/tools/sailpoint/definitions'
3+
import type { SailPointSearchAggregateParams } from '@/tools/sailpoint/types'
4+
5+
const credentials = { clientId: 'client', clientSecret: 'secret', tenant: 'tenant' }
6+
7+
function operationInput(params: SailPointSearchAggregateParams) {
8+
return sailpointSearchAggregateTool.operation.input(params)
9+
}
10+
11+
describe('SailPoint Search Aggregate tool input', () => {
12+
it.each([
13+
['aggregationsDsl', {}],
14+
['aggregationsDsl', '{}'],
15+
['aggregations', {}],
16+
['aggregations', '{}'],
17+
] as const)('rejects an empty %s definition', (field, value) => {
18+
expect(() => operationInput({ ...credentials, [field]: value })).toThrow(
19+
'aggregationsDsl or aggregations must be a non-empty object'
20+
)
21+
})
22+
23+
it('accepts either non-empty aggregation representation', () => {
24+
expect(
25+
operationInput({ ...credentials, aggregationsDsl: { names: { terms: { field: 'name' } } } })
26+
).toMatchObject({ aggregationsDsl: { names: { terms: { field: 'name' } } } })
27+
expect(
28+
operationInput({ ...credentials, aggregations: { names: { type: 'TERM', field: 'name' } } })
29+
).toMatchObject({ aggregations: { names: { type: 'TERM', field: 'name' } } })
30+
})
31+
})

‎apps/sim/tools/sailpoint/definitions.ts‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { isRecordLike } from '@sim/utils/object'
12
import {
23
createSailPointListOutputs,
34
createSailPointOperationInput,
@@ -170,7 +171,8 @@ function segmentedListInput(params: SailPointSegmentedListParams, maxLimit = 250
170171

171172
function searchInput(
172173
params: SailPointSearchBodyParams,
173-
queryRequired = false
174+
queryRequired = false,
175+
aggregationsRequired = false
174176
): Record<string, unknown> {
175177
const query = parseJsonValue(params.query, 'query')
176178
const queryDsl = parseJsonValue(params.queryDsl, 'queryDsl')
@@ -181,6 +183,15 @@ function searchInput(
181183
const aggregations = parseJsonValue(params.aggregations, 'aggregations')
182184
const filters = parseJsonValue(params.filters, 'filters')
183185

186+
if (aggregationsRequired) {
187+
const hasAggregationsDsl =
188+
isRecordLike(aggregationsDsl) && Object.keys(aggregationsDsl).length > 0
189+
const hasAggregations = isRecordLike(aggregations) && Object.keys(aggregations).length > 0
190+
if (!hasAggregationsDsl && !hasAggregations) {
191+
throw new Error('aggregationsDsl or aggregations must be a non-empty object')
192+
}
193+
}
194+
184195
const hasQueryInput = Boolean(query || queryDsl || textQuery || typeAheadQuery)
185196
if (queryRequired || params.queryType || hasQueryInput) {
186197
const queryType = params.queryType ?? 'SAILPOINT'
@@ -441,7 +452,7 @@ export const sailpointSearchAggregateTool = defineSailPointTool<SailPointSearchA
441452
input: (params) => {
442453
validatePagination(params.limit, params.offset)
443454
return {
444-
...searchInput(params),
455+
...searchInput(params, false, true),
445456
limit: params.limit,
446457
offset: params.offset,
447458
count: params.count,

0 commit comments

Comments
 (0)