@backstage-community/plugin-servicenow-backend 1.5.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/service/router.cjs.js +12 -2
- package/dist/service/router.cjs.js.map +1 -1
- package/dist/service/validator.cjs.js +23 -13
- package/dist/service/validator.cjs.js.map +1 -1
- package/dist/service-now-rest/client.cjs.js +40 -150
- package/dist/service-now-rest/client.cjs.js.map +1 -1
- package/dist/service-now-rest/connection.cjs.js +156 -0
- package/dist/service-now-rest/connection.cjs.js.map +1 -0
- package/dist/service-now-rest/schema-checker.cjs.js +73 -0
- package/dist/service-now-rest/schema-checker.cjs.js.map +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,8 @@ var validator = require('./validator.cjs.js');
|
|
|
5
5
|
var express = require('express');
|
|
6
6
|
var Router = require('express-promise-router');
|
|
7
7
|
var client = require('../service-now-rest/client.cjs.js');
|
|
8
|
+
var schemaChecker = require('../service-now-rest/schema-checker.cjs.js');
|
|
9
|
+
var connection = require('../service-now-rest/connection.cjs.js');
|
|
8
10
|
|
|
9
11
|
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
10
12
|
|
|
@@ -16,10 +18,15 @@ async function createRouter(options) {
|
|
|
16
18
|
logger.info(
|
|
17
19
|
`Creating router for ServiceNow with instance URL: ${servicenowConfig.servicenow?.instanceUrl}`
|
|
18
20
|
);
|
|
19
|
-
const
|
|
21
|
+
const conn = new connection.ServiceNowConnection(
|
|
20
22
|
servicenowConfig,
|
|
23
|
+
logger.child({ service: "servicenow-connection" })
|
|
24
|
+
);
|
|
25
|
+
const client$1 = new client.DefaultServiceNowClient(
|
|
26
|
+
conn,
|
|
21
27
|
logger.child({ service: "servicenow-client" })
|
|
22
28
|
);
|
|
29
|
+
const schemaChecker$1 = new schemaChecker.ServiceNowSchemaChecker(conn);
|
|
23
30
|
const router = Router__default.default();
|
|
24
31
|
router.use(express__default.default.json());
|
|
25
32
|
router.get("/health", (_, response) => {
|
|
@@ -30,7 +37,10 @@ async function createRouter(options) {
|
|
|
30
37
|
await httpAuth.credentials(req, {
|
|
31
38
|
allow: ["user"]
|
|
32
39
|
});
|
|
33
|
-
const validatedParams = validator.validateIncidentQueryParams(
|
|
40
|
+
const validatedParams = await validator.validateIncidentQueryParams(
|
|
41
|
+
req.query,
|
|
42
|
+
schemaChecker$1
|
|
43
|
+
);
|
|
34
44
|
try {
|
|
35
45
|
const incidents = await client$1.fetchIncidents(validatedParams);
|
|
36
46
|
res.json(incidents);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.cjs.js","sources":["../../src/service/router.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { InputError } from '@backstage/errors';\nimport { validateIncidentQueryParams } from './validator';\nimport { HttpAuthService, LoggerService } from '@backstage/backend-plugin-api';\nimport express from 'express';\nimport Router from 'express-promise-router';\nimport { DefaultServiceNowClient } from '../service-now-rest/client';\nimport { ServiceNowConfig } from '../../config';\n\nexport interface RouterOptions {\n logger: LoggerService;\n servicenowConfig: ServiceNowConfig;\n httpAuth: HttpAuthService;\n}\n\nexport async function createRouter(\n options: RouterOptions,\n): Promise<express.Router> {\n const { logger, servicenowConfig, httpAuth } = options;\n\n logger.info(\n `Creating router for ServiceNow with instance URL: ${servicenowConfig.servicenow?.instanceUrl}`,\n );\n\n const client = new DefaultServiceNowClient(\n
|
|
1
|
+
{"version":3,"file":"router.cjs.js","sources":["../../src/service/router.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { InputError } from '@backstage/errors';\nimport { validateIncidentQueryParams } from './validator';\nimport { HttpAuthService, LoggerService } from '@backstage/backend-plugin-api';\nimport express from 'express';\nimport Router from 'express-promise-router';\nimport { DefaultServiceNowClient } from '../service-now-rest/client';\nimport { ServiceNowConfig } from '../../config';\nimport { ServiceNowSchemaChecker } from '../service-now-rest/schema-checker';\nimport { ServiceNowConnection } from '../service-now-rest/connection';\n\nexport interface RouterOptions {\n logger: LoggerService;\n servicenowConfig: ServiceNowConfig;\n httpAuth: HttpAuthService;\n}\n\nexport async function createRouter(\n options: RouterOptions,\n): Promise<express.Router> {\n const { logger, servicenowConfig, httpAuth } = options;\n\n logger.info(\n `Creating router for ServiceNow with instance URL: ${servicenowConfig.servicenow?.instanceUrl}`,\n );\n\n const conn = new ServiceNowConnection(\n servicenowConfig,\n logger.child({ service: 'servicenow-connection' }),\n );\n const client = new DefaultServiceNowClient(\n conn,\n logger.child({ service: 'servicenow-client' }),\n );\n const schemaChecker = new ServiceNowSchemaChecker(conn);\n const router = Router();\n router.use(express.json());\n\n router.get('/health', (_, response) => {\n logger.info('PONG!');\n response.json({ status: 'ok' });\n });\n\n router.get('/incidents', async (req, res) => {\n await httpAuth.credentials(req, {\n allow: ['user'],\n });\n\n const validatedParams = await validateIncidentQueryParams(\n req.query,\n schemaChecker,\n );\n\n try {\n const incidents = await client.fetchIncidents(validatedParams);\n res.json(incidents);\n } catch (error) {\n if (error instanceof InputError) {\n throw error;\n }\n\n // Log the full error and throw a generic one\n logger.error('Failed to fetch incidents from ServiceNow', error);\n throw new Error('Failed to fetch incidents from ServiceNow');\n }\n });\n\n return router;\n}\n"],"names":["ServiceNowConnection","client","DefaultServiceNowClient","schemaChecker","ServiceNowSchemaChecker","Router","express","validateIncidentQueryParams","InputError"],"mappings":";;;;;;;;;;;;;;;AA+BA,eAAsB,aACpB,OACyB,EAAA;AACzB,EAAA,MAAM,EAAE,MAAA,EAAQ,gBAAkB,EAAA,QAAA,EAAa,GAAA,OAAA;AAE/C,EAAO,MAAA,CAAA,IAAA;AAAA,IACL,CAAA,kDAAA,EAAqD,gBAAiB,CAAA,UAAA,EAAY,WAAW,CAAA;AAAA,GAC/F;AAEA,EAAA,MAAM,OAAO,IAAIA,+BAAA;AAAA,IACf,gBAAA;AAAA,IACA,MAAO,CAAA,KAAA,CAAM,EAAE,OAAA,EAAS,yBAAyB;AAAA,GACnD;AACA,EAAA,MAAMC,WAAS,IAAIC,8BAAA;AAAA,IACjB,IAAA;AAAA,IACA,MAAO,CAAA,KAAA,CAAM,EAAE,OAAA,EAAS,qBAAqB;AAAA,GAC/C;AACA,EAAM,MAAAC,eAAA,GAAgB,IAAIC,qCAAA,CAAwB,IAAI,CAAA;AACtD,EAAA,MAAM,SAASC,uBAAO,EAAA;AACtB,EAAO,MAAA,CAAA,GAAA,CAAIC,wBAAQ,CAAA,IAAA,EAAM,CAAA;AAEzB,EAAA,MAAA,CAAO,GAAI,CAAA,SAAA,EAAW,CAAC,CAAA,EAAG,QAAa,KAAA;AACrC,IAAA,MAAA,CAAO,KAAK,OAAO,CAAA;AACnB,IAAA,QAAA,CAAS,IAAK,CAAA,EAAE,MAAQ,EAAA,IAAA,EAAM,CAAA;AAAA,GAC/B,CAAA;AAED,EAAA,MAAA,CAAO,GAAI,CAAA,YAAA,EAAc,OAAO,GAAA,EAAK,GAAQ,KAAA;AAC3C,IAAM,MAAA,QAAA,CAAS,YAAY,GAAK,EAAA;AAAA,MAC9B,KAAA,EAAO,CAAC,MAAM;AAAA,KACf,CAAA;AAED,IAAA,MAAM,kBAAkB,MAAMC,qCAAA;AAAA,MAC5B,GAAI,CAAA,KAAA;AAAA,MACJJ;AAAA,KACF;AAEA,IAAI,IAAA;AACF,MAAA,MAAM,SAAY,GAAA,MAAMF,QAAO,CAAA,cAAA,CAAe,eAAe,CAAA;AAC7D,MAAA,GAAA,CAAI,KAAK,SAAS,CAAA;AAAA,aACX,KAAO,EAAA;AACd,MAAA,IAAI,iBAAiBO,iBAAY,EAAA;AAC/B,QAAM,MAAA,KAAA;AAAA;AAIR,MAAO,MAAA,CAAA,KAAA,CAAM,6CAA6C,KAAK,CAAA;AAC/D,MAAM,MAAA,IAAI,MAAM,2CAA2C,CAAA;AAAA;AAC7D,GACD,CAAA;AAED,EAAO,OAAA,MAAA;AACT;;;;"}
|
|
@@ -51,9 +51,8 @@ function parseAndValidateMultiValueQueryParam(paramQueryValue, allowedValues, pa
|
|
|
51
51
|
}
|
|
52
52
|
return valueAsString;
|
|
53
53
|
}
|
|
54
|
-
function validateIncidentQueryParams(query) {
|
|
54
|
+
async function validateIncidentQueryParams(query, schemaChecker) {
|
|
55
55
|
const {
|
|
56
|
-
entityId: entityIdQuery,
|
|
57
56
|
state: stateQuery,
|
|
58
57
|
priority: priorityQuery,
|
|
59
58
|
search: searchQuery,
|
|
@@ -61,9 +60,29 @@ function validateIncidentQueryParams(query) {
|
|
|
61
60
|
offset: offsetQuery,
|
|
62
61
|
order: orderQuery,
|
|
63
62
|
orderBy: orderByQuery,
|
|
64
|
-
userEmail: userEmailQuery
|
|
63
|
+
userEmail: userEmailQuery,
|
|
64
|
+
entityId: entityIdQuery,
|
|
65
|
+
...rest
|
|
65
66
|
} = query;
|
|
66
|
-
const validatedParams = {};
|
|
67
|
+
const validatedParams = { ...rest };
|
|
68
|
+
if (entityIdQuery !== void 0) {
|
|
69
|
+
if (typeof entityIdQuery !== "string" || !entityIdQuery.trim()) {
|
|
70
|
+
throw new errors.InputError("entityId must be a non-empty string.");
|
|
71
|
+
}
|
|
72
|
+
validatedParams.u_backstage_entity_id = entityIdQuery.trim();
|
|
73
|
+
}
|
|
74
|
+
const annotationFields = Object.keys(rest);
|
|
75
|
+
if (validatedParams.u_backstage_entity_id) {
|
|
76
|
+
annotationFields.push("u_backstage_entity_id");
|
|
77
|
+
}
|
|
78
|
+
const allFieldsExist = await schemaChecker.fieldExists(...annotationFields);
|
|
79
|
+
if (!allFieldsExist) {
|
|
80
|
+
throw new errors.InputError(
|
|
81
|
+
`One or more annotation fields do not exist in the ServiceNow incidents schema: ${annotationFields.join(
|
|
82
|
+
", "
|
|
83
|
+
)}`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
67
86
|
if (userEmailQuery !== void 0) {
|
|
68
87
|
if (typeof userEmailQuery !== "string" || !userEmailQuery.trim()) {
|
|
69
88
|
throw new errors.InputError("userEmail must be a non-empty string.");
|
|
@@ -73,15 +92,6 @@ function validateIncidentQueryParams(query) {
|
|
|
73
92
|
}
|
|
74
93
|
validatedParams.userEmail = userEmailQuery.trim();
|
|
75
94
|
}
|
|
76
|
-
if (entityIdQuery !== void 0) {
|
|
77
|
-
if (typeof entityIdQuery !== "string" || !entityIdQuery.trim()) {
|
|
78
|
-
throw new errors.InputError("entityId must be a non-empty string.");
|
|
79
|
-
}
|
|
80
|
-
validatedParams.entityId = entityIdQuery.trim();
|
|
81
|
-
}
|
|
82
|
-
if (!validatedParams.userEmail && !validatedParams.entityId) {
|
|
83
|
-
throw new errors.InputError("entityId is required if userEmail is not present.");
|
|
84
|
-
}
|
|
85
95
|
if (limitQuery !== void 0) {
|
|
86
96
|
if (typeof limitQuery !== "string") {
|
|
87
97
|
throw new errors.InputError("limit must be a string with number value.");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validator.cjs.js","sources":["../../src/service/validator.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { InputError } from '@backstage/errors';\nimport type { ParsedQs } from 'qs';\nimport type { Order } from '@backstage-community/plugin-servicenow-common';\nimport { IncidentFieldEnum } from '@backstage-community/plugin-servicenow-common';\nimport { IncidentQueryParams } from '../service-now-rest';\n\nconst ALLOWED_STATES: readonly string[] = ['1', '2', '3', '6', '7', '8'];\nconst ALLOWED_PRIORITIES: readonly string[] = ['1', '2', '3', '4', '5'];\nconst ALLOWED_ORDER_BY_FIELDS: string[] = Object.values(IncidentFieldEnum);\n\nfunction parseAndValidateMultiValueQueryParam(\n paramQueryValue: string | string[] | ParsedQs | ParsedQs[] | undefined,\n allowedValues: readonly string[],\n paramName: string,\n): string | undefined {\n if (paramQueryValue === undefined || paramQueryValue === null) {\n return undefined;\n }\n\n if (Array.isArray(paramQueryValue) || typeof paramQueryValue === 'object') {\n throw new InputError(\n `Query parameter '${paramName}' must be a single string.`,\n );\n }\n\n const valueAsString = String(paramQueryValue);\n\n if (valueAsString.trim() === '') {\n return undefined;\n }\n\n if (!valueAsString.startsWith('IN')) {\n throw new InputError(\n `Query parameter '${paramName}' must use the 'IN' prefix format (e.g., 'INvalue1,value2' or 'INvalue').`,\n );\n }\n\n // Extract the values after the 'IN' prefix\n const valuesStr = valueAsString.substring(2);\n if (!valuesStr) {\n throw new InputError(\n `Query parameter '${paramName}' with 'IN' prefix cannot have empty values.`,\n );\n }\n\n const items = valuesStr.split(',');\n for (const item of items) {\n const trimmedItem = item.trim();\n if (!trimmedItem) {\n throw new InputError(\n `Invalid ${paramName} format: empty value found in '${valueAsString}'. Allowed values for 'IN' list are: ${allowedValues.join(\n ', ',\n )}.`,\n );\n }\n\n const isValid = allowedValues.includes(trimmedItem);\n\n if (!isValid) {\n throw new InputError(\n `Invalid value '${trimmedItem}' for query parameter '${paramName}' in 'IN' list '${valueAsString}'. Allowed values are: ${allowedValues.join(\n ', ',\n )}.`,\n );\n }\n }\n return valueAsString;\n}\n\nexport function validateIncidentQueryParams(\n query: ParsedQs,\n): IncidentQueryParams {\n const {\n entityId: entityIdQuery,\n state: stateQuery,\n priority: priorityQuery,\n search: searchQuery,\n limit: limitQuery,\n offset: offsetQuery,\n order: orderQuery,\n orderBy: orderByQuery,\n userEmail: userEmailQuery,\n } = query;\n const validatedParams: IncidentQueryParams = {};\n\n // userEmail validation\n if (userEmailQuery !== undefined) {\n if (typeof userEmailQuery !== 'string' || !userEmailQuery.trim()) {\n throw new InputError('userEmail must be a non-empty string.');\n }\n // Basic email format validation\n if (!/.+@.+\\..+/.test(userEmailQuery)) {\n throw new InputError('userEmail must be a valid email address.');\n }\n validatedParams.userEmail = userEmailQuery.trim();\n }\n\n if (entityIdQuery !== undefined) {\n if (typeof entityIdQuery !== 'string' || !entityIdQuery.trim()) {\n throw new InputError('entityId must be a non-empty string.');\n }\n validatedParams.entityId = entityIdQuery.trim();\n }\n\n if (!validatedParams.userEmail && !validatedParams.entityId) {\n throw new InputError('entityId is required if userEmail is not present.');\n }\n\n // limit validation\n if (limitQuery !== undefined) {\n if (typeof limitQuery !== 'string') {\n throw new InputError('limit must be a string with number value.');\n }\n const parsedLimit = parseInt(limitQuery, 10);\n if (isNaN(parsedLimit) || parsedLimit < 0) {\n throw new InputError('limit must be a non-negative integer.');\n }\n validatedParams.limit = parsedLimit;\n }\n\n // offset validation\n if (offsetQuery !== undefined) {\n if (typeof offsetQuery !== 'string') {\n throw new InputError('offset must be a string with number value.');\n }\n const parsedOffset = parseInt(offsetQuery, 10);\n if (isNaN(parsedOffset) || parsedOffset < 0) {\n throw new InputError('offset must be a non-negative integer.');\n }\n validatedParams.offset = parsedOffset;\n }\n\n // order validation\n if (orderQuery !== undefined) {\n if (typeof orderQuery !== 'string') {\n throw new InputError('order must be a string.');\n }\n if (orderQuery !== 'asc' && orderQuery !== 'desc') {\n throw new InputError(\"order must be 'asc' or 'desc'.\");\n }\n validatedParams.order = orderQuery as Order;\n }\n\n // state validation\n validatedParams.state = parseAndValidateMultiValueQueryParam(\n stateQuery as string | undefined,\n ALLOWED_STATES,\n 'state',\n );\n\n // priority validation\n validatedParams.priority = parseAndValidateMultiValueQueryParam(\n priorityQuery as string | undefined,\n ALLOWED_PRIORITIES,\n 'priority',\n );\n\n // search validation\n if (searchQuery !== undefined) {\n if (typeof searchQuery !== 'string') {\n throw new InputError(`search must be a string.`);\n }\n validatedParams.search = searchQuery;\n }\n\n // orderBy validation\n if (orderByQuery !== undefined) {\n if (typeof orderByQuery !== 'string') {\n throw new InputError('orderBy must be a string.');\n }\n if (!ALLOWED_ORDER_BY_FIELDS.includes(orderByQuery)) {\n throw new InputError(\n `Invalid orderBy field. Allowed values are: ${ALLOWED_ORDER_BY_FIELDS.join(\n ', ',\n )}`,\n );\n }\n validatedParams.orderBy = orderByQuery;\n }\n\n return validatedParams;\n}\n"],"names":["IncidentFieldEnum","InputError"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"validator.cjs.js","sources":["../../src/service/validator.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { InputError } from '@backstage/errors';\nimport type { ParsedQs } from 'qs';\nimport type { Order } from '@backstage-community/plugin-servicenow-common';\nimport { IncidentFieldEnum } from '@backstage-community/plugin-servicenow-common';\nimport { IncidentQueryParams } from '../service-now-rest';\nimport { ServiceNowSchemaChecker } from '../service-now-rest/schema-checker';\n\nconst ALLOWED_STATES: readonly string[] = ['1', '2', '3', '6', '7', '8'];\nconst ALLOWED_PRIORITIES: readonly string[] = ['1', '2', '3', '4', '5'];\nconst ALLOWED_ORDER_BY_FIELDS: string[] = Object.values(IncidentFieldEnum);\n\nfunction parseAndValidateMultiValueQueryParam(\n paramQueryValue: string | string[] | ParsedQs | ParsedQs[] | undefined,\n allowedValues: readonly string[],\n paramName: string,\n): string | undefined {\n if (paramQueryValue === undefined || paramQueryValue === null) {\n return undefined;\n }\n\n if (Array.isArray(paramQueryValue) || typeof paramQueryValue === 'object') {\n throw new InputError(\n `Query parameter '${paramName}' must be a single string.`,\n );\n }\n\n const valueAsString = String(paramQueryValue);\n\n if (valueAsString.trim() === '') {\n return undefined;\n }\n\n if (!valueAsString.startsWith('IN')) {\n throw new InputError(\n `Query parameter '${paramName}' must use the 'IN' prefix format (e.g., 'INvalue1,value2' or 'INvalue').`,\n );\n }\n\n // Extract the values after the 'IN' prefix\n const valuesStr = valueAsString.substring(2);\n if (!valuesStr) {\n throw new InputError(\n `Query parameter '${paramName}' with 'IN' prefix cannot have empty values.`,\n );\n }\n\n const items = valuesStr.split(',');\n for (const item of items) {\n const trimmedItem = item.trim();\n if (!trimmedItem) {\n throw new InputError(\n `Invalid ${paramName} format: empty value found in '${valueAsString}'. Allowed values for 'IN' list are: ${allowedValues.join(\n ', ',\n )}.`,\n );\n }\n\n const isValid = allowedValues.includes(trimmedItem);\n\n if (!isValid) {\n throw new InputError(\n `Invalid value '${trimmedItem}' for query parameter '${paramName}' in 'IN' list '${valueAsString}'. Allowed values are: ${allowedValues.join(\n ', ',\n )}.`,\n );\n }\n }\n return valueAsString;\n}\n\nexport async function validateIncidentQueryParams(\n query: ParsedQs,\n schemaChecker: ServiceNowSchemaChecker,\n): Promise<IncidentQueryParams> {\n const {\n state: stateQuery,\n priority: priorityQuery,\n search: searchQuery,\n limit: limitQuery,\n offset: offsetQuery,\n order: orderQuery,\n orderBy: orderByQuery,\n userEmail: userEmailQuery,\n entityId: entityIdQuery,\n ...rest\n } = query;\n const validatedParams: IncidentQueryParams = { ...rest };\n if (entityIdQuery !== undefined) {\n if (typeof entityIdQuery !== 'string' || !entityIdQuery.trim()) {\n throw new InputError('entityId must be a non-empty string.');\n }\n validatedParams.u_backstage_entity_id = entityIdQuery.trim();\n }\n\n const annotationFields = Object.keys(rest);\n if (validatedParams.u_backstage_entity_id) {\n annotationFields.push('u_backstage_entity_id');\n }\n const allFieldsExist = await schemaChecker.fieldExists(...annotationFields);\n if (!allFieldsExist) {\n throw new InputError(\n `One or more annotation fields do not exist in the ServiceNow incidents schema: ${annotationFields.join(\n ', ',\n )}`,\n );\n }\n\n // userEmail validation\n if (userEmailQuery !== undefined) {\n if (typeof userEmailQuery !== 'string' || !userEmailQuery.trim()) {\n throw new InputError('userEmail must be a non-empty string.');\n }\n // Basic email format validation\n if (!/.+@.+\\..+/.test(userEmailQuery)) {\n throw new InputError('userEmail must be a valid email address.');\n }\n validatedParams.userEmail = userEmailQuery.trim();\n }\n\n // limit validation\n if (limitQuery !== undefined) {\n if (typeof limitQuery !== 'string') {\n throw new InputError('limit must be a string with number value.');\n }\n const parsedLimit = parseInt(limitQuery, 10);\n if (isNaN(parsedLimit) || parsedLimit < 0) {\n throw new InputError('limit must be a non-negative integer.');\n }\n validatedParams.limit = parsedLimit;\n }\n\n // offset validation\n if (offsetQuery !== undefined) {\n if (typeof offsetQuery !== 'string') {\n throw new InputError('offset must be a string with number value.');\n }\n const parsedOffset = parseInt(offsetQuery, 10);\n if (isNaN(parsedOffset) || parsedOffset < 0) {\n throw new InputError('offset must be a non-negative integer.');\n }\n validatedParams.offset = parsedOffset;\n }\n\n // order validation\n if (orderQuery !== undefined) {\n if (typeof orderQuery !== 'string') {\n throw new InputError('order must be a string.');\n }\n if (orderQuery !== 'asc' && orderQuery !== 'desc') {\n throw new InputError(\"order must be 'asc' or 'desc'.\");\n }\n validatedParams.order = orderQuery as Order;\n }\n\n // state validation\n validatedParams.state = parseAndValidateMultiValueQueryParam(\n stateQuery as string | undefined,\n ALLOWED_STATES,\n 'state',\n );\n\n // priority validation\n validatedParams.priority = parseAndValidateMultiValueQueryParam(\n priorityQuery as string | undefined,\n ALLOWED_PRIORITIES,\n 'priority',\n );\n\n // search validation\n if (searchQuery !== undefined) {\n if (typeof searchQuery !== 'string') {\n throw new InputError(`search must be a string.`);\n }\n validatedParams.search = searchQuery;\n }\n\n // orderBy validation\n if (orderByQuery !== undefined) {\n if (typeof orderByQuery !== 'string') {\n throw new InputError('orderBy must be a string.');\n }\n if (!ALLOWED_ORDER_BY_FIELDS.includes(orderByQuery)) {\n throw new InputError(\n `Invalid orderBy field. Allowed values are: ${ALLOWED_ORDER_BY_FIELDS.join(\n ', ',\n )}`,\n );\n }\n validatedParams.orderBy = orderByQuery;\n }\n\n return validatedParams;\n}\n"],"names":["IncidentFieldEnum","InputError"],"mappings":"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"}
|
|
@@ -1,147 +1,42 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
const INCIDENT_QUERY_KEYS_ARRAY = [
|
|
4
|
+
"userEmail",
|
|
5
|
+
"u_backstage_entity_id",
|
|
6
|
+
"state",
|
|
7
|
+
"priority",
|
|
8
|
+
"search",
|
|
9
|
+
"limit",
|
|
10
|
+
"offset",
|
|
11
|
+
"order",
|
|
12
|
+
"orderBy"
|
|
13
|
+
];
|
|
14
|
+
function isPredefinedIncidentQueryKey(keyName) {
|
|
15
|
+
return INCIDENT_QUERY_KEYS_ARRAY.includes(keyName);
|
|
16
|
+
}
|
|
10
17
|
class DefaultServiceNowClient {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
logger;
|
|
14
|
-
oauthClient;
|
|
15
|
-
constructor(config, logger) {
|
|
16
|
-
this.config = config;
|
|
18
|
+
constructor(conn, logger) {
|
|
19
|
+
this.conn = conn;
|
|
17
20
|
this.logger = logger;
|
|
18
|
-
if (!config.servicenow?.instanceUrl) {
|
|
19
|
-
logger.error("ServiceNow instance url is missing. Please configure it.");
|
|
20
|
-
throw new Error(
|
|
21
|
-
"ServiceNow instance url is missing. Please configure it."
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
this.instanceUrl = config.servicenow?.instanceUrl.replace(/\/$/, "");
|
|
25
|
-
if (!config.servicenow?.oauth && !config.servicenow?.basicAuth) {
|
|
26
|
-
logger.error(
|
|
27
|
-
"ServiceNow authentication configuration is missing. Please configure either OAuth or Basic Auth."
|
|
28
|
-
);
|
|
29
|
-
throw new Error(
|
|
30
|
-
"ServiceNow authentication configuration is missing. Please configure either OAuth or Basic Auth."
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
if (config.servicenow?.oauth) {
|
|
34
|
-
this.setupOAuthClient(config.servicenow.oauth);
|
|
35
|
-
}
|
|
36
|
-
if (config.servicenow?.basicAuth) {
|
|
37
|
-
logger.warn(
|
|
38
|
-
"Basic authentication is configured for ServiceNow. This is not recommended for production environments."
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
setupOAuthClient(oauth) {
|
|
43
|
-
const determinedTokenUrl = `${this.instanceUrl}/oauth_token.do`;
|
|
44
|
-
let tokenHost;
|
|
45
|
-
let tokenPath;
|
|
46
|
-
try {
|
|
47
|
-
const parsedTokenUrl = new URL(determinedTokenUrl);
|
|
48
|
-
tokenHost = parsedTokenUrl.origin;
|
|
49
|
-
tokenPath = parsedTokenUrl.pathname;
|
|
50
|
-
} catch (e) {
|
|
51
|
-
this.logger.error(
|
|
52
|
-
`Invalid tokenUrl constructed or provided: ${determinedTokenUrl}. Error: ${e.message}`
|
|
53
|
-
);
|
|
54
|
-
throw new Error(`Invalid tokenUrl: ${determinedTokenUrl}`);
|
|
55
|
-
}
|
|
56
|
-
const oauthModuleOptions = {
|
|
57
|
-
client: {
|
|
58
|
-
id: oauth.clientId,
|
|
59
|
-
secret: oauth.clientSecret
|
|
60
|
-
},
|
|
61
|
-
auth: {
|
|
62
|
-
tokenHost,
|
|
63
|
-
tokenPath
|
|
64
|
-
},
|
|
65
|
-
options: {
|
|
66
|
-
authorizationMethod: "body"
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
if (oauth.grantType === "client_credentials") {
|
|
70
|
-
this.oauthClient = new simpleOauth2.ClientCredentials(oauthModuleOptions);
|
|
71
|
-
} else if (oauth.grantType === "password") {
|
|
72
|
-
if (!oauth.username || !oauth.password) {
|
|
73
|
-
this.logger.error(
|
|
74
|
-
"Username and/or password missing for 'password' grant type in ServiceNow OAuth config."
|
|
75
|
-
);
|
|
76
|
-
throw new Error(
|
|
77
|
-
"Username and/or password missing for 'password' grant type."
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
this.oauthClient = new simpleOauth2.ResourceOwnerPassword(oauthModuleOptions);
|
|
81
|
-
} else {
|
|
82
|
-
const grantType = oauth.grantType;
|
|
83
|
-
this.logger.error(`Unsupported OAuth grantType: ${grantType}`);
|
|
84
|
-
throw new Error(`Unsupported OAuth grantType: ${grantType}`);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
async getAuthHeaders() {
|
|
88
|
-
if (this.config.servicenow?.basicAuth) {
|
|
89
|
-
const { username, password } = this.config.servicenow.basicAuth;
|
|
90
|
-
const encodedCredentials = Buffer.from(
|
|
91
|
-
`${username}:${password}`
|
|
92
|
-
).toString("base64");
|
|
93
|
-
return { Authorization: `Basic ${encodedCredentials}` };
|
|
94
|
-
}
|
|
95
|
-
if (this.config.servicenow?.oauth && this.oauthClient) {
|
|
96
|
-
let accessToken;
|
|
97
|
-
try {
|
|
98
|
-
if (this.config.servicenow.oauth.grantType === "client_credentials") {
|
|
99
|
-
accessToken = await this.oauthClient.getToken(
|
|
100
|
-
{}
|
|
101
|
-
);
|
|
102
|
-
} else if (this.config.servicenow.oauth.grantType === "password") {
|
|
103
|
-
if (!this.config.servicenow.oauth.username || !this.config.servicenow.oauth.password) {
|
|
104
|
-
throw new Error(
|
|
105
|
-
"Username or password missing for 'password' grant type during token acquisition."
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
accessToken = await this.oauthClient.getToken({
|
|
109
|
-
username: this.config.servicenow.oauth.username,
|
|
110
|
-
password: this.config.servicenow.oauth.password
|
|
111
|
-
});
|
|
112
|
-
} else {
|
|
113
|
-
throw new Error(
|
|
114
|
-
`Unsupported grantType in getAuthHeaders: ${this.config.servicenow.oauth.grantType}`
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
const tokenData = accessToken.token;
|
|
118
|
-
if (!tokenData || typeof tokenData.access_token !== "string" || !tokenData.access_token) {
|
|
119
|
-
throw new Error(
|
|
120
|
-
"Failed to obtain access_token string (token data is invalid or missing)."
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
return { Authorization: `Bearer ${tokenData.access_token}` };
|
|
124
|
-
} catch (error) {
|
|
125
|
-
this.logger.error(`Error fetching ServiceNow token: ${error.message}`, {
|
|
126
|
-
error: error.stack || error
|
|
127
|
-
});
|
|
128
|
-
if (error.isAxiosError && error.response) {
|
|
129
|
-
this.logger.error(
|
|
130
|
-
`OAuth2 token error details: Status ${error.response.status}, Data: ${JSON.stringify(error.response.data)}`
|
|
131
|
-
);
|
|
132
|
-
} else if (error.data && error.data.payload) {
|
|
133
|
-
this.logger.error(
|
|
134
|
-
`OAuth2 token error payload: ${JSON.stringify(error.data.payload)}`
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
throw new Error(`Failed to obtain access token: ${error.message}`);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
throw new Error("No authentication method configured.");
|
|
141
21
|
}
|
|
142
22
|
async fetchIncidents(options) {
|
|
143
|
-
const authHeaders = await this.getAuthHeaders();
|
|
23
|
+
const authHeaders = await this.conn.getAuthHeaders();
|
|
144
24
|
const queryParts = [];
|
|
25
|
+
const responseFields = [
|
|
26
|
+
"sys_id",
|
|
27
|
+
"number",
|
|
28
|
+
"short_description",
|
|
29
|
+
"description",
|
|
30
|
+
"sys_created_on",
|
|
31
|
+
"priority",
|
|
32
|
+
"incident_state"
|
|
33
|
+
];
|
|
34
|
+
for (const [key, value] of Object.entries(options)) {
|
|
35
|
+
if (!isPredefinedIncidentQueryKey(key)) {
|
|
36
|
+
queryParts.push(`${key}=${value}`);
|
|
37
|
+
responseFields.push(key);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
145
40
|
if (options.userEmail) {
|
|
146
41
|
const id = await this.getUserSysIdByEmail(options.userEmail);
|
|
147
42
|
queryParts.push(`caller_id=${id}^ORopened_by=${id}^ORassigned_to=${id}`);
|
|
@@ -154,9 +49,6 @@ class DefaultServiceNowClient {
|
|
|
154
49
|
`numberLIKE${searchTerm}^ORshort_descriptionLIKE${searchTerm}^ORdescriptionLIKE${searchTerm}`
|
|
155
50
|
);
|
|
156
51
|
}
|
|
157
|
-
if (options.entityId) {
|
|
158
|
-
queryParts.push(`u_backstage_entity_id=${options.entityId}`);
|
|
159
|
-
}
|
|
160
52
|
if (options.orderBy) {
|
|
161
53
|
queryParts.push(
|
|
162
54
|
`${options.order === "desc" ? "ORDERBYDESC" : "ORDERBY"}${options.orderBy}`
|
|
@@ -169,15 +61,12 @@ class DefaultServiceNowClient {
|
|
|
169
61
|
params.append("sysparm_limit", String(options.limit));
|
|
170
62
|
if (options.offset !== void 0)
|
|
171
63
|
params.append("sysparm_offset", String(options.offset));
|
|
172
|
-
params.append(
|
|
173
|
-
"sysparm_fields",
|
|
174
|
-
"sys_id,number,short_description,description,sys_created_on,priority,incident_state"
|
|
175
|
-
);
|
|
64
|
+
params.append("sysparm_fields", responseFields.join(","));
|
|
176
65
|
params.append("sysparm_count", "true");
|
|
177
|
-
const requestUrl =
|
|
66
|
+
const requestUrl = `api/now/table/incident?${params.toString()}`;
|
|
178
67
|
this.logger.info(`Fetching incidents from ServiceNow: ${requestUrl}`);
|
|
179
68
|
try {
|
|
180
|
-
const response = await
|
|
69
|
+
const response = await this.conn.getAxiosInstance().get(requestUrl, {
|
|
181
70
|
headers: {
|
|
182
71
|
...authHeaders,
|
|
183
72
|
Accept: "application/json"
|
|
@@ -188,7 +77,7 @@ class DefaultServiceNowClient {
|
|
|
188
77
|
const totalCount = Number(countHeader ?? 0);
|
|
189
78
|
const items = response.data?.result?.map((incident) => ({
|
|
190
79
|
...incident,
|
|
191
|
-
url:
|
|
80
|
+
url: `nav_to.do?uri=incident.do?sys_id=${incident.sys_id}`
|
|
192
81
|
})) ?? [];
|
|
193
82
|
return { items, totalCount };
|
|
194
83
|
} catch (error) {
|
|
@@ -199,11 +88,11 @@ class DefaultServiceNowClient {
|
|
|
199
88
|
}
|
|
200
89
|
}
|
|
201
90
|
async getUserSysIdByEmail(email) {
|
|
202
|
-
const authHeaders = await this.getAuthHeaders();
|
|
203
|
-
const url =
|
|
91
|
+
const authHeaders = await this.conn.getAuthHeaders();
|
|
92
|
+
const url = `api/now/table/sys_user?sysparm_query=email=${encodeURIComponent(
|
|
204
93
|
email
|
|
205
94
|
)}&sysparm_fields=sys_id`;
|
|
206
|
-
const response = await
|
|
95
|
+
const response = await this.conn.getAxiosInstance().get(url, {
|
|
207
96
|
headers: {
|
|
208
97
|
...authHeaders,
|
|
209
98
|
Accept: "application/json"
|
|
@@ -216,4 +105,5 @@ class DefaultServiceNowClient {
|
|
|
216
105
|
}
|
|
217
106
|
|
|
218
107
|
exports.DefaultServiceNowClient = DefaultServiceNowClient;
|
|
108
|
+
exports.isPredefinedIncidentQueryKey = isPredefinedIncidentQueryKey;
|
|
219
109
|
//# sourceMappingURL=client.cjs.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.cjs.js","sources":["../../src/service-now-rest/client.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport {\n ClientCredentials,\n ResourceOwnerPassword,\n ModuleOptions,\n AccessToken,\n} from 'simple-oauth2';\nimport axios from 'axios';\nimport {\n IncidentPick,\n PaginatedIncidents,\n} from '@backstage-community/plugin-servicenow-common';\nimport { OAuthConfig, ServiceNowConfig } from '../../config';\n\nexport type IncidentQueryParams = {\n userEmail?: string;\n entityId?: string;\n state?: string;\n priority?: string;\n search?: string;\n limit?: number;\n offset?: number;\n order?: 'asc' | 'desc';\n orderBy?: string;\n};\n\nexport interface ServiceNowClient {\n fetchIncidents(options: IncidentQueryParams): Promise<PaginatedIncidents>;\n}\n\nexport class DefaultServiceNowClient implements ServiceNowClient {\n private readonly instanceUrl: string;\n private readonly config: ServiceNowConfig;\n private readonly logger: LoggerService;\n private oauthClient?: ClientCredentials | ResourceOwnerPassword;\n\n constructor(config: ServiceNowConfig, logger: LoggerService) {\n this.config = config;\n this.logger = logger;\n\n if (!config.servicenow?.instanceUrl) {\n logger.error('ServiceNow instance url is missing. Please configure it.');\n throw new Error(\n 'ServiceNow instance url is missing. Please configure it.',\n );\n }\n this.instanceUrl = config.servicenow?.instanceUrl.replace(/\\/$/, '');\n\n if (!config.servicenow?.oauth && !config.servicenow?.basicAuth) {\n logger.error(\n 'ServiceNow authentication configuration is missing. Please configure either OAuth or Basic Auth.',\n );\n throw new Error(\n 'ServiceNow authentication configuration is missing. Please configure either OAuth or Basic Auth.',\n );\n }\n\n if (config.servicenow?.oauth) {\n this.setupOAuthClient(config.servicenow.oauth);\n }\n\n if (config.servicenow?.basicAuth) {\n logger.warn(\n 'Basic authentication is configured for ServiceNow. This is not recommended for production environments.',\n );\n }\n }\n\n private setupOAuthClient(oauth: OAuthConfig) {\n const determinedTokenUrl = `${this.instanceUrl}/oauth_token.do`;\n\n let tokenHost: string;\n let tokenPath: string;\n try {\n const parsedTokenUrl = new URL(determinedTokenUrl);\n tokenHost = parsedTokenUrl.origin;\n tokenPath = parsedTokenUrl.pathname;\n } catch (e: any) {\n this.logger.error(\n `Invalid tokenUrl constructed or provided: ${determinedTokenUrl}. Error: ${e.message}`,\n );\n throw new Error(`Invalid tokenUrl: ${determinedTokenUrl}`);\n }\n\n const oauthModuleOptions: ModuleOptions = {\n client: {\n id: oauth.clientId,\n secret: oauth.clientSecret,\n },\n auth: {\n tokenHost: tokenHost,\n tokenPath: tokenPath,\n },\n options: {\n authorizationMethod: 'body',\n },\n };\n\n if (oauth.grantType === 'client_credentials') {\n this.oauthClient = new ClientCredentials(oauthModuleOptions);\n } else if (oauth.grantType === 'password') {\n if (!oauth.username || !oauth.password) {\n this.logger.error(\n \"Username and/or password missing for 'password' grant type in ServiceNow OAuth config.\",\n );\n throw new Error(\n \"Username and/or password missing for 'password' grant type.\",\n );\n }\n this.oauthClient = new ResourceOwnerPassword(oauthModuleOptions);\n } else {\n const grantType = (oauth as any).grantType;\n this.logger.error(`Unsupported OAuth grantType: ${grantType}`);\n throw new Error(`Unsupported OAuth grantType: ${grantType}`);\n }\n }\n\n private async getAuthHeaders(): Promise<{ Authorization: string }> {\n if (this.config.servicenow?.basicAuth) {\n const { username, password } = this.config.servicenow.basicAuth;\n const encodedCredentials = Buffer.from(\n `${username}:${password}`,\n ).toString('base64');\n return { Authorization: `Basic ${encodedCredentials}` };\n }\n\n if (this.config.servicenow?.oauth && this.oauthClient) {\n let accessToken: AccessToken;\n try {\n if (this.config.servicenow.oauth.grantType === 'client_credentials') {\n accessToken = await (this.oauthClient as ClientCredentials).getToken(\n {},\n );\n } else if (this.config.servicenow.oauth.grantType === 'password') {\n if (\n !this.config.servicenow.oauth.username ||\n !this.config.servicenow.oauth.password\n ) {\n throw new Error(\n \"Username or password missing for 'password' grant type during token acquisition.\",\n );\n }\n accessToken = await (\n this.oauthClient as ResourceOwnerPassword\n ).getToken({\n username: this.config.servicenow.oauth.username,\n password: this.config.servicenow.oauth.password,\n });\n } else {\n throw new Error(\n `Unsupported grantType in getAuthHeaders: ${\n (this.config.servicenow.oauth as any).grantType\n }`,\n );\n }\n\n const tokenData = accessToken.token;\n if (\n !tokenData ||\n typeof tokenData.access_token !== 'string' ||\n !tokenData.access_token\n ) {\n throw new Error(\n 'Failed to obtain access_token string (token data is invalid or missing).',\n );\n }\n return { Authorization: `Bearer ${tokenData.access_token}` };\n } catch (error: any) {\n this.logger.error(`Error fetching ServiceNow token: ${error.message}`, {\n error: error.stack || error,\n });\n if (error.isAxiosError && error.response) {\n this.logger.error(\n `OAuth2 token error details: Status ${\n error.response.status\n }, Data: ${JSON.stringify(error.response.data)}`,\n );\n } else if (error.data && error.data.payload) {\n this.logger.error(\n `OAuth2 token error payload: ${JSON.stringify(error.data.payload)}`,\n );\n }\n throw new Error(`Failed to obtain access token: ${error.message}`);\n }\n }\n\n throw new Error('No authentication method configured.');\n }\n\n async fetchIncidents(options: IncidentQueryParams): Promise<{\n items: IncidentPick[];\n totalCount: number;\n }> {\n const authHeaders = await this.getAuthHeaders();\n const queryParts: string[] = [];\n\n if (options.userEmail) {\n const id = await this.getUserSysIdByEmail(options.userEmail);\n queryParts.push(`caller_id=${id}^ORopened_by=${id}^ORassigned_to=${id}`);\n }\n\n if (options.state) queryParts.push(`state${options.state}`);\n if (options.priority) queryParts.push(`priority${options.priority}`);\n\n if (options.search) {\n const searchTerm = options.search;\n queryParts.push(\n `numberLIKE${searchTerm}^ORshort_descriptionLIKE${searchTerm}^ORdescriptionLIKE${searchTerm}`,\n );\n }\n\n if (options.entityId) {\n queryParts.push(`u_backstage_entity_id=${options.entityId}`);\n }\n\n if (options.orderBy) {\n queryParts.push(\n `${options.order === 'desc' ? 'ORDERBYDESC' : 'ORDERBY'}${\n options.orderBy\n }`,\n );\n }\n\n const sysparmQuery = queryParts.join('^');\n\n const params = new URLSearchParams();\n if (sysparmQuery) params.append('sysparm_query', sysparmQuery);\n if (options.limit !== undefined)\n params.append('sysparm_limit', String(options.limit));\n if (options.offset !== undefined)\n params.append('sysparm_offset', String(options.offset));\n params.append(\n 'sysparm_fields',\n 'sys_id,number,short_description,description,sys_created_on,priority,incident_state',\n );\n\n params.append('sysparm_count', 'true');\n\n const requestUrl = `${\n this.instanceUrl\n }/api/now/table/incident?${params.toString()}`;\n this.logger.info(`Fetching incidents from ServiceNow: ${requestUrl}`);\n\n try {\n const response = await axios.get(requestUrl, {\n headers: {\n ...authHeaders,\n Accept: 'application/json',\n },\n timeout: 30000,\n });\n\n const countHeader =\n response.headers['x-total-count'] ?? response.headers['X-Total-Count'];\n const totalCount = Number(countHeader ?? 0);\n\n const items =\n response.data?.result?.map((incident: any) => ({\n ...incident,\n url: `${this.instanceUrl}/nav_to.do?uri=incident.do?sys_id=${incident.sys_id}`,\n })) ?? [];\n\n return { items, totalCount };\n } catch (error: any) {\n this.logger.error(`Failed to fetch incidents: ${error.message}`, {\n error,\n });\n throw new Error(`Failed to fetch incidents: ${error.message}`);\n }\n }\n\n private async getUserSysIdByEmail(email: string): Promise<string | null> {\n const authHeaders = await this.getAuthHeaders();\n const url = `${\n this.instanceUrl\n }/api/now/table/sys_user?sysparm_query=email=${encodeURIComponent(\n email,\n )}&sysparm_fields=sys_id`;\n const response = await axios.get(url, {\n headers: {\n ...authHeaders,\n Accept: 'application/json',\n },\n });\n const users = response.data?.result;\n if (users && users.length > 0) return users[0].sys_id;\n\n throw new Error(`User with email ${email} not found in ServiceNow.`);\n }\n}\n"],"names":["ClientCredentials","ResourceOwnerPassword","axios"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.cjs.js","sources":["../../src/service-now-rest/client.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport {\n IncidentPick,\n PaginatedIncidents,\n} from '@backstage-community/plugin-servicenow-common';\nimport { ServiceNowConnection } from './connection';\n\nconst INCIDENT_QUERY_KEYS_ARRAY = [\n 'userEmail',\n 'u_backstage_entity_id',\n 'state',\n 'priority',\n 'search',\n 'limit',\n 'offset',\n 'order',\n 'orderBy',\n] as const;\n\nexport type IncidentQueryKeyString = (typeof INCIDENT_QUERY_KEYS_ARRAY)[number];\n\n// Plugin UI specific query parameters (pagination, search, ordering, tickets filter)\nexport type PredefinedIncidentQueryParams = {\n [K in IncidentQueryKeyString]?: K extends 'order'\n ? 'asc' | 'desc'\n : K extends 'limit' | 'offset'\n ? number\n : string;\n};\n\n// UI-specific query parameters joined with user's custom incident table fields query parameters\nexport type IncidentQueryParams = PredefinedIncidentQueryParams & {\n [key: string]: any;\n};\n\nexport type IncidentQueryKeys = keyof IncidentQueryParams;\n\nexport function isPredefinedIncidentQueryKey(keyName: string): boolean {\n return INCIDENT_QUERY_KEYS_ARRAY.includes(keyName as IncidentQueryKeyString);\n}\n\nexport interface ServiceNowClient {\n fetchIncidents(options: IncidentQueryParams): Promise<PaginatedIncidents>;\n}\n\nexport class DefaultServiceNowClient implements ServiceNowClient {\n constructor(\n private readonly conn: ServiceNowConnection,\n private readonly logger: LoggerService,\n ) {}\n\n async fetchIncidents(options: IncidentQueryParams): Promise<{\n items: IncidentPick[];\n totalCount: number;\n }> {\n const authHeaders = await this.conn.getAuthHeaders();\n const queryParts: string[] = [];\n\n const responseFields = [\n 'sys_id',\n 'number',\n 'short_description',\n 'description',\n 'sys_created_on',\n 'priority',\n 'incident_state',\n ];\n for (const [key, value] of Object.entries(options)) {\n if (!isPredefinedIncidentQueryKey(key)) {\n queryParts.push(`${key}=${value}`);\n responseFields.push(key);\n }\n }\n\n if (options.userEmail) {\n const id = await this.getUserSysIdByEmail(options.userEmail);\n queryParts.push(`caller_id=${id}^ORopened_by=${id}^ORassigned_to=${id}`);\n }\n\n if (options.state) queryParts.push(`state${options.state}`);\n if (options.priority) queryParts.push(`priority${options.priority}`);\n\n if (options.search) {\n const searchTerm = options.search;\n queryParts.push(\n `numberLIKE${searchTerm}^ORshort_descriptionLIKE${searchTerm}^ORdescriptionLIKE${searchTerm}`,\n );\n }\n\n if (options.orderBy) {\n queryParts.push(\n `${options.order === 'desc' ? 'ORDERBYDESC' : 'ORDERBY'}${\n options.orderBy\n }`,\n );\n }\n\n const sysparmQuery = queryParts.join('^');\n\n const params = new URLSearchParams();\n if (sysparmQuery) params.append('sysparm_query', sysparmQuery);\n if (options.limit !== undefined)\n params.append('sysparm_limit', String(options.limit));\n if (options.offset !== undefined)\n params.append('sysparm_offset', String(options.offset));\n\n params.append('sysparm_fields', responseFields.join(','));\n params.append('sysparm_count', 'true');\n\n const requestUrl = `api/now/table/incident?${params.toString()}`;\n this.logger.info(`Fetching incidents from ServiceNow: ${requestUrl}`);\n\n try {\n const response = await this.conn.getAxiosInstance().get(requestUrl, {\n headers: {\n ...authHeaders,\n Accept: 'application/json',\n },\n timeout: 30000,\n });\n\n const countHeader =\n response.headers['x-total-count'] ?? response.headers['X-Total-Count'];\n const totalCount = Number(countHeader ?? 0);\n\n const items =\n response.data?.result?.map((incident: any) => ({\n ...incident,\n url: `nav_to.do?uri=incident.do?sys_id=${incident.sys_id}`,\n })) ?? [];\n\n return { items, totalCount };\n } catch (error: any) {\n this.logger.error(`Failed to fetch incidents: ${error.message}`, {\n error,\n });\n throw new Error(`Failed to fetch incidents: ${error.message}`);\n }\n }\n\n private async getUserSysIdByEmail(email: string): Promise<string | null> {\n const authHeaders = await this.conn.getAuthHeaders();\n const url = `api/now/table/sys_user?sysparm_query=email=${encodeURIComponent(\n email,\n )}&sysparm_fields=sys_id`;\n const response = await this.conn.getAxiosInstance().get(url, {\n headers: {\n ...authHeaders,\n Accept: 'application/json',\n },\n });\n const users = response.data?.result;\n if (users && users.length > 0) return users[0].sys_id;\n\n throw new Error(`User with email ${email} not found in ServiceNow.`);\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var simpleOauth2 = require('simple-oauth2');
|
|
4
|
+
var axios = require('axios');
|
|
5
|
+
var https = require('https');
|
|
6
|
+
|
|
7
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
8
|
+
|
|
9
|
+
var axios__default = /*#__PURE__*/_interopDefaultCompat(axios);
|
|
10
|
+
var https__default = /*#__PURE__*/_interopDefaultCompat(https);
|
|
11
|
+
|
|
12
|
+
class ServiceNowConnection {
|
|
13
|
+
oauthClient;
|
|
14
|
+
instanceUrl;
|
|
15
|
+
config;
|
|
16
|
+
logger;
|
|
17
|
+
axiosInstance;
|
|
18
|
+
constructor(config, logger) {
|
|
19
|
+
this.config = config;
|
|
20
|
+
this.logger = logger;
|
|
21
|
+
if (!config.servicenow?.instanceUrl) {
|
|
22
|
+
logger.error("ServiceNow instance url is missing. Please configure it.");
|
|
23
|
+
throw new Error(
|
|
24
|
+
"ServiceNow instance url is missing. Please configure it."
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
this.instanceUrl = config.servicenow?.instanceUrl.replace(/\/$/, "");
|
|
28
|
+
if (!config.servicenow?.oauth && !config.servicenow?.basicAuth) {
|
|
29
|
+
logger.error(
|
|
30
|
+
"ServiceNow authentication configuration is missing. Please configure either OAuth or Basic Auth."
|
|
31
|
+
);
|
|
32
|
+
throw new Error(
|
|
33
|
+
"ServiceNow authentication configuration is missing. Please configure either OAuth or Basic Auth."
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
if (config.servicenow?.oauth) {
|
|
37
|
+
this.setupOAuthClient(config.servicenow.oauth);
|
|
38
|
+
}
|
|
39
|
+
if (config.servicenow?.basicAuth) {
|
|
40
|
+
logger.warn(
|
|
41
|
+
"Basic authentication is configured for ServiceNow. This is not recommended for production environments."
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
this.axiosInstance = axios__default.default.create({
|
|
45
|
+
baseURL: this.instanceUrl,
|
|
46
|
+
httpsAgent: new https__default.default.Agent({ keepAlive: true, maxSockets: 10 }),
|
|
47
|
+
timeout: 1e4
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
getAxiosInstance() {
|
|
51
|
+
return this.axiosInstance;
|
|
52
|
+
}
|
|
53
|
+
setupOAuthClient(oauth) {
|
|
54
|
+
const determinedTokenUrl = `${this.instanceUrl}/oauth_token.do`;
|
|
55
|
+
let tokenHost;
|
|
56
|
+
let tokenPath;
|
|
57
|
+
try {
|
|
58
|
+
const parsedTokenUrl = new URL(determinedTokenUrl);
|
|
59
|
+
tokenHost = parsedTokenUrl.origin;
|
|
60
|
+
tokenPath = parsedTokenUrl.pathname;
|
|
61
|
+
} catch (e) {
|
|
62
|
+
this.logger.error(
|
|
63
|
+
`Invalid tokenUrl constructed or provided: ${determinedTokenUrl}. Error: ${e.message}`
|
|
64
|
+
);
|
|
65
|
+
throw new Error(`Invalid tokenUrl: ${determinedTokenUrl}`);
|
|
66
|
+
}
|
|
67
|
+
const oauthModuleOptions = {
|
|
68
|
+
client: {
|
|
69
|
+
id: oauth.clientId,
|
|
70
|
+
secret: oauth.clientSecret
|
|
71
|
+
},
|
|
72
|
+
auth: {
|
|
73
|
+
tokenHost,
|
|
74
|
+
tokenPath
|
|
75
|
+
},
|
|
76
|
+
options: {
|
|
77
|
+
authorizationMethod: "body"
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
if (oauth.grantType === "client_credentials") {
|
|
81
|
+
this.oauthClient = new simpleOauth2.ClientCredentials(oauthModuleOptions);
|
|
82
|
+
} else if (oauth.grantType === "password") {
|
|
83
|
+
if (!oauth.username || !oauth.password) {
|
|
84
|
+
this.logger.error(
|
|
85
|
+
"Username and/or password missing for 'password' grant type in ServiceNow OAuth config."
|
|
86
|
+
);
|
|
87
|
+
throw new Error(
|
|
88
|
+
"Username and/or password missing for 'password' grant type."
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
this.oauthClient = new simpleOauth2.ResourceOwnerPassword(oauthModuleOptions);
|
|
92
|
+
} else {
|
|
93
|
+
const grantType = oauth.grantType;
|
|
94
|
+
this.logger.error(`Unsupported OAuth grantType: ${grantType}`);
|
|
95
|
+
throw new Error(`Unsupported OAuth grantType: ${grantType}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async getAuthHeaders() {
|
|
99
|
+
if (this.config.servicenow?.basicAuth) {
|
|
100
|
+
const { username, password } = this.config.servicenow.basicAuth;
|
|
101
|
+
const encodedCredentials = Buffer.from(
|
|
102
|
+
`${username}:${password}`
|
|
103
|
+
).toString("base64");
|
|
104
|
+
return { Authorization: `Basic ${encodedCredentials}` };
|
|
105
|
+
}
|
|
106
|
+
if (this.config.servicenow?.oauth && this.oauthClient) {
|
|
107
|
+
let accessToken;
|
|
108
|
+
try {
|
|
109
|
+
if (this.config.servicenow.oauth.grantType === "client_credentials") {
|
|
110
|
+
accessToken = await this.oauthClient.getToken(
|
|
111
|
+
{}
|
|
112
|
+
);
|
|
113
|
+
} else if (this.config.servicenow.oauth.grantType === "password") {
|
|
114
|
+
if (!this.config.servicenow.oauth.username || !this.config.servicenow.oauth.password) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
"Username or password missing for 'password' grant type during token acquisition."
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
accessToken = await this.oauthClient.getToken({
|
|
120
|
+
username: this.config.servicenow.oauth.username,
|
|
121
|
+
password: this.config.servicenow.oauth.password
|
|
122
|
+
});
|
|
123
|
+
} else {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`Unsupported grantType in getAuthHeaders: ${this.config.servicenow.oauth.grantType}`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const tokenData = accessToken.token;
|
|
129
|
+
if (!tokenData || typeof tokenData.access_token !== "string" || !tokenData.access_token) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
"Failed to obtain access_token string (token data is invalid or missing)."
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
return { Authorization: `Bearer ${tokenData.access_token}` };
|
|
135
|
+
} catch (error) {
|
|
136
|
+
this.logger.error(`Error fetching ServiceNow token: ${error.message}`, {
|
|
137
|
+
error: error.stack || error
|
|
138
|
+
});
|
|
139
|
+
if (error.isAxiosError && error.response) {
|
|
140
|
+
this.logger.error(
|
|
141
|
+
`OAuth2 token error details: Status ${error.response.status}, Data: ${JSON.stringify(error.response.data)}`
|
|
142
|
+
);
|
|
143
|
+
} else if (error.data && error.data.payload) {
|
|
144
|
+
this.logger.error(
|
|
145
|
+
`OAuth2 token error payload: ${JSON.stringify(error.data.payload)}`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
throw new Error(`Failed to obtain access token: ${error.message}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
throw new Error("No authentication method configured.");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
exports.ServiceNowConnection = ServiceNowConnection;
|
|
156
|
+
//# sourceMappingURL=connection.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection.cjs.js","sources":["../../src/service-now-rest/connection.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { OAuthConfig, ServiceNowConfig } from '../../config';\nimport {\n ClientCredentials,\n ResourceOwnerPassword,\n ModuleOptions,\n AccessToken,\n} from 'simple-oauth2';\nimport axios, { AxiosInstance } from 'axios';\nimport https from 'https';\n\nexport class ServiceNowConnection {\n private oauthClient?: ClientCredentials | ResourceOwnerPassword;\n private readonly instanceUrl: string;\n private readonly config: ServiceNowConfig;\n private readonly logger: LoggerService;\n private axiosInstance: AxiosInstance;\n\n constructor(config: ServiceNowConfig, logger: LoggerService) {\n this.config = config;\n this.logger = logger;\n\n if (!config.servicenow?.instanceUrl) {\n logger.error('ServiceNow instance url is missing. Please configure it.');\n throw new Error(\n 'ServiceNow instance url is missing. Please configure it.',\n );\n }\n this.instanceUrl = config.servicenow?.instanceUrl.replace(/\\/$/, '');\n\n if (!config.servicenow?.oauth && !config.servicenow?.basicAuth) {\n logger.error(\n 'ServiceNow authentication configuration is missing. Please configure either OAuth or Basic Auth.',\n );\n throw new Error(\n 'ServiceNow authentication configuration is missing. Please configure either OAuth or Basic Auth.',\n );\n }\n\n if (config.servicenow?.oauth) {\n this.setupOAuthClient(config.servicenow.oauth);\n }\n\n if (config.servicenow?.basicAuth) {\n logger.warn(\n 'Basic authentication is configured for ServiceNow. This is not recommended for production environments.',\n );\n }\n\n this.axiosInstance = axios.create({\n baseURL: this.instanceUrl,\n httpsAgent: new https.Agent({ keepAlive: true, maxSockets: 10 }),\n timeout: 10000,\n });\n }\n\n getAxiosInstance(): AxiosInstance {\n return this.axiosInstance;\n }\n\n private setupOAuthClient(oauth: OAuthConfig) {\n const determinedTokenUrl = `${this.instanceUrl}/oauth_token.do`;\n\n let tokenHost: string;\n let tokenPath: string;\n try {\n const parsedTokenUrl = new URL(determinedTokenUrl);\n tokenHost = parsedTokenUrl.origin;\n tokenPath = parsedTokenUrl.pathname;\n } catch (e: any) {\n this.logger.error(\n `Invalid tokenUrl constructed or provided: ${determinedTokenUrl}. Error: ${e.message}`,\n );\n throw new Error(`Invalid tokenUrl: ${determinedTokenUrl}`);\n }\n\n const oauthModuleOptions: ModuleOptions = {\n client: {\n id: oauth.clientId,\n secret: oauth.clientSecret,\n },\n auth: {\n tokenHost: tokenHost,\n tokenPath: tokenPath,\n },\n options: {\n authorizationMethod: 'body',\n },\n };\n\n if (oauth.grantType === 'client_credentials') {\n this.oauthClient = new ClientCredentials(oauthModuleOptions);\n } else if (oauth.grantType === 'password') {\n if (!oauth.username || !oauth.password) {\n this.logger.error(\n \"Username and/or password missing for 'password' grant type in ServiceNow OAuth config.\",\n );\n throw new Error(\n \"Username and/or password missing for 'password' grant type.\",\n );\n }\n this.oauthClient = new ResourceOwnerPassword(oauthModuleOptions);\n } else {\n const grantType = (oauth as any).grantType;\n this.logger.error(`Unsupported OAuth grantType: ${grantType}`);\n throw new Error(`Unsupported OAuth grantType: ${grantType}`);\n }\n }\n\n async getAuthHeaders(): Promise<{ Authorization: string }> {\n if (this.config.servicenow?.basicAuth) {\n const { username, password } = this.config.servicenow.basicAuth;\n const encodedCredentials = Buffer.from(\n `${username}:${password}`,\n ).toString('base64');\n return { Authorization: `Basic ${encodedCredentials}` };\n }\n\n if (this.config.servicenow?.oauth && this.oauthClient) {\n let accessToken: AccessToken;\n try {\n if (this.config.servicenow.oauth.grantType === 'client_credentials') {\n accessToken = await (this.oauthClient as ClientCredentials).getToken(\n {},\n );\n } else if (this.config.servicenow.oauth.grantType === 'password') {\n if (\n !this.config.servicenow.oauth.username ||\n !this.config.servicenow.oauth.password\n ) {\n throw new Error(\n \"Username or password missing for 'password' grant type during token acquisition.\",\n );\n }\n accessToken = await (\n this.oauthClient as ResourceOwnerPassword\n ).getToken({\n username: this.config.servicenow.oauth.username,\n password: this.config.servicenow.oauth.password,\n });\n } else {\n throw new Error(\n `Unsupported grantType in getAuthHeaders: ${\n (this.config.servicenow.oauth as any).grantType\n }`,\n );\n }\n\n const tokenData = accessToken.token;\n if (\n !tokenData ||\n typeof tokenData.access_token !== 'string' ||\n !tokenData.access_token\n ) {\n throw new Error(\n 'Failed to obtain access_token string (token data is invalid or missing).',\n );\n }\n return { Authorization: `Bearer ${tokenData.access_token}` };\n } catch (error: any) {\n this.logger.error(`Error fetching ServiceNow token: ${error.message}`, {\n error: error.stack || error,\n });\n if (error.isAxiosError && error.response) {\n this.logger.error(\n `OAuth2 token error details: Status ${\n error.response.status\n }, Data: ${JSON.stringify(error.response.data)}`,\n );\n } else if (error.data && error.data.payload) {\n this.logger.error(\n `OAuth2 token error payload: ${JSON.stringify(error.data.payload)}`,\n );\n }\n throw new Error(`Failed to obtain access token: ${error.message}`);\n }\n }\n\n throw new Error('No authentication method configured.');\n }\n}\n"],"names":["axios","https","ClientCredentials","ResourceOwnerPassword"],"mappings":"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"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const INCIDENT_TABLE_NAME = "incident";
|
|
4
|
+
const SCHEMA_TTL_MS = 5 * 60 * 1e3;
|
|
5
|
+
class ServiceNowSchemaChecker {
|
|
6
|
+
constructor(conn) {
|
|
7
|
+
this.conn = conn;
|
|
8
|
+
}
|
|
9
|
+
cache;
|
|
10
|
+
async fetchIncidentSchema() {
|
|
11
|
+
try {
|
|
12
|
+
const authHeaders = await this.conn.getAuthHeaders();
|
|
13
|
+
const url = `api/now/table/sys_dictionary`;
|
|
14
|
+
const response = await this.conn.getAxiosInstance().get(url, {
|
|
15
|
+
headers: {
|
|
16
|
+
...authHeaders,
|
|
17
|
+
Accept: "application/json"
|
|
18
|
+
},
|
|
19
|
+
params: {
|
|
20
|
+
sysparm_query: `name=${INCIDENT_TABLE_NAME}`,
|
|
21
|
+
sysparm_fields: "element",
|
|
22
|
+
sysparm_limit: 500
|
|
23
|
+
},
|
|
24
|
+
responseType: "json"
|
|
25
|
+
});
|
|
26
|
+
if (response.status !== 200) {
|
|
27
|
+
throw new Error(`ServiceNow API returned status ${response.status}`);
|
|
28
|
+
}
|
|
29
|
+
const contentType = response.headers["content-type"] ?? "";
|
|
30
|
+
if (!contentType.includes("application/json")) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Expected JSON response but got Content-Type: ${contentType}`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
const data = response.data;
|
|
36
|
+
if (!data || typeof data !== "object") {
|
|
37
|
+
throw new Error("ServiceNow API returned invalid JSON response");
|
|
38
|
+
}
|
|
39
|
+
if (data.error) {
|
|
40
|
+
throw new Error(`ServiceNow API Error: ${data.error.message}`);
|
|
41
|
+
}
|
|
42
|
+
const result = data.result ?? [];
|
|
43
|
+
return new Set(result.map((r) => r.element));
|
|
44
|
+
} catch (e) {
|
|
45
|
+
if (e instanceof Error) {
|
|
46
|
+
throw new Error(`Failed to fetch incident schema: ${e.message}`);
|
|
47
|
+
}
|
|
48
|
+
throw e;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async getIncidentFields() {
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
if (this.cache && now - this.cache.fetchedAt < SCHEMA_TTL_MS) {
|
|
54
|
+
return this.cache.fields;
|
|
55
|
+
}
|
|
56
|
+
const fields = await this.fetchIncidentSchema();
|
|
57
|
+
this.cache = {
|
|
58
|
+
fields,
|
|
59
|
+
fetchedAt: now
|
|
60
|
+
};
|
|
61
|
+
return fields;
|
|
62
|
+
}
|
|
63
|
+
async fieldExists(...fields) {
|
|
64
|
+
if (fields.length === 0) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
const incidentFields = await this.getIncidentFields();
|
|
68
|
+
return fields.every((field) => incidentFields.has(field));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
exports.ServiceNowSchemaChecker = ServiceNowSchemaChecker;
|
|
73
|
+
//# sourceMappingURL=schema-checker.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-checker.cjs.js","sources":["../../src/service-now-rest/schema-checker.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { ServiceNowConnection } from './connection';\n\ntype CachedSchema = {\n fields: Set<string>;\n fetchedAt: number;\n};\n\nconst INCIDENT_TABLE_NAME = 'incident';\nconst SCHEMA_TTL_MS = 5 * 60 * 1000; // 5 minutes cache timeout\n\nexport class ServiceNowSchemaChecker {\n private cache?: CachedSchema;\n\n constructor(private readonly conn: ServiceNowConnection) {}\n\n private async fetchIncidentSchema(): Promise<Set<string>> {\n try {\n const authHeaders = await this.conn.getAuthHeaders();\n const url = `api/now/table/sys_dictionary`;\n\n const response = await this.conn.getAxiosInstance().get(url, {\n headers: {\n ...authHeaders,\n Accept: 'application/json',\n },\n params: {\n sysparm_query: `name=${INCIDENT_TABLE_NAME}`,\n sysparm_fields: 'element',\n sysparm_limit: 500,\n },\n responseType: 'json',\n });\n\n if (response.status !== 200) {\n throw new Error(`ServiceNow API returned status ${response.status}`);\n }\n\n const contentType = response.headers['content-type'] ?? '';\n if (!contentType.includes('application/json')) {\n throw new Error(\n `Expected JSON response but got Content-Type: ${contentType}`,\n );\n }\n\n const data = response.data;\n if (!data || typeof data !== 'object') {\n throw new Error('ServiceNow API returned invalid JSON response');\n }\n\n if (data.error) {\n throw new Error(`ServiceNow API Error: ${data.error.message}`);\n }\n\n const result = data.result ?? [];\n return new Set(result.map((r: any) => r.element));\n } catch (e) {\n if (e instanceof Error) {\n throw new Error(`Failed to fetch incident schema: ${e.message}`);\n }\n throw e;\n }\n }\n\n private async getIncidentFields(): Promise<Set<string>> {\n const now = Date.now();\n\n if (this.cache && now - this.cache.fetchedAt < SCHEMA_TTL_MS) {\n return this.cache.fields;\n }\n\n const fields = await this.fetchIncidentSchema();\n\n this.cache = {\n fields,\n fetchedAt: now,\n };\n\n return fields;\n }\n\n public async fieldExists(...fields: string[]): Promise<boolean> {\n if (fields.length === 0) {\n return true;\n }\n const incidentFields = await this.getIncidentFields();\n return fields.every(field => incidentFields.has(field));\n }\n}\n"],"names":[],"mappings":";;AAuBA,MAAM,mBAAsB,GAAA,UAAA;AAC5B,MAAM,aAAA,GAAgB,IAAI,EAAK,GAAA,GAAA;AAExB,MAAM,uBAAwB,CAAA;AAAA,EAGnC,YAA6B,IAA4B,EAAA;AAA5B,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AAAA;AAA6B,EAFlD,KAAA;AAAA,EAIR,MAAc,mBAA4C,GAAA;AACxD,IAAI,IAAA;AACF,MAAA,MAAM,WAAc,GAAA,MAAM,IAAK,CAAA,IAAA,CAAK,cAAe,EAAA;AACnD,MAAA,MAAM,GAAM,GAAA,CAAA,4BAAA,CAAA;AAEZ,MAAA,MAAM,WAAW,MAAM,IAAA,CAAK,KAAK,gBAAiB,EAAA,CAAE,IAAI,GAAK,EAAA;AAAA,QAC3D,OAAS,EAAA;AAAA,UACP,GAAG,WAAA;AAAA,UACH,MAAQ,EAAA;AAAA,SACV;AAAA,QACA,MAAQ,EAAA;AAAA,UACN,aAAA,EAAe,QAAQ,mBAAmB,CAAA,CAAA;AAAA,UAC1C,cAAgB,EAAA,SAAA;AAAA,UAChB,aAAe,EAAA;AAAA,SACjB;AAAA,QACA,YAAc,EAAA;AAAA,OACf,CAAA;AAED,MAAI,IAAA,QAAA,CAAS,WAAW,GAAK,EAAA;AAC3B,QAAA,MAAM,IAAI,KAAA,CAAM,CAAkC,+BAAA,EAAA,QAAA,CAAS,MAAM,CAAE,CAAA,CAAA;AAAA;AAGrE,MAAA,MAAM,WAAc,GAAA,QAAA,CAAS,OAAQ,CAAA,cAAc,CAAK,IAAA,EAAA;AACxD,MAAA,IAAI,CAAC,WAAA,CAAY,QAAS,CAAA,kBAAkB,CAAG,EAAA;AAC7C,QAAA,MAAM,IAAI,KAAA;AAAA,UACR,gDAAgD,WAAW,CAAA;AAAA,SAC7D;AAAA;AAGF,MAAA,MAAM,OAAO,QAAS,CAAA,IAAA;AACtB,MAAA,IAAI,CAAC,IAAA,IAAQ,OAAO,IAAA,KAAS,QAAU,EAAA;AACrC,QAAM,MAAA,IAAI,MAAM,+CAA+C,CAAA;AAAA;AAGjE,MAAA,IAAI,KAAK,KAAO,EAAA;AACd,QAAA,MAAM,IAAI,KAAM,CAAA,CAAA,sBAAA,EAAyB,IAAK,CAAA,KAAA,CAAM,OAAO,CAAE,CAAA,CAAA;AAAA;AAG/D,MAAM,MAAA,MAAA,GAAS,IAAK,CAAA,MAAA,IAAU,EAAC;AAC/B,MAAO,OAAA,IAAI,IAAI,MAAO,CAAA,GAAA,CAAI,CAAC,CAAW,KAAA,CAAA,CAAE,OAAO,CAAC,CAAA;AAAA,aACzC,CAAG,EAAA;AACV,MAAA,IAAI,aAAa,KAAO,EAAA;AACtB,QAAA,MAAM,IAAI,KAAA,CAAM,CAAoC,iCAAA,EAAA,CAAA,CAAE,OAAO,CAAE,CAAA,CAAA;AAAA;AAEjE,MAAM,MAAA,CAAA;AAAA;AACR;AACF,EAEA,MAAc,iBAA0C,GAAA;AACtD,IAAM,MAAA,GAAA,GAAM,KAAK,GAAI,EAAA;AAErB,IAAA,IAAI,KAAK,KAAS,IAAA,GAAA,GAAM,IAAK,CAAA,KAAA,CAAM,YAAY,aAAe,EAAA;AAC5D,MAAA,OAAO,KAAK,KAAM,CAAA,MAAA;AAAA;AAGpB,IAAM,MAAA,MAAA,GAAS,MAAM,IAAA,CAAK,mBAAoB,EAAA;AAE9C,IAAA,IAAA,CAAK,KAAQ,GAAA;AAAA,MACX,MAAA;AAAA,MACA,SAAW,EAAA;AAAA,KACb;AAEA,IAAO,OAAA,MAAA;AAAA;AACT,EAEA,MAAa,eAAe,MAAoC,EAAA;AAC9D,IAAI,IAAA,MAAA,CAAO,WAAW,CAAG,EAAA;AACvB,MAAO,OAAA,IAAA;AAAA;AAET,IAAM,MAAA,cAAA,GAAiB,MAAM,IAAA,CAAK,iBAAkB,EAAA;AACpD,IAAA,OAAO,OAAO,KAAM,CAAA,CAAA,KAAA,KAAS,cAAe,CAAA,GAAA,CAAI,KAAK,CAAC,CAAA;AAAA;AAE1D;;;;"}
|