Skip to content

Commit 131d2a7

Browse files
committed
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.
1 parent 39d0b56 commit 131d2a7

29 files changed

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

0 commit comments

Comments
 (0)