Skip to content

Commit dd32abe

Browse files
authored
feat(jsm): add Atlassian Assets (Insight/CMDB) tools for asset management (#5072)
* feat(jsm): add Atlassian Assets (Insight/CMDB) tools for asset management Add nine JSM Assets tools so workflows can read and write Atlassian Assets (Insight/CMDB) objects — the foundation for keeping JSM asset tables in sync for software/hardware asset management. Tools (wired into the Jira Service Management block): - jsm_list_object_schemas, jsm_get_object_schema - jsm_list_object_types, jsm_get_object_type_attributes - jsm_search_objects_aql (AQL search with pagination) - jsm_get_object, jsm_create_object, jsm_update_object, jsm_delete_object Each tool proxies through an internal route that resolves the Jira cloudId and the Assets workspaceId, then calls the Assets API via the OAuth 2.0 (3LO) gateway form (/ex/jira/{cloudId}/jsm/assets/workspace/{workspaceId}/v1). Adds the CMDB OAuth scopes to the jira provider (read/write/delete cmdb-object, read cmdb-schema/type/attribute) with descriptions, contract schemas for each route, and block operations/subBlocks/outputs. Bumps the API-validation route baseline for the nine new routes. * refactor(jsm): harden Assets param coercion and response typing - Add toOptionalInt helper so non-numeric pagination inputs never emit NaN into the Assets query string (startAt/maxResults/page/resultsPerPage) - Replace Record<string, any> in mapAssetObject with typed Raw* interfaces * fix(jsm): validate Assets workspaceId and honor `last` pagination flag Address review findings on the Assets tools: - Add validateAssetsWorkspaceId and guard the workspaceId in every Assets route before it is interpolated into the API path (mirrors the existing cloudId guard) — prevents a crafted workspaceId from escaping the workspace-scoped path - Object schema list now falls back to the `last` flag when `isLast` is absent, so pagination doesn't stop early * feat(jsm): allow overriding the auto-resolved Assets workspace Atlassian provisions one Assets workspace per site, so workspace discovery uses values[0] by design. For the rare multi-workspace site, expose an advanced "Assets Workspace ID" override on the block that flows through to every Assets operation, and document the single-workspace assumption. * refactor(jsm): include Assets responses in the JsmResponse union Append the nine Assets tool response types to JsmResponse for completeness and consistency with the rest of the JSM tool surface.
1 parent d538b76 commit dd32abe

30 files changed

Lines changed: 3045 additions & 3 deletions

File tree

apps/docs/content/docs/en/integrations/jira_service_management.mdx

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -988,6 +988,272 @@ Copy forms from one Jira issue to another
988988
| `copiedForms` | json | Array of successfully copied forms |
989989
| `errors` | json | Array of errors encountered during copy |
990990

991+
### `jsm_list_object_schemas`
992+
993+
List Assets (Insight/CMDB) object schemas in Jira Service Management
994+
995+
#### Input
996+
997+
| Parameter | Type | Required | Description |
998+
| --------- | ---- | -------- | ----------- |
999+
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
1000+
| `cloudId` | string | No | Jira Cloud ID for the instance |
1001+
| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) |
1002+
| `startAt` | number | No | Pagination start index \(e.g., 0, 50\) |
1003+
| `maxResults` | number | No | Maximum schemas to return \(e.g., 25, 50\) |
1004+
| `includeCounts` | boolean | No | Include object and object-type counts per schema |
1005+
1006+
#### Output
1007+
1008+
| Parameter | Type | Description |
1009+
| --------- | ---- | ----------- |
1010+
| `ts` | string | Timestamp of the operation |
1011+
| `schemas` | array | List of Assets object schemas |
1012+
|`id` | string | Schema ID |
1013+
|`name` | string | Schema name |
1014+
|`objectSchemaKey` | string | Schema key |
1015+
|`status` | string | Schema status |
1016+
|`description` | string | Schema description |
1017+
|`objectCount` | number | Number of objects |
1018+
|`objectTypeCount` | number | Number of object types |
1019+
| `total` | number | Total number of schemas |
1020+
| `isLast` | boolean | Whether this is the last page |
1021+
1022+
### `jsm_get_object_schema`
1023+
1024+
Get a single Assets (Insight/CMDB) object schema by ID
1025+
1026+
#### Input
1027+
1028+
| Parameter | Type | Required | Description |
1029+
| --------- | ---- | -------- | ----------- |
1030+
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
1031+
| `cloudId` | string | No | Jira Cloud ID for the instance |
1032+
| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) |
1033+
| `schemaId` | string | Yes | The Assets object schema ID |
1034+
1035+
#### Output
1036+
1037+
| Parameter | Type | Description |
1038+
| --------- | ---- | ----------- |
1039+
| `ts` | string | Timestamp of the operation |
1040+
| `schema` | json | The Assets object schema |
1041+
|`id` | string | Schema ID |
1042+
|`name` | string | Schema name |
1043+
|`objectSchemaKey` | string | Schema key |
1044+
|`status` | string | Schema status |
1045+
|`description` | string | Schema description |
1046+
|`objectCount` | number | Number of objects |
1047+
|`objectTypeCount` | number | Number of object types |
1048+
1049+
### `jsm_list_object_types`
1050+
1051+
List object types within an Assets (Insight/CMDB) object schema
1052+
1053+
#### Input
1054+
1055+
| Parameter | Type | Required | Description |
1056+
| --------- | ---- | -------- | ----------- |
1057+
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
1058+
| `cloudId` | string | No | Jira Cloud ID for the instance |
1059+
| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) |
1060+
| `schemaId` | string | Yes | The Assets object schema ID to list object types for |
1061+
| `excludeAbstract` | boolean | No | Exclude abstract object types from the result |
1062+
1063+
#### Output
1064+
1065+
| Parameter | Type | Description |
1066+
| --------- | ---- | ----------- |
1067+
| `ts` | string | Timestamp of the operation |
1068+
| `objectTypes` | array | List of object types in the schema |
1069+
|`id` | string | Object type ID |
1070+
|`name` | string | Object type name |
1071+
|`description` | string | Object type description |
1072+
|`objectSchemaId` | string | Parent schema ID |
1073+
|`objectCount` | number | Number of objects |
1074+
|`abstractObjectType` | boolean | Whether the type is abstract |
1075+
|`inherited` | boolean | Whether the type inherits attributes |
1076+
| `total` | number | Total number of object types |
1077+
1078+
### `jsm_get_object_type_attributes`
1079+
1080+
Get the attribute definitions for an Assets (Insight/CMDB) object type. Use the returned attribute IDs to build create/update payloads or map columns.
1081+
1082+
#### Input
1083+
1084+
| Parameter | Type | Required | Description |
1085+
| --------- | ---- | -------- | ----------- |
1086+
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
1087+
| `cloudId` | string | No | Jira Cloud ID for the instance |
1088+
| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) |
1089+
| `objectTypeId` | string | Yes | The Assets object type ID |
1090+
| `onlyValueEditable` | boolean | No | Return only attributes whose values can be edited |
1091+
| `query` | string | No | Filter attributes by a search query |
1092+
1093+
#### Output
1094+
1095+
| Parameter | Type | Description |
1096+
| --------- | ---- | ----------- |
1097+
| `ts` | string | Timestamp of the operation |
1098+
| `attributes` | array | Attribute definitions for the object type |
1099+
|`id` | string | Attribute definition ID — use as objectTypeAttributeId in create/update |
1100+
|`name` | string | Attribute name |
1101+
|`label` | boolean | Whether this attribute is the object label |
1102+
|`type` | number | Data type discriminator \(integer enum\) |
1103+
|`defaultType` | json | Default data type \{ id, name \} |
1104+
|`editable` | boolean | Whether the value is editable |
1105+
|`minimumCardinality` | number | Minimum number of values \(&gt;= 1 means required\) |
1106+
|`maximumCardinality` | number | Maximum number of values |
1107+
|`uniqueAttribute` | boolean | Whether values must be unique |
1108+
| `total` | number | Total number of attributes |
1109+
1110+
### `jsm_search_objects_aql`
1111+
1112+
Search Assets (Insight/CMDB) objects using AQL (Assets Query Language), e.g. objectType =
1113+
1114+
#### Input
1115+
1116+
| Parameter | Type | Required | Description |
1117+
| --------- | ---- | -------- | ----------- |
1118+
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
1119+
| `cloudId` | string | No | Jira Cloud ID for the instance |
1120+
| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) |
1121+
| `qlQuery` | string | Yes | AQL query string \(e.g., objectType = "Host" AND "Operating System" = "Ubuntu"\) |
1122+
| `page` | number | No | Page number \(1-based, defaults to 1\) |
1123+
| `resultsPerPage` | number | No | Results per page \(e.g., 25, 50\) |
1124+
| `includeAttributes` | boolean | No | Include resolved attribute values on each object \(defaults to true\) |
1125+
| `objectTypeId` | string | No | Optionally scope the search to a single object type ID |
1126+
| `objectSchemaId` | string | No | Optionally scope the search to a single object schema ID |
1127+
1128+
#### Output
1129+
1130+
| Parameter | Type | Description |
1131+
| --------- | ---- | ----------- |
1132+
| `ts` | string | Timestamp of the operation |
1133+
| `objects` | array | Matching Assets objects |
1134+
|`id` | string | Object ID |
1135+
|`label` | string | Object label |
1136+
|`objectKey` | string | Object key \(e.g., HOST-123\) |
1137+
|`objectType` | json | Object type metadata |
1138+
|`attributes` | json | Resolved attribute values |
1139+
| `total` | number | Total number of matching objects \(totalFilterCount\) |
1140+
| `pageNumber` | number | Current page number |
1141+
| `pageSize` | number | Number of objects on this page |
1142+
1143+
### `jsm_get_object`
1144+
1145+
Get a single Assets (Insight/CMDB) object by ID, including its attribute values
1146+
1147+
#### Input
1148+
1149+
| Parameter | Type | Required | Description |
1150+
| --------- | ---- | -------- | ----------- |
1151+
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
1152+
| `cloudId` | string | No | Jira Cloud ID for the instance |
1153+
| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) |
1154+
| `objectId` | string | Yes | The Assets object ID |
1155+
1156+
#### Output
1157+
1158+
| Parameter | Type | Description |
1159+
| --------- | ---- | ----------- |
1160+
| `ts` | string | Timestamp of the operation |
1161+
| `object` | json | The Assets object |
1162+
|`id` | string | Object ID |
1163+
|`label` | string | Human-readable object label |
1164+
|`objectKey` | string | Object key \(e.g., HOST-123\) |
1165+
|`globalId` | string | Global object ID |
1166+
|`objectType` | json | Object type metadata |
1167+
|`attributes` | json | Resolved attribute values for the object |
1168+
|`hasAvatar` | boolean | Whether the object has an avatar |
1169+
|`created` | string | Creation timestamp |
1170+
|`updated` | string | Last update timestamp |
1171+
|`link` | string | Self link to the object |
1172+
1173+
### `jsm_create_object`
1174+
1175+
Create an Assets (Insight/CMDB) object of a given object type. Attributes use objectTypeAttributeId values from the object type definition.
1176+
1177+
#### Input
1178+
1179+
| Parameter | Type | Required | Description |
1180+
| --------- | ---- | -------- | ----------- |
1181+
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
1182+
| `cloudId` | string | No | Jira Cloud ID for the instance |
1183+
| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) |
1184+
| `objectTypeId` | string | Yes | The object type ID to create the object under |
1185+
| `attributes` | json | Yes | Array of attributes: \[\{ objectTypeAttributeId, objectAttributeValues: \[\{ value \}\] \}\] |
1186+
1187+
#### Output
1188+
1189+
| Parameter | Type | Description |
1190+
| --------- | ---- | ----------- |
1191+
| `ts` | string | Timestamp of the operation |
1192+
| `object` | json | The created Assets object |
1193+
|`id` | string | Object ID |
1194+
|`label` | string | Human-readable object label |
1195+
|`objectKey` | string | Object key \(e.g., HOST-123\) |
1196+
|`globalId` | string | Global object ID |
1197+
|`objectType` | json | Object type metadata |
1198+
|`attributes` | json | Resolved attribute values for the object |
1199+
|`hasAvatar` | boolean | Whether the object has an avatar |
1200+
|`created` | string | Creation timestamp |
1201+
|`updated` | string | Last update timestamp |
1202+
|`link` | string | Self link to the object |
1203+
1204+
### `jsm_update_object`
1205+
1206+
Update an existing Assets (Insight/CMDB) object. Provide the attributes to change using their objectTypeAttributeId values.
1207+
1208+
#### Input
1209+
1210+
| Parameter | Type | Required | Description |
1211+
| --------- | ---- | -------- | ----------- |
1212+
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
1213+
| `cloudId` | string | No | Jira Cloud ID for the instance |
1214+
| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) |
1215+
| `objectId` | string | Yes | The Assets object ID to update |
1216+
| `attributes` | json | Yes | Array of attributes to set: \[\{ objectTypeAttributeId, objectAttributeValues: \[\{ value \}\] \}\] |
1217+
| `objectTypeId` | string | No | Optional object type ID \(only if changing the type\) |
1218+
1219+
#### Output
1220+
1221+
| Parameter | Type | Description |
1222+
| --------- | ---- | ----------- |
1223+
| `ts` | string | Timestamp of the operation |
1224+
| `object` | json | The updated Assets object |
1225+
|`id` | string | Object ID |
1226+
|`label` | string | Human-readable object label |
1227+
|`objectKey` | string | Object key \(e.g., HOST-123\) |
1228+
|`globalId` | string | Global object ID |
1229+
|`objectType` | json | Object type metadata |
1230+
|`attributes` | json | Resolved attribute values for the object |
1231+
|`hasAvatar` | boolean | Whether the object has an avatar |
1232+
|`created` | string | Creation timestamp |
1233+
|`updated` | string | Last update timestamp |
1234+
|`link` | string | Self link to the object |
1235+
1236+
### `jsm_delete_object`
1237+
1238+
Delete an Assets (Insight/CMDB) object by ID
1239+
1240+
#### Input
1241+
1242+
| Parameter | Type | Required | Description |
1243+
| --------- | ---- | -------- | ----------- |
1244+
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
1245+
| `cloudId` | string | No | Jira Cloud ID for the instance |
1246+
| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) |
1247+
| `objectId` | string | Yes | The Assets object ID to delete |
1248+
1249+
#### Output
1250+
1251+
| Parameter | Type | Description |
1252+
| --------- | ---- | ----------- |
1253+
| `ts` | string | Timestamp of the operation |
1254+
| `objectId` | string | The deleted object ID |
1255+
| `deleted` | boolean | Whether the object was deleted |
1256+
9911257

9921258

9931259
## Triggers
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { createLogger } from '@sim/logger'
2+
import { getErrorMessage, toError } from '@sim/utils/errors'
3+
import { type NextRequest, NextResponse } from 'next/server'
4+
import { jsmObjectTypeAttributesContract } from '@/lib/api/contracts/selectors/jsm'
5+
import { parseRequest } from '@/lib/api/server'
6+
import { checkInternalAuth } from '@/lib/auth/hybrid'
7+
import {
8+
validateAssetsWorkspaceId,
9+
validateJiraCloudId,
10+
} from '@/lib/core/security/input-validation'
11+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
12+
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
13+
import { getAssetsApiBaseUrl, getJsmHeaders, resolveAssetsContext } from '@/tools/jsm/utils'
14+
15+
export const dynamic = 'force-dynamic'
16+
17+
const logger = createLogger('JsmAssetsAttributesAPI')
18+
19+
export const POST = withRouteHandler(async (request: NextRequest) => {
20+
const auth = await checkInternalAuth(request)
21+
if (!auth.success || !auth.userId) {
22+
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
23+
}
24+
25+
try {
26+
const parsed = await parseRequest(jsmObjectTypeAttributesContract, request, {})
27+
if (!parsed.success) return parsed.response
28+
29+
const {
30+
domain,
31+
accessToken,
32+
cloudId: cloudIdParam,
33+
workspaceId: workspaceIdParam,
34+
objectTypeId,
35+
onlyValueEditable,
36+
query: searchQuery,
37+
} = parsed.data.body
38+
39+
const { cloudId, workspaceId } = await resolveAssetsContext(
40+
domain,
41+
accessToken,
42+
cloudIdParam,
43+
workspaceIdParam
44+
)
45+
46+
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
47+
if (!cloudIdValidation.isValid) {
48+
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
49+
}
50+
51+
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
52+
if (!workspaceIdValidation.isValid) {
53+
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
54+
}
55+
56+
const query = new URLSearchParams()
57+
if (onlyValueEditable !== undefined) {
58+
query.append('onlyValueEditable', String(onlyValueEditable))
59+
}
60+
if (searchQuery) query.append('query', searchQuery)
61+
62+
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/objecttype/${encodeURIComponent(
63+
objectTypeId
64+
)}/attributes${query.toString() ? `?${query.toString()}` : ''}`
65+
66+
const response = await fetch(url, { method: 'GET', headers: getJsmHeaders(accessToken) })
67+
68+
if (!response.ok) {
69+
const errorText = await response.text()
70+
logger.error('Assets API error getting attributes', { status: response.status, errorText })
71+
return NextResponse.json(
72+
{
73+
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
74+
details: errorText,
75+
},
76+
{ status: response.status }
77+
)
78+
}
79+
80+
const data = await response.json()
81+
const attributes = Array.isArray(data) ? data : (data.values ?? [])
82+
83+
return NextResponse.json({
84+
success: true,
85+
output: {
86+
ts: new Date().toISOString(),
87+
attributes,
88+
total: attributes.length,
89+
},
90+
})
91+
} catch (error) {
92+
logger.error('Error getting Assets attributes', { error: toError(error).message })
93+
return NextResponse.json(
94+
{ error: getErrorMessage(error, 'Internal server error'), success: false },
95+
{ status: 500 }
96+
)
97+
}
98+
})

0 commit comments

Comments
 (0)