diff --git a/components/chattermill/actions/create-response/create-response.mjs b/components/chattermill/actions/create-response/create-response.mjs new file mode 100644 index 0000000000000..d2c5d904f470c --- /dev/null +++ b/components/chattermill/actions/create-response/create-response.mjs @@ -0,0 +1,83 @@ +import chattermill from "../../chattermill.app.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "chattermill-create-response", + name: "Create Response", + description: "Create response model with given attributes. [See the documentation](https://apidocs.chattermill.com/#70001058-ac53-eec1-7c44-c836fb0b2489)", + version: "0.0.1", + type: "action", + props: { + chattermill, + projectId: { + propDefinition: [ + chattermill, + "projectId", + ], + }, + score: { + type: "integer", + label: "Score", + description: "A score of 1 - 10 to add to the response", + min: 1, + max: 10, + }, + comment: { + type: "string", + label: "Comment", + description: "The comment to add to the response", + }, + userMeta: { + propDefinition: [ + chattermill, + "userMeta", + ], + }, + segments: { + propDefinition: [ + chattermill, + "segments", + ], + }, + dataType: { + propDefinition: [ + chattermill, + "dataType", + (c) => ({ + projectId: c.projectId, + }), + ], + }, + dataSource: { + propDefinition: [ + chattermill, + "dataSource", + (c) => ({ + projectId: c.projectId, + }), + ], + }, + }, + async run({ $ }) { + try { + const response = await this.chattermill.createResponse({ + $, + projectId: this.projectId, + data: { + response: { + score: this.score, + comment: this.comment, + user_meta: parseObject(this.userMeta), + segments: parseObject(this.segments), + data_type: this.dataType, + data_source: this.dataSource, + }, + }, + }); + $.export("$summary", "Successfully created response."); + return response; + } catch { + throw new Error("Failed to create response"); + } + }, +}; diff --git a/components/chattermill/actions/get-response/get-response.mjs b/components/chattermill/actions/get-response/get-response.mjs new file mode 100644 index 0000000000000..a3bc1e4fada72 --- /dev/null +++ b/components/chattermill/actions/get-response/get-response.mjs @@ -0,0 +1,36 @@ +import chattermill from "../../chattermill.app.mjs"; + +export default { + key: "chattermill-get-response", + name: "Get Response", + description: "Get a response by ID. [See the documentation](https://apidocs.chattermill.com/#ace8b4a6-4e39-a1d2-e443-2ed1f10cd589)", + version: "0.0.1", + type: "action", + props: { + chattermill, + projectId: { + propDefinition: [ + chattermill, + "projectId", + ], + }, + responseId: { + propDefinition: [ + chattermill, + "responseId", + (c) => ({ + projectId: c.projectId, + }), + ], + }, + }, + async run({ $ }) { + const response = await this.chattermill.getResponse({ + $, + projectId: this.projectId, + responseId: this.responseId, + }); + $.export("$summary", `Successfully retrieved response with ID: ${this.responseId}`); + return response; + }, +}; diff --git a/components/chattermill/actions/search-responses/search-responses.mjs b/components/chattermill/actions/search-responses/search-responses.mjs new file mode 100644 index 0000000000000..a4250f32b5bd2 --- /dev/null +++ b/components/chattermill/actions/search-responses/search-responses.mjs @@ -0,0 +1,61 @@ +import chattermill from "../../chattermill.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "chattermill-search-responses", + name: "Search Responses", + description: "Search for responses. [See the documentation](https://apidocs.chattermill.com/#3dd30375-7956-b872-edbd-873eef126b2d)", + version: "0.0.1", + type: "action", + props: { + chattermill, + projectId: { + propDefinition: [ + chattermill, + "projectId", + ], + }, + filterProperty: { + type: "string", + label: "Filter Property", + description: "Segment property to filter by", + optional: true, + }, + filterValue: { + type: "string", + label: "Filter Value", + description: "Segment value to filter by", + optional: true, + }, + maxResults: { + type: "integer", + label: "Max Results", + description: "The maximum number of results to return", + default: 100, + optional: true, + }, + }, + async run({ $ }) { + if ((this.filterProperty && !this.filterValue) + || (!this.filterProperty && this.filterValue)) { + throw new ConfigurationError("Filter Property and Value must be provided together"); + } + + const responses = await this.chattermill.getPaginatedResources({ + fn: this.chattermill.listResponses, + args: { + $, + projectId: this.projectId, + params: { + filter_property: this.filterProperty, + filter_value: this.filterValue, + }, + }, + resourceKey: "responses", + max: this.maxResults, + }); + + $.export("$summary", `Found ${responses.length} responses.`); + return responses; + }, +}; diff --git a/components/chattermill/actions/update-response/update-response.mjs b/components/chattermill/actions/update-response/update-response.mjs new file mode 100644 index 0000000000000..9d2a033ae3e18 --- /dev/null +++ b/components/chattermill/actions/update-response/update-response.mjs @@ -0,0 +1,57 @@ +import chattermill from "../../chattermill.app.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "chattermill-update-response", + name: "Update Response", + description: "Update a response by ID. [See the documentation](https://apidocs.chattermill.com/#a632c60d-ccda-74b3-b9e7-b5a0c4917e1a)", + version: "0.0.1", + type: "action", + props: { + chattermill, + projectId: { + propDefinition: [ + chattermill, + "projectId", + ], + }, + responseId: { + propDefinition: [ + chattermill, + "responseId", + (c) => ({ + projectId: c.projectId, + }), + ], + }, + userMeta: { + propDefinition: [ + chattermill, + "userMeta", + ], + optional: true, + }, + segments: { + propDefinition: [ + chattermill, + "segments", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.chattermill.updateResponse({ + $, + projectId: this.projectId, + responseId: this.responseId, + data: { + response: { + user_meta: parseObject(this.userMeta), + segments: parseObject(this.segments), + }, + }, + }); + $.export("$summary", `Successfully updated response with ID: ${this.responseId}`); + return response; + }, +}; diff --git a/components/chattermill/chattermill.app.mjs b/components/chattermill/chattermill.app.mjs index 098c30fbce1ff..a3aa942d8422d 100644 --- a/components/chattermill/chattermill.app.mjs +++ b/components/chattermill/chattermill.app.mjs @@ -1,11 +1,181 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "chattermill", - propDefinitions: {}, + propDefinitions: { + projectId: { + type: "string", + label: "Project ID", + description: "The ID of a project", + async options() { + const { projects } = await this.listProjects(); + return projects.map((project) => ({ + label: project.name, + value: project.id, + })); + }, + }, + responseId: { + type: "string", + label: "Response ID", + description: "The ID of a response", + async options({ + projectId, page, + }) { + const { responses } = await this.listResponses({ + projectId, + params: { + page: page + 1, + }, + }); + return responses?.map((response) => ({ + label: response.comment || response.id, + value: response.id, + })) || []; + }, + }, + dataType: { + type: "string", + label: "Data Type", + description: "The type of data to add to the response. Note: Not all combinations of data type and data source are valid.", + default: "Comment", + async options({ projectId }) { + const { data_types: types } = await this.listDataTypes({ + projectId, + }); + return types.map((type) => type.name); + }, + }, + dataSource: { + type: "string", + label: "Data Source", + description: "The source of the data to add to the response. Note: Not all combinations of data type and data source are valid.", + default: "CSV Upload", + async options({ projectId }) { + const { data_sources: sources } = await this.listDataSources({ + projectId, + }); + return sources.map((source) => source.name); + }, + }, + userMeta: { + type: "object", + label: "User Meta", + description: "The user meta to add to the response. Example: `{ \"customer_id\": { \"type\": \"text\", \"value\": \"1234\", \"name\": \"Customer ID\" } }`", + }, + segments: { + type: "object", + label: "Segments", + description: "The segments to add to the response. Example: `{ \"customer_type\": { \"type\": \"text\", \"value\": \"New\", \"name\": \"Customer Type\" } }`", + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.chattermill.com/v1"; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `Bearer ${this.$auth.api_key}`, + }, + ...opts, + }); + }, + getResponse({ + projectId, responseId, ...opts + }) { + return this._makeRequest({ + path: `/${projectId}/responses/${responseId}`, + ...opts, + }); + }, + listProjects(opts = {}) { + return this._makeRequest({ + path: "/projects", + ...opts, + }); + }, + listResponses({ + projectId, ...opts + }) { + return this._makeRequest({ + path: `/${projectId}/responses`, + ...opts, + }); + }, + listDataTypes({ + projectId, ...opts + }) { + return this._makeRequest({ + path: `/${projectId}/data_types`, + ...opts, + }); + }, + listDataSources({ + projectId, ...opts + }) { + return this._makeRequest({ + path: `/${projectId}/data_sources`, + ...opts, + }); + }, + createResponse({ + projectId, ...opts + }) { + return this._makeRequest({ + path: `/${projectId}/responses`, + method: "POST", + ...opts, + }); + }, + updateResponse({ + projectId, responseId, ...opts + }) { + return this._makeRequest({ + path: `/${projectId}/responses/${responseId}`, + method: "PUT", + ...opts, + }); + }, + async *paginate({ + fn, args, resourceKey, max, + }) { + const limit = 100; + args = { + ...args, + params: { + ...args.params, + per_page: limit, + page: 1, + }, + }; + let total, count = 0; + do { + const response = await fn(args); + const items = response[resourceKey]; + if (!items?.length) { + return; + } + for (const item of items) { + yield item; + if (max && ++count >= max) { + return; + } + } + total = items.length; + args.params.page++; + } while (total === limit); + }, + async getPaginatedResources(opts) { + const resources = []; + for await (const resource of this.paginate(opts)) { + resources.push(resource); + } + return resources; }, }, }; diff --git a/components/chattermill/common/utils.mjs b/components/chattermill/common/utils.mjs new file mode 100644 index 0000000000000..c2b75bdc360c0 --- /dev/null +++ b/components/chattermill/common/utils.mjs @@ -0,0 +1,25 @@ +export const parseObject = (obj) => { + if (!obj) { + return {}; + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch { + return obj; + } + } + if (Array.isArray(obj)) { + return obj.map(parseObject); + } + if (typeof obj === "object") { + return Object.fromEntries(Object.entries(obj).map(([ + key, + value, + ]) => [ + key, + parseObject(value), + ])); + } + return obj; +}; diff --git a/components/chattermill/package.json b/components/chattermill/package.json index 081e4745e9b6f..24dd2e1797681 100644 --- a/components/chattermill/package.json +++ b/components/chattermill/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/chattermill", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Chattermill Components", "main": "chattermill.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.1.0" } -} \ No newline at end of file +} diff --git a/components/chattermill/sources/new-response-created/new-response-created.mjs b/components/chattermill/sources/new-response-created/new-response-created.mjs new file mode 100644 index 0000000000000..039863e0d1cb9 --- /dev/null +++ b/components/chattermill/sources/new-response-created/new-response-created.mjs @@ -0,0 +1,90 @@ +import chattermill from "../../chattermill.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "chattermill-new-response-created", + name: "New Response Created", + description: "Emit new event when a new response is created. [See the documentation](https://apidocs.chattermill.com/#3dd30375-7956-b872-edbd-873eef126b2d)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + chattermill, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + projectId: { + propDefinition: [ + chattermill, + "projectId", + ], + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs"); + }, + _setLastTs(ts) { + this.db.set("lastTs", ts); + }, + generateMeta(response) { + return { + id: response.id, + summary: `New response created: ${response.id}`, + ts: Date.parse(response.created_at), + }; + }, + async processEvent(max) { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + + const results = this.chattermill.paginate({ + fn: this.chattermill.listResponses, + args: { + projectId: this.projectId, + params: { + from: lastTs, + }, + }, + resourceKey: "responses", + }); + + let responses = []; + for await (const response of results) { + if (!maxTs || Date.parse(response.created_at) > Date.parse(maxTs)) { + maxTs = response.created_at; + } + responses.push(response); + } + + if (!responses.length) { + return; + } + + this._setLastTs(maxTs); + + if (max && responses.length > max) { + responses = responses.slice(0, max); + } + + responses.forEach((response) => { + const meta = this.generateMeta(response); + this.$emit(response, meta); + }); + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + async run() { + await this.processEvent(); + }, + sampleEmit, +}; diff --git a/components/chattermill/sources/new-response-created/test-event.mjs b/components/chattermill/sources/new-response-created/test-event.mjs new file mode 100644 index 0000000000000..c08bd5f8771b7 --- /dev/null +++ b/components/chattermill/sources/new-response-created/test-event.mjs @@ -0,0 +1,27 @@ +export default { + "id": 1130766113, + "score": 10, + "comment": "sample comment", + "original_comment": "sample comment", + "created_at": "2025-07-15T20:00:14.712Z", + "updated_at": "2025-07-15T20:00:22.679Z", + "user_attributes": { + "language": { + "name": "Language", + "value": "English" + }, + "customer_id": { + "name": "Customer ID", + "value": "1234" + }, + "customer_type": { + "name": "Customer Type", + "value": "New" + } + }, + "data_type": "NPS", + "data_source": "Survey", + "dataset": "NPS - Survey", + "tags": [], + "phrases": [] +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5097d77eb36e3..62fa2c3b29588 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1872,8 +1872,7 @@ importers: specifier: ^3.0.0 version: 3.0.3 - components/bright_data: - specifiers: {} + components/bright_data: {} components/brilliant_directories: dependencies: @@ -2402,7 +2401,11 @@ importers: components/chatsonic: {} - components/chattermill: {} + components/chattermill: + dependencies: + '@pipedream/platform': + specifier: ^3.1.0 + version: 3.1.0 components/chatwork: dependencies: @@ -2646,14 +2649,11 @@ importers: specifier: ^1.0.1 version: 1.0.1 - components/clio_australia: - specifiers: {} + components/clio_australia: {} - components/clio_canada: - specifiers: {} + components/clio_canada: {} - components/clio_eu: - specifiers: {} + components/clio_eu: {} components/clockify: dependencies: @@ -11470,8 +11470,7 @@ importers: specifier: ^1.5.1 version: 1.6.6 - components/rhombus: - specifiers: {} + components/rhombus: {} components/richpanel: dependencies: