diff --git a/components/ninjaone/actions/create-ticket/create-ticket.mjs b/components/ninjaone/actions/create-ticket/create-ticket.mjs new file mode 100644 index 0000000000000..965ea7d1611a0 --- /dev/null +++ b/components/ninjaone/actions/create-ticket/create-ticket.mjs @@ -0,0 +1,128 @@ +import app from "../../ninjaone.app.mjs"; + +export default { + key: "ninjaone-create-ticket", + name: "Create Ticket", + description: "Create a new support ticket in NinjaOne. [See the documentation](https://app.ninjarmm.com/apidocs/?links.active=core#/ticketing/create).", + version: "0.0.1", + type: "action", + props: { + app, + clientId: { + label: "Client ID", + description: "The identifier of the client. Organization identifier.", + propDefinition: [ + app, + "organizationId", + ], + }, + ticketFormId: { + propDefinition: [ + app, + "ticketFormId", + ], + }, + subject: { + type: "string", + label: "Subject", + description: "The subject of the ticket. Eg. `CPU with problems`.", + }, + status: { + propDefinition: [ + app, + "ticketStatus", + ], + }, + type: { + type: "string", + label: "Ticket Type", + description: "The type of the ticket.", + optional: true, + options: [ + "PROBLEM", + "QUESTION", + "INCIDENT", + "TASK", + ], + }, + assignedAppUserId: { + propDefinition: [ + app, + "assignedAppUserId", + ], + }, + description: { + type: "string", + label: "Description", + description: "The description of the support ticket", + optional: false, + }, + isDescriptionPublic: { + type: "boolean", + label: "Description Public", + description: "Whether the description of the ticket is public.", + optional: true, + }, + parentTicketId: { + type: "string", + label: "Parent Ticket ID", + description: "The identifier of the parent ticket.", + optional: true, + }, + priority: { + type: "string", + label: "Ticket Priority", + description: "The priority of the ticket", + optional: true, + options: [ + "NONE", + "LOW", + "MEDIUM", + "HIGH", + ], + }, + }, + methods: { + createTicket(args = {}) { + return this.app.post({ + path: "/ticketing/ticket", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createTicket, + clientId, + ticketFormId, + subject, + status, + type, + assignedAppUserId, + description, + // isDescriptionPublic, + parentTicketId, + priority, + } = this; + + const response = await createTicket({ + $, + data: { + clientId, + ticketFormId, + subject, + status, + type, + assignedAppUserId, + description, + // isDescriptionPublic, + parentTicketId, + priority, + }, + }); + + $.export("$summary", "Successfully created ticket."); + + return response; + }, +}; diff --git a/components/ninjaone/actions/update-device-custom-fields/update-device-custom-fields.mjs b/components/ninjaone/actions/update-device-custom-fields/update-device-custom-fields.mjs new file mode 100644 index 0000000000000..a7791680284b5 --- /dev/null +++ b/components/ninjaone/actions/update-device-custom-fields/update-device-custom-fields.mjs @@ -0,0 +1,50 @@ +import app from "../../ninjaone.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "ninjaone-update-device-custom-fields", + name: "Update Device Custom Fields", + description: "Update custom fields for a device in NinjaOne. Requires a device identifier. Optionally update the device name, group, or status. [See the documentation](https://app.ninjarmm.com/apidocs/?links.active=core#/devices/updateNodeAttributeValues).", + version: "0.0.1", + type: "action", + props: { + app, + deviceId: { + propDefinition: [ + app, + "deviceId", + ], + }, + data: { + type: "object", + label: "Custom Fields", + description: "Additional custom fields to update.", + }, + }, + methods: { + updateDeviceCustomFields({ + deviceId, ...args + } = {}) { + return this.app.patch({ + path: `/device/${deviceId}/custom-fields`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + updateDeviceCustomFields, + deviceId, + data, + } = this; + + const response = await updateDeviceCustomFields({ + $, + deviceId, + data: utils.parse(data), + }); + + $.export("$summary", "Successfully updated device custom fields."); + return response; + }, +}; diff --git a/components/ninjaone/common/constants.mjs b/components/ninjaone/common/constants.mjs new file mode 100644 index 0000000000000..c809b5ffc5bb3 --- /dev/null +++ b/components/ninjaone/common/constants.mjs @@ -0,0 +1,15 @@ +const BASE_URL = "https://app.ninjarmm.com"; +const VERSION_PATH = "/api/v2"; +const LAST_CREATED_AT = "lastCreatedAt"; +const DEFAULT_LIMIT = 50; +const DEFAULT_MAX = 600; +const WEBHOOK_ID = "webhookId"; + +export default { + BASE_URL, + VERSION_PATH, + DEFAULT_MAX, + LAST_CREATED_AT, + DEFAULT_LIMIT, + WEBHOOK_ID, +}; diff --git a/components/ninjaone/common/utils.mjs b/components/ninjaone/common/utils.mjs new file mode 100644 index 0000000000000..ab604f89c76d1 --- /dev/null +++ b/components/ninjaone/common/utils.mjs @@ -0,0 +1,24 @@ +import { ConfigurationError } from "@pipedream/platform"; + +function emptyStrToUndefined(value) { + const trimmed = typeof(value) === "string" && value.trim(); + return trimmed === "" + ? undefined + : value; +} + +function parse(value) { + const valueToParse = emptyStrToUndefined(value); + if (typeof(valueToParse) === "object" || valueToParse === undefined) { + return valueToParse; + } + try { + return JSON.parse(valueToParse); + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid object."); + } +} + +export default { + parse, +}; diff --git a/components/ninjaone/ninjaone.app.mjs b/components/ninjaone/ninjaone.app.mjs index 0945dafedfbfa..9ff7b8cdb926a 100644 --- a/components/ninjaone/ninjaone.app.mjs +++ b/components/ninjaone/ninjaone.app.mjs @@ -1,11 +1,195 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "ninjaone", - propDefinitions: {}, + propDefinitions: { + organizationId: { + type: "string", + label: "Organization ID", + description: "The identifier of the organization.", + async options({ prevContext: { after } }) { + if (after === null) { + return []; + } + const data = await this.listOrganizations({ + params: { + pageSize: constants.DEFAULT_LIMIT, + after: after || 0, + }, + }); + const options = data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + return { + options, + context: { + after: data.length + ? data[data.length - 1].id + : null, + }, + }; + }, + }, + ticketFormId: { + type: "string", + label: "Ticket Form ID", + description: "The identifier of the ticket form", + async options() { + const data = await this.listTicketForms(); + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + ticketStatus: { + type: "string", + label: "Ticket Status", + description: "The status of the ticket.", + async options() { + const data = await this.listTicketStatuses(); + return data.map(({ + name: value, displayName: label, + }) => ({ + label, + value, + })); + }, + }, + assignedAppUserId: { + type: "string", + label: "Assigned Technician", + description: "The technician assigned to the ticket", + optional: true, + async options({ params }) { + const data = await this.listAppUserContacts({ + params, + }); + return data.map(({ + id: value, firstName, lastName, email, + }) => ({ + label: `${firstName} ${lastName} (${email})`, + value, + })); + }, + }, + deviceId: { + type: "string", + label: "Device ID", + description: "The identifier of the device.", + async options({ prevContext: { after } }) { + if (after === null) { + return []; + } + const data = await this.listDevices({ + params: { + pageSize: constants.DEFAULT_LIMIT, + after: after || 0, + }, + }); + const options = data.map(({ + id: value, displayName: label, + }) => ({ + label, + value, + })); + return { + options, + context: { + after: data.length + ? data[data.length - 1].id + : null, + }, + }; + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + debug: true, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + patch(args = {}) { + return this._makeRequest({ + method: "PATCH", + ...args, + }); + }, + delete(args = {}) { + return this._makeRequest({ + method: "DELETE", + ...args, + }); + }, + listOrganizations(args = {}) { + return this._makeRequest({ + path: "/organizations", + ...args, + }); + }, + listTicketForms(args = {}) { + return this._makeRequest({ + path: "/ticketing/ticket-form", + ...args, + }); + }, + listTicketStatuses(args = {}) { + return this._makeRequest({ + path: "/ticketing/statuses", + ...args, + }); + }, + listGroups(args = {}) { + return this._makeRequest({ + path: "/groups", + ...args, + }); + }, + listDeviceTypes(args = {}) { + return this._makeRequest({ + path: "/device/types", + ...args, + }); + }, + listAppUserContacts(args = {}) { + return this._makeRequest({ + path: "/ticketing/app-user-contact", + ...args, + }); + }, + listDevices(args = {}) { + return this._makeRequest({ + path: "/devices", + ...args, + }); }, }, }; diff --git a/components/ninjaone/package.json b/components/ninjaone/package.json index 968834403f082..8afd687a4e5f1 100644 --- a/components/ninjaone/package.json +++ b/components/ninjaone/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/ninjaone", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream NinjaOne Components", "main": "ninjaone.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.3" } } diff --git a/components/ninjaone/sources/common/webhook.mjs b/components/ninjaone/sources/common/webhook.mjs new file mode 100644 index 0000000000000..fd91500317db1 --- /dev/null +++ b/components/ninjaone/sources/common/webhook.mjs @@ -0,0 +1,91 @@ +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../ninjaone.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + app, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + }, + hooks: { + async activate() { + const { + createWebhook, + getEventName, + http: { endpoint: url }, + setWebhookId, + } = this; + + const response = + await createWebhook({ + data: { + url, + event: getEventName(), + }, + }); + + setWebhookId(response.id); + }, + async deactivate() { + const { + deleteWebhook, + getWebhookId, + } = this; + + const webhookId = getWebhookId(); + if (webhookId) { + await deleteWebhook({ + webhookId, + }); + } + }, + }, + methods: { + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + setWebhookId(value) { + this.db.set(constants.WEBHOOK_ID, value); + }, + getWebhookId() { + return this.db.get(constants.WEBHOOK_ID); + }, + getEventName() { + throw new ConfigurationError("getEventName is not implemented"); + }, + getResourcesFn() { + throw new ConfigurationError("getResourcesFn is not implemented"); + }, + getResourcesFnArgs() { + throw new ConfigurationError("getResourcesFnArgs is not implemented"); + }, + processResource(resource) { + this.$emit(resource, this.generateMeta(resource)); + }, + createWebhook(args = {}) { + return this.app.post({ + path: "/webhooks", + ...args, + }); + }, + deleteWebhook({ + webhookId, ...args + } = {}) { + return this.app.delete({ + path: `/webhooks/${webhookId}`, + ...args, + }); + }, + }, + async run({ body }) { + this.http.respond({ + status: 200, + }); + + this.processResource(body); + }, +}; diff --git a/components/ninjaone/sources/new-device-online-instant/new-device-online-instant.mjs b/components/ninjaone/sources/new-device-online-instant/new-device-online-instant.mjs new file mode 100644 index 0000000000000..d37d645b64c81 --- /dev/null +++ b/components/ninjaone/sources/new-device-online-instant/new-device-online-instant.mjs @@ -0,0 +1,25 @@ +import common from "../common/webhook.mjs"; +import events from "../common/events.mjs"; + +export default { + ...common, + key: "ninjaone-new-device-online-instant", + name: "New Device Online (Instant)", + description: "Emit new event when a monitored device comes online. [See the documentation](https://app.ninjarmm.com/apidocs/?links.active=core#/webhooks/configureWebhook).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventName() { + return events.DEFAULT; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Resource: ${resource.name}`, + ts: Date.parse(resource.created_at), + }; + }, + }, +}; diff --git a/components/ninjaone/sources/new-remote-session-instant/new-remote-session-instant.mjs b/components/ninjaone/sources/new-remote-session-instant/new-remote-session-instant.mjs new file mode 100644 index 0000000000000..0943d9c773200 --- /dev/null +++ b/components/ninjaone/sources/new-remote-session-instant/new-remote-session-instant.mjs @@ -0,0 +1,25 @@ +import common from "../common/webhook.mjs"; +import events from "../common/events.mjs"; + +export default { + ...common, + key: "ninjaone-new-remote-session-instant", + name: "New Remote Session (Instant)", + description: "Emit new event when a remote access session is initiated. [See the documentation](https://app.ninjarmm.com/apidocs/?links.active=core#/webhooks/configureWebhook).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventName() { + return events.DEFAULT; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Resource: ${resource.name}`, + ts: Date.parse(resource.created_at), + }; + }, + }, +}; diff --git a/components/ninjaone/sources/new-ticket-instant/new-ticket-instant.mjs b/components/ninjaone/sources/new-ticket-instant/new-ticket-instant.mjs new file mode 100644 index 0000000000000..2dcbcd3836593 --- /dev/null +++ b/components/ninjaone/sources/new-ticket-instant/new-ticket-instant.mjs @@ -0,0 +1,25 @@ +import common from "../common/webhook.mjs"; +import events from "../common/events.mjs"; + +export default { + ...common, + key: "ninjaone-new-ticket-instant", + name: "New Ticket (Instant)", + description: "Emit new event when a new support ticket is created in NinjaOne. [See the documentation](https://app.ninjarmm.com/apidocs/?links.active=core#/webhooks/configureWebhook).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventName() { + return events.DEFAULT; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Resource: ${resource.name}`, + ts: Date.parse(resource.created_at), + }; + }, + }, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0537faf1705ad..2687d6a863fa2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8678,7 +8678,11 @@ importers: specifier: ^1.5.1 version: 1.6.6 - components/ninjaone: {} + components/ninjaone: + dependencies: + '@pipedream/platform': + specifier: 3.0.3 + version: 3.0.3 components/ninox: dependencies: @@ -34790,8 +34794,6 @@ snapshots: '@putout/operator-filesystem': 5.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3)) '@putout/operator-json': 2.2.0 putout: 36.13.1(eslint@8.57.1)(typescript@5.6.3) - transitivePeerDependencies: - - supports-color '@putout/operator-regexp@1.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3))': dependencies: