@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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @backstage-community/plugin-servicenow-backend
2
2
 
3
+ ## 1.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8472540: Support custom ServiceNow incident table fields in entity annotations
8
+
3
9
  ## 1.5.1
4
10
 
5
11
  ### Patch Changes
@@ -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 client$1 = new client.DefaultServiceNowClient(
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(req.query);
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 servicenowConfig,\n logger.child({ service: 'servicenow-client' }),\n );\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 = validateIncidentQueryParams(req.query);\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":["client","DefaultServiceNowClient","Router","express","validateIncidentQueryParams","InputError"],"mappings":";;;;;;;;;;;;;AA6BA,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,MAAMA,WAAS,IAAIC,8BAAA;AAAA,IACjB,gBAAA;AAAA,IACA,MAAO,CAAA,KAAA,CAAM,EAAE,OAAA,EAAS,qBAAqB;AAAA,GAC/C;AACA,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,IAAM,MAAA,eAAA,GAAkBC,qCAA4B,CAAA,GAAA,CAAI,KAAK,CAAA;AAC7D,IAAI,IAAA;AACF,MAAA,MAAM,SAAY,GAAA,MAAMJ,QAAO,CAAA,cAAA,CAAe,eAAe,CAAA;AAC7D,MAAA,GAAA,CAAI,KAAK,SAAS,CAAA;AAAA,aACX,KAAO,EAAA;AACd,MAAA,IAAI,iBAAiBK,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;;;;"}
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
- var simpleOauth2 = require('simple-oauth2');
4
- var axios = require('axios');
5
-
6
- function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
7
-
8
- var axios__default = /*#__PURE__*/_interopDefaultCompat(axios);
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
- instanceUrl;
12
- config;
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 = `${this.instanceUrl}/api/now/table/incident?${params.toString()}`;
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 axios__default.default.get(requestUrl, {
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: `${this.instanceUrl}/nav_to.do?uri=incident.do?sys_id=${incident.sys_id}`
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 = `${this.instanceUrl}/api/now/table/sys_user?sysparm_query=email=${encodeURIComponent(
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 axios__default.default.get(url, {
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":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage-community/plugin-servicenow-backend",
3
- "version": "1.5.1",
3
+ "version": "1.6.0",
4
4
  "license": "Apache-2.0",
5
5
  "main": "dist/index.cjs.js",
6
6
  "types": "dist/index.d.ts",