diff --git a/components/elevio/actions/create-article/create-article.mjs b/components/elevio/actions/create-article/create-article.mjs new file mode 100644 index 0000000000000..c2d48729094a7 --- /dev/null +++ b/components/elevio/actions/create-article/create-article.mjs @@ -0,0 +1,128 @@ +import app from "../../elevio.app.mjs"; + +export default { + key: "elevio-create-article", + name: "Create Article", + description: "Creates a new article in the Elevio knowledge base. [See the documentation](https://api-docs.elevio.help/en/articles/71-rest-api-articles).", + version: "0.0.1", + type: "action", + props: { + app, + categoryId: { + propDefinition: [ + app, + "categoryId", + ], + }, + restriction: { + propDefinition: [ + app, + "restriction", + ], + }, + discoverable: { + propDefinition: [ + app, + "discoverable", + ], + }, + isInternal: { + propDefinition: [ + app, + "isInternal", + ], + }, + notes: { + propDefinition: [ + app, + "notes", + ], + }, + status: { + propDefinition: [ + app, + "status", + ], + }, + title: { + propDefinition: [ + app, + "title", + ], + }, + body: { + propDefinition: [ + app, + "body", + ], + }, + keywords: { + propDefinition: [ + app, + "keywords", + ], + }, + tags: { + propDefinition: [ + app, + "tags", + ], + }, + externalId: { + propDefinition: [ + app, + "externalId", + ], + }, + }, + methods: { + createArticle(args = {}) { + return this.app.post({ + path: "/articles", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createArticle, + externalId, + restriction, + discoverable, + isInternal, + notes, + status, + title, + body, + keywords, + tags, + categoryId, + } = this; + const response = await createArticle({ + $, + data: { + article: { + external_id: externalId, + restriction, + discoverable, + is_internal: isInternal, + notes, + status, + category_id: categoryId, + keywords, + tags, + translations: [ + { + language_id: "en", + title, + body, + }, + ], + }, + }, + }); + + $.export("$summary", `Successfully created article with ID \`${response.article.id}\`.`); + return response; + }, +}; diff --git a/components/elevio/actions/delete-article/delete-article.mjs b/components/elevio/actions/delete-article/delete-article.mjs new file mode 100644 index 0000000000000..c1dddbe32577a --- /dev/null +++ b/components/elevio/actions/delete-article/delete-article.mjs @@ -0,0 +1,41 @@ +import app from "../../elevio.app.mjs"; + +export default { + key: "elevio-delete-article", + name: "Delete Article", + description: "Deletes an existing article from the Elevio knowledge base. [See the documentation](https://api-docs.elevio.help/en/articles/71-rest-api-articles).", + version: "0.0.1", + type: "action", + props: { + app, + articleId: { + propDefinition: [ + app, + "articleId", + ], + }, + }, + methods: { + deleteArticle({ + articleId, ...args + }) { + return this.app.delete({ + path: `/articles/${articleId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + deleteArticle, + articleId, + } = this; + + const response = await deleteArticle({ + $, + articleId, + }); + $.export("$summary", "Successfully deleted article."); + return response; + }, +}; diff --git a/components/elevio/actions/update-article/update-article.mjs b/components/elevio/actions/update-article/update-article.mjs new file mode 100644 index 0000000000000..1839f17903a37 --- /dev/null +++ b/components/elevio/actions/update-article/update-article.mjs @@ -0,0 +1,149 @@ +import app from "../../elevio.app.mjs"; + +export default { + key: "elevio-update-article", + name: "Update Article", + description: "Updates an existing article in the Elevio knowledge base. [See the documentation](https://api-docs.elevio.help/en/articles/71-rest-api-articles).", + version: "0.0.1", + type: "action", + props: { + app, + articleId: { + propDefinition: [ + app, + "articleId", + ], + }, + categoryId: { + propDefinition: [ + app, + "categoryId", + ], + }, + restriction: { + optional: true, + propDefinition: [ + app, + "restriction", + ], + }, + discoverable: { + optional: true, + propDefinition: [ + app, + "discoverable", + ], + }, + isInternal: { + optional: true, + propDefinition: [ + app, + "isInternal", + ], + }, + notes: { + optional: true, + propDefinition: [ + app, + "notes", + ], + }, + status: { + optional: true, + propDefinition: [ + app, + "status", + ], + }, + title: { + optional: true, + propDefinition: [ + app, + "title", + ], + }, + body: { + optional: true, + propDefinition: [ + app, + "body", + ], + }, + keywords: { + propDefinition: [ + app, + "keywords", + ], + }, + tags: { + propDefinition: [ + app, + "tags", + ], + }, + externalId: { + propDefinition: [ + app, + "externalId", + ], + }, + }, + methods: { + updateArticle({ + articleId, ...args + } = {}) { + return this.app.put({ + path: `/articles/${articleId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + updateArticle, + articleId, + externalId, + restriction, + discoverable, + isInternal, + notes, + status, + title, + body, + keywords, + tags, + categoryId, + } = this; + const response = await updateArticle({ + $, + articleId, + data: { + article: { + external_id: externalId, + restriction, + discoverable, + is_internal: isInternal, + notes, + status, + keywords, + tags, + category_id: categoryId, + ...((title || body) + ? { + translations: [ + { + language_id: "en", + title, + body, + }, + ], + } + : {}), + }, + }, + }); + + $.export("$summary", `Successfully updated article with ID \`${response.article.id}\`.`); + return response; + }, +}; diff --git a/components/elevio/common/constants.mjs b/components/elevio/common/constants.mjs new file mode 100644 index 0000000000000..ae8614c5a8fd5 --- /dev/null +++ b/components/elevio/common/constants.mjs @@ -0,0 +1,15 @@ +const BASE_URL = "https://api.elev.io"; +const VERSION_PATH = "/v1"; +const IS_FIRST_RUN = "isFirstRun"; +const LAST_DATE_AT = "lastDateAt"; +const DEFAULT_LIMIT = 100; +const DEFAULT_MAX = 600; + +export default { + BASE_URL, + VERSION_PATH, + IS_FIRST_RUN, + LAST_DATE_AT, + DEFAULT_LIMIT, + DEFAULT_MAX, +}; diff --git a/components/elevio/common/utils.mjs b/components/elevio/common/utils.mjs new file mode 100644 index 0000000000000..de7ee6c4a4692 --- /dev/null +++ b/components/elevio/common/utils.mjs @@ -0,0 +1,17 @@ +async function iterate(iterations) { + const items = []; + for await (const item of iterations) { + items.push(item); + } + return items; +} + +function getNestedProperty(obj, propertyString) { + const properties = propertyString.split("."); + return properties.reduce((prev, curr) => prev?.[curr], obj); +} + +export default { + iterate, + getNestedProperty, +}; diff --git a/components/elevio/elevio.app.mjs b/components/elevio/elevio.app.mjs index 896b2d48698b1..6dbde8783ac30 100644 --- a/components/elevio/elevio.app.mjs +++ b/components/elevio/elevio.app.mjs @@ -1,11 +1,225 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; +import utils from "./common/utils.mjs"; + export default { type: "app", app: "elevio", - propDefinitions: {}, + propDefinitions: { + articleId: { + type: "string", + label: "Article ID", + description: "The identifier of the article", + async options({ page }) { + const { articles } = await this.listArticles({ + params: { + page: page + 1, + }, + }); + return articles.map(({ + id: value, title: label, + }) => ({ + label, + value, + })); + }, + }, + restriction: { + type: "string", + label: "Restriction", + description: "Restriction level of the article", + default: "unrestricted", + options: [ + "restricted", + "unrestricted", + "noaccess", + ], + }, + discoverable: { + type: "boolean", + label: "Discoverable", + description: "Whether the article is discoverable", + default: true, + }, + isInternal: { + type: "boolean", + label: "Internal", + description: "Whether the article is internal", + default: false, + }, + notes: { + type: "string", + label: "Notes", + description: "Notes associated with the article", + }, + status: { + type: "string", + label: "Status", + description: "Status of the article", + default: "published", + options: [ + "draft", + "published", + ], + }, + title: { + type: "string", + label: "Title", + description: "The title of the article", + }, + body: { + type: "string", + label: "Content", + description: "The content body of the article", + }, + categoryId: { + type: "string", + label: "Category ID", + description: "The category ID for the article", + async options() { + const { categories } = await this.listCategories(); + return categories.map(({ + id: value, translations, + }) => ({ + label: translations[0].title, + value, + })); + }, + }, + externalId: { + type: "string", + label: "External ID", + description: "The external identifier of the article", + optional: true, + }, + keywords: { + type: "string[]", + label: "Keywords", + description: "List of global keywords for tagging and discoverability.", + optional: true, + }, + tags: { + type: "string[]", + label: "Tags", + description: "Tags associated with this article.", + optional: true, + }, + }, 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) { + const { + api_token: apiToken, + api_key: apiKey, + } = this.$auth; + return { + "Authorization": `Bearer ${apiToken}`, + "x-api-key": apiKey, + "Content-Type": "application/json", + ...headers, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + delete(args = {}) { + return this._makeRequest({ + method: "DELETE", + ...args, + }); + }, + put(args = {}) { + return this._makeRequest({ + method: "PUT", + ...args, + }); + }, + listCategories(args = {}) { + return this._makeRequest({ + path: "/categories", + ...args, + }); + }, + listArticles(args = {}) { + return this._makeRequest({ + path: "/articles", + ...args, + }); + }, + listCards({ + folder, ...args + } = {}) { + return this._makeRequest({ + path: `/cards/folders/${folder}`, + ...args, + }); + }, + async *getIterations({ + resourcesFn, resourcesFnArgs, resourceName, + lastDateAt, dateField, + max = constants.DEFAULT_MAX, + }) { + let page = 1; + let resourcesCount = 0; + + while (true) { + const response = + await resourcesFn({ + ...resourcesFnArgs, + params: { + ...resourcesFnArgs?.params, + page, + page_size: constants.DEFAULT_LIMIT, + }, + }); + + const nextResources = utils.getNestedProperty(response, resourceName); + + if (!nextResources?.length) { + console.log("No more resources found"); + return; + } + + for (const resource of nextResources) { + const isDateGreater = + lastDateAt + && Date.parse(resource[dateField]) > Date.parse(lastDateAt); + + if (!lastDateAt || isDateGreater) { + yield resource; + resourcesCount += 1; + } + + if (resourcesCount >= max) { + console.log("Reached max resources"); + return; + } + } + + if (nextResources.length < constants.DEFAULT_LIMIT) { + console.log("No next page found"); + return; + } + + page += 1; + } + }, + paginate(args = {}) { + return utils.iterate(this.getIterations(args)); }, }, }; diff --git a/components/elevio/package.json b/components/elevio/package.json index d180113a84684..9b8949c35ffcc 100644 --- a/components/elevio/package.json +++ b/components/elevio/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/elevio", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Elevio Components", "main": "elevio.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } -} \ No newline at end of file +} diff --git a/components/elevio/sources/common/polling.mjs b/components/elevio/sources/common/polling.mjs new file mode 100644 index 0000000000000..06208d5e3c029 --- /dev/null +++ b/components/elevio/sources/common/polling.mjs @@ -0,0 +1,130 @@ +import { + ConfigurationError, + DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import app from "../../elevio.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + app, + db: "$.service.db", + timer: { + type: "$.interface.timer", + label: "Polling Schedule", + description: "How often to poll the API", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + hooks: { + deploy() { + this.setIsFirstRun(true); + }, + }, + methods: { + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + setIsFirstRun(value) { + this.db.set(constants.IS_FIRST_RUN, value); + }, + getIsFirstRun() { + return this.db.get(constants.IS_FIRST_RUN); + }, + setLastDateAt(value) { + this.db.set(constants.LAST_DATE_AT, value); + }, + getLastDateAt() { + return this.db.get(constants.LAST_DATE_AT); + }, + getDateField() { + throw new ConfigurationError("getDateField is not implemented"); + }, + sortFn() { + return; + }, + isResourceRelevant() { + return true; + }, + getResourceName() { + throw new ConfigurationError("getResourceName is not implemented"); + }, + getResourcesFn() { + throw new ConfigurationError("getResourcesFn is not implemented"); + }, + getResourcesFnArgs() { + throw new ConfigurationError("getResourcesFnArgs is not implemented"); + }, + processResource(resource) { + const meta = this.generateMeta(resource); + this.$emit(resource, meta); + }, + async processResources(resources) { + const { + isResourceRelevant, + processResource, + } = this; + + return Array.from(resources) + .reverse() + .filter(isResourceRelevant) + .forEach(processResource); + }, + }, + async run() { + const { + app, + getDateField, + getLastDateAt, + getResourcesFn, + getResourcesFnArgs, + getResourceName, + processResources, + getIsFirstRun, + setIsFirstRun, + setLastDateAt, + sortFn, + } = this; + + const isFirstRun = getIsFirstRun(); + const dateField = getDateField(); + const lastDateAt = getLastDateAt(); + + const otherArgs = isFirstRun + ? { + max: constants.DEFAULT_LIMIT, + } + : { + dateField, + lastDateAt, + }; + + const resources = await app.paginate({ + resourcesFn: getResourcesFn(), + resourcesFnArgs: getResourcesFnArgs(), + resourceName: getResourceName(), + ...otherArgs, + }); + + const resourcesInDescendingOrder = + Array.from(resources) + .sort(sortFn); + + if (resourcesInDescendingOrder.length) { + const [ + firstResource, + ] = Array.from(resourcesInDescendingOrder); + if (firstResource) { + setLastDateAt(firstResource[dateField]); + } + } + + processResources(resourcesInDescendingOrder); + + if (isFirstRun) { + setIsFirstRun(false); + } + }, +}; diff --git a/components/elevio/sources/new-article-created/new-article-created.mjs b/components/elevio/sources/new-article-created/new-article-created.mjs new file mode 100644 index 0000000000000..be7878c105d7d --- /dev/null +++ b/components/elevio/sources/new-article-created/new-article-created.mjs @@ -0,0 +1,40 @@ +import common from "../common/polling.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "elevio-new-article-created", + name: "New Article Created", + description: "Emit new event any time a new article is created. [See the documentation](https://api-docs.elevio.help/en/articles/71-rest-api-articles).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + sortFn(a, b) { + return new Date(b.created_at) - new Date(a.created_at); + }, + getDateField() { + return "created_at"; + }, + getResourceName() { + return "articles"; + }, + getResourcesFn() { + return this.app.listArticles; + }, + getResourcesFnArgs() { + return { + debug: true, + }; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Article: ${resource.title}`, + ts: Date.parse(resource.created_at), + }; + }, + }, + sampleEmit, +}; diff --git a/components/elevio/sources/new-article-created/test-event.mjs b/components/elevio/sources/new-article-created/test-event.mjs new file mode 100644 index 0000000000000..39e3dcf542492 --- /dev/null +++ b/components/elevio/sources/new-article-created/test-event.mjs @@ -0,0 +1,56 @@ +export default { + "access": "public", + "access_domains": [], + "access_emails": [], + "article_groups": [ + "allizwell" + ], + "author": { + "email": "user@pipedream.com", + "gravatar": "https://www.gravatar.com/avatar/4acf7f734332df6f6a65789330874e23?e=user@pipedream.com&s=300&d=https%3A%2F%2Fui-avatars.com%2Fapi%2F/user%2Btest/300/69D59A/FFF/2/0.40", + "id": 70369, + "name": "user test" + }, + "category_groups": [ + "allizwell" + ], + "category_id": 2, + "created_at": "2025-02-06T21:23:20Z", + "creator": { + "email": "user@pipedream.com", + "gravatar": "https://www.gravatar.com/avatar/4acf7f734332df6f6a65789330874e23?e=user@pipedream.com&s=300&d=https%3A%2F%2Fui-avatars.com%2Fapi%2F/user%2Btest/300/69D59A/FFF/2/0.40", + "id": 70369, + "name": "user test" + }, + "discoverable": true, + "editor_version": "3", + "external_id": null, + "has_revision": false, + "id": 13, + "is_internal": false, + "keywords": [], + "last_published_at": "2025-02-06T21:23:42Z", + "notes": null, + "order": 999, + "pinned": false, + "restriction": "unrestricted", + "revision_status": null, + "slug": "test-6", + "smart_groups": [], + "source": "custom", + "status": "published", + "subcategory_groups": [ + "allizwell" + ], + "tags": [], + "title": "test 6", + "translations": [ + { + "id": 17, + "language_id": "en", + "title": "test 6", + "updated_at": "2025-02-06T21:23:42Z" + } + ], + "updated_at": "2025-02-06T21:23:42Z" +}; diff --git a/components/elevio/sources/new-feedback-created/new-feedback-created.mjs b/components/elevio/sources/new-feedback-created/new-feedback-created.mjs new file mode 100644 index 0000000000000..0cef86a01285a --- /dev/null +++ b/components/elevio/sources/new-feedback-created/new-feedback-created.mjs @@ -0,0 +1,57 @@ +import common from "../common/polling.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "elevio-new-feedback-created", + name: "New Feedback Created", + description: "Emit new event each time new feedback is submitted by a user via the elevio widget. [See the documentation](https://api-docs.elevio.help/en/articles/85-rest-api-hub-cards).", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + folder: { + type: "string", + label: "Folder", + description: "The folder to which the feedback belongs.", + options: [ + "inbox", + "completed", + "archive", + ], + }, + }, + methods: { + ...common.methods, + sortFn(a, b) { + return new Date(b.created_at) - new Date(a.created_at); + }, + isResourceRelevant(resource) { + return resource.type?.includes("feedback"); + }, + getDateField() { + return "created_at"; + }, + getResourceName() { + return "cards"; + }, + getResourcesFn() { + return this.app.listCards; + }, + getResourcesFnArgs() { + return { + debug: true, + folder: this.folder, + }; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Feedback: ${resource.title}`, + ts: Date.parse(resource.created_at), + }; + }, + }, + sampleEmit, +}; diff --git a/components/elevio/sources/new-feedback-created/test-event.mjs b/components/elevio/sources/new-feedback-created/test-event.mjs new file mode 100644 index 0000000000000..baa0cf89296d0 --- /dev/null +++ b/components/elevio/sources/new-feedback-created/test-event.mjs @@ -0,0 +1,36 @@ +export default { + "id": 1, + "type": "feedback_text", + "folder": "inbox", + "title": "Syncing articles", + "description": "This article is incomplete. Please fix.", + "relates_to_type": "article", + "relates_to_id": 42, + "custom_attributes": { "user_email": "roseanne@example.com" }, + "assignees": [ + { + "id": 1, + "email": "agent.amber@example.com", + "name": "Agent Amber", + "gravatar": "https://gravatar.com/avatar/abc123", + } + ], + "notes": "Add more details on different types of sync.", + "created_at": "2018-05-28T01:45:17.000000Z", + "updated_at": "2018-05-28T01:45:35.000000Z", + "last_updated_by": + { + "id": 1, + "email": "agent.amber@example.com", + "name": "Agent Amber", + "gravatar": "https://gravatar.com/avatar/abc123", + }, + "last_status_updated_by": + { + "id": 1, + "email": "agent.amber@example.com", + "name": "Agent Amber", + "gravatar": "https://gravatar.com/avatar/abc123", + }, + "last_status_update_at": "2018-05-28T01:45:35.000000Z" +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad98518188904..3d17fcbf5b12e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3267,7 +3267,11 @@ importers: specifier: ^4.0.0 version: 4.0.1 - components/elevio: {} + components/elevio: + dependencies: + '@pipedream/platform': + specifier: ^3.0.3 + version: 3.0.3 components/elmah_io: dependencies: @@ -32209,8 +32213,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: