diff --git a/components/heylibby/heylibby.app.mjs b/components/heylibby/heylibby.app.mjs index e7c464e6320c1..2578ea65fcea3 100644 --- a/components/heylibby/heylibby.app.mjs +++ b/components/heylibby/heylibby.app.mjs @@ -8,4 +8,4 @@ export default { console.log(Object.keys(this.$auth)); }, }, -}; \ No newline at end of file +}; diff --git a/components/neetocal/neetocal.app.mjs b/components/neetocal/neetocal.app.mjs index 56fada3732086..3feef768e101c 100644 --- a/components/neetocal/neetocal.app.mjs +++ b/components/neetocal/neetocal.app.mjs @@ -8,4 +8,4 @@ export default { console.log(Object.keys(this.$auth)); }, }, -}; \ No newline at end of file +}; diff --git a/components/neetodesk/neetodesk.app.mjs b/components/neetodesk/neetodesk.app.mjs index c899e4fccf7be..f8e250e4e2160 100644 --- a/components/neetodesk/neetodesk.app.mjs +++ b/components/neetodesk/neetodesk.app.mjs @@ -8,4 +8,4 @@ export default { console.log(Object.keys(this.$auth)); }, }, -}; \ No newline at end of file +}; 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..c26cdb4c419b2 --- /dev/null +++ b/components/ninjaone/actions/create-ticket/create-ticket.mjs @@ -0,0 +1,180 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { + PRIORITY_OPTIONS, + SEVERITY_OPTIONS, + TYPE_OPTIONS, +} from "../../common/constants.mjs"; +import { + normalCase, + parseObject, +} from "../../common/utils.mjs"; +import ninjaone from "../../ninjaone.app.mjs"; + +export default { + key: "ninjaone-create-ticket", + name: "Create Support Ticket", + description: "Creates 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: { + ninjaone, + clientId: { + propDefinition: [ + ninjaone, + "clientId", + ], + }, + ticketFormId: { + propDefinition: [ + ninjaone, + "ticketFormId", + ], + }, + organizationId: { + propDefinition: [ + ninjaone, + "organizationId", + ], + optional: true, + }, + locationId: { + propDefinition: [ + ninjaone, + "locationId", + ({ organizationId }) => ({ + organizationId, + }), + ], + optional: true, + }, + nodeId: { + propDefinition: [ + ninjaone, + "deviceId", + ({ organizationId }) => ({ + organizationId, + }), + ], + optional: true, + }, + subject: { + type: "string", + label: "Subject", + description: "The subject of the ticket", + }, + descriptionPublic: { + type: "boolean", + label: "Public Description", + description: "Whether the ticket's description is public or not", + }, + descriptionBody: { + type: "string", + label: "Description Body", + description: "The description of the ticket", + optional: true, + }, + descriptionHTML: { + type: "string", + label: "Description HTML", + description: "The description HTML of the ticket", + optional: true, + }, + descriptiontimeTracked: { + type: "integer", + label: "Time Tracked", + description: "Time in seconds", + optional: true, + }, + descriptionDuplicateInIncidents: { + type: "boolean", + label: "Duplicate In Incidents", + description: "Whether the ticket will duplicate in the same incident", + optional: true, + }, + status: { + propDefinition: [ + ninjaone, + "status", + ], + }, + type: { + type: "string", + label: "Type", + description: "The type of the ticket", + options: TYPE_OPTIONS, + optional: true, + }, + cc: { + type: "string[]", + label: "CC", + description: "A list of emails to be copied in the notification email", + optional: true, + }, + assignedAppUserId: { + propDefinition: [ + ninjaone, + "assignedAppUserId", + ], + optional: true, + }, + severity: { + type: "string", + label: "Severity", + description: "The severity's level of the ticket", + options: SEVERITY_OPTIONS, + optional: true, + }, + priority: { + type: "string", + label: "Priority", + description: "The priority's level of the ticket", + options: PRIORITY_OPTIONS, + optional: true, + }, + tags: { + propDefinition: [ + ninjaone, + "tags", + ], + optional: true, + }, + }, + async run({ $ }) { + try { + const { + ninjaone, + descriptionPublic, + descriptionBody, + descriptionHTML, + descriptiontimeTracked, + descriptionDuplicateInIncidents, + cc, + tags, + ...data + } = this; + + const response = await ninjaone.createSupportTicket({ + $, + data: { + ...data, + description: { + public: descriptionPublic, + body: descriptionBody, + htmlBody: descriptionHTML, + timeTracked: descriptiontimeTracked, + duplicateInIncidents: descriptionDuplicateInIncidents, + }, + cc: { + emails: parseObject(cc), + }, + tags: parseObject(tags), + }, + }); + + $.export("$summary", `Ticket created successfully with ID: ${response.id}`); + return response; + } catch ({ response }) { + throw new ConfigurationError(normalCase(response.data.resultCode) || response.data); + } + }, +}; diff --git a/components/ninjaone/actions/update-device/update-device.mjs b/components/ninjaone/actions/update-device/update-device.mjs new file mode 100644 index 0000000000000..f9e242e64cf11 --- /dev/null +++ b/components/ninjaone/actions/update-device/update-device.mjs @@ -0,0 +1,72 @@ +import ninjaone from "../../ninjaone.app.mjs"; + +export default { + key: "ninjaone-update-device", + name: "Update Device", + description: "Update details for a specific device in NinjaOne. [See the documentation](https://app.ninjarmm.com/apidocs/?links.active=core)", + version: "0.0.1", + type: "action", + props: { + ninjaone, + deviceId: { + propDefinition: [ + ninjaone, + "deviceId", + ], + description: "The ID of the device to update ", + }, + displayName: { + type: "string", + label: "Display Name", + description: "The name of the device", + optional: true, + }, + nodeRoleId: { + propDefinition: [ + ninjaone, + "nodeRoleId", + ], + optional: true, + }, + policyId: { + propDefinition: [ + ninjaone, + "policyId", + ], + optional: true, + }, + organizationId: { + propDefinition: [ + ninjaone, + "organizationId", + ], + optional: true, + }, + locationId: { + propDefinition: [ + ninjaone, + "locationId", + ({ organizationId }) => ({ + organizationId, + }), + ], + optional: true, + }, + }, + async run({ $ }) { + const { + ninjaone, + deviceId, + ...data + } = this; + + const response = await ninjaone.updateDevice({ + $, + deviceId, + data, + }); + + $.export("$summary", `Successfully updated device with ID ${deviceId}`); + return response; + }, +}; diff --git a/components/ninjaone/common/constants.mjs b/components/ninjaone/common/constants.mjs new file mode 100644 index 0000000000000..3f2ba884706a7 --- /dev/null +++ b/components/ninjaone/common/constants.mjs @@ -0,0 +1,23 @@ +export const LIMIT = 100; + +export const TYPE_OPTIONS = [ + "PROBLEM", + "QUESTION", + "INCIDENT", + "TASK", +]; + +export const SEVERITY_OPTIONS = [ + "NONE", + "MINOR", + "MODERATE", + "MAJOR", + "CRITICAL", +]; + +export const PRIORITY_OPTIONS = [ + "NONE", + "LOW", + "MEDIUM", + "HIGH", +]; diff --git a/components/ninjaone/common/utils.mjs b/components/ninjaone/common/utils.mjs new file mode 100644 index 0000000000000..59f5a843b38ac --- /dev/null +++ b/components/ninjaone/common/utils.mjs @@ -0,0 +1,28 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; + +export const normalCase = (s) => + s?.replace (/^[-_]*(.)/, (_, c) => c.toUpperCase()) + .replace (/[-_]+(.)/g, (_, c) => " " + c); diff --git a/components/ninjaone/ninjaone.app.mjs b/components/ninjaone/ninjaone.app.mjs index 0945dafedfbfa..7a66f9eccf78f 100644 --- a/components/ninjaone/ninjaone.app.mjs +++ b/components/ninjaone/ninjaone.app.mjs @@ -1,11 +1,338 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + export default { type: "app", app: "ninjaone", - propDefinitions: {}, + propDefinitions: { + clientId: { + type: "string", + label: "Client ID", + description: "The ID of the client related to the ticket", + async options({ prevContext }) { + const data = await this.listOrganizations({ + params: { + pageSize: LIMIT, + after: prevContext.lastId, + }, + }); + + return { + options: data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })), + context: { + lastId: data[data.length - 1]?.id, + }, + }; + }, + }, + ticketFormId: { + type: "string", + label: "Ticket Form ID", + description: "The ID of the ticket form to the ticket", + async options() { + const data = await this.listTicketForms(); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + locationId: { + type: "string", + label: "Location ID", + description: "The ID of the location to the ticket", + async options({ + prevContext, organizationId, + }) { + const data = await this.listLocations({ + organizationId, + params: { + pageSize: LIMIT, + after: prevContext.lastId, + }, + }); + + return { + options: data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })), + context: { + lastId: data[data.length - 1]?.id, + }, + }; + }, + }, + deviceId: { + type: "string", + label: "Device ID", + description: "The ID of the device to the ticket", + async options({ + prevContext, organizationId, + }) { + const data = await this.listDevices({ + organizationId, + params: { + pageSize: LIMIT, + after: prevContext.lastId, + }, + }); + + return { + options: data.map(({ + id: value, systemName: label, + }) => ({ + label, + value, + })), + context: { + lastId: data[data.length - 1]?.id, + }, + }; + }, + }, + tags: { + type: "string[]", + label: "Tags", + description: "A list of tags related to ticket.", + async options({ prevContext }) { + const { tags } = await this.listTags({ + params: { + pageSize: LIMIT, + after: prevContext.lastId, + }, + }); + + return tags.map(({ name }) => name); + }, + }, + status: { + type: "string", + label: "Status", + description: "The status of the ticket", + async options() { + const data = await this.listStatuses(); + + return data.map(({ + statusId: value, displayName: label, + }) => ({ + label, + value, + })); + }, + }, + assignedAppUserId: { + type: "string", + label: "Assigned App User ID", + description: "User ID that will be assigned to the ticket", + async options() { + const data = await this.listUsers({ + params: { + userType: "TECHNICIAN", + }, + }); + + return data.map(({ + id: value, firstName, lastName, email, + }) => ({ + label: `${firstName} ${lastName} - ${email}`, + value, + })); + }, + }, + nodeRoleId: { + type: "string", + label: "Node Role Id", + description: "The ID of the device role", + async options() { + const data = await this.listRoles(); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + policyId: { + type: "string", + label: "Policy ID", + description: "The ID of the policy override", + async options() { + const data = await this.listPolicies(); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + organizationId: { + type: "string", + label: "Organization ID", + description: "The ID of the organization", + async options({ prevContext }) { + const data = await this.listOrganizations({ + params: { + pageSize: LIMIT, + after: prevContext.lastId, + }, + }); + + return { + options: data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })), + context: { + lastId: data[data.length - 1]?.id, + }, + }; + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.ninjaone.com/v2"; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + createSupportTicket(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/ticketing/ticket", + ...opts, + }); + }, + listOrganizations(opts = {}) { + return this._makeRequest({ + path: "/organizations", + ...opts, + }); + }, + listTicketForms(opts = {}) { + return this._makeRequest({ + path: "/ticketing/ticket-form", + ...opts, + }); + }, + listLocations({ + organizationId, ...opts + }) { + return this._makeRequest({ + path: `/organization/${organizationId}/locations`, + ...opts, + }); + }, + listDevices({ + organizationId, ...opts + }) { + return this._makeRequest({ + path: `/organization/${organizationId}/devices`, + ...opts, + }); + }, + listTags(opts = {}) { + return this._makeRequest({ + path: "/tag", + ...opts, + }); + }, + listStatuses(opts = {}) { + return this._makeRequest({ + path: "/ticketing/statuses", + ...opts, + }); + }, + listUsers(opts = {}) { + return this._makeRequest({ + path: "/users", + ...opts, + }); + }, + listRoles(opts = {}) { + return this._makeRequest({ + path: "/roles", + ...opts, + }); + }, + listPolicies(opts = {}) { + return this._makeRequest({ + path: "/policies", + ...opts, + }); + }, + listActivities(opts = {}) { + return this._makeRequest({ + path: "/activities", + ...opts, + }); + }, + updateDevice({ + deviceId, ...opts + }) { + return this._makeRequest({ + method: "PATCH", + path: `/device/${deviceId}`, + ...opts, + }); + }, + async *paginate({ + fn, params = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let lastId = 0; + const newerThan = params.newerThan; + + do { + if (lastId) { + params.olderThan = lastId; + if (params.newerThan) delete params.newerThan; + } + + const { activities } = await fn({ + params, + ...opts, + }); + for (const d of activities) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = activities.length && (lastId && (lastId > newerThan)); + + } while (hasMore); }, }, }; diff --git a/components/ninjaone/package.json b/components/ninjaone/package.json index 968834403f082..8115cd7a84d3d 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/base.mjs b/components/ninjaone/sources/common/base.mjs new file mode 100644 index 0000000000000..1a7ee2318dc4a --- /dev/null +++ b/components/ninjaone/sources/common/base.mjs @@ -0,0 +1,64 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import ninjaone from "../../ninjaone.app.mjs"; + +export default { + props: { + ninjaone, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastId() { + return this.db.get("lastId") || 0; + }, + _setLastId(lastId) { + this.db.set("lastId", lastId); + }, + async emitEvent(maxResults = false) { + const lastId = this._getLastId(); + + const response = this.ninjaone.paginate({ + fn: this.ninjaone.listActivities, + params: { + newerThan: lastId, + ...this.getParams(), + }, + }); + + let responseArray = []; + for await (const item of response) { + responseArray.push(item); + } + + responseArray = this.filterEvents(responseArray); + + if (responseArray.length) { + if (maxResults && (responseArray.length > maxResults)) { + responseArray.length = maxResults; + } + this._setLastId(responseArray[0].id); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item.id, + summary: this.getSummary(item), + ts: Date.parse(item.activityTime || new Date()), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/ninjaone/sources/new-device-online/new-device-online.mjs b/components/ninjaone/sources/new-device-online/new-device-online.mjs new file mode 100644 index 0000000000000..4c0f321b3a014 --- /dev/null +++ b/components/ninjaone/sources/new-device-online/new-device-online.mjs @@ -0,0 +1,27 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "ninjaone-new-device-online", + name: "New Device Online", + description: "Emit new event when a monitored device comes online.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getParams() { + return { + class: "DEVICE", + }; + }, + filterEvents(events) { + return events.filter((item) => item.statusCode === "SYSTEM_REBOOTED"); + }, + getSummary(item) { + return `Device online: ${item.deviceId}`; + }, + }, + sampleEmit, +}; diff --git a/components/ninjaone/sources/new-device-online/test-event.mjs b/components/ninjaone/sources/new-device-online/test-event.mjs new file mode 100644 index 0000000000000..a2e807d01d927 --- /dev/null +++ b/components/ninjaone/sources/new-device-online/test-event.mjs @@ -0,0 +1,18 @@ +export default { + "id": 26, + "activityTime": 1747946704, + "deviceId": 1, + "activityType": "MONITOR", + "statusCode": "SYSTEM_REBOOTED", + "status": "Rebooted", + "message": "System rebooted at '2025-05-22T20:43:26Z'", + "type": "Monitor", + "data": { + "message": { + "code": "agent_act_sys_reboot", + "params": { + "reboot_date": "2025-05-22T20:43:26Z" + } + } + } +} \ No newline at end of file diff --git a/components/scrapecreators/scrapecreators.app.mjs b/components/scrapecreators/scrapecreators.app.mjs index 6a78a552c873c..b4ae4ca84cff8 100644 --- a/components/scrapecreators/scrapecreators.app.mjs +++ b/components/scrapecreators/scrapecreators.app.mjs @@ -8,4 +8,4 @@ export default { console.log(Object.keys(this.$auth)); }, }, -}; \ No newline at end of file +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc3a4eb1cfec6..9cfc2b68be450 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6045,8 +6045,7 @@ importers: specifier: ^1.5.1 version: 1.6.6 - components/heylibby: - specifiers: {} + components/heylibby: {} components/heyreach: {} @@ -8573,11 +8572,9 @@ importers: specifier: ^3.0.3 version: 3.0.3 - components/neetocal: - specifiers: {} + components/neetocal: {} - components/neetodesk: - specifiers: {} + components/neetodesk: {} components/neetoform: {} @@ -8774,7 +8771,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: @@ -15481,14 +15482,6 @@ importers: specifier: ^6.0.0 version: 6.2.0 - modelcontextprotocol/node_modules2/@modelcontextprotocol/sdk/dist/cjs: {} - - modelcontextprotocol/node_modules2/@modelcontextprotocol/sdk/dist/esm: {} - - modelcontextprotocol/node_modules2/zod-to-json-schema/dist/cjs: {} - - modelcontextprotocol/node_modules2/zod-to-json-schema/dist/esm: {} - packages/ai: dependencies: '@pipedream/sdk':