diff --git a/components/servicenow/actions/add-item-to-cart/add-item-to-cart.mjs b/components/servicenow/actions/add-item-to-cart/add-item-to-cart.mjs new file mode 100644 index 0000000000000..dab1df7f49cb3 --- /dev/null +++ b/components/servicenow/actions/add-item-to-cart/add-item-to-cart.mjs @@ -0,0 +1,58 @@ +import servicenow from "../../servicenow.app.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "servicenow-add-item-to-cart", + name: "Add Item to Cart", + description: "Add a ServiceNow catalog item to the current user's cart. Run **Search Catalog Items** to find the item `sys_id` and **Get Catalog Item Variables** to learn which variable names to supply. After adding items, use **View Cart**, then **Checkout Cart** or **Submit Cart Order** to submit. [See the documentation](https://www.servicenow.com/docs/r/zurich/api-reference/rest-apis/c_ServiceCatalogAPI.html)", + version: "0.0.1", + type: "action", + annotations: { + readOnlyHint: false, + destructiveHint: false, + openWorldHint: true, + }, + props: { + servicenow, + catalogItemSysId: { + propDefinition: [ + servicenow, + "catalogItemSysId", + ], + description: "The `sys_id` of the catalog item to add. Run **Search Catalog Items** first to find this value. Example: `e8d3d2f1c0a8016400e6b9e0f6e6f6e6`.", + }, + quantity: { + propDefinition: [ + servicenow, + "quantity", + ], + }, + variables: { + propDefinition: [ + servicenow, + "variables", + ], + }, + requestedFor: { + propDefinition: [ + servicenow, + "requestedFor", + ], + }, + }, + async run({ $ }) { + const response = await this.servicenow.addItemToCart({ + $, + catalogItemSysId: this.catalogItemSysId, + data: { + sysparm_quantity: this.quantity, + variables: parseObject(this.variables), + sysparm_requested_for: this.requestedFor, + }, + }); + + $.export("$summary", `Successfully added catalog item ${this.catalogItemSysId} to cart`); + + return response; + }, +}; diff --git a/components/servicenow/actions/check-order-status/check-order-status.mjs b/components/servicenow/actions/check-order-status/check-order-status.mjs new file mode 100644 index 0000000000000..4b03be0a979a2 --- /dev/null +++ b/components/servicenow/actions/check-order-status/check-order-status.mjs @@ -0,0 +1,62 @@ +import servicenow from "../../servicenow.app.mjs"; +import { assertSafeQueryValue } from "../../common/utils.mjs"; + +export default { + key: "servicenow-check-order-status", + name: "Check Order Status", + description: "Retrieve the status of a ServiceNow catalog request (REQ) from the `sc_request` table, including request state, approval, and stage. Provide the request number returned by **Checkout Cart**, **Submit Cart Order**, or **Order Catalog Item**. [See the documentation](https://www.servicenow.com/docs/r/zurich/api-reference/rest-apis/c_TableAPI.html)", + version: "0.0.1", + type: "action", + annotations: { + readOnlyHint: true, + destructiveHint: false, + openWorldHint: true, + }, + props: { + servicenow, + requestNumber: { + type: "string", + label: "Request Number", + description: "The request number to look up (matched against `number` on `sc_request`). Example: `REQ0010001`.", + }, + requestedFor: { + propDefinition: [ + servicenow, + "requestedFor", + ], + description: "Optional `sys_id` of the requested-for user to additionally filter by (matched against `requested_for` on `sc_request`). Run **Find Users** to find it.", + }, + }, + async run({ $ }) { + assertSafeQueryValue(this.requestNumber, "Request Number"); + assertSafeQueryValue(this.requestedFor, "Requested For"); + + const queryParts = [ + `number=${this.requestNumber}`, + ]; + if (this.requestedFor) { + queryParts.push(`requested_for=${this.requestedFor}`); + } + const response = await this.servicenow.getRequests({ + $, + params: { + sysparm_query: queryParts.join("^"), + sysparm_limit: 1, + }, + }); + + const request = response?.[0]; + let summary; + if (!request) { + summary = `No request found with number ${this.requestNumber}`; + } else { + const state = request.state ?? request.request_state; + summary = state + ? `Retrieved status for request ${this.requestNumber}: ${state}` + : `Retrieved status for request ${this.requestNumber}`; + } + $.export("$summary", summary); + + return response; + }, +}; diff --git a/components/servicenow/actions/checkout-cart/checkout-cart.mjs b/components/servicenow/actions/checkout-cart/checkout-cart.mjs new file mode 100644 index 0000000000000..7ab1e73e2bb74 --- /dev/null +++ b/components/servicenow/actions/checkout-cart/checkout-cart.mjs @@ -0,0 +1,30 @@ +import servicenow from "../../servicenow.app.mjs"; + +export default { + key: "servicenow-checkout-cart", + name: "Checkout Cart", + description: "Submit the current user's ServiceNow cart via the standard `/cart/checkout` endpoint, generating a request (REQ). The result depends on the instance's one-step vs two-step checkout configuration (in two-step mode it returns the order summary/status to confirm rather than finalizing). Add items first with **Add Item to Cart** and inspect with **View Cart**. Use **Check Order Status** afterward to track the resulting request. [See the documentation](https://www.servicenow.com/docs/r/zurich/api-reference/rest-apis/c_ServiceCatalogAPI.html)", + version: "0.0.1", + type: "action", + annotations: { + readOnlyHint: false, + destructiveHint: false, + openWorldHint: true, + }, + props: { + servicenow, + }, + async run({ $ }) { + const response = await this.servicenow.checkoutCart({ + $, + }); + + const requestNumber = response?.request_number ?? response?.number ?? response?.request_id; + const summary = requestNumber + ? `Successfully checked out cart - request ${requestNumber}` + : "Successfully checked out cart"; + $.export("$summary", summary); + + return response; + }, +}; diff --git a/components/servicenow/actions/create-table-record/create-table-record.mjs b/components/servicenow/actions/create-table-record/create-table-record.mjs index de332f9586ab9..33bb35ec12b30 100644 --- a/components/servicenow/actions/create-table-record/create-table-record.mjs +++ b/components/servicenow/actions/create-table-record/create-table-record.mjs @@ -5,7 +5,7 @@ export default { key: "servicenow-create-table-record", name: "Create Table Record", description: "Inserts one record in the specified table. [See the documentation](https://www.servicenow.com/docs/bundle/zurich-api-reference/page/integrate/inbound-rest/concept/c_TableAPI.html#title_table-POST)", - version: "1.0.3", + version: "1.0.4", annotations: { destructiveHint: false, openWorldHint: true, diff --git a/components/servicenow/actions/delete-cart-item/delete-cart-item.mjs b/components/servicenow/actions/delete-cart-item/delete-cart-item.mjs new file mode 100644 index 0000000000000..7d93cc2ac213e --- /dev/null +++ b/components/servicenow/actions/delete-cart-item/delete-cart-item.mjs @@ -0,0 +1,32 @@ +import servicenow from "../../servicenow.app.mjs"; + +export default { + key: "servicenow-delete-cart-item", + name: "Delete Cart Item", + description: "Remove an item from the current user's ServiceNow cart. Run **View Cart** first to obtain the `cart_item` id to delete. This permanently removes the line item from the cart. [See the documentation](https://www.servicenow.com/docs/r/zurich/api-reference/rest-apis/c_ServiceCatalogAPI.html)", + version: "0.0.1", + type: "action", + annotations: { + readOnlyHint: false, + destructiveHint: true, + openWorldHint: true, + }, + props: { + servicenow, + cartItemId: { + type: "string", + label: "Cart Item ID", + description: "The cart item id to remove. Run **View Cart** first to find this value. Example: `0f3b2e2e1b223010d3f5a6c1cd4bcb12`.", + }, + }, + async run({ $ }) { + const response = await this.servicenow.deleteCartItem({ + $, + cartItemId: this.cartItemId, + }); + + $.export("$summary", `Successfully removed cart item ${this.cartItemId}`); + + return response; + }, +}; diff --git a/components/servicenow/actions/delete-table-record/delete-table-record.mjs b/components/servicenow/actions/delete-table-record/delete-table-record.mjs index cf5654085362c..93b31f73f0a50 100644 --- a/components/servicenow/actions/delete-table-record/delete-table-record.mjs +++ b/components/servicenow/actions/delete-table-record/delete-table-record.mjs @@ -4,7 +4,7 @@ export default { key: "servicenow-delete-table-record", name: "Delete Table Record", description: "Deletes the specified record from a table. [See the documentation](https://www.servicenow.com/docs/bundle/zurich-api-reference/page/integrate/inbound-rest/concept/c_TableAPI.html#title_table-DELETE)", - version: "0.0.4", + version: "0.0.5", annotations: { destructiveHint: true, openWorldHint: true, diff --git a/components/servicenow/actions/empty-cart/empty-cart.mjs b/components/servicenow/actions/empty-cart/empty-cart.mjs new file mode 100644 index 0000000000000..4adac671a6b50 --- /dev/null +++ b/components/servicenow/actions/empty-cart/empty-cart.mjs @@ -0,0 +1,31 @@ +import servicenow from "../../servicenow.app.mjs"; + +export default { + key: "servicenow-empty-cart", + name: "Empty Cart", + description: "Empty and delete the current user's ServiceNow cart, removing all of its items. The action resolves the active cart automatically, so no cart ID is required. Emptying a cart that still has items requires the `catalog_admin` role (a plain `admin` can only delete an already-empty cart). [See the documentation](https://www.servicenow.com/docs/r/zurich/api-reference/rest-apis/c_ServiceCatalogAPI.html)", + version: "0.0.1", + type: "action", + annotations: { + readOnlyHint: false, + destructiveHint: true, + openWorldHint: true, + }, + props: { + servicenow, + }, + async run({ $ }) { + const { cart_id: cartSysId } = await this.servicenow.getCart({ + $, + }); + + const response = await this.servicenow.emptyCart({ + $, + cartSysId, + }); + + $.export("$summary", `Successfully emptied cart ${cartSysId}`); + + return response; + }, +}; diff --git a/components/servicenow/actions/find-users/find-users.mjs b/components/servicenow/actions/find-users/find-users.mjs new file mode 100644 index 0000000000000..7ff54240d8d6b --- /dev/null +++ b/components/servicenow/actions/find-users/find-users.mjs @@ -0,0 +1,66 @@ +import servicenow from "../../servicenow.app.mjs"; +import { assertSafeQueryValue } from "../../common/utils.mjs"; + +export default { + key: "servicenow-find-users", + name: "Find Users", + description: "Search ServiceNow `sys_user` records to find a user's `sys_id` (used by props like the requested-for field on **Add Item to Cart**). Choose whether to match on name (partial) or email (exact). [See the documentation](https://www.servicenow.com/docs/r/zurich/api-reference/rest-apis/c_TableAPI.html)", + version: "0.0.1", + type: "action", + annotations: { + readOnlyHint: true, + destructiveHint: false, + openWorldHint: true, + }, + props: { + servicenow, + searchField: { + type: "string", + label: "Search Field", + description: "Which `sys_user` field to match on. `Name` performs a partial (contains) match; `Email` requires an exact match.", + options: [ + { + label: "Name (partial match)", + value: "name", + }, + { + label: "Email (exact match)", + value: "email", + }, + ], + default: "name", + }, + searchValue: { + type: "string", + label: "Search Value", + description: "The value to search for. Example: `Jane Doe` or `jane.doe@example.com`.", + }, + limit: { + propDefinition: [ + servicenow, + "limit", + ], + }, + }, + async run({ $ }) { + assertSafeQueryValue(this.searchValue, "Search Value"); + + const operator = this.searchField === "name" + ? "LIKE" + : "="; + const response = await this.servicenow.listUsers({ + $, + params: { + sysparm_query: `${this.searchField}${operator}${this.searchValue}`, + sysparm_limit: this.limit, + }, + }); + + const users = Array.isArray(response) + ? response + : []; + $.export("$summary", `Found ${users.length} user(s) matching ${this.searchField} "${this.searchValue}"`); + + return response; + }, +}; diff --git a/components/servicenow/actions/get-catalog-item-variables/get-catalog-item-variables.mjs b/components/servicenow/actions/get-catalog-item-variables/get-catalog-item-variables.mjs new file mode 100644 index 0000000000000..b232ba9c5c2a5 --- /dev/null +++ b/components/servicenow/actions/get-catalog-item-variables/get-catalog-item-variables.mjs @@ -0,0 +1,36 @@ +import servicenow from "../../servicenow.app.mjs"; + +export default { + key: "servicenow-get-catalog-item-variables", + name: "Get Catalog Item Variables", + description: "Retrieve the ordered variables (form fields) for a ServiceNow catalog item. Run **Search Catalog Items** first to obtain the item `sys_id`, then use the returned variable names to build the `variables` payload for **Add Item to Cart**, **Order Catalog Item**, or **Submit Record Producer**. [See the documentation](https://www.servicenow.com/docs/r/zurich/api-reference/rest-apis/c_ServiceCatalogAPI.html)", + version: "0.0.1", + type: "action", + annotations: { + readOnlyHint: true, + destructiveHint: false, + openWorldHint: true, + }, + props: { + servicenow, + catalogItemSysId: { + propDefinition: [ + servicenow, + "catalogItemSysId", + ], + }, + }, + async run({ $ }) { + const response = await this.servicenow.getCatalogItemVariables({ + $, + catalogItemSysId: this.catalogItemSysId, + }); + + const variables = Array.isArray(response) + ? response + : (response?.variables ?? []); + $.export("$summary", `Successfully retrieved ${variables.length} variable(s) for catalog item ${this.catalogItemSysId}`); + + return variables; + }, +}; diff --git a/components/servicenow/actions/get-current-user/get-current-user.mjs b/components/servicenow/actions/get-current-user/get-current-user.mjs index 91cbb0edd020f..a270329036db0 100644 --- a/components/servicenow/actions/get-current-user/get-current-user.mjs +++ b/components/servicenow/actions/get-current-user/get-current-user.mjs @@ -4,7 +4,7 @@ export default { key: "servicenow-get-current-user", name: "Get Current User", description: "Returns the authenticated ServiceNow user's sys_id, name, email, username, and instance URL. Call this first when the user says 'my incidents', 'my cases', 'assigned to me', or needs their ServiceNow identity. Use `sys_id` to filter records in **Get Table Records** (e.g. `assigned_to={sys_id}`) or **Create Table Record**. [See the documentation](https://docs.servicenow.com/bundle/vancouver-api-reference/page/integrate/inbound-rest/concept/c_TableAPI.html).", - version: "0.0.2", + version: "0.0.3", type: "action", annotations: { destructiveHint: false, diff --git a/components/servicenow/actions/get-record-counts-by-field/get-record-counts-by-field.mjs b/components/servicenow/actions/get-record-counts-by-field/get-record-counts-by-field.mjs index aae18a1e5fac5..e4ded86db9c47 100644 --- a/components/servicenow/actions/get-record-counts-by-field/get-record-counts-by-field.mjs +++ b/components/servicenow/actions/get-record-counts-by-field/get-record-counts-by-field.mjs @@ -5,7 +5,7 @@ export default { key: "servicenow-get-record-counts-by-field", name: "Get Record Counts by Field", description: "Retrieves the count of records grouped by a specified field from a ServiceNow table. [See the documentation](https://www.servicenow.com/docs/bundle/zurich-api-reference/page/integrate/inbound-rest/concept/c_AggregateAPI.html#title_aggregate-GET-stats)", - version: "0.0.4", + version: "0.0.5", annotations: { destructiveHint: false, openWorldHint: true, diff --git a/components/servicenow/actions/get-table-record-by-id/get-table-record-by-id.mjs b/components/servicenow/actions/get-table-record-by-id/get-table-record-by-id.mjs index 205b9ef1c4798..cb49e97ddc3b6 100644 --- a/components/servicenow/actions/get-table-record-by-id/get-table-record-by-id.mjs +++ b/components/servicenow/actions/get-table-record-by-id/get-table-record-by-id.mjs @@ -4,7 +4,7 @@ export default { key: "servicenow-get-table-record-by-id", name: "Get Table Record by ID", description: "Retrieves a single record from a table by its ID. [See the documentation](https://www.servicenow.com/docs/bundle/zurich-api-reference/page/integrate/inbound-rest/concept/c_TableAPI.html#title_table-GET-id)", - version: "1.0.3", + version: "1.0.4", annotations: { destructiveHint: false, openWorldHint: true, diff --git a/components/servicenow/actions/get-table-records/get-table-records.mjs b/components/servicenow/actions/get-table-records/get-table-records.mjs index be057951bdb6a..2315486718834 100644 --- a/components/servicenow/actions/get-table-records/get-table-records.mjs +++ b/components/servicenow/actions/get-table-records/get-table-records.mjs @@ -5,7 +5,7 @@ export default { key: "servicenow-get-table-records", name: "Get Table Records", description: "Retrieves multiple records for the specified table. [See the documentation](https://www.servicenow.com/docs/bundle/zurich-api-reference/page/integrate/inbound-rest/concept/c_TableAPI.html#title_table-GET)", - version: "1.0.3", + version: "1.0.4", annotations: { destructiveHint: false, openWorldHint: true, diff --git a/components/servicenow/actions/list-tables/list-tables.mjs b/components/servicenow/actions/list-tables/list-tables.mjs index 2e726d105d340..cc38b6c717c6a 100644 --- a/components/servicenow/actions/list-tables/list-tables.mjs +++ b/components/servicenow/actions/list-tables/list-tables.mjs @@ -4,7 +4,7 @@ export default { key: "servicenow-list-tables", name: "List Tables", description: "List all tables in the ServiceNow instance. [See the documentation](https://www.servicenow.com/docs/r/api-reference/rest-apis/c_TableAPI.html)", - version: "0.0.2", + version: "0.0.3", type: "action", annotations: { destructiveHint: false, diff --git a/components/servicenow/actions/order-catalog-item/order-catalog-item.mjs b/components/servicenow/actions/order-catalog-item/order-catalog-item.mjs new file mode 100644 index 0000000000000..a970d79d9ce36 --- /dev/null +++ b/components/servicenow/actions/order-catalog-item/order-catalog-item.mjs @@ -0,0 +1,62 @@ +import servicenow from "../../servicenow.app.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "servicenow-order-catalog-item", + name: "Order Catalog Item", + description: "Place a one-step Order Now request for a single ServiceNow catalog item, bypassing the cart. Run **Search Catalog Items** to find the item `sys_id` and **Get Catalog Item Variables** to learn which variable names to supply. Use **Check Order Status** afterward to track the resulting request. [See the documentation](https://www.servicenow.com/docs/r/zurich/api-reference/rest-apis/c_ServiceCatalogAPI.html)", + version: "0.0.1", + type: "action", + annotations: { + readOnlyHint: false, + destructiveHint: false, + openWorldHint: true, + }, + props: { + servicenow, + catalogItemSysId: { + propDefinition: [ + servicenow, + "catalogItemSysId", + ], + description: "The `sys_id` of the catalog item to order. Run **Search Catalog Items** first to find this value. Example: `e8d3d2f1c0a8016400e6b9e0f6e6f6e6`.", + }, + quantity: { + propDefinition: [ + servicenow, + "quantity", + ], + }, + variables: { + propDefinition: [ + servicenow, + "variables", + ], + }, + requestedFor: { + propDefinition: [ + servicenow, + "requestedFor", + ], + }, + }, + async run({ $ }) { + const response = await this.servicenow.orderNow({ + $, + catalogItemSysId: this.catalogItemSysId, + data: { + sysparm_quantity: this.quantity, + variables: parseObject(this.variables), + sysparm_requested_for: this.requestedFor, + }, + }); + + const requestNumber = response?.request_number ?? response?.number ?? response?.request_id; + const summary = requestNumber + ? `Successfully ordered catalog item ${this.catalogItemSysId} - request ${requestNumber}` + : `Successfully ordered catalog item ${this.catalogItemSysId}`; + $.export("$summary", summary); + + return response; + }, +}; diff --git a/components/servicenow/actions/search-catalog-items/search-catalog-items.mjs b/components/servicenow/actions/search-catalog-items/search-catalog-items.mjs new file mode 100644 index 0000000000000..9eb951e8203ee --- /dev/null +++ b/components/servicenow/actions/search-catalog-items/search-catalog-items.mjs @@ -0,0 +1,67 @@ +import servicenow from "../../servicenow.app.mjs"; + +export default { + key: "servicenow-search-catalog-items", + name: "Search Catalog Items", + description: "Search the ServiceNow Service Catalog for orderable items. Use this first to discover catalog item `sys_id` values needed by **Get Catalog Item Variables**, **Add Item to Cart**, **Order Catalog Item**, and **Submit Record Producer**. [See the documentation](https://www.servicenow.com/docs/r/zurich/api-reference/rest-apis/c_ServiceCatalogAPI.html)", + version: "0.0.1", + type: "action", + annotations: { + readOnlyHint: true, + destructiveHint: false, + openWorldHint: true, + }, + props: { + servicenow, + query: { + type: "string", + label: "Query", + description: "Free-text search term to match catalog items (maps to `sysparm_text`). Example: `laptop`.", + optional: true, + }, + catalogSysId: { + type: "string", + label: "Catalog Sys ID", + description: "Optional catalog `sys_id` to restrict results to a single catalog. Find it via **Get Table Records** on the `sc_catalog` table. Example: `e0d08b13c3330100c8b837659bba8fb4`.", + optional: true, + }, + categorySysId: { + type: "string", + label: "Category Sys ID", + description: "Optional category `sys_id` to restrict results to a single category. Find it via **Get Table Records** on the `sc_category` table. Example: `d258b953c611227a0146101fb1be7c31`.", + optional: true, + }, + limit: { + propDefinition: [ + servicenow, + "limit", + ], + }, + offset: { + type: "integer", + label: "Offset", + description: "Number of items to skip for pagination (maps to `sysparm_offset`).", + min: 0, + optional: true, + }, + }, + async run({ $ }) { + const response = await this.servicenow.searchCatalogItems({ + $, + params: { + sysparm_text: this.query, + sysparm_catalog: this.catalogSysId, + sysparm_category: this.categorySysId, + sysparm_limit: this.limit, + sysparm_offset: this.offset, + }, + }); + + const items = Array.isArray(response) + ? response + : (response?.items ?? []); + $.export("$summary", `Successfully retrieved ${items.length} catalog item(s)`); + + return response; + }, +}; diff --git a/components/servicenow/actions/search-knowledge-base/search-knowledge-base.mjs b/components/servicenow/actions/search-knowledge-base/search-knowledge-base.mjs new file mode 100644 index 0000000000000..4327ee55da08c --- /dev/null +++ b/components/servicenow/actions/search-knowledge-base/search-knowledge-base.mjs @@ -0,0 +1,56 @@ +import servicenow from "../../servicenow.app.mjs"; + +export default { + key: "servicenow-search-knowledge-base", + name: "Search Knowledge Base", + description: "Search ServiceNow knowledge base articles via the Knowledge Management API. Returns matching articles with snippets; retrieve a full article body by its id with **Get Table Records** on `kb_knowledge` or the article detail endpoint. [See the documentation](https://www.servicenow.com/docs/r/zurich/api-reference/rest-apis/knowledge-api.html)", + version: "0.0.1", + type: "action", + annotations: { + readOnlyHint: true, + destructiveHint: false, + openWorldHint: true, + }, + props: { + servicenow, + query: { + type: "string", + label: "Query", + description: "Free-text search term to match knowledge articles (maps to `query`). Example: `vpn setup`.", + }, + limit: { + propDefinition: [ + servicenow, + "limit", + ], + }, + fields: { + type: "string[]", + label: "Fields", + description: "Optional list of additional article fields to return (maps to repeated `fields` query params). Example: `short_description`, `sys_class_name`.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.servicenow.searchKnowledgeArticles({ + $, + params: { + query: this.query, + limit: this.limit, + fields: this.fields?.length + ? this.fields + : undefined, + }, + paramsSerializer: { + indexes: null, + }, + }); + + const articles = Array.isArray(response) + ? response + : (response?.articles ?? []); + $.export("$summary", `Found ${articles.length} knowledge article(s) matching "${this.query}"`); + + return response; + }, +}; diff --git a/components/servicenow/actions/search-records-by-keyword/search-records-by-keyword.mjs b/components/servicenow/actions/search-records-by-keyword/search-records-by-keyword.mjs index 9fe878d7d834a..6eb4bb92280cf 100644 --- a/components/servicenow/actions/search-records-by-keyword/search-records-by-keyword.mjs +++ b/components/servicenow/actions/search-records-by-keyword/search-records-by-keyword.mjs @@ -4,7 +4,7 @@ export default { key: "servicenow-search-records-by-keyword", name: "Search Records by Keyword", description: "Search for records by keyword. [See the documentation](https://www.servicenow.com/docs/bundle/zurich-api-reference/page/integrate/inbound-rest/concept/c_TableAPI.html#title_table-GET)", - version: "0.0.1", + version: "0.0.2", annotations: { destructiveHint: false, openWorldHint: true, diff --git a/components/servicenow/actions/submit-cart-order/submit-cart-order.mjs b/components/servicenow/actions/submit-cart-order/submit-cart-order.mjs new file mode 100644 index 0000000000000..871727dab75bc --- /dev/null +++ b/components/servicenow/actions/submit-cart-order/submit-cart-order.mjs @@ -0,0 +1,30 @@ +import servicenow from "../../servicenow.app.mjs"; + +export default { + key: "servicenow-submit-cart-order", + name: "Submit Cart Order", + description: "Submit the current user's ServiceNow cart via the `/cart/submit_order` endpoint, generating a request (REQ). Like **Checkout Cart**, the result depends on the instance's one-step vs two-step checkout configuration. Add items first with **Add Item to Cart** and inspect with **View Cart**. Use **Check Order Status** afterward to track the resulting request. [See the documentation](https://www.servicenow.com/docs/r/zurich/api-reference/rest-apis/c_ServiceCatalogAPI.html)", + version: "0.0.1", + type: "action", + annotations: { + readOnlyHint: false, + destructiveHint: false, + openWorldHint: true, + }, + props: { + servicenow, + }, + async run({ $ }) { + const response = await this.servicenow.submitOrder({ + $, + }); + + const requestNumber = response?.request_number ?? response?.number ?? response?.request_id; + const summary = requestNumber + ? `Successfully submitted cart order - request ${requestNumber}` + : "Successfully submitted cart order"; + $.export("$summary", summary); + + return response; + }, +}; diff --git a/components/servicenow/actions/submit-record-producer/submit-record-producer.mjs b/components/servicenow/actions/submit-record-producer/submit-record-producer.mjs new file mode 100644 index 0000000000000..4b7cd541f198a --- /dev/null +++ b/components/servicenow/actions/submit-record-producer/submit-record-producer.mjs @@ -0,0 +1,60 @@ +import servicenow from "../../servicenow.app.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "servicenow-submit-record-producer", + name: "Submit Record Producer", + description: "Submit a ServiceNow record producer to create the target record (e.g. an incident or RITM) from catalog variables. Run **Search Catalog Items** to find the record producer `sys_id` and **Get Catalog Item Variables** to learn which variable names to supply. [See the documentation](https://www.servicenow.com/docs/r/zurich/api-reference/rest-apis/c_ServiceCatalogAPI.html)", + version: "0.0.1", + type: "action", + annotations: { + readOnlyHint: false, + destructiveHint: false, + openWorldHint: true, + }, + props: { + servicenow, + catalogItemSysId: { + propDefinition: [ + servicenow, + "catalogItemSysId", + ], + description: "The `sys_id` of the record producer to submit. Run **Search Catalog Items** first to find this value. Example: `e8d3d2f1c0a8016400e6b9e0f6e6f6e6`.", + }, + variables: { + propDefinition: [ + servicenow, + "variables", + ], + description: "JSON object of variable name-value pairs for the record producer. Run **Get Catalog Item Variables** to discover valid names. Example: `{\"short_description\": \"laptop broken\"}`.", + }, + sysparmView: { + type: "string", + label: "View", + description: "Optional UI view used to render the response. Example: `ess`.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.servicenow.submitRecordProducer({ + $, + catalogItemSysId: this.catalogItemSysId, + data: { + variables: parseObject(this.variables), + }, + ...(this.sysparmView && { + params: { + sysparm_view: this.sysparmView, + }, + }), + }); + + const recordId = response?.sys_id ?? response?.record_id; + const summary = recordId + ? `Successfully submitted record producer ${this.catalogItemSysId} - created record ${recordId}` + : `Successfully submitted record producer ${this.catalogItemSysId}`; + $.export("$summary", summary); + + return response; + }, +}; diff --git a/components/servicenow/actions/update-table-record/update-table-record.mjs b/components/servicenow/actions/update-table-record/update-table-record.mjs index bc1437a923172..f55d580d1398d 100644 --- a/components/servicenow/actions/update-table-record/update-table-record.mjs +++ b/components/servicenow/actions/update-table-record/update-table-record.mjs @@ -5,7 +5,7 @@ export default { key: "servicenow-update-table-record", name: "Update Table Record", description: "Updates the specified record with the name-value pairs included in the request body. [See the documentation](https://www.servicenow.com/docs/bundle/zurich-api-reference/page/integrate/inbound-rest/concept/c_TableAPI.html#title_table-PATCH)", - version: "1.0.3", + version: "1.0.4", annotations: { destructiveHint: true, openWorldHint: true, diff --git a/components/servicenow/actions/view-cart/view-cart.mjs b/components/servicenow/actions/view-cart/view-cart.mjs new file mode 100644 index 0000000000000..1650fef92e844 --- /dev/null +++ b/components/servicenow/actions/view-cart/view-cart.mjs @@ -0,0 +1,46 @@ +import servicenow from "../../servicenow.app.mjs"; + +// The cart response returns line items either as a flat `items` array or grouped +// under recurring-frequency buckets (e.g. `one_time.items`, `monthly.items`, +// `yearly.items`). Collect items from every bucket that exposes an `items` array. +function collectCartItems(cart) { + if (!cart || typeof cart !== "object") { + return []; + } + const items = []; + if (Array.isArray(cart.items)) { + items.push(...cart.items); + } + for (const value of Object.values(cart)) { + if (value && typeof value === "object" && !Array.isArray(value) && Array.isArray(value.items)) { + items.push(...value.items); + } + } + return items; +} + +export default { + key: "servicenow-view-cart", + name: "View Cart", + description: "Retrieve the current user's ServiceNow cart contents, including the `cart_item` ids needed by **Delete Cart Item**. Use before **Checkout Cart** or **Submit Cart Order** to confirm what will be ordered. [See the documentation](https://www.servicenow.com/docs/r/zurich/api-reference/rest-apis/c_ServiceCatalogAPI.html)", + version: "0.0.1", + type: "action", + annotations: { + readOnlyHint: true, + destructiveHint: false, + openWorldHint: true, + }, + props: { + servicenow, + }, + async run({ $ }) { + const response = await this.servicenow.getCart({ + $, + }); + + const cartItems = collectCartItems(response); + $.export("$summary", `Retrieved cart with ${cartItems.length} item(s)`); + + return response; + }, +}; diff --git a/components/servicenow/common/constants.mjs b/components/servicenow/common/constants.mjs index 441a75d22ddb0..9f86736122741 100644 --- a/components/servicenow/common/constants.mjs +++ b/components/servicenow/common/constants.mjs @@ -25,7 +25,18 @@ const INCIDENT_SEVERITY_OPTIONS = [ }, ]; +const SERVICE_CATALOG_BASE_PATH = "/api/sn_sc/servicecatalog"; +const KNOWLEDGE_BASE_PATH = "/api/sn_km_api/knowledge"; +const SYS_USER_TABLE = "sys_user"; +const SC_REQUEST_TABLE = "sc_request"; +const MAX_LIMIT = 1000; + export default { DEFAULT_SEVERITY_OPTIONS, INCIDENT_SEVERITY_OPTIONS, + SERVICE_CATALOG_BASE_PATH, + KNOWLEDGE_BASE_PATH, + SYS_USER_TABLE, + SC_REQUEST_TABLE, + MAX_LIMIT, }; diff --git a/components/servicenow/common/utils.mjs b/components/servicenow/common/utils.mjs index 0d27916cdf391..91dc1b8c8f316 100644 --- a/components/servicenow/common/utils.mjs +++ b/components/servicenow/common/utils.mjs @@ -13,3 +13,9 @@ export function parseObject(value) { return value; } + +export function assertSafeQueryValue(value, label) { + if (typeof value === "string" && value.includes("^")) { + throw new ConfigurationError(`\`${label}\` cannot contain the \`^\` character, which is reserved by ServiceNow's encoded-query syntax.`); + } +} diff --git a/components/servicenow/package.json b/components/servicenow/package.json index 6fc849bbc518c..540c0a068640b 100644 --- a/components/servicenow/package.json +++ b/components/servicenow/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/servicenow", - "version": "0.10.2", + "version": "0.11.0", "description": "Pipedream ServiceNow Components", "main": "servicenow.app.mjs", "keywords": [ diff --git a/components/servicenow/servicenow.app.mjs b/components/servicenow/servicenow.app.mjs index 6af5804952615..5a99a878ae4d0 100644 --- a/components/servicenow/servicenow.app.mjs +++ b/components/servicenow/servicenow.app.mjs @@ -1,4 +1,13 @@ import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + +const { + SERVICE_CATALOG_BASE_PATH, + KNOWLEDGE_BASE_PATH, + SYS_USER_TABLE, + SC_REQUEST_TABLE, + MAX_LIMIT, +} = constants; export default { type: "app", @@ -109,6 +118,39 @@ export default { description: "If true, allows access to data across domains (if authorized)", optional: true, }, + limit: { + type: "integer", + label: "Limit", + description: `Maximum number of results to return (1-${MAX_LIMIT}).`, + min: 1, + max: MAX_LIMIT, + optional: true, + }, + catalogItemSysId: { + type: "string", + label: "Catalog Item Sys ID", + description: "The `sys_id` of the catalog item. Run **Search Catalog Items** first to find this value. Example: `e8d3d2f1c0a8016400e6b9e0f6e6f6e6`.", + }, + quantity: { + type: "integer", + label: "Quantity", + description: "Quantity to submit (maps to `sysparm_quantity`). Min 1. Example: `1`.", + min: 1, + default: 1, + optional: true, + }, + variables: { + type: "object", + label: "Variables", + description: "JSON object of variable name-value pairs for the item. Run **Get Catalog Item Variables** to discover valid names. Example: `{\"justification\": \"new hire\"}`.", + optional: true, + }, + requestedFor: { + type: "string", + label: "Requested For", + description: "Optional `sys_id` of the user this item is requested for (maps to `sysparm_requested_for`). Run **Find Users** to find it.", + optional: true, + }, }, methods: { async _makeRequest({ @@ -185,5 +227,131 @@ export default { ...args, }); }, + _instanceBaseUrl() { + return `https://${this.$auth.instance_name}.service-now.com`; + }, + async searchCatalogItems({ ...args }) { + return this._makeRequest({ + baseURL: `${this._instanceBaseUrl()}${SERVICE_CATALOG_BASE_PATH}`, + url: "/items", + ...args, + }); + }, + async getCatalogItemVariables({ + catalogItemSysId, ...args + }) { + return this._makeRequest({ + baseURL: `${this._instanceBaseUrl()}${SERVICE_CATALOG_BASE_PATH}`, + url: `/items/${catalogItemSysId}`, + ...args, + }); + }, + async addItemToCart({ + catalogItemSysId, ...args + }) { + return this._makeRequest({ + method: "post", + baseURL: `${this._instanceBaseUrl()}${SERVICE_CATALOG_BASE_PATH}`, + url: `/items/${catalogItemSysId}/add_to_cart`, + headers: { + "Content-Type": "application/json", + }, + ...args, + }); + }, + async checkoutCart({ ...args }) { + return this._makeRequest({ + method: "post", + baseURL: `${this._instanceBaseUrl()}${SERVICE_CATALOG_BASE_PATH}`, + url: "/cart/checkout", + headers: { + "Content-Type": "application/json", + }, + ...args, + }); + }, + async submitOrder({ ...args }) { + return this._makeRequest({ + method: "post", + baseURL: `${this._instanceBaseUrl()}${SERVICE_CATALOG_BASE_PATH}`, + url: "/cart/submit_order", + headers: { + "Content-Type": "application/json", + }, + ...args, + }); + }, + async getCart({ ...args }) { + return this._makeRequest({ + baseURL: `${this._instanceBaseUrl()}${SERVICE_CATALOG_BASE_PATH}`, + url: "/cart", + ...args, + }); + }, + async deleteCartItem({ + cartItemId, ...args + }) { + return this._makeRequest({ + method: "delete", + baseURL: `${this._instanceBaseUrl()}${SERVICE_CATALOG_BASE_PATH}`, + url: `/cart/${cartItemId}`, + ...args, + }); + }, + async emptyCart({ + cartSysId, ...args + }) { + return this._makeRequest({ + method: "delete", + baseURL: `${this._instanceBaseUrl()}${SERVICE_CATALOG_BASE_PATH}`, + url: `/cart/${cartSysId}/empty`, + ...args, + }); + }, + async orderNow({ + catalogItemSysId, ...args + }) { + return this._makeRequest({ + method: "post", + baseURL: `${this._instanceBaseUrl()}${SERVICE_CATALOG_BASE_PATH}`, + url: `/items/${catalogItemSysId}/order_now`, + headers: { + "Content-Type": "application/json", + }, + ...args, + }); + }, + async submitRecordProducer({ + catalogItemSysId, ...args + }) { + return this._makeRequest({ + method: "post", + baseURL: `${this._instanceBaseUrl()}${SERVICE_CATALOG_BASE_PATH}`, + url: `/items/${catalogItemSysId}/submit_producer`, + headers: { + "Content-Type": "application/json", + }, + ...args, + }); + }, + async searchKnowledgeArticles({ ...args }) { + return this._makeRequest({ + baseURL: `${this._instanceBaseUrl()}${KNOWLEDGE_BASE_PATH}`, + url: "/articles", + ...args, + }); + }, + async listUsers({ ...args }) { + return this.getTableRecords({ + table: SYS_USER_TABLE, + ...args, + }); + }, + async getRequests({ ...args }) { + return this.getTableRecords({ + table: SC_REQUEST_TABLE, + ...args, + }); + }, }, };