Skip to content

Commit 11c7a36

Browse files
authored
feat(hex): expand API coverage, fix ID trimming, add cursor pagination (#5372)
* feat(hex): expand API coverage, fix ID trimming, add cursor pagination - Add hex_update_collection, hex_create_group, hex_update_group, hex_delete_group, hex_deactivate_user tools + block wiring - Add missing run_project fields (viewId, notifications) and get_project_runs runTriggerFilter - Add after/before cursor pagination to list_projects, list_groups, list_data_connections, list_collections - Add .trim() on all interpolated ID path params to guard against copy-paste whitespace * fix(hex): guard JSON.parse on user-supplied array/object params Wrap JSON.parse calls for memberUserIds, addUserIds, removeUserIds, inputParams, and notifications in try/catch so malformed input throws a clear error instead of an opaque parse exception. * fix(hex): forward empty collection description on update update_collection's params() mapping only copied collectionDescription when truthy, so clearing it to an empty string in the UI never reached the API. Untouched fields resolve to null (not undefined), so use a loose null check to distinguish "cleared" from "never touched" without sending description: null on every unrelated update. * fix(hex): close remaining API coverage gaps found via raw OpenAPI spec Pulled Hex's actual OpenAPI spec (static.hex.site/openapi.json) as ground truth for a final verification pass: - list_users was missing after/before cursor pagination and the userIds filter, which the spec confirms it supports (an earlier doc-summary pass had incorrectly flagged this as unverifiable) - list_users response also exposes lastLoginDate per user, not previously surfaced - list_projects was missing includeComponents, includeTrashed, creatorEmail, ownerEmail, collectionId, categories, sortBy, and sortDirection filters that the spec confirms are real query params * fix(hex): trim remaining user-suppliable IDs for consistency Trim collectionId (list_projects filter), viewId (run_project body), and group member UUIDs (create_group/update_group) to match the .trim() convention already applied to path-segment IDs elsewhere. * fix(hex): validate parsed JSON is actually an array before iterating categories, memberUserIds, addUserIds, and removeUserIds could parse as valid JSON that isn't an array (e.g. an object), which would throw an opaque "not iterable"/"map is not a function" error deeper in the call. Validate Array.isArray after parsing and fail with a clear message instead. * fix(hex): validate array element types, send explicit ALL trigger filter - notifications now gets the same Array.isArray guard already applied to categories/memberUserIds/addUserIds/removeUserIds - member/category array elements are now validated as strings before .trim()/append, instead of crashing on non-string entries - runTriggerFilter "All" option now sends the documented ALL enum value explicitly instead of relying on omission
1 parent 9977407 commit 11c7a36

24 files changed

Lines changed: 1101 additions & 26 deletions

apps/sim/blocks/blocks/hex.ts

Lines changed: 324 additions & 12 deletions
Large diffs are not rendered by default.

apps/sim/tools/hex/cancel_run.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export const cancelRunTool: ToolConfig<HexCancelRunParams, HexCancelRunResponse>
3030

3131
request: {
3232
url: (params) =>
33-
`https://app.hex.tech/api/v1/projects/${params.projectId}/runs/${params.runId}`,
33+
`https://app.hex.tech/api/v1/projects/${params.projectId.trim()}/runs/${params.runId.trim()}`,
3434
method: 'DELETE',
3535
headers: (params) => ({
3636
Authorization: `Bearer ${params.apiKey}`,

apps/sim/tools/hex/create_group.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import type { HexCreateGroupParams, HexCreateGroupResponse } from '@/tools/hex/types'
2+
import type { ToolConfig } from '@/tools/types'
3+
4+
export const createGroupTool: ToolConfig<HexCreateGroupParams, HexCreateGroupResponse> = {
5+
id: 'hex_create_group',
6+
name: 'Hex Create Group',
7+
description: 'Create a new group in the Hex workspace, optionally with initial members.',
8+
version: '1.0.0',
9+
10+
params: {
11+
apiKey: {
12+
type: 'string',
13+
required: true,
14+
visibility: 'user-only',
15+
description: 'Hex API token (Personal or Workspace)',
16+
},
17+
name: {
18+
type: 'string',
19+
required: true,
20+
visibility: 'user-or-llm',
21+
description: 'Name for the new group',
22+
},
23+
memberUserIds: {
24+
type: 'json',
25+
required: false,
26+
visibility: 'user-or-llm',
27+
description:
28+
'JSON array of user UUIDs to add as initial group members (e.g., ["uuid1", "uuid2"])',
29+
},
30+
},
31+
32+
request: {
33+
url: 'https://app.hex.tech/api/v1/groups',
34+
method: 'POST',
35+
headers: (params) => ({
36+
Authorization: `Bearer ${params.apiKey}`,
37+
'Content-Type': 'application/json',
38+
}),
39+
body: (params) => {
40+
const body: Record<string, unknown> = { name: params.name }
41+
if (params.memberUserIds) {
42+
let userIds: unknown
43+
try {
44+
userIds =
45+
typeof params.memberUserIds === 'string'
46+
? JSON.parse(params.memberUserIds)
47+
: params.memberUserIds
48+
} catch {
49+
throw new Error('memberUserIds must be a valid JSON array of user UUID strings')
50+
}
51+
if (!Array.isArray(userIds) || !userIds.every((id) => typeof id === 'string')) {
52+
throw new Error('memberUserIds must be a valid JSON array of user UUID strings')
53+
}
54+
body.members = { users: userIds.map((id: string) => ({ id: id.trim() })) }
55+
}
56+
return body
57+
},
58+
},
59+
60+
transformResponse: async (response: Response) => {
61+
const data = await response.json()
62+
63+
return {
64+
success: true,
65+
output: {
66+
id: data.id ?? null,
67+
name: data.name ?? null,
68+
createdAt: data.createdAt ?? null,
69+
},
70+
}
71+
},
72+
73+
outputs: {
74+
id: { type: 'string', description: 'Newly created group UUID' },
75+
name: { type: 'string', description: 'Group name' },
76+
createdAt: { type: 'string', description: 'Creation timestamp' },
77+
},
78+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import type { HexDeactivateUserParams, HexDeactivateUserResponse } from '@/tools/hex/types'
2+
import type { ToolConfig } from '@/tools/types'
3+
4+
export const deactivateUserTool: ToolConfig<HexDeactivateUserParams, HexDeactivateUserResponse> = {
5+
id: 'hex_deactivate_user',
6+
name: 'Hex Deactivate User',
7+
description: 'Deactivate a user in the Hex workspace.',
8+
version: '1.0.0',
9+
10+
params: {
11+
apiKey: {
12+
type: 'string',
13+
required: true,
14+
visibility: 'user-only',
15+
description: 'Hex API token (Personal or Workspace)',
16+
},
17+
userId: {
18+
type: 'string',
19+
required: true,
20+
visibility: 'user-or-llm',
21+
description: 'The UUID of the user to deactivate',
22+
},
23+
},
24+
25+
request: {
26+
url: (params) => `https://app.hex.tech/api/v1/users/${params.userId.trim()}/deactivate`,
27+
method: 'POST',
28+
headers: (params) => ({
29+
Authorization: `Bearer ${params.apiKey}`,
30+
'Content-Type': 'application/json',
31+
}),
32+
},
33+
34+
transformResponse: async (response: Response, params) => {
35+
if (response.status === 204 || response.ok) {
36+
return {
37+
success: true,
38+
output: {
39+
success: true,
40+
userId: params?.userId ?? '',
41+
},
42+
}
43+
}
44+
45+
const data = await response.json().catch(() => ({}))
46+
return {
47+
success: false,
48+
output: {
49+
success: false,
50+
userId: params?.userId ?? '',
51+
},
52+
error: (data as Record<string, string>).message ?? 'Failed to deactivate user',
53+
}
54+
},
55+
56+
outputs: {
57+
success: { type: 'boolean', description: 'Whether the user was successfully deactivated' },
58+
userId: { type: 'string', description: 'User UUID that was deactivated' },
59+
},
60+
}

apps/sim/tools/hex/delete_group.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import type { HexDeleteGroupParams, HexDeleteGroupResponse } from '@/tools/hex/types'
2+
import type { ToolConfig } from '@/tools/types'
3+
4+
export const deleteGroupTool: ToolConfig<HexDeleteGroupParams, HexDeleteGroupResponse> = {
5+
id: 'hex_delete_group',
6+
name: 'Hex Delete Group',
7+
description: 'Delete a group from the Hex workspace.',
8+
version: '1.0.0',
9+
10+
params: {
11+
apiKey: {
12+
type: 'string',
13+
required: true,
14+
visibility: 'user-only',
15+
description: 'Hex API token (Personal or Workspace)',
16+
},
17+
groupId: {
18+
type: 'string',
19+
required: true,
20+
visibility: 'user-or-llm',
21+
description: 'The UUID of the group to delete',
22+
},
23+
},
24+
25+
request: {
26+
url: (params) => `https://app.hex.tech/api/v1/groups/${params.groupId.trim()}`,
27+
method: 'DELETE',
28+
headers: (params) => ({
29+
Authorization: `Bearer ${params.apiKey}`,
30+
'Content-Type': 'application/json',
31+
}),
32+
},
33+
34+
transformResponse: async (response: Response, params) => {
35+
if (response.status === 204 || response.ok) {
36+
return {
37+
success: true,
38+
output: {
39+
success: true,
40+
groupId: params?.groupId ?? '',
41+
},
42+
}
43+
}
44+
45+
const data = await response.json().catch(() => ({}))
46+
return {
47+
success: false,
48+
output: {
49+
success: false,
50+
groupId: params?.groupId ?? '',
51+
},
52+
error: (data as Record<string, string>).message ?? 'Failed to delete group',
53+
}
54+
},
55+
56+
outputs: {
57+
success: { type: 'boolean', description: 'Whether the group was successfully deleted' },
58+
groupId: { type: 'string', description: 'Group UUID that was deleted' },
59+
},
60+
}

apps/sim/tools/hex/get_collection.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const getCollectionTool: ToolConfig<HexGetCollectionParams, HexGetCollect
2323
},
2424

2525
request: {
26-
url: (params) => `https://app.hex.tech/api/v1/collections/${params.collectionId}`,
26+
url: (params) => `https://app.hex.tech/api/v1/collections/${params.collectionId.trim()}`,
2727
method: 'GET',
2828
headers: (params) => ({
2929
Authorization: `Bearer ${params.apiKey}`,

apps/sim/tools/hex/get_data_connection.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ export const getDataConnectionTool: ToolConfig<
2727
},
2828

2929
request: {
30-
url: (params) => `https://app.hex.tech/api/v1/data-connections/${params.dataConnectionId}`,
30+
url: (params) =>
31+
`https://app.hex.tech/api/v1/data-connections/${params.dataConnectionId.trim()}`,
3132
method: 'GET',
3233
headers: (params) => ({
3334
Authorization: `Bearer ${params.apiKey}`,

apps/sim/tools/hex/get_group.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const getGroupTool: ToolConfig<HexGetGroupParams, HexGetGroupResponse> =
2323
},
2424

2525
request: {
26-
url: (params) => `https://app.hex.tech/api/v1/groups/${params.groupId}`,
26+
url: (params) => `https://app.hex.tech/api/v1/groups/${params.groupId.trim()}`,
2727
method: 'GET',
2828
headers: (params) => ({
2929
Authorization: `Bearer ${params.apiKey}`,

apps/sim/tools/hex/get_project.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const getProjectTool: ToolConfig<HexGetProjectParams, HexGetProjectRespon
2424
},
2525

2626
request: {
27-
url: (params) => `https://app.hex.tech/api/v1/projects/${params.projectId}`,
27+
url: (params) => `https://app.hex.tech/api/v1/projects/${params.projectId.trim()}`,
2828
method: 'GET',
2929
headers: (params) => ({
3030
Authorization: `Bearer ${params.apiKey}`,

apps/sim/tools/hex/get_project_runs.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ export const getProjectRunsTool: ToolConfig<HexGetProjectRunsParams, HexGetProje
4040
description:
4141
'Filter by run status: PENDING, RUNNING, ERRORED, COMPLETED, KILLED, UNABLE_TO_ALLOCATE_KERNEL',
4242
},
43+
runTriggerFilter: {
44+
type: 'string',
45+
required: false,
46+
visibility: 'user-or-llm',
47+
description: 'Filter by how the run was triggered: ALL, API, SCHEDULED, or APP_REFRESH',
48+
},
4349
},
4450

4551
request: {
@@ -48,8 +54,9 @@ export const getProjectRunsTool: ToolConfig<HexGetProjectRunsParams, HexGetProje
4854
if (params.limit) searchParams.set('limit', String(params.limit))
4955
if (params.offset) searchParams.set('offset', String(params.offset))
5056
if (params.statusFilter) searchParams.set('statusFilter', params.statusFilter)
57+
if (params.runTriggerFilter) searchParams.set('runTriggerFilter', params.runTriggerFilter)
5158
const qs = searchParams.toString()
52-
return `https://app.hex.tech/api/v1/projects/${params.projectId}/runs${qs ? `?${qs}` : ''}`
59+
return `https://app.hex.tech/api/v1/projects/${params.projectId.trim()}/runs${qs ? `?${qs}` : ''}`
5360
},
5461
method: 'GET',
5562
headers: (params) => ({
@@ -78,6 +85,8 @@ export const getProjectRunsTool: ToolConfig<HexGetProjectRunsParams, HexGetProje
7885
})),
7986
total: runs.length,
8087
traceId: data.traceId ?? null,
88+
nextPage: data.nextPage ?? null,
89+
previousPage: data.previousPage ?? null,
8190
},
8291
}
8392
},
@@ -111,5 +120,11 @@ export const getProjectRunsTool: ToolConfig<HexGetProjectRunsParams, HexGetProje
111120
},
112121
total: { type: 'number', description: 'Total number of runs returned' },
113122
traceId: { type: 'string', description: 'Top-level trace ID', optional: true },
123+
nextPage: { type: 'string', description: 'Cursor for the next page of runs', optional: true },
124+
previousPage: {
125+
type: 'string',
126+
description: 'Cursor for the previous page of runs',
127+
optional: true,
128+
},
114129
},
115130
}

0 commit comments

Comments
 (0)