From 2de1b1387f13c60abc2fa3cf4c47663cc43246a4 Mon Sep 17 00:00:00 2001 From: Job Nijenhuis Date: Fri, 22 Aug 2025 17:48:05 +0200 Subject: [PATCH 01/24] trustpilot fixes --- .../fetch-product-reviews.mjs | 16 ++++ components/trustpilot/common/constants.mjs | 2 +- components/trustpilot/trustpilot.app.mjs | 80 ++++++++++++++----- pnpm-lock.yaml | 14 ++-- 4 files changed, 88 insertions(+), 24 deletions(-) diff --git a/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs b/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs index 5a19583331584..0de07eb6d1a90 100644 --- a/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs +++ b/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs @@ -14,6 +14,18 @@ export default { "businessUnitId", ], }, + sku: { + propDefinition: [ + trustpilot, + "sku", + ], + }, + productUrl: { + propDefinition: [ + trustpilot, + "productUrl", + ], + }, stars: { propDefinition: [ trustpilot, @@ -62,6 +74,8 @@ export default { async run({ $ }) { const { businessUnitId, + sku, + productUrl, stars, sortBy, limit, @@ -74,6 +88,8 @@ export default { try { const result = await this.trustpilot.getProductReviews({ businessUnitId, + sku, + productUrl, stars, sortBy, limit, diff --git a/components/trustpilot/common/constants.mjs b/components/trustpilot/common/constants.mjs index d993e6bb5b0b9..aa18cbeb8585e 100644 --- a/components/trustpilot/common/constants.mjs +++ b/components/trustpilot/common/constants.mjs @@ -11,7 +11,7 @@ export const WEBHOOK_EVENTS = { export const ENDPOINTS = { // Business Units - BUSINESS_UNITS: "/business-units", + BUSINESS_UNITS: "/business-units/search", BUSINESS_UNIT_BY_ID: "/business-units/{businessUnitId}", // Public Reviews diff --git a/components/trustpilot/trustpilot.app.mjs b/components/trustpilot/trustpilot.app.mjs index b777ae3a68851..c1031f046a96c 100644 --- a/components/trustpilot/trustpilot.app.mjs +++ b/components/trustpilot/trustpilot.app.mjs @@ -31,11 +31,11 @@ export default defineApp({ type: "string", label: "Business Unit ID", description: "The unique identifier for your business unit on Trustpilot", - async options() { + async options(page, prevContext, query) { try { const businessUnits = await this.searchBusinessUnits({ - query: "", - limit: 20, + query, + page, }); return businessUnits.map(({ id, displayName, name: { identifying }, @@ -54,6 +54,18 @@ export default defineApp({ label: "Review ID", description: "The unique identifier for a review", }, + sku: { + type: "string", + label: "SKU", + description: "Filter by SKU", + optional: true, + }, + productUrl: { + type: "string", + label: "Product URL", + description: "Filter by product URL", + optional: true, + }, stars: { type: "integer", label: "Star Rating", @@ -106,25 +118,50 @@ export default defineApp({ }, methods: { // Authentication and base request methods - _getAuthHeaders() { + _isPrivateURL(url) { + return url.includes("private"); + }, + + _getAuthHeadersForPrivateURL() { + if (!this.$auth?.oauth_access_token) { + throw new Error("Authentication required: OAuth token is required for private requests"); + } else { + return { + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + }; + } + }, + + _getAuthHeadersForPublicURL() { + if (!this.$auth?.api_key) { + throw new Error("Authentication required: API key is required for public requests"); + } else { + return { + "apikey": this.$auth.api_key, + }; + } + }, + + _getAuthHeaders(url) { const headers = { "Content-Type": "application/json", "User-Agent": "Pipedream/1.0", }; - if (!this.$auth?.api_key && !this.$auth?.oauth_access_token) { - throw new Error("Authentication required: Configure either API key or OAuth token"); - } - - if (this.$auth?.api_key) { - headers["apikey"] = this.$auth.api_key; - } + const isPrivate = this._isPrivateURL(url); + console.log("isPrivate", isPrivate); - if (this.$auth?.oauth_access_token) { - headers["Authorization"] = `Bearer ${this.$auth.oauth_access_token}`; + if (isPrivate) { + return { + ...headers, + ...this._getAuthHeadersForPrivateURL(), + }; + } else { + return { + ...headers, + ...this._getAuthHeadersForPublicURL(), + }; } - - return headers; }, async _makeRequest({ @@ -182,14 +219,13 @@ export default defineApp({ }, async searchBusinessUnits({ - query = "", limit = DEFAULT_LIMIT, offset = 0, + query = "", page = 1, } = {}) { const response = await this._makeRequest({ endpoint: ENDPOINTS.BUSINESS_UNITS, params: { query, - limit, - offset, + page, }, }); @@ -265,6 +301,8 @@ export default defineApp({ async _getReviews({ endpoint, businessUnitId, + sku = null, + productUrl = null, stars = null, sortBy = SORT_OPTIONS.CREATED_AT_DESC, limit = DEFAULT_LIMIT, @@ -277,7 +315,13 @@ export default defineApp({ throw new Error("Invalid business unit ID"); } + if (sku === null && productUrl === null) { + throw new Error("Either SKU or product URL is required"); + } + const params = { + sku, + productUrl, stars, orderBy: sortBy, perPage: limit, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 53220ec198c45..d48ba839f33bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8809,8 +8809,7 @@ importers: components/mindbody: {} - components/mindee: - specifiers: {} + components/mindee: {} components/mindmeister: {} @@ -11030,7 +11029,11 @@ importers: specifier: ^3.1.0 version: 3.1.0 - components/prisma_management_api: {} + components/prisma_management_api: + dependencies: + '@pipedream/platform': + specifier: ^1.5.1 + version: 1.6.6 components/prismic: {} @@ -16028,8 +16031,7 @@ importers: specifier: ^4.0.0 version: 4.0.1 - components/zapr_link: - specifiers: {} + components/zapr_link: {} components/zendesk: dependencies: @@ -40402,6 +40404,8 @@ 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: From 0a1b390b4ffdb520632b0ebbadd9fffaf033aadb Mon Sep 17 00:00:00 2001 From: Job Nijenhuis Date: Fri, 22 Aug 2025 18:20:48 +0200 Subject: [PATCH 02/24] more fixes --- components/trustpilot/common/constants.mjs | 12 +++++------ components/trustpilot/trustpilot.app.mjs | 25 ++++++++++++++++++---- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/components/trustpilot/common/constants.mjs b/components/trustpilot/common/constants.mjs index aa18cbeb8585e..e3dcd23838b95 100644 --- a/components/trustpilot/common/constants.mjs +++ b/components/trustpilot/common/constants.mjs @@ -18,10 +18,10 @@ export const ENDPOINTS = { PUBLIC_REVIEWS: "/business-units/{businessUnitId}/reviews", PUBLIC_REVIEW_BY_ID: "/business-units/{businessUnitId}/reviews/{reviewId}", - // Private Reviews (Service) - PRIVATE_SERVICE_REVIEWS: "/private/business-units/{businessUnitId}/reviews", - PRIVATE_SERVICE_REVIEW_BY_ID: "/private/business-units/{businessUnitId}/reviews/{reviewId}", - REPLY_TO_SERVICE_REVIEW: "/private/business-units/{businessUnitId}/reviews/{reviewId}/reply", + // Service Reviews + SERVICE_REVIEWS: "/reviews", + SERVICE_REVIEW_BY_ID: "/reviews/{reviewId}", + REPLY_TO_SERVICE_REVIEW: "/private/reviews/{reviewId}/reply", // Public Reviews (Product) PUBLIC_PRODUCT_REVIEWS: "/product-reviews/business-units/{businessUnitId}/reviews", @@ -30,12 +30,12 @@ export const ENDPOINTS = { // Private Reviews (Product) PRIVATE_PRODUCT_REVIEWS: "/private/product-reviews/business-units/{businessUnitId}/reviews", PRIVATE_PRODUCT_REVIEW_BY_ID: "/private/product-reviews/{reviewId}", - REPLY_TO_PRODUCT_REVIEW: "/private/product-reviews/{reviewId}/reply", + CREATE_CONVERSATION_FOR_REVIEW: "/private/product-reviews/{reviewId}/create-conversation", // Conversations CONVERSATIONS: "/private/conversations", CONVERSATION_BY_ID: "/private/conversations/{conversationId}", - REPLY_TO_CONVERSATION: "/private/conversations/{conversationId}/reply", + REPLY_TO_CONVERSATION: "/private/conversations/{conversationId}/comments", // Invitations EMAIL_INVITATIONS: "/private/business-units/{businessUnitId}/email-invitations", diff --git a/components/trustpilot/trustpilot.app.mjs b/components/trustpilot/trustpilot.app.mjs index c1031f046a96c..25972be34e32e 100644 --- a/components/trustpilot/trustpilot.app.mjs +++ b/components/trustpilot/trustpilot.app.mjs @@ -335,7 +335,7 @@ export default defineApp({ } const response = await this._makeRequestWithRetry({ - endpoint: endpoint || ENDPOINTS.PRIVATE_SERVICE_REVIEWS, + endpoint: endpoint || ENDPOINTS.SERVICE_REVIEWS, params, }); @@ -371,7 +371,7 @@ export default defineApp({ throw new Error("Invalid review ID"); } - const endpoint = buildUrl(ENDPOINTS.PRIVATE_SERVICE_REVIEW_BY_ID, { + const endpoint = buildUrl(ENDPOINTS.SERVICE_REVIEW_BY_ID, { businessUnitId, reviewId, }); @@ -455,11 +455,28 @@ export default defineApp({ throw new Error("Reply message cannot be empty after sanitization"); } - const endpoint = buildUrl(ENDPOINTS.REPLY_TO_PRODUCT_REVIEW, { + const review = await this.getProductReviewById({ reviewId, }); + + let conversationId = review.conversationId; + + if (!conversationId) { + const createConversationEndpoint = buildUrl(ENDPOINTS.CREATE_CONVERSATION_FOR_REVIEW, { + reviewId, + }); + const createConversationResponse = await this._makeRequest({ + endpoint: createConversationEndpoint, + method: "POST", + }); + conversationId = createConversationResponse.conversationId; + } + + const replyToConversationEndpoint = buildUrl(ENDPOINTS.REPLY_TO_CONVERSATION, { + conversationId, + }); const response = await this._makeRequest({ - endpoint, + endpoint: replyToConversationEndpoint, method: "POST", data: { message: sanitizedMessage, From f1f83fcca9f2cdb835a64b12f01e2b826ed811a1 Mon Sep 17 00:00:00 2001 From: Job Nijenhuis Date: Wed, 27 Aug 2025 08:34:05 +0200 Subject: [PATCH 03/24] update versions --- .../fetch-product-review-by-id/fetch-product-review-by-id.mjs | 2 +- .../fetch-service-review-by-id/fetch-service-review-by-id.mjs | 2 +- .../actions/fetch-service-reviews/fetch-service-reviews.mjs | 2 +- .../actions/reply-to-product-review/reply-to-product-review.mjs | 2 +- .../actions/reply-to-service-review/reply-to-service-review.mjs | 2 +- .../trustpilot/sources/new-conversations/new-conversations.mjs | 2 +- .../new-product-review-replies/new-product-review-replies.mjs | 2 +- .../sources/new-product-reviews/new-product-reviews.mjs | 2 +- .../new-service-review-replies/new-service-review-replies.mjs | 2 +- .../sources/new-service-reviews/new-service-reviews.mjs | 2 +- .../sources/updated-conversations/updated-conversations.mjs | 2 +- .../sources/updated-product-reviews/updated-product-reviews.mjs | 2 +- .../sources/updated-service-reviews/updated-service-reviews.mjs | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/components/trustpilot/actions/fetch-product-review-by-id/fetch-product-review-by-id.mjs b/components/trustpilot/actions/fetch-product-review-by-id/fetch-product-review-by-id.mjs index 8360d522e88b1..09d883038b228 100644 --- a/components/trustpilot/actions/fetch-product-review-by-id/fetch-product-review-by-id.mjs +++ b/components/trustpilot/actions/fetch-product-review-by-id/fetch-product-review-by-id.mjs @@ -4,7 +4,7 @@ export default { key: "trustpilot-fetch-product-review-by-id", name: "Fetch Product Review by ID", description: "Retrieves detailed information about a specific product review on Trustpilot. Use this action to get comprehensive data about a single product review, including customer feedback, star rating, review text, and metadata. Perfect for analyzing individual customer experiences, responding to specific feedback, or integrating review data into your customer service workflows. [See the documentation](https://developers.trustpilot.com/product-reviews-api#get-private-product-review)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { trustpilot, diff --git a/components/trustpilot/actions/fetch-service-review-by-id/fetch-service-review-by-id.mjs b/components/trustpilot/actions/fetch-service-review-by-id/fetch-service-review-by-id.mjs index 539ec50372681..733a9beb68432 100644 --- a/components/trustpilot/actions/fetch-service-review-by-id/fetch-service-review-by-id.mjs +++ b/components/trustpilot/actions/fetch-service-review-by-id/fetch-service-review-by-id.mjs @@ -4,7 +4,7 @@ export default { key: "trustpilot-fetch-service-review-by-id", name: "Fetch Service Review by ID", description: "Retrieves detailed information about a specific service review for your business on Trustpilot. Use this action to access comprehensive data about an individual service review, including the customer's rating, review content, date, and any responses. Essential for customer service teams to analyze specific feedback, track review history, or integrate individual review data into CRM systems and support tickets. [See the documentation](https://developers.trustpilot.com/business-units-api#get-business-unit-review)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { trustpilot, diff --git a/components/trustpilot/actions/fetch-service-reviews/fetch-service-reviews.mjs b/components/trustpilot/actions/fetch-service-reviews/fetch-service-reviews.mjs index 8408483dc736a..e1c05b1d5ba04 100644 --- a/components/trustpilot/actions/fetch-service-reviews/fetch-service-reviews.mjs +++ b/components/trustpilot/actions/fetch-service-reviews/fetch-service-reviews.mjs @@ -4,7 +4,7 @@ export default { key: "trustpilot-fetch-service-reviews", name: "Fetch Service Reviews", description: "Fetches service reviews for a specific business unit from Trustpilot with support for filtering by star rating, tags, language, and more. [See the documentation](https://developers.trustpilot.com/business-units-api#get-business-unit-reviews)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { trustpilot, diff --git a/components/trustpilot/actions/reply-to-product-review/reply-to-product-review.mjs b/components/trustpilot/actions/reply-to-product-review/reply-to-product-review.mjs index 527c9d6af84b3..8ea062039de5e 100644 --- a/components/trustpilot/actions/reply-to-product-review/reply-to-product-review.mjs +++ b/components/trustpilot/actions/reply-to-product-review/reply-to-product-review.mjs @@ -5,7 +5,7 @@ export default { key: "trustpilot-reply-to-product-review", name: "Reply to Product Review", description: "Posts a public reply to a product review on Trustpilot on behalf of your business. This action allows you to respond to customer feedback, address concerns, thank customers for positive reviews, or provide additional information about products. Replies help demonstrate your commitment to customer satisfaction and can improve your overall reputation. Note that replies are publicly visible and cannot be edited once posted. [See the documentation](https://developers.trustpilot.com/product-reviews-api#reply-to-product-review)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { trustpilot, diff --git a/components/trustpilot/actions/reply-to-service-review/reply-to-service-review.mjs b/components/trustpilot/actions/reply-to-service-review/reply-to-service-review.mjs index fdc8b71e01360..aef88b61b1b3d 100644 --- a/components/trustpilot/actions/reply-to-service-review/reply-to-service-review.mjs +++ b/components/trustpilot/actions/reply-to-service-review/reply-to-service-review.mjs @@ -5,7 +5,7 @@ export default { key: "trustpilot-reply-to-service-review", name: "Reply to Service Review", description: "Posts a public reply to a service review on Trustpilot on behalf of your business. This action enables you to engage with customers who have reviewed your services, allowing you to address complaints, clarify misunderstandings, express gratitude for positive feedback, or provide updates on how you're improving based on their input. Professional responses to reviews can significantly impact your business reputation and show potential customers that you value feedback. Remember that all replies are permanent and publicly visible. [See the documentation](https://developers.trustpilot.com/business-units-api#reply-to-review)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { trustpilot, diff --git a/components/trustpilot/sources/new-conversations/new-conversations.mjs b/components/trustpilot/sources/new-conversations/new-conversations.mjs index d9671f964605e..b47affe831f41 100644 --- a/components/trustpilot/sources/new-conversations/new-conversations.mjs +++ b/components/trustpilot/sources/new-conversations/new-conversations.mjs @@ -9,7 +9,7 @@ export default { key: "trustpilot-new-conversations", name: "New Conversations", description: "Emit new event when a new conversation is started on Trustpilot. This source periodically polls the Trustpilot API to detect new customer-business conversations. Each event contains conversation details including participants, subject, business unit, and creation timestamp. Useful for tracking customer inquiries, support requests, and maintaining real-time communication with customers.", - version: "0.0.3", + version: "0.0.4", type: "source", dedupe: "unique", methods: { diff --git a/components/trustpilot/sources/new-product-review-replies/new-product-review-replies.mjs b/components/trustpilot/sources/new-product-review-replies/new-product-review-replies.mjs index faeb1b447d0e2..0446c732f5f3a 100644 --- a/components/trustpilot/sources/new-product-review-replies/new-product-review-replies.mjs +++ b/components/trustpilot/sources/new-product-review-replies/new-product-review-replies.mjs @@ -9,7 +9,7 @@ export default { key: "trustpilot-new-product-review-replies", name: "New Product Review Replies", description: "Emit new event when a business replies to a product review on Trustpilot. This source periodically polls the Trustpilot API to detect new replies to product reviews. Each event includes the reply text, creation timestamp, and associated review details (product name, star rating, consumer info). Ideal for monitoring business responses to customer feedback, tracking customer service performance, and ensuring timely engagement with product reviews.", - version: "0.0.3", + version: "0.0.4", type: "source", dedupe: "unique", methods: { diff --git a/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs b/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs index 8a68b21f553a1..238967e963e74 100644 --- a/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs +++ b/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs @@ -9,7 +9,7 @@ export default { key: "trustpilot-new-product-reviews", name: "New Product Reviews", description: "Emit new event when a customer posts a new product review on Trustpilot. This source periodically polls the Trustpilot API to detect new product reviews. Each event contains the complete review data including star rating, review text, product information, consumer details, and timestamps. Perfect for monitoring product feedback, analyzing customer satisfaction trends, and triggering automated responses or alerts for specific products.", - version: "0.0.3", + version: "0.0.4", type: "source", dedupe: "unique", methods: { diff --git a/components/trustpilot/sources/new-service-review-replies/new-service-review-replies.mjs b/components/trustpilot/sources/new-service-review-replies/new-service-review-replies.mjs index 98e65170cccdf..f1ce68a558546 100644 --- a/components/trustpilot/sources/new-service-review-replies/new-service-review-replies.mjs +++ b/components/trustpilot/sources/new-service-review-replies/new-service-review-replies.mjs @@ -9,7 +9,7 @@ export default { key: "trustpilot-new-service-review-replies", name: "New Service Review Replies", description: "Emit new event when a business replies to a service review on Trustpilot. This source periodically polls the Trustpilot API to detect new replies to service reviews. Each event includes the reply text, creation timestamp, and associated review details (star rating, review title, consumer info). Essential for tracking business engagement with customer feedback, monitoring response times, and ensuring all service reviews receive appropriate attention.", - version: "0.0.3", + version: "0.0.4", type: "source", dedupe: "unique", methods: { diff --git a/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs b/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs index 22c2787be72a8..91b3e60fece49 100644 --- a/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs +++ b/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs @@ -9,7 +9,7 @@ export default { key: "trustpilot-new-service-reviews", name: "New Service Reviews", description: "Emit new event when a customer posts a new service review on Trustpilot. This source periodically polls the Trustpilot API to detect new service reviews, combining both public and private reviews for comprehensive coverage. Each event contains the complete review data including star rating, review text, consumer details, business unit info, and timestamps. Ideal for monitoring overall business reputation, tracking customer satisfaction metrics, and triggering workflows based on review ratings or content.", - version: "0.0.3", + version: "0.0.4", type: "source", dedupe: "unique", methods: { diff --git a/components/trustpilot/sources/updated-conversations/updated-conversations.mjs b/components/trustpilot/sources/updated-conversations/updated-conversations.mjs index fe839cb905253..24140a78c49b9 100644 --- a/components/trustpilot/sources/updated-conversations/updated-conversations.mjs +++ b/components/trustpilot/sources/updated-conversations/updated-conversations.mjs @@ -9,7 +9,7 @@ export default { key: "trustpilot-updated-conversations", name: "New Updated Conversations", description: "Emit new event when an existing conversation is updated with new messages on Trustpilot. This source periodically polls the Trustpilot API to detect conversations that have received new messages. Each event contains updated conversation details including participants, subject, message count, and latest update timestamp. Useful for tracking ongoing customer interactions, ensuring timely responses to follow-up messages, and maintaining conversation continuity.", - version: "0.0.3", + version: "0.0.4", type: "source", dedupe: "unique", methods: { diff --git a/components/trustpilot/sources/updated-product-reviews/updated-product-reviews.mjs b/components/trustpilot/sources/updated-product-reviews/updated-product-reviews.mjs index 1d2cec1ae341a..dd0f0e0d3eb83 100644 --- a/components/trustpilot/sources/updated-product-reviews/updated-product-reviews.mjs +++ b/components/trustpilot/sources/updated-product-reviews/updated-product-reviews.mjs @@ -9,7 +9,7 @@ export default { key: "trustpilot-updated-product-reviews", name: "New Updated Product Reviews", description: "Emit new event when an existing product review is updated or revised on Trustpilot. This source periodically polls the Trustpilot API to detect product reviews that have been modified. Each event contains the updated review data including any changes to star rating, review text, or other review attributes. Perfect for tracking review modifications, monitoring changes in customer sentiment, and ensuring product feedback accuracy over time.", - version: "0.0.3", + version: "0.0.4", type: "source", dedupe: "unique", methods: { diff --git a/components/trustpilot/sources/updated-service-reviews/updated-service-reviews.mjs b/components/trustpilot/sources/updated-service-reviews/updated-service-reviews.mjs index 13f9721b634a8..9e9a1b5815701 100644 --- a/components/trustpilot/sources/updated-service-reviews/updated-service-reviews.mjs +++ b/components/trustpilot/sources/updated-service-reviews/updated-service-reviews.mjs @@ -9,7 +9,7 @@ export default { key: "trustpilot-updated-service-reviews", name: "New Updated Service Reviews", description: "Emit new event when an existing service review is updated or revised on Trustpilot. This source periodically polls the Trustpilot API to detect service reviews that have been modified. Each event contains the updated review data including any changes to star rating, review text, or other review attributes. Essential for tracking review modifications, monitoring evolving customer feedback, and identifying patterns in review updates.", - version: "0.0.3", + version: "0.0.4", type: "source", dedupe: "unique", methods: { From f20340d69127352522725cd8d6a79332683eed0a Mon Sep 17 00:00:00 2001 From: Job Nijenhuis Date: Wed, 27 Aug 2025 08:40:19 +0200 Subject: [PATCH 04/24] more version updates --- .../actions/fetch-product-reviews/fetch-product-reviews.mjs | 2 +- components/trustpilot/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs b/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs index 0de07eb6d1a90..2dd87d4b2e598 100644 --- a/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs +++ b/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs @@ -4,7 +4,7 @@ export default { key: "trustpilot-fetch-product-reviews", name: "Fetch Product Reviews", description: "Retrieves a list of product reviews for a specific business unit on Trustpilot. This action enables you to fetch multiple product reviews with powerful filtering options including star ratings, language, tags, and sorting preferences. Ideal for monitoring product feedback trends, generating reports, analyzing customer sentiment across your product catalog, or building review dashboards. Supports pagination for handling large review volumes. [See the documentation](https://developers.trustpilot.com/product-reviews-api#get-private-product-reviews)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { trustpilot, diff --git a/components/trustpilot/package.json b/components/trustpilot/package.json index eac454d0d144d..7e03ac2b66435 100644 --- a/components/trustpilot/package.json +++ b/components/trustpilot/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/trustpilot", - "version": "0.1.2", + "version": "0.1.3", "description": "Pipedream Trustpilot Components", "main": "trustpilot.app.mjs", "keywords": [ From 518aedc447c97f8d7fbcf6a3855c530ea9a215ef Mon Sep 17 00:00:00 2001 From: Job Nijenhuis Date: Wed, 3 Sep 2025 13:01:03 +0200 Subject: [PATCH 05/24] fixes --- .../fetch-product-review-by-id.mjs | 28 +- .../fetch-product-reviews.mjs | 115 +++---- .../fetch-service-review-by-id.mjs | 45 ++- .../fetch-service-reviews.mjs | 266 ++++++++++++---- .../get-conversation-from-product-review.mjs | 94 ++++++ .../reply-to-product-review.mjs | 135 ++++++-- .../reply-to-service-review.mjs | 94 ++++-- .../search-business-units-test.mjs | 66 ++++ components/trustpilot/common/api-client.mjs | 135 ++++++++ components/trustpilot/common/constants.mjs | 21 +- components/trustpilot/common/utils.mjs | 131 ++++++-- components/trustpilot/trustpilot.app.mjs | 293 ++++++------------ 12 files changed, 1007 insertions(+), 416 deletions(-) create mode 100644 components/trustpilot/actions/get-conversation-from-product-review/get-conversation-from-product-review.mjs create mode 100644 components/trustpilot/actions/search-business-units-test/search-business-units-test.mjs create mode 100644 components/trustpilot/common/api-client.mjs diff --git a/components/trustpilot/actions/fetch-product-review-by-id/fetch-product-review-by-id.mjs b/components/trustpilot/actions/fetch-product-review-by-id/fetch-product-review-by-id.mjs index 09d883038b228..82fba2aa096e5 100644 --- a/components/trustpilot/actions/fetch-product-review-by-id/fetch-product-review-by-id.mjs +++ b/components/trustpilot/actions/fetch-product-review-by-id/fetch-product-review-by-id.mjs @@ -1,10 +1,17 @@ import trustpilot from "../../trustpilot.app.mjs"; +import { makeRequest } from "../../common/api-client.mjs"; +import { ENDPOINTS } from "../../common/constants.mjs"; +import { + buildUrl, + parseProductReview, + validateReviewId, +} from "../../common/utils.mjs"; export default { key: "trustpilot-fetch-product-review-by-id", name: "Fetch Product Review by ID", description: "Retrieves detailed information about a specific product review on Trustpilot. Use this action to get comprehensive data about a single product review, including customer feedback, star rating, review text, and metadata. Perfect for analyzing individual customer experiences, responding to specific feedback, or integrating review data into your customer service workflows. [See the documentation](https://developers.trustpilot.com/product-reviews-api#get-private-product-review)", - version: "0.0.4", + version: "0.0.5", type: "action", props: { trustpilot, @@ -18,11 +25,28 @@ export default { async run({ $ }) { const { reviewId } = this; + // Validate required parameters + if (!reviewId) { + throw new Error("Review ID is required"); + } + if (!validateReviewId(reviewId)) { + throw new Error("Invalid review ID format"); + } + try { - const review = await this.trustpilot.getProductReviewById({ + // Build the endpoint URL + const endpoint = buildUrl(ENDPOINTS.PRIVATE_PRODUCT_REVIEW_BY_ID, { reviewId, }); + // Make the API request + const response = await makeRequest(this.trustpilot, { + endpoint, + }); + + // Parse the product review with the correct parser + const review = parseProductReview(response); + $.export("$summary", `Successfully fetched product review ${reviewId}`); return { diff --git a/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs b/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs index 2dd87d4b2e598..110f83d08ca33 100644 --- a/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs +++ b/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs @@ -1,10 +1,16 @@ import trustpilot from "../../trustpilot.app.mjs"; +import { makeRequest } from "../../common/api-client.mjs"; +import { ENDPOINTS } from "../../common/constants.mjs"; +import { + buildUrl, + parseProductReview, +} from "../../common/utils.mjs"; export default { key: "trustpilot-fetch-product-reviews", name: "Fetch Product Reviews", description: "Retrieves a list of product reviews for a specific business unit on Trustpilot. This action enables you to fetch multiple product reviews with powerful filtering options including star ratings, language, tags, and sorting preferences. Ideal for monitoring product feedback trends, generating reports, analyzing customer sentiment across your product catalog, or building review dashboards. Supports pagination for handling large review volumes. [See the documentation](https://developers.trustpilot.com/product-reviews-api#get-private-product-reviews)", - version: "0.0.4", + version: "0.0.20", type: "action", props: { trustpilot, @@ -14,94 +20,92 @@ export default { "businessUnitId", ], }, - sku: { - propDefinition: [ - trustpilot, - "sku", - ], - }, - productUrl: { + page: { propDefinition: [ trustpilot, - "productUrl", + "page", ], }, - stars: { + perPage: { propDefinition: [ trustpilot, - "stars", + "perPage", ], }, - sortBy: { - propDefinition: [ - trustpilot, - "sortBy", - ], - }, - limit: { + sku: { propDefinition: [ trustpilot, - "limit", + "sku", ], }, - includeReportedReviews: { + language: { propDefinition: [ trustpilot, - "includeReportedReviews", + "language", ], }, - tags: { + state: { propDefinition: [ trustpilot, - "tags", + "state", ], }, - language: { + locale: { propDefinition: [ trustpilot, - "language", + "locale", ], }, - offset: { - type: "integer", - label: "Offset", - description: "Number of results to skip (for pagination)", - min: 0, - default: 0, - optional: true, - }, }, async run({ $ }) { const { businessUnitId, + page, + perPage, sku, - productUrl, - stars, - sortBy, - limit, - includeReportedReviews, - tags, language, - offset, + state, + locale, } = this; + // Validate required parameters + if (!businessUnitId) { + throw new Error("Business Unit ID is required"); + } + try { - const result = await this.trustpilot.getProductReviews({ + // Build the endpoint URL + const endpoint = buildUrl(ENDPOINTS.PRIVATE_PRODUCT_REVIEWS, { businessUnitId, + }); + + // Prepare query parameters + const params = { sku, - productUrl, - stars, - sortBy, - limit, - includeReportedReviews, - tags, + state, + locale, + perPage, + page, + includeReportedReviews: false, language, - offset, + }; + + // Make the API request + const response = await makeRequest(this.trustpilot, { + endpoint, + params, }); - const { - reviews, pagination, - } = result; + // Handle the correct response structure (productReviews, not reviews) + const reviews = response.productReviews?.map(parseProductReview) || []; + const pagination = { + total: response.links?.total || 0, + page: params.page, + perPage: params.perPage, + hasMore: response.links?.next + ? true + : false, + }; $.export("$summary", `Successfully fetched ${reviews.length} product review(s) for business unit ${businessUnitId}`); @@ -111,10 +115,11 @@ export default { metadata: { businessUnitId, filters: { - stars, - sortBy, - includeReportedReviews, - tags, + sku, + page, + perPage, + state, + locale, language, }, requestTime: new Date().toISOString(), diff --git a/components/trustpilot/actions/fetch-service-review-by-id/fetch-service-review-by-id.mjs b/components/trustpilot/actions/fetch-service-review-by-id/fetch-service-review-by-id.mjs index 733a9beb68432..ebe8b6c3b524b 100644 --- a/components/trustpilot/actions/fetch-service-review-by-id/fetch-service-review-by-id.mjs +++ b/components/trustpilot/actions/fetch-service-review-by-id/fetch-service-review-by-id.mjs @@ -1,19 +1,20 @@ import trustpilot from "../../trustpilot.app.mjs"; +import { makeRequest } from "../../common/api-client.mjs"; +import { ENDPOINTS } from "../../common/constants.mjs"; +import { + buildUrl, + parseServiceReview, + validateReviewId, +} from "../../common/utils.mjs"; export default { key: "trustpilot-fetch-service-review-by-id", name: "Fetch Service Review by ID", - description: "Retrieves detailed information about a specific service review for your business on Trustpilot. Use this action to access comprehensive data about an individual service review, including the customer's rating, review content, date, and any responses. Essential for customer service teams to analyze specific feedback, track review history, or integrate individual review data into CRM systems and support tickets. [See the documentation](https://developers.trustpilot.com/business-units-api#get-business-unit-review)", - version: "0.0.4", + description: "Get a private service review by ID, including customer email and order ID. Access comprehensive data about an individual service review for your business. [See the documentation](https://developers.trustpilot.com/business-units-api#get-private-review-by-id)", + version: "0.0.6", type: "action", props: { trustpilot, - businessUnitId: { - propDefinition: [ - trustpilot, - "businessUnitId", - ], - }, reviewId: { propDefinition: [ trustpilot, @@ -22,23 +23,35 @@ export default { }, }, async run({ $ }) { - const { - businessUnitId, - reviewId, - } = this; + const { reviewId } = this; + + // Validate required parameters + if (!reviewId) { + throw new Error("Review ID is required"); + } + if (!validateReviewId(reviewId)) { + throw new Error("Invalid review ID format"); + } try { - const review = await this.trustpilot.getServiceReviewById({ - businessUnitId, + // Build the endpoint URL for private service review + const endpoint = buildUrl(ENDPOINTS.SERVICE_REVIEW_BY_ID, { reviewId, }); - $.export("$summary", `Successfully fetched service review ${reviewId} for business unit ${businessUnitId}`); + // Make the API request + const response = await makeRequest(this.trustpilot, { + endpoint, + }); + + // Parse the service review with the correct parser + const review = parseServiceReview(response); + + $.export("$summary", `Successfully fetched service review ${reviewId}`); return { review, metadata: { - businessUnitId, reviewId, requestTime: new Date().toISOString(), }, diff --git a/components/trustpilot/actions/fetch-service-reviews/fetch-service-reviews.mjs b/components/trustpilot/actions/fetch-service-reviews/fetch-service-reviews.mjs index e1c05b1d5ba04..1fcdf5b55ae08 100644 --- a/components/trustpilot/actions/fetch-service-reviews/fetch-service-reviews.mjs +++ b/components/trustpilot/actions/fetch-service-reviews/fetch-service-reviews.mjs @@ -1,10 +1,17 @@ import trustpilot from "../../trustpilot.app.mjs"; +import { makeRequest } from "../../common/api-client.mjs"; +import { ENDPOINTS } from "../../common/constants.mjs"; +import { + buildUrl, + parseServiceReview, + validateBusinessUnitId, +} from "../../common/utils.mjs"; export default { key: "trustpilot-fetch-service-reviews", name: "Fetch Service Reviews", - description: "Fetches service reviews for a specific business unit from Trustpilot with support for filtering by star rating, tags, language, and more. [See the documentation](https://developers.trustpilot.com/business-units-api#get-business-unit-reviews)", - version: "0.0.4", + description: "Get private reviews for a business unit, limited to 100,000 records. Response includes customer email and order ID. [See the documentation](https://developers.trustpilot.com/business-units-api#get-private-reviews-for-business-unit)", + version: "0.0.18", type: "action", props: { trustpilot, @@ -14,48 +21,141 @@ export default { "businessUnitId", ], }, - stars: { + language: { propDefinition: [ trustpilot, - "stars", + "language", ], }, - sortBy: { - propDefinition: [ - trustpilot, - "sortBy", - ], + page: { + type: "integer", + label: "Page", + description: "The page to retrieve. If the page number requested is higher than the available number of pages an empty array will be returned.", + min: 1, + default: 1, + optional: true, }, - limit: { - propDefinition: [ - trustpilot, - "limit", - ], + stars: { + type: "string", + label: "Star Rating", + description: "Filter by reviews with a specific star rating. 1-5, separated by commas.", + optional: true, }, - includeReportedReviews: { - propDefinition: [ - trustpilot, - "includeReportedReviews", - ], + internalLocationId: { + type: "string", + label: "Internal Location ID", + description: "Filter by reviews with a specific location", + optional: true, }, - tags: { - propDefinition: [ - trustpilot, - "tags", - ], + perPage: { + type: "integer", + label: "Per Page", + description: "The number of reviews to retrieve per page", + min: 1, + max: 100, + default: 20, + optional: true, }, - language: { - propDefinition: [ - trustpilot, - "language", + orderBy: { + type: "string", + label: "Order By", + description: "The order in which the results should be sorted", + options: [ + { + label: "Created At (Ascending)", + value: "createdat.asc", + }, + { + label: "Created At (Descending)", + value: "createdat.desc", + }, + { + label: "Stars (Ascending)", + value: "stars.asc", + }, + { + label: "Stars (Descending)", + value: "stars.desc", + }, ], + default: "createdat.desc", + optional: true, }, - offset: { - type: "integer", - label: "Offset", - description: "Number of results to skip (for pagination)", - min: 0, - default: 0, + tagGroup: { + type: "string", + label: "Tag Group", + description: "Filtering reviews on Tag group", + optional: true, + }, + tagValue: { + type: "string", + label: "Tag Value", + description: "Filtering reviews on Tag value", + optional: true, + }, + ignoreTagValueCase: { + type: "boolean", + label: "Ignore Tag Value Case", + description: "Ignore tag value case", + default: false, + optional: true, + }, + responded: { + type: "boolean", + label: "Responded", + description: "Filter reviews by responded state", + optional: true, + }, + referenceId: { + type: "string", + label: "Reference ID", + description: "Filter reviews by reference Id", + optional: true, + }, + referralEmail: { + type: "string", + label: "Referral Email", + description: "Filter reviews by referral email", + optional: true, + }, + reported: { + type: "boolean", + label: "Reported", + description: "Filter reviews by reported state", + optional: true, + }, + startDateTime: { + type: "string", + label: "Start Date Time", + description: "Filter reviews by datetime range. If no time is specified than time is implicit 00:00:00. Format: 2013-09-07T13:37:00", + optional: true, + }, + endDateTime: { + type: "string", + label: "End Date Time", + description: "Filter reviews by datetime range. If no time is specified than time is implicit 00:00:00. Format: 2013-09-07T13:37:00", + optional: true, + }, + source: { + type: "string", + label: "Source", + description: "Filter reviews by source", + optional: true, + }, + username: { + type: "string", + label: "Username", + description: "Filter reviews by user name", + optional: true, + }, + findReviewer: { + type: "string", + label: "Find Reviewer", + description: "Filter reviews by Find Reviewer requests (contacted or not contacted)", + options: [ + "contacted", + "notContacted", + ], optional: true, }, }, @@ -63,29 +163,76 @@ export default { const { businessUnitId, stars, - sortBy, - limit, - includeReportedReviews, - tags, language, - offset, + page = 1, + internalLocationId, + perPage = 20, + orderBy = "createdat.desc", + tagGroup, + tagValue, + ignoreTagValueCase = false, + responded, + referenceId, + referralEmail, + reported, + startDateTime, + endDateTime, + source, + username, + findReviewer, } = this; + // Validate required parameters + if (!businessUnitId) { + throw new Error("Business Unit ID is required"); + } + if (!validateBusinessUnitId(businessUnitId)) { + throw new Error("Invalid business unit ID format"); + } + try { - const result = await this.trustpilot.getServiceReviews({ + // Build the endpoint URL + const endpoint = buildUrl(ENDPOINTS.SERVICE_REVIEWS, { businessUnitId, - stars, - sortBy, - limit, - includeReportedReviews, - tags, - language, - offset, }); - const { - reviews, pagination, - } = result; + // Prepare query parameters + const params = {}; + + // Add optional parameters if provided + if (stars) params.stars = stars; + if (language) params.language = language; + if (page) params.page = page; + if (internalLocationId) params.internalLocationId = internalLocationId; + if (perPage) params.perPage = perPage; + if (orderBy) params.orderBy = orderBy; + if (tagGroup) params.tagGroup = tagGroup; + if (tagValue) params.tagValue = tagValue; + if (ignoreTagValueCase !== undefined) params.ignoreTagValueCase = ignoreTagValueCase; + if (responded !== undefined) params.responded = responded; + if (referenceId) params.referenceId = referenceId; + if (referralEmail) params.referralEmail = referralEmail; + if (reported !== undefined) params.reported = reported; + if (startDateTime) params.startDateTime = startDateTime; + if (endDateTime) params.endDateTime = endDateTime; + if (source) params.source = source; + if (username) params.username = username; + if (findReviewer) params.findReviewer = findReviewer; + + // Make the API request + const response = await makeRequest(this.trustpilot, { + endpoint, + params, + }); + + // Handle the correct response structure (reviews array) + const reviews = response.reviews?.map(parseServiceReview) || []; + const pagination = { + total: response.pagination?.total || 0, + page: response.pagination?.page || page, + perPage: response.pagination?.perPage || perPage, + hasMore: response.pagination?.hasMore || false, + }; $.export("$summary", `Successfully fetched ${reviews.length} service review(s) for business unit ${businessUnitId}`); @@ -96,10 +243,23 @@ export default { businessUnitId, filters: { stars, - sortBy, - includeReportedReviews, - tags, language, + page, + internalLocationId, + perPage, + orderBy, + tagGroup, + tagValue, + ignoreTagValueCase, + responded, + referenceId, + referralEmail, + reported, + startDateTime, + endDateTime, + source, + username, + findReviewer, }, requestTime: new Date().toISOString(), }, diff --git a/components/trustpilot/actions/get-conversation-from-product-review/get-conversation-from-product-review.mjs b/components/trustpilot/actions/get-conversation-from-product-review/get-conversation-from-product-review.mjs new file mode 100644 index 0000000000000..9dd89dca6d9c9 --- /dev/null +++ b/components/trustpilot/actions/get-conversation-from-product-review/get-conversation-from-product-review.mjs @@ -0,0 +1,94 @@ +import { ConfigurationError } from "@pipedream/platform"; +import trustpilot from "../../trustpilot.app.mjs"; +import { makeRequest } from "../../common/api-client.mjs"; +import { ENDPOINTS } from "../../common/constants.mjs"; +import { + buildUrl, + validateReviewId, +} from "../../common/utils.mjs"; + +export default { + key: "trustpilot-get-conversation-from-product-review", + name: "Get Conversation from Product Review", + description: "Get conversation and related comments from a product review. First fetches the review to get the conversationId, then retrieves the full conversation details. [See the documentation](https://developers.trustpilot.com/conversations-api#get-conversation)", + version: "0.0.1", + type: "action", + props: { + trustpilot, + reviewId: { + propDefinition: [ + trustpilot, + "reviewId", + ], + }, + }, + async run({ $ }) { + const { reviewId } = this; + + // Validate required parameters + if (!reviewId) { + throw new ConfigurationError("Review ID is required"); + } + if (!validateReviewId(reviewId)) { + throw new ConfigurationError("Invalid review ID format"); + } + + try { + // Step 1: Get the product review to get the conversationId + $.export("$summary", "Fetching product review details..."); + + const getReviewEndpoint = buildUrl(ENDPOINTS.PRIVATE_PRODUCT_REVIEW_BY_ID, { + reviewId, + }); + + const review = await makeRequest(this.trustpilot, { + endpoint: getReviewEndpoint, + }); + + const conversationId = review.conversationId; + + if (!conversationId) { + return { + success: false, + message: "No conversation found for this product review", + review: { + id: reviewId, + hasConversation: false, + }, + metadata: { + reviewId, + requestTime: new Date().toISOString(), + }, + }; + } + + // Step 2: Get the conversation details + $.export("$summary", "Fetching conversation details..."); + + const getConversationEndpoint = buildUrl(ENDPOINTS.CONVERSATION_BY_ID, { + conversationId, + }); + + const conversation = await makeRequest(this.trustpilot, { + endpoint: getConversationEndpoint, + }); + + $.export("$summary", `Successfully retrieved conversation ${conversationId} for product review ${reviewId}`); + + return { + success: true, + conversation, + metadata: { + reviewId, + conversationId, + commentCount: conversation.comments?.length || 0, + conversationState: conversation.state, + source: conversation.source, + requestTime: new Date().toISOString(), + }, + }; + } catch (error) { + throw new ConfigurationError(`Failed to get conversation from product review: ${error.message}`); + } + }, +}; diff --git a/components/trustpilot/actions/reply-to-product-review/reply-to-product-review.mjs b/components/trustpilot/actions/reply-to-product-review/reply-to-product-review.mjs index 8ea062039de5e..5b3ac8f0deb1a 100644 --- a/components/trustpilot/actions/reply-to-product-review/reply-to-product-review.mjs +++ b/components/trustpilot/actions/reply-to-product-review/reply-to-product-review.mjs @@ -1,11 +1,17 @@ import { ConfigurationError } from "@pipedream/platform"; import trustpilot from "../../trustpilot.app.mjs"; +import { makeRequest } from "../../common/api-client.mjs"; +import { ENDPOINTS } from "../../common/constants.mjs"; +import { + buildUrl, + validateReviewId, +} from "../../common/utils.mjs"; export default { key: "trustpilot-reply-to-product-review", name: "Reply to Product Review", - description: "Posts a public reply to a product review on Trustpilot on behalf of your business. This action allows you to respond to customer feedback, address concerns, thank customers for positive reviews, or provide additional information about products. Replies help demonstrate your commitment to customer satisfaction and can improve your overall reputation. Note that replies are publicly visible and cannot be edited once posted. [See the documentation](https://developers.trustpilot.com/product-reviews-api#reply-to-product-review)", - version: "0.0.4", + description: "Reply to a product review by creating a conversation and posting a comment. This follows the proper flow: fetch review -> create conversation if needed -> post comment. [See the documentation](https://developers.trustpilot.com/product-reviews-api#reply-to-product-review)", + version: "0.0.8", type: "action", props: { trustpilot, @@ -15,37 +21,124 @@ export default { "reviewId", ], }, - message: { + content: { type: "string", - label: "Reply Message", - description: "The message to reply to the review with", + label: "Reply Content", + description: "The content of your reply to the review", + }, + integrationId: { + type: "string", + label: "Integration ID", + description: "Optional integration ID to track the source of the reply", + optional: true, + }, + businessUserId: { + type: "string", + label: "Business User ID", + description: "The ID of the business user posting the reply (required for creating comments)", }, }, async run({ $ }) { const { reviewId, - message, + content, + integrationId, + businessUserId, } = this; - if (!message || message.trim().length === 0) { - throw new ConfigurationError("Reply message cannot be empty"); + // Validate required parameters + if (!reviewId) { + throw new ConfigurationError("Review ID is required"); + } + if (!validateReviewId(reviewId)) { + throw new ConfigurationError("Invalid review ID format"); + } + if (!content || content.trim().length === 0) { + throw new ConfigurationError("Reply content cannot be empty"); + } + if (!businessUserId) { + throw new ConfigurationError("Business User ID is required"); } - const result = await this.trustpilot.replyToProductReview({ - reviewId, - message: message.trim(), - }); + const trimmedContent = content.trim(); - $.export("$summary", `Successfully replied to product review ${reviewId}`); + try { + // Step 1: Get the product review to check if it has a conversationId + $.export("$summary", "Fetching product review details..."); - return { - success: true, - reply: result, - metadata: { + const getReviewEndpoint = buildUrl(ENDPOINTS.PRIVATE_PRODUCT_REVIEW_BY_ID, { reviewId, - messageLength: message.trim().length, - requestTime: new Date().toISOString(), - }, - }; + }); + + const review = await makeRequest(this.trustpilot, { + endpoint: getReviewEndpoint, + }); + + let conversationId = review.conversationId; + + // Step 2: Create conversation if it doesn't exist + if (!conversationId) { + $.export("$summary", "Creating conversation for review..."); + + const createConversationEndpoint = buildUrl(ENDPOINTS.CREATE_CONVERSATION_FOR_REVIEW, { + reviewId, + }); + + const createConversationResponse = await makeRequest(this.trustpilot, { + endpoint: createConversationEndpoint, + method: "POST", + }); + + conversationId = createConversationResponse.conversationId; + + if (!conversationId) { + throw new Error("Failed to create conversation - no conversationId returned"); + } + } + + // Step 3: Create comment on the conversation + $.export("$summary", "Posting reply comment..."); + + const replyEndpoint = buildUrl(ENDPOINTS.REPLY_TO_CONVERSATION, { + conversationId, + }); + + // Prepare request data + const requestData = { + content: trimmedContent, + }; + + // Add integrationId if provided + if (integrationId) { + requestData.integrationId = integrationId; + } + + const replyResponse = await makeRequest(this.trustpilot, { + endpoint: replyEndpoint, + method: "POST", + data: requestData, + additionalHeaders: { + "x-business-user-id": businessUserId, + }, + }); + + $.export("$summary", `Successfully replied to product review ${reviewId}`); + + return { + success: true, + comment: replyResponse, + metadata: { + reviewId, + conversationId, + businessUserId, + contentLength: trimmedContent.length, + integrationId: integrationId || null, + wasConversationCreated: !review.conversationId, + requestTime: new Date().toISOString(), + }, + }; + } catch (error) { + throw new ConfigurationError(`Failed to reply to product review: ${error.message}`); + } }, }; diff --git a/components/trustpilot/actions/reply-to-service-review/reply-to-service-review.mjs b/components/trustpilot/actions/reply-to-service-review/reply-to-service-review.mjs index aef88b61b1b3d..199e24e3951a2 100644 --- a/components/trustpilot/actions/reply-to-service-review/reply-to-service-review.mjs +++ b/components/trustpilot/actions/reply-to-service-review/reply-to-service-review.mjs @@ -1,20 +1,20 @@ import { ConfigurationError } from "@pipedream/platform"; import trustpilot from "../../trustpilot.app.mjs"; +import { makeRequest } from "../../common/api-client.mjs"; +import { ENDPOINTS } from "../../common/constants.mjs"; +import { + buildUrl, + validateReviewId, +} from "../../common/utils.mjs"; export default { key: "trustpilot-reply-to-service-review", name: "Reply to Service Review", - description: "Posts a public reply to a service review on Trustpilot on behalf of your business. This action enables you to engage with customers who have reviewed your services, allowing you to address complaints, clarify misunderstandings, express gratitude for positive feedback, or provide updates on how you're improving based on their input. Professional responses to reviews can significantly impact your business reputation and show potential customers that you value feedback. Remember that all replies are permanent and publicly visible. [See the documentation](https://developers.trustpilot.com/business-units-api#reply-to-review)", - version: "0.0.4", + description: "Reply to a service review on Trustpilot. Posts a public reply on behalf of your business using the private reviews API. [See the documentation](https://developers.trustpilot.com/private-reviews-api#reply-to-review)", + version: "0.0.6", type: "action", props: { trustpilot, - businessUnitId: { - propDefinition: [ - trustpilot, - "businessUnitId", - ], - }, reviewId: { propDefinition: [ trustpilot, @@ -24,37 +24,79 @@ export default { message: { type: "string", label: "Reply Message", - description: "The message to reply to the review with", + description: "The message content of your reply to the review", + }, + authorBusinessUserId: { + type: "string", + label: "Author Business User ID", + description: "The ID of the business user posting the reply", }, }, async run({ $ }) { const { - businessUnitId, reviewId, message, + authorBusinessUserId, } = this; + // Validate required parameters + if (!reviewId) { + throw new ConfigurationError("Review ID is required"); + } + if (!validateReviewId(reviewId)) { + throw new ConfigurationError("Invalid review ID format"); + } if (!message || message.trim().length === 0) { throw new ConfigurationError("Reply message cannot be empty"); } + if (!authorBusinessUserId) { + throw new ConfigurationError("Author Business User ID is required"); + } - const result = await this.trustpilot.replyToServiceReview({ - businessUnitId, - reviewId, - message: message.trim(), - }); - - $.export("$summary", `Successfully replied to service review ${reviewId}`); + const trimmedMessage = message.trim(); - return { - success: true, - reply: result, - metadata: { - businessUnitId, + try { + // Build the endpoint URL for replying to service review + const endpoint = buildUrl(ENDPOINTS.REPLY_TO_SERVICE_REVIEW, { reviewId, - messageLength: message.trim().length, - requestTime: new Date().toISOString(), - }, - }; + }); + + // Prepare request data according to API specification + const requestData = { + authorBusinessUserId, + message: trimmedMessage, + }; + + // Make the API request + await makeRequest(this.trustpilot, { + endpoint, + method: "POST", + data: requestData, + }); + + $.export("$summary", `Successfully replied to service review ${reviewId}`); + + // API returns 201 Created on success, response body may be empty + return { + success: true, + reply: { + message: trimmedMessage, + authorBusinessUserId, + reviewId, + status: "created", + statusCode: 201, + postedAt: new Date().toISOString(), + }, + metadata: { + reviewId, + authorBusinessUserId, + messageLength: trimmedMessage.length, + requestTime: new Date().toISOString(), + httpStatus: "201 Created", + }, + }; + } catch (error) { + throw new ConfigurationError(`Failed to reply to service review: ${error.message}`); + } }, }; diff --git a/components/trustpilot/actions/search-business-units-test/search-business-units-test.mjs b/components/trustpilot/actions/search-business-units-test/search-business-units-test.mjs new file mode 100644 index 0000000000000..2e846fb7ecdf1 --- /dev/null +++ b/components/trustpilot/actions/search-business-units-test/search-business-units-test.mjs @@ -0,0 +1,66 @@ +import trustpilot from "../../trustpilot.app.mjs"; + +export default { + key: "trustpilot-search-business-units-test", + name: "Search Business Units (Test)", + description: "Temporary action to test the searchBusinessUnits implementation. Searches for business units by name or domain.", + version: "0.0.4", + type: "action", + props: { + trustpilot, + query: { + type: "string", + label: "Search Query", + description: "Search term to find business units (name or domain)", + default: "stoov", + optional: true, + }, + page: { + type: "integer", + label: "Page", + description: "Page number for pagination", + min: 1, + default: 1, + optional: true, + }, + }, + async run({ $ }) { + const { + query, + page, + } = this; + + try { + const businessUnits = await this.trustpilot.searchBusinessUnits({ + query, + page, + }); + + const options = businessUnits.map((businessUnit) => { + const { + id, displayName, + } = businessUnit; + + return { + label: displayName, + value: id, + }; + }); + + $.export("$summary", `Found ${businessUnits.length} business unit(s) for query: "${query}"`); + + return { + businessUnits, + options, + metadata: { + query, + page, + count: businessUnits.length, + requestTime: new Date().toISOString(), + }, + }; + } catch (error) { + throw new Error(`Failed to search business units: ${error.message}`); + } + }, +}; diff --git a/components/trustpilot/common/api-client.mjs b/components/trustpilot/common/api-client.mjs new file mode 100644 index 0000000000000..3275ad9b6dcfc --- /dev/null +++ b/components/trustpilot/common/api-client.mjs @@ -0,0 +1,135 @@ +import { axios } from "@pipedream/platform"; +import { + BASE_URL, HTTP_STATUS, RETRY_CONFIG, +} from "./constants.mjs"; +import { + formatQueryParams, sleep, +} from "./utils.mjs"; + +/** + * Make an authenticated request to the Trustpilot API + * @param {object} trustpilotApp - The Trustpilot app instance with auth credentials + * @param {object} options - Request options + * @param {string} options.endpoint - API endpoint path + * @param {string} [options.method="GET"] - HTTP method + * @param {object} [options.params={}] - Query parameters + * @param {object} [options.data] - Request body data + * @param {object} [options.additionalHeaders={}] - Additional headers to include in the request + * @param {number} [options.timeout=30000] - Request timeout + * @param {number} [retries=RETRY_CONFIG.MAX_RETRIES] - Number of retries for rate limiting + * @returns {Promise} API response data + */ +export async function makeRequest(trustpilotApp, { + endpoint, + method = "GET", + params = {}, + data = null, + timeout = 30000, + additionalHeaders = {}, + ...args +}, retries = RETRY_CONFIG.MAX_RETRIES) { + const url = `${BASE_URL}${endpoint}`; + const headers = { + ...getAuthHeaders(trustpilotApp, url), + ...additionalHeaders, + }; + + const config = { + method, + url, + headers, + params: formatQueryParams(params), + timeout, + ...args, + }; + + if (data) { + config.data = data; + } + + try { + const response = await axios(trustpilotApp, config); + return response.data || response; + } catch (error) { + if (retries > 0 && error.response?.status === HTTP_STATUS.TOO_MANY_REQUESTS) { + const delay = Math.min( + RETRY_CONFIG.INITIAL_DELAY * (RETRY_CONFIG.MAX_RETRIES - retries + 1), + RETRY_CONFIG.MAX_DELAY, + ); + await sleep(delay); + return makeRequest(trustpilotApp, { + endpoint, + method, + params, + data, + timeout, + ...args, + }, retries - 1); + } + throw error; + } +} + +/** + * Determine if a URL requires private authentication + * @param {string} url - The full URL + * @returns {boolean} - True if URL requires OAuth token + */ +function isPrivateURL(url) { + return url.includes("private"); +} + +/** + * Get authentication headers for private URLs (OAuth) + * @param {object} trustpilotApp - The Trustpilot app instance + * @returns {object} - Headers with OAuth token + */ +function getAuthHeadersForPrivateURL(trustpilotApp) { + if (!trustpilotApp.$auth?.oauth_access_token) { + throw new Error("Authentication required: OAuth token is required for private requests"); + } + return { + "Authorization": `Bearer ${trustpilotApp.$auth.oauth_access_token}`, + }; +} + +/** + * Get authentication headers for public URLs (API key) + * @param {object} trustpilotApp - The Trustpilot app instance + * @returns {object} - Headers with API key + */ +function getAuthHeadersForPublicURL(trustpilotApp) { + if (!trustpilotApp.$auth?.api_key) { + throw new Error("Authentication required: API key is required for public requests"); + } + return { + "apikey": trustpilotApp.$auth.api_key, + }; +} + +/** + * Get appropriate authentication headers based on URL + * @param {object} trustpilotApp - The Trustpilot app instance + * @param {string} url - The full URL + * @returns {object} - Complete headers for the request + */ +function getAuthHeaders(trustpilotApp, url) { + const headers = { + "Content-Type": "application/json", + "User-Agent": "Pipedream/1.0", + }; + + const isPrivate = isPrivateURL(url); + + if (isPrivate) { + return { + ...headers, + ...getAuthHeadersForPrivateURL(trustpilotApp), + }; + } else { + return { + ...headers, + ...getAuthHeadersForPublicURL(trustpilotApp), + }; + } +} diff --git a/components/trustpilot/common/constants.mjs b/components/trustpilot/common/constants.mjs index e3dcd23838b95..576dccb2c2c02 100644 --- a/components/trustpilot/common/constants.mjs +++ b/components/trustpilot/common/constants.mjs @@ -16,17 +16,13 @@ export const ENDPOINTS = { // Public Reviews PUBLIC_REVIEWS: "/business-units/{businessUnitId}/reviews", - PUBLIC_REVIEW_BY_ID: "/business-units/{businessUnitId}/reviews/{reviewId}", + PUBLIC_REVIEW_BY_ID: "/reviews/{reviewId}", // Service Reviews - SERVICE_REVIEWS: "/reviews", - SERVICE_REVIEW_BY_ID: "/reviews/{reviewId}", + SERVICE_REVIEWS: "/private/business-units/{businessUnitId}/reviews", + SERVICE_REVIEW_BY_ID: "/private/reviews/{reviewId}", REPLY_TO_SERVICE_REVIEW: "/private/reviews/{reviewId}/reply", - // Public Reviews (Product) - PUBLIC_PRODUCT_REVIEWS: "/product-reviews/business-units/{businessUnitId}/reviews", - PUBLIC_PRODUCT_REVIEW_BY_ID: "/product-reviews/{reviewId}", - // Private Reviews (Product) PRIVATE_PRODUCT_REVIEWS: "/private/product-reviews/business-units/{businessUnitId}/reviews", PRIVATE_PRODUCT_REVIEW_BY_ID: "/private/product-reviews/{reviewId}", @@ -36,17 +32,6 @@ export const ENDPOINTS = { CONVERSATIONS: "/private/conversations", CONVERSATION_BY_ID: "/private/conversations/{conversationId}", REPLY_TO_CONVERSATION: "/private/conversations/{conversationId}/comments", - - // Invitations - EMAIL_INVITATIONS: "/private/business-units/{businessUnitId}/email-invitations", - - // Webhooks - // Note: This integration uses polling sources instead of webhooks for better reliability - // and simpler implementation. Webhook signature validation is implemented in the app - // using HMAC-SHA256 with the x-trustpilot-signature header for future webhook sources. - // These endpoints and validation methods are ready for webhook implementation if needed. - WEBHOOKS: "/private/webhooks", - WEBHOOK_BY_ID: "/private/webhooks/{webhookId}", }; export const REVIEW_TYPES = { diff --git a/components/trustpilot/common/utils.mjs b/components/trustpilot/common/utils.mjs index 20677e7cac566..93c0b4a18b1d2 100644 --- a/components/trustpilot/common/utils.mjs +++ b/components/trustpilot/common/utils.mjs @@ -100,46 +100,113 @@ export function parseReview(review) { } /** - * Parse Trustpilot business unit data - * @param {object} businessUnit - Raw business unit data from API - * @returns {object} - Parsed business unit data + * Parse Trustpilot product review data + * @param {object} review - Raw product review data from API + * @returns {object} - Parsed product review data */ -export function parseBusinessUnit(businessUnit) { +export function parseProductReview(review) { return { - id: businessUnit.id, - displayName: businessUnit.displayName, - identifyingName: businessUnit.identifyingName, - trustScore: businessUnit.trustScore, - stars: businessUnit.stars, - numberOfReviews: businessUnit.numberOfReviews, - profileUrl: businessUnit.profileUrl, - websiteUrl: businessUnit.websiteUrl, - country: businessUnit.country, - status: businessUnit.status, - createdAt: businessUnit.createdAt, - categories: businessUnit.categories || [], - images: businessUnit.images || [], + id: review.id, + createdAt: review.createdAt, + updatedAt: review.updatedAt, + businessUnitId: review.businessUnitId, + stars: review.stars, + content: escapeHtml(review.content), + product: review.product + ? { + id: review.product.id, + productUrl: review.product.productUrl, + productImages: review.product.productImages || [], + name: escapeHtml(review.product.name), + sku: review.product.sku, + gtin: review.product.gtin, + mpn: review.product.mpn, + brand: escapeHtml(review.product.brand), + } + : null, + consumer: review.consumer + ? { + id: review.consumer.id, + email: review.consumer.email, + name: escapeHtml(review.consumer.name), + } + : null, + referenceId: review.referenceId, + locale: review.locale, + language: review.language, + redirectUri: review.redirectUri, + state: review.state, + hasModerationHistory: review.hasModerationHistory || false, + conversationId: review.conversationId, + attributeRatings: review.attributeRatings?.map((attr) => ({ + attributeId: attr.attributeId, + attributeName: escapeHtml(attr.attributeName), + attributeType: attr.attributeType, + attributeOptions: attr.attributeOptions, + rating: attr.rating, + })) || [], + attachments: review.attachments || [], }; } /** - * Parse webhook payload - * @param {object} payload - Raw webhook payload - * @returns {object} - Parsed webhook data + * Parse Trustpilot service review data + * @param {object} review - Raw service review data from API + * @returns {object} - Parsed service review data */ -export function parseWebhookPayload(payload) { - const { - event, data, - } = payload; - +export function parseServiceReview(review) { return { - event: event?.type || payload.eventType, - timestamp: event?.timestamp || payload.timestamp, - businessUnitId: data?.businessUnit?.id || payload.businessUnitId, - reviewId: data?.review?.id || payload.reviewId, - consumerId: data?.consumer?.id || payload.consumerId, - data: data || payload.data, - raw: payload, + links: review.links || [], + id: review.id, + consumer: review.consumer + ? { + links: review.consumer.links || [], + id: review.consumer.id, + displayName: escapeHtml(review.consumer.displayName), + displayLocation: escapeHtml(review.consumer.displayLocation), + numberOfReviews: review.consumer.numberOfReviews, + } + : null, + businessUnit: review.businessUnit + ? { + links: review.businessUnit.links || [], + id: review.businessUnit.id, + identifyingName: escapeHtml(review.businessUnit.identifyingName), + displayName: escapeHtml(review.businessUnit.displayName), + } + : null, + location: escapeHtml(review.location), + stars: review.stars, + title: escapeHtml(review.title), + text: escapeHtml(review.text), + language: review.language, + createdAt: review.createdAt, + experiencedAt: review.experiencedAt, + updatedAt: review.updatedAt, + companyReply: review.companyReply + ? { + text: escapeHtml(review.companyReply.text), + authorBusinessUserId: review.companyReply.authorBusinessUserId, + authorBusinessUserName: escapeHtml(review.companyReply.authorBusinessUserName), + createdAt: review.companyReply.createdAt, + updatedAt: review.companyReply.updatedAt, + } + : null, + isVerified: review.isVerified || false, + source: review.source, + numberOfLikes: review.numberOfLikes || 0, + status: review.status, + reportData: review.reportData, + complianceLabels: review.complianceLabels || [], + countsTowardsTrustScore: review.countsTowardsTrustScore || false, + countsTowardsLocationTrustScore: review.countsTowardsLocationTrustScore, + invitation: review.invitation + ? { + businessUnitId: review.invitation.businessUnitId, + } + : null, + businessUnitHistory: review.businessUnitHistory || [], + reviewVerificationLevel: review.reviewVerificationLevel, }; } diff --git a/components/trustpilot/trustpilot.app.mjs b/components/trustpilot/trustpilot.app.mjs index 25972be34e32e..440d9e727c2ce 100644 --- a/components/trustpilot/trustpilot.app.mjs +++ b/components/trustpilot/trustpilot.app.mjs @@ -1,24 +1,15 @@ -import { axios } from "@pipedream/platform"; import { defineApp } from "@pipedream/types"; -import * as crypto from "crypto"; +import { makeRequest } from "./common/api-client.mjs"; import { - BASE_URL, DEFAULT_LIMIT, ENDPOINTS, - HTTP_STATUS, MAX_LIMIT, - RATING_SCALE, - RETRY_CONFIG, SORT_OPTIONS, } from "./common/constants.mjs"; import { buildUrl, - formatQueryParams, - parseBusinessUnit, parseReview, - parseWebhookPayload, sanitizeInput, - sleep, validateBusinessUnitId, validateReviewId, } from "./common/utils.mjs"; @@ -31,18 +22,26 @@ export default defineApp({ type: "string", label: "Business Unit ID", description: "The unique identifier for your business unit on Trustpilot", - async options(page, prevContext, query) { + useQuery: true, + async options({ + page, query, + }) { try { const businessUnits = await this.searchBusinessUnits({ - query, page, + query, + }); + + return businessUnits.map((businessUnit) => { + const { + id, displayName, + } = businessUnit; + + return { + label: displayName, + value: id, + }; }); - return businessUnits.map(({ - id, displayName, name: { identifying }, - }) => ({ - label: `${identifying || displayName}`, - value: id, - })); } catch (error) { console.error("Error fetching business units:", error); return []; @@ -66,13 +65,6 @@ export default defineApp({ description: "Filter by product URL", optional: true, }, - stars: { - type: "integer", - label: "Star Rating", - description: "Filter by star rating (1-5)", - options: RATING_SCALE, - optional: true, - }, sortBy: { type: "string", label: "Sort By", @@ -115,113 +107,28 @@ export default defineApp({ description: "Filter reviews by language (ISO 639-1 code)", optional: true, }, - }, - methods: { - // Authentication and base request methods - _isPrivateURL(url) { - return url.includes("private"); - }, - - _getAuthHeadersForPrivateURL() { - if (!this.$auth?.oauth_access_token) { - throw new Error("Authentication required: OAuth token is required for private requests"); - } else { - return { - "Authorization": `Bearer ${this.$auth.oauth_access_token}`, - }; - } - }, - - _getAuthHeadersForPublicURL() { - if (!this.$auth?.api_key) { - throw new Error("Authentication required: API key is required for public requests"); - } else { - return { - "apikey": this.$auth.api_key, - }; - } - }, - - _getAuthHeaders(url) { - const headers = { - "Content-Type": "application/json", - "User-Agent": "Pipedream/1.0", - }; - - const isPrivate = this._isPrivateURL(url); - console.log("isPrivate", isPrivate); - - if (isPrivate) { - return { - ...headers, - ...this._getAuthHeadersForPrivateURL(), - }; - } else { - return { - ...headers, - ...this._getAuthHeadersForPublicURL(), - }; - } - }, - - async _makeRequest({ - endpoint, method = "GET", params = {}, data = null, ...args - }) { - const url = `${BASE_URL}${endpoint}`; - const headers = this._getAuthHeaders(); - - const config = { - method, - url, - headers, - params: formatQueryParams(params), - timeout: 30000, - ...args, - }; - - if (data) { - config.data = data; - } - - const response = await axios(this, config); - return response.data || response; - }, - - async _makeRequestWithRetry(config, retries = RETRY_CONFIG.MAX_RETRIES) { - try { - return await this._makeRequest(config); - } catch (error) { - if (retries > 0 && error.response?.status === HTTP_STATUS.TOO_MANY_REQUESTS) { - const delay = Math.min( - RETRY_CONFIG.INITIAL_DELAY * (RETRY_CONFIG.MAX_RETRIES - retries + 1), - RETRY_CONFIG.MAX_DELAY, - ); - await sleep(delay); - return this._makeRequestWithRetry(config, retries - 1); - } - throw error; - } + state: { + type: "string", + label: "State", + description: "Which reviews to retrieve according to their review state. Default is Published.", + options: [ + "published", + "unpublished", + ], + optional: true, }, - - // Business Unit methods - async getBusinessUnit(businessUnitId) { - if (!validateBusinessUnitId(businessUnitId)) { - throw new Error("Invalid business unit ID"); - } - - const endpoint = buildUrl(ENDPOINTS.BUSINESS_UNIT_BY_ID, { - businessUnitId, - }); - const response = await this._makeRequest({ - endpoint, - }); - return parseBusinessUnit(response); + locale: { + type: "string", + label: "Locale", + description: "The language in which the attributes, if any, are returned", + optional: true, }, - + }, + methods: { async searchBusinessUnits({ - query = "", page = 1, + query = "a", page = 1, } = {}) { - const response = await this._makeRequest({ + const response = await makeRequest(this, { endpoint: ENDPOINTS.BUSINESS_UNITS, params: { query, @@ -229,7 +136,7 @@ export default defineApp({ }, }); - return response.businessUnits?.map(parseBusinessUnit) || []; + return response.businessUnits || []; }, // Public Review methods (no auth required for basic info) @@ -297,45 +204,40 @@ export default defineApp({ return parseReview(response); }, - // Private helper for fetching reviews - async _getReviews({ - endpoint, + // Service Review methods - simplified for sources + async getServiceReviews({ businessUnitId, - sku = null, - productUrl = null, stars = null, sortBy = SORT_OPTIONS.CREATED_AT_DESC, limit = DEFAULT_LIMIT, offset = 0, - includeReportedReviews = false, tags = [], language = null, - }) { - if (businessUnitId && !validateBusinessUnitId(businessUnitId)) { + } = {}) { + if (!validateBusinessUnitId(businessUnitId)) { throw new Error("Invalid business unit ID"); } - if (sku === null && productUrl === null) { - throw new Error("Either SKU or product URL is required"); - } + const endpoint = buildUrl(ENDPOINTS.PUBLIC_REVIEWS, { + businessUnitId, + }); const params = { - sku, - productUrl, stars, orderBy: sortBy, perPage: limit, page: Math.floor(offset / limit) + 1, - includeReportedReviews, language, }; - if (tags.length > 0) { - params.tags = tags.join(","); + if (tags && tags.length > 0) { + params.tags = Array.isArray(tags) + ? tags.join(",") + : tags; } - const response = await this._makeRequestWithRetry({ - endpoint: endpoint || ENDPOINTS.SERVICE_REVIEWS, + const response = await makeRequest(this, { + endpoint, params, }); @@ -350,17 +252,6 @@ export default defineApp({ }; }, - // Private Service Review methods - async getServiceReviews(options = {}) { - const endpoint = buildUrl(ENDPOINTS.PUBLIC_REVIEWS, { - businessUnitId: options.businessUnitId, - }); - return this._getReviews({ - endpoint, - ...options, - }); - }, - async getServiceReviewById({ businessUnitId, reviewId, }) { @@ -414,15 +305,60 @@ export default defineApp({ return response; }, - // Product Review methods - async getProductReviews(options = {}) { - const endpoint = buildUrl(ENDPOINTS.PUBLIC_PRODUCT_REVIEWS, { - businessUnitId: options.businessUnitId, + // Product Review methods - simplified for sources + async getProductReviews({ + businessUnitId, + sku = null, + productUrl = null, + stars = null, + sortBy = SORT_OPTIONS.CREATED_AT_DESC, + limit = DEFAULT_LIMIT, + offset = 0, + includeReportedReviews = false, + tags = [], + language = null, + } = {}) { + if (!validateBusinessUnitId(businessUnitId)) { + throw new Error("Invalid business unit ID"); + } + + const endpoint = buildUrl(ENDPOINTS.PRIVATE_PRODUCT_REVIEWS, { + businessUnitId, }); - return this._getReviews({ + + const params = { + sku, + productUrl, + stars, + orderBy: sortBy, + perPage: limit, + page: Math.floor(offset / limit) + 1, + includeReportedReviews, + language, + }; + + if (tags && tags.length > 0) { + params.tags = Array.isArray(tags) + ? tags.join(",") + : tags; + } + + const response = await makeRequest(this, { endpoint, - ...options, + params, }); + + return { + reviews: response.productReviews?.map(parseReview) || [], // Note: productReviews not reviews + pagination: { + total: response.links?.total || 0, + page: params.page, + perPage: params.perPage, + hasMore: response.links?.next + ? true + : false, + }, + }; }, async getProductReviewById({ reviewId }) { @@ -609,34 +545,5 @@ export default defineApp({ }); return response.webhooks || []; }, - - // Utility methods - parseWebhookPayload(payload) { - return parseWebhookPayload(payload); - }, - - validateWebhookSignature(payload, signature, secret) { - // Trustpilot uses HMAC-SHA256 for webhook signature validation - // The signature is sent in the x-trustpilot-signature header - if (!signature || !secret) { - return false; - } - - const payloadString = typeof payload === "string" - ? payload - : JSON.stringify(payload); - - const expectedSignature = crypto - .createHmac("sha256", secret) - .update(payloadString) - .digest("hex"); - - // Constant time comparison to prevent timing attacks - return crypto.timingSafeEqual( - Buffer.from(signature), - Buffer.from(expectedSignature), - ); - }, - }, }); From ed7d780a3e7a7ec684f34233a8798546aaa7d2f6 Mon Sep 17 00:00:00 2001 From: Job Nijenhuis Date: Wed, 3 Sep 2025 13:03:25 +0200 Subject: [PATCH 06/24] Bump all Trustpilot actions to version 0.1.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major improvements and API updates across all actions: - Enhanced private API support with proper authentication - Improved parameter handling and validation - Better error handling and response structures - Added new conversation flow for product reviews - Fixed endpoint URLs to match latest API documentation - Streamlined request/response processing 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../fetch-product-review-by-id/fetch-product-review-by-id.mjs | 2 +- .../actions/fetch-product-reviews/fetch-product-reviews.mjs | 2 +- .../fetch-service-review-by-id/fetch-service-review-by-id.mjs | 2 +- .../actions/fetch-service-reviews/fetch-service-reviews.mjs | 2 +- .../get-conversation-from-product-review.mjs | 2 +- .../actions/reply-to-product-review/reply-to-product-review.mjs | 2 +- .../actions/reply-to-service-review/reply-to-service-review.mjs | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/components/trustpilot/actions/fetch-product-review-by-id/fetch-product-review-by-id.mjs b/components/trustpilot/actions/fetch-product-review-by-id/fetch-product-review-by-id.mjs index 82fba2aa096e5..62d8457887b9b 100644 --- a/components/trustpilot/actions/fetch-product-review-by-id/fetch-product-review-by-id.mjs +++ b/components/trustpilot/actions/fetch-product-review-by-id/fetch-product-review-by-id.mjs @@ -11,7 +11,7 @@ export default { key: "trustpilot-fetch-product-review-by-id", name: "Fetch Product Review by ID", description: "Retrieves detailed information about a specific product review on Trustpilot. Use this action to get comprehensive data about a single product review, including customer feedback, star rating, review text, and metadata. Perfect for analyzing individual customer experiences, responding to specific feedback, or integrating review data into your customer service workflows. [See the documentation](https://developers.trustpilot.com/product-reviews-api#get-private-product-review)", - version: "0.0.5", + version: "0.1.0", type: "action", props: { trustpilot, diff --git a/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs b/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs index 110f83d08ca33..57d922b42f5c8 100644 --- a/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs +++ b/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs @@ -10,7 +10,7 @@ export default { key: "trustpilot-fetch-product-reviews", name: "Fetch Product Reviews", description: "Retrieves a list of product reviews for a specific business unit on Trustpilot. This action enables you to fetch multiple product reviews with powerful filtering options including star ratings, language, tags, and sorting preferences. Ideal for monitoring product feedback trends, generating reports, analyzing customer sentiment across your product catalog, or building review dashboards. Supports pagination for handling large review volumes. [See the documentation](https://developers.trustpilot.com/product-reviews-api#get-private-product-reviews)", - version: "0.0.20", + version: "0.1.0", type: "action", props: { trustpilot, diff --git a/components/trustpilot/actions/fetch-service-review-by-id/fetch-service-review-by-id.mjs b/components/trustpilot/actions/fetch-service-review-by-id/fetch-service-review-by-id.mjs index ebe8b6c3b524b..e382e8b29629d 100644 --- a/components/trustpilot/actions/fetch-service-review-by-id/fetch-service-review-by-id.mjs +++ b/components/trustpilot/actions/fetch-service-review-by-id/fetch-service-review-by-id.mjs @@ -11,7 +11,7 @@ export default { key: "trustpilot-fetch-service-review-by-id", name: "Fetch Service Review by ID", description: "Get a private service review by ID, including customer email and order ID. Access comprehensive data about an individual service review for your business. [See the documentation](https://developers.trustpilot.com/business-units-api#get-private-review-by-id)", - version: "0.0.6", + version: "0.1.0", type: "action", props: { trustpilot, diff --git a/components/trustpilot/actions/fetch-service-reviews/fetch-service-reviews.mjs b/components/trustpilot/actions/fetch-service-reviews/fetch-service-reviews.mjs index 1fcdf5b55ae08..be161b169ef49 100644 --- a/components/trustpilot/actions/fetch-service-reviews/fetch-service-reviews.mjs +++ b/components/trustpilot/actions/fetch-service-reviews/fetch-service-reviews.mjs @@ -11,7 +11,7 @@ export default { key: "trustpilot-fetch-service-reviews", name: "Fetch Service Reviews", description: "Get private reviews for a business unit, limited to 100,000 records. Response includes customer email and order ID. [See the documentation](https://developers.trustpilot.com/business-units-api#get-private-reviews-for-business-unit)", - version: "0.0.18", + version: "0.1.0", type: "action", props: { trustpilot, diff --git a/components/trustpilot/actions/get-conversation-from-product-review/get-conversation-from-product-review.mjs b/components/trustpilot/actions/get-conversation-from-product-review/get-conversation-from-product-review.mjs index 9dd89dca6d9c9..0b347f6671ab4 100644 --- a/components/trustpilot/actions/get-conversation-from-product-review/get-conversation-from-product-review.mjs +++ b/components/trustpilot/actions/get-conversation-from-product-review/get-conversation-from-product-review.mjs @@ -11,7 +11,7 @@ export default { key: "trustpilot-get-conversation-from-product-review", name: "Get Conversation from Product Review", description: "Get conversation and related comments from a product review. First fetches the review to get the conversationId, then retrieves the full conversation details. [See the documentation](https://developers.trustpilot.com/conversations-api#get-conversation)", - version: "0.0.1", + version: "0.1.0", type: "action", props: { trustpilot, diff --git a/components/trustpilot/actions/reply-to-product-review/reply-to-product-review.mjs b/components/trustpilot/actions/reply-to-product-review/reply-to-product-review.mjs index 5b3ac8f0deb1a..68c9f9a66cd07 100644 --- a/components/trustpilot/actions/reply-to-product-review/reply-to-product-review.mjs +++ b/components/trustpilot/actions/reply-to-product-review/reply-to-product-review.mjs @@ -11,7 +11,7 @@ export default { key: "trustpilot-reply-to-product-review", name: "Reply to Product Review", description: "Reply to a product review by creating a conversation and posting a comment. This follows the proper flow: fetch review -> create conversation if needed -> post comment. [See the documentation](https://developers.trustpilot.com/product-reviews-api#reply-to-product-review)", - version: "0.0.8", + version: "0.1.0", type: "action", props: { trustpilot, diff --git a/components/trustpilot/actions/reply-to-service-review/reply-to-service-review.mjs b/components/trustpilot/actions/reply-to-service-review/reply-to-service-review.mjs index 199e24e3951a2..747d67dfdb861 100644 --- a/components/trustpilot/actions/reply-to-service-review/reply-to-service-review.mjs +++ b/components/trustpilot/actions/reply-to-service-review/reply-to-service-review.mjs @@ -11,7 +11,7 @@ export default { key: "trustpilot-reply-to-service-review", name: "Reply to Service Review", description: "Reply to a service review on Trustpilot. Posts a public reply on behalf of your business using the private reviews API. [See the documentation](https://developers.trustpilot.com/private-reviews-api#reply-to-review)", - version: "0.0.6", + version: "0.1.0", type: "action", props: { trustpilot, From 459cc8b8e1d7d963ccab337f87c80f4b9e1fe64b Mon Sep 17 00:00:00 2001 From: Job Nijenhuis Date: Wed, 3 Sep 2025 13:22:29 +0200 Subject: [PATCH 07/24] up version and clean up sources --- .../fetch-product-reviews.mjs | 62 +-- .../fetch-service-reviews.mjs | 106 +--- components/trustpilot/package.json | 2 +- .../trustpilot/sources/common/polling.mjs | 188 ------- .../new-conversations/new-conversations.mjs | 41 -- .../new-product-review-replies.mjs | 72 --- .../new-product-reviews.mjs | 114 +++- .../new-service-review-replies.mjs | 71 --- .../new-service-reviews.mjs | 106 +++- .../updated-conversations.mjs | 42 -- .../updated-product-reviews.mjs | 40 -- .../updated-service-reviews.mjs | 39 -- components/trustpilot/trustpilot.app.mjs | 523 ++++-------------- 13 files changed, 318 insertions(+), 1088 deletions(-) delete mode 100644 components/trustpilot/sources/common/polling.mjs delete mode 100644 components/trustpilot/sources/new-conversations/new-conversations.mjs delete mode 100644 components/trustpilot/sources/new-product-review-replies/new-product-review-replies.mjs delete mode 100644 components/trustpilot/sources/new-service-review-replies/new-service-review-replies.mjs delete mode 100644 components/trustpilot/sources/updated-conversations/updated-conversations.mjs delete mode 100644 components/trustpilot/sources/updated-product-reviews/updated-product-reviews.mjs delete mode 100644 components/trustpilot/sources/updated-service-reviews/updated-service-reviews.mjs diff --git a/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs b/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs index 57d922b42f5c8..637297268c099 100644 --- a/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs +++ b/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs @@ -1,10 +1,4 @@ import trustpilot from "../../trustpilot.app.mjs"; -import { makeRequest } from "../../common/api-client.mjs"; -import { ENDPOINTS } from "../../common/constants.mjs"; -import { - buildUrl, - parseProductReview, -} from "../../common/utils.mjs"; export default { key: "trustpilot-fetch-product-reviews", @@ -68,63 +62,21 @@ export default { locale, } = this; - // Validate required parameters - if (!businessUnitId) { - throw new Error("Business Unit ID is required"); - } - try { - // Build the endpoint URL - const endpoint = buildUrl(ENDPOINTS.PRIVATE_PRODUCT_REVIEWS, { + // Use the shared method from the app + const result = await this.trustpilot.fetchProductReviews({ businessUnitId, - }); - - // Prepare query parameters - const params = { + page, + perPage, sku, + language, state, locale, - perPage, - page, - includeReportedReviews: false, - language, - }; - - // Make the API request - const response = await makeRequest(this.trustpilot, { - endpoint, - params, }); - // Handle the correct response structure (productReviews, not reviews) - const reviews = response.productReviews?.map(parseProductReview) || []; - const pagination = { - total: response.links?.total || 0, - page: params.page, - perPage: params.perPage, - hasMore: response.links?.next - ? true - : false, - }; - - $.export("$summary", `Successfully fetched ${reviews.length} product review(s) for business unit ${businessUnitId}`); + $.export("$summary", `Successfully fetched ${result.reviews.length} product review(s) for business unit ${businessUnitId}`); - return { - reviews, - pagination, - metadata: { - businessUnitId, - filters: { - sku, - page, - perPage, - state, - locale, - language, - }, - requestTime: new Date().toISOString(), - }, - }; + return result; } catch (error) { throw new Error(`Failed to fetch product reviews: ${error.message}`); } diff --git a/components/trustpilot/actions/fetch-service-reviews/fetch-service-reviews.mjs b/components/trustpilot/actions/fetch-service-reviews/fetch-service-reviews.mjs index be161b169ef49..de27cc8a1bca6 100644 --- a/components/trustpilot/actions/fetch-service-reviews/fetch-service-reviews.mjs +++ b/components/trustpilot/actions/fetch-service-reviews/fetch-service-reviews.mjs @@ -1,11 +1,4 @@ import trustpilot from "../../trustpilot.app.mjs"; -import { makeRequest } from "../../common/api-client.mjs"; -import { ENDPOINTS } from "../../common/constants.mjs"; -import { - buildUrl, - parseServiceReview, - validateBusinessUnitId, -} from "../../common/utils.mjs"; export default { key: "trustpilot-fetch-service-reviews", @@ -182,88 +175,33 @@ export default { findReviewer, } = this; - // Validate required parameters - if (!businessUnitId) { - throw new Error("Business Unit ID is required"); - } - if (!validateBusinessUnitId(businessUnitId)) { - throw new Error("Invalid business unit ID format"); - } - try { - // Build the endpoint URL - const endpoint = buildUrl(ENDPOINTS.SERVICE_REVIEWS, { + // Use the shared method from the app + const result = await this.trustpilot.fetchServiceReviews({ businessUnitId, + stars, + language, + page, + internalLocationId, + perPage, + orderBy, + tagGroup, + tagValue, + ignoreTagValueCase, + responded, + referenceId, + referralEmail, + reported, + startDateTime, + endDateTime, + source, + username, + findReviewer, }); - // Prepare query parameters - const params = {}; + $.export("$summary", `Successfully fetched ${result.reviews.length} service review(s) for business unit ${businessUnitId}`); - // Add optional parameters if provided - if (stars) params.stars = stars; - if (language) params.language = language; - if (page) params.page = page; - if (internalLocationId) params.internalLocationId = internalLocationId; - if (perPage) params.perPage = perPage; - if (orderBy) params.orderBy = orderBy; - if (tagGroup) params.tagGroup = tagGroup; - if (tagValue) params.tagValue = tagValue; - if (ignoreTagValueCase !== undefined) params.ignoreTagValueCase = ignoreTagValueCase; - if (responded !== undefined) params.responded = responded; - if (referenceId) params.referenceId = referenceId; - if (referralEmail) params.referralEmail = referralEmail; - if (reported !== undefined) params.reported = reported; - if (startDateTime) params.startDateTime = startDateTime; - if (endDateTime) params.endDateTime = endDateTime; - if (source) params.source = source; - if (username) params.username = username; - if (findReviewer) params.findReviewer = findReviewer; - - // Make the API request - const response = await makeRequest(this.trustpilot, { - endpoint, - params, - }); - - // Handle the correct response structure (reviews array) - const reviews = response.reviews?.map(parseServiceReview) || []; - const pagination = { - total: response.pagination?.total || 0, - page: response.pagination?.page || page, - perPage: response.pagination?.perPage || perPage, - hasMore: response.pagination?.hasMore || false, - }; - - $.export("$summary", `Successfully fetched ${reviews.length} service review(s) for business unit ${businessUnitId}`); - - return { - reviews, - pagination, - metadata: { - businessUnitId, - filters: { - stars, - language, - page, - internalLocationId, - perPage, - orderBy, - tagGroup, - tagValue, - ignoreTagValueCase, - responded, - referenceId, - referralEmail, - reported, - startDateTime, - endDateTime, - source, - username, - findReviewer, - }, - requestTime: new Date().toISOString(), - }, - }; + return result; } catch (error) { throw new Error(`Failed to fetch service reviews: ${error.message}`); } diff --git a/components/trustpilot/package.json b/components/trustpilot/package.json index 7e03ac2b66435..5c3271fa982b2 100644 --- a/components/trustpilot/package.json +++ b/components/trustpilot/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/trustpilot", - "version": "0.1.3", + "version": "0.2.0", "description": "Pipedream Trustpilot Components", "main": "trustpilot.app.mjs", "keywords": [ diff --git a/components/trustpilot/sources/common/polling.mjs b/components/trustpilot/sources/common/polling.mjs deleted file mode 100644 index 2af5b59d4c30a..0000000000000 --- a/components/trustpilot/sources/common/polling.mjs +++ /dev/null @@ -1,188 +0,0 @@ -import trustpilot from "../../trustpilot.app.mjs"; -import { - POLLING_CONFIG, SOURCE_TYPES, -} from "../../common/constants.mjs"; - -/** - * Base polling source for Trustpilot integration - * - * This integration uses polling instead of webhooks for the following reasons: - * 1. Better reliability - polling ensures no events are missed - * 2. Simpler implementation - no need for webhook endpoint management - * 3. Consistent data retrieval - can backfill historical data if needed - * 4. Works with all authentication methods (API key and OAuth) - * - * All sources poll every 15 minutes by default and maintain deduplication - * to ensure events are only emitted once. - */ -export default { - props: { - trustpilot, - db: "$.service.db", - timer: { - type: "$.interface.timer", - default: { - intervalSeconds: POLLING_CONFIG.DEFAULT_TIMER_INTERVAL_SECONDS, - }, - }, - businessUnitId: { - propDefinition: [ - trustpilot, - "businessUnitId", - ], - optional: true, - description: "Business Unit ID to filter events for. If not provided, will receive events for all business units.", - }, - }, - methods: { - _getLastPolled() { - return this.db.get("lastPolled"); - }, - _setLastPolled(timestamp) { - this.db.set("lastPolled", timestamp); - }, - _getSeenItems() { - return this.db.get("seenItems") || {}; - }, - _setSeenItems(seenItems) { - this.db.set("seenItems", seenItems); - }, - _cleanupSeenItems(seenItems, hoursToKeep = 72) { - const cutoff = Date.now() - (hoursToKeep * 60 * 60 * 1000); - const cleaned = {}; - - Object.entries(seenItems).forEach(([ - key, - timestamp, - ]) => { - if (timestamp > cutoff) { - cleaned[key] = timestamp; - } - }); - - return cleaned; - }, - getSourceType() { - // Override in child classes - return SOURCE_TYPES.NEW_REVIEWS; - }, - getPollingMethod() { - // Override in child classes to return the app method to call - throw new Error("getPollingMethod must be implemented in child class"); - }, - getPollingParams() { - // Override in child classes to return method-specific parameters - return { - businessUnitId: this.businessUnitId, - limit: POLLING_CONFIG.MAX_ITEMS_PER_POLL, - sortBy: "createdat.desc", // Most recent first - }; - }, - isNewItem(item, sourceType) { - // For "new" sources, check creation date - // For "updated" sources, check update date - const itemDate = sourceType.includes("updated") - ? new Date(item.updatedAt) - : new Date(item.createdAt || item.updatedAt); - - const lastPolled = this._getLastPolled(); - return !lastPolled || itemDate > new Date(lastPolled); - }, - generateDedupeKey(item, sourceType) { - // Create unique key: itemId + relevant timestamp - const timestamp = sourceType.includes("updated") - ? item.updatedAt - : (item.createdAt || item.updatedAt); - - return `${item.id}_${timestamp}`; - }, - generateMeta(item, sourceType) { - const dedupeKey = this.generateDedupeKey(item, sourceType); - const summary = this.generateSummary(item); - const timestamp = sourceType.includes("updated") - ? item.updatedAt - : (item.createdAt || item.updatedAt); - - return { - id: dedupeKey, - summary, - ts: new Date(timestamp).getTime(), - }; - }, - generateSummary(item) { - // Override in child classes for specific summaries - return `${this.getSourceType()} - ${item.id}`; - }, - async fetchItems() { - const method = this.getPollingMethod(); - const params = this.getPollingParams(); - - try { - const result = await this.trustpilot[method](params); - - // Handle different response formats - if (result.reviews) { - return result.reviews; - } else if (result.conversations) { - return result.conversations; - } else if (Array.isArray(result)) { - return result; - } else { - return []; - } - } catch (error) { - console.error(`Error fetching items with ${method}:`, error); - throw error; - } - }, - async pollForItems() { - const sourceType = this.getSourceType(); - const lastPolled = this._getLastPolled(); - const seenItems = this._getSeenItems(); - - // If first run, look back 24 hours - const lookbackMs = POLLING_CONFIG.LOOKBACK_HOURS * 60 * 60 * 1000; - const since = lastPolled || new Date(Date.now() - lookbackMs).toISOString(); - - console.log(`Polling for ${sourceType} since ${since}`); - - try { - const items = await this.fetchItems(since); - const newItems = []; - const currentTime = Date.now(); - - for (const item of items) { - // Check if item is new based on source type - if (this.isNewItem(item, sourceType)) { - const dedupeKey = this.generateDedupeKey(item, sourceType); - - // Check if we've already seen this exact item+timestamp - if (!seenItems[dedupeKey]) { - seenItems[dedupeKey] = currentTime; - newItems.push(item); - } - } - } - - // Emit new items - for (const item of newItems.reverse()) { // Oldest first - const meta = this.generateMeta(item, sourceType); - this.$emit(item, meta); - } - - // Update state - this._setLastPolled(new Date().toISOString()); - this._setSeenItems(this._cleanupSeenItems(seenItems)); - - console.log(`Found ${newItems.length} new items of type ${sourceType}`); - - } catch (error) { - console.error(`Polling failed for ${sourceType}:`, error); - throw error; - } - }, - }, - async run() { - await this.pollForItems(); - }, -}; diff --git a/components/trustpilot/sources/new-conversations/new-conversations.mjs b/components/trustpilot/sources/new-conversations/new-conversations.mjs deleted file mode 100644 index b47affe831f41..0000000000000 --- a/components/trustpilot/sources/new-conversations/new-conversations.mjs +++ /dev/null @@ -1,41 +0,0 @@ -import { - SORT_OPTIONS, - SOURCE_TYPES, -} from "../../common/constants.mjs"; -import common from "../common/polling.mjs"; - -export default { - ...common, - key: "trustpilot-new-conversations", - name: "New Conversations", - description: "Emit new event when a new conversation is started on Trustpilot. This source periodically polls the Trustpilot API to detect new customer-business conversations. Each event contains conversation details including participants, subject, business unit, and creation timestamp. Useful for tracking customer inquiries, support requests, and maintaining real-time communication with customers.", - version: "0.0.4", - type: "source", - dedupe: "unique", - methods: { - ...common.methods, - getSourceType() { - return SOURCE_TYPES.NEW_CONVERSATIONS; - }, - getPollingMethod() { - return "getConversations"; - }, - getPollingParams() { - return { - businessUnitId: this.businessUnitId, - limit: 100, - sortBy: SORT_OPTIONS.CREATED_AT_DESC, - offset: 0, - }; - }, - generateSummary(item) { - const participantName = item.participants?.[0]?.displayName || - item.consumer?.displayName || - "Anonymous"; - const subject = item.subject || item.title || "New conversation"; - const businessUnit = item.businessUnit?.displayName || this.businessUnitId || "Unknown"; - - return `New conversation "${subject}" started by ${participantName} (${businessUnit})`; - }, - }, -}; diff --git a/components/trustpilot/sources/new-product-review-replies/new-product-review-replies.mjs b/components/trustpilot/sources/new-product-review-replies/new-product-review-replies.mjs deleted file mode 100644 index 0446c732f5f3a..0000000000000 --- a/components/trustpilot/sources/new-product-review-replies/new-product-review-replies.mjs +++ /dev/null @@ -1,72 +0,0 @@ -import { - SORT_OPTIONS, - SOURCE_TYPES, -} from "../../common/constants.mjs"; -import common from "../common/polling.mjs"; - -export default { - ...common, - key: "trustpilot-new-product-review-replies", - name: "New Product Review Replies", - description: "Emit new event when a business replies to a product review on Trustpilot. This source periodically polls the Trustpilot API to detect new replies to product reviews. Each event includes the reply text, creation timestamp, and associated review details (product name, star rating, consumer info). Ideal for monitoring business responses to customer feedback, tracking customer service performance, and ensuring timely engagement with product reviews.", - version: "0.0.4", - type: "source", - dedupe: "unique", - methods: { - ...common.methods, - getSourceType() { - return SOURCE_TYPES.NEW_REPLIES; - }, - getPollingMethod() { - return "getProductReviews"; - }, - getPollingParams() { - return { - businessUnitId: this.businessUnitId, - limit: 100, - sortBy: SORT_OPTIONS.UPDATED_AT_DESC, // Use updated to catch new replies - offset: 0, - }; - }, - async fetchItems() { - const result = await this.trustpilot.getProductReviews(this.getPollingParams()); - - // Filter for reviews that have replies and extract the replies - const repliesWithReviews = []; - - if (result.reviews) { - for (const review of result.reviews) { - if (review.company?.reply) { - // Create a pseudo-reply object that includes review context - repliesWithReviews.push({ - id: `reply_${review.id}`, - reviewId: review.id, - text: review.company.reply.text, - createdAt: review.company.reply.createdAt, - updatedAt: review.company.reply.createdAt, // Replies don't get updated - review: { - id: review.id, - title: review.title, - stars: review.stars, - consumer: review.consumer, - product: review.product, - }, - }); - } - } - } - - return repliesWithReviews; - }, - generateSummary(item) { - const productName = item.review?.product?.title || "Unknown Product"; - const consumerName = item.review?.consumer?.displayName || "Anonymous"; - const replyPreview = item.text?.substring(0, 50) || ""; - const preview = replyPreview.length > 50 - ? `${replyPreview}...` - : replyPreview; - - return `New reply to product "${productName}" review by ${consumerName}: "${preview}"`; - }, - }, -}; diff --git a/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs b/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs index 238967e963e74..7027ee9fac784 100644 --- a/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs +++ b/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs @@ -1,40 +1,104 @@ -import { - SORT_OPTIONS, - SOURCE_TYPES, -} from "../../common/constants.mjs"; -import common from "../common/polling.mjs"; +import trustpilot from "../../trustpilot.app.mjs"; export default { - ...common, key: "trustpilot-new-product-reviews", name: "New Product Reviews", description: "Emit new event when a customer posts a new product review on Trustpilot. This source periodically polls the Trustpilot API to detect new product reviews. Each event contains the complete review data including star rating, review text, product information, consumer details, and timestamps. Perfect for monitoring product feedback, analyzing customer satisfaction trends, and triggering automated responses or alerts for specific products.", - version: "0.0.4", + version: "0.1.0", type: "source", dedupe: "unique", - methods: { - ...common.methods, - getSourceType() { - return SOURCE_TYPES.NEW_REVIEWS; + props: { + trustpilot, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: 15 * 60, // 15 minutes + }, }, - getPollingMethod() { - return "getProductReviews"; + businessUnitId: { + propDefinition: [ + trustpilot, + "businessUnitId", + ], }, - getPollingParams() { - return { - businessUnitId: this.businessUnitId, - limit: 100, - sortBy: SORT_OPTIONS.CREATED_AT_DESC, - offset: 0, - }; + }, + methods: { + _getLastReviewTime() { + return this.db.get("lastReviewTime"); }, - generateSummary(item) { - const stars = item.stars || "N/A"; - const consumerName = item.consumer?.displayName || "Anonymous"; - const productName = item.product?.title || "Unknown Product"; - const businessUnit = item.businessUnit?.displayName || this.businessUnitId || "Unknown"; + _setLastReviewTime(time) { + this.db.set("lastReviewTime", time); + }, + generateSummary(review) { + const stars = review.stars || "N/A"; + const consumerName = review.consumer?.displayName || "Anonymous"; + const productName = review.product?.title || "Unknown Product"; + const businessUnit = review.businessUnit?.displayName || this.businessUnitId || "Unknown"; return `New ${stars}-star product review by ${consumerName} for "${productName}" (${businessUnit})`; }, }, + async run() { + try { + // Get the last review time for filtering new reviews + const lastReviewTime = this._getLastReviewTime(); + + // Use the fetch-product-reviews action to get reviews + // Note: Product reviews API doesn't support time-based filtering, + // so we'll rely on pagination and client-side filtering + const fetchParams = { + businessUnitId: this.businessUnitId, + perPage: 100, + page: 1, + }; + + // Use the shared method from the app directly + const result = await this.trustpilot.fetchProductReviews(fetchParams); + + const reviews = result.reviews || []; + + if (!reviews.length) { + console.log("No new product reviews found"); + return; + } + + // Filter for new reviews since last poll (client-side filtering) + let newReviews = reviews; + if (lastReviewTime) { + newReviews = reviews.filter((review) => + new Date(review.createdAt).getTime() > new Date(lastReviewTime).getTime()); + } + + if (!newReviews.length) { + console.log("No new product reviews since last poll"); + return; + } + + // Track the latest review time + let latestReviewTime = lastReviewTime; + + for (const review of newReviews) { + // Track the latest review time + if (!latestReviewTime || review.createdAt > latestReviewTime) { + latestReviewTime = review.createdAt; + } + + // Emit the review with unique ID and summary + this.$emit(review, { + id: review.id, + summary: this.generateSummary(review), + ts: new Date(review.createdAt).getTime(), + }); + } + + // Update the last review time for next poll + if (latestReviewTime && latestReviewTime !== lastReviewTime) { + this._setLastReviewTime(latestReviewTime); + } + + } catch (error) { + throw new Error(`Failed to fetch new product reviews: ${error.message}`); + } + }, }; diff --git a/components/trustpilot/sources/new-service-review-replies/new-service-review-replies.mjs b/components/trustpilot/sources/new-service-review-replies/new-service-review-replies.mjs deleted file mode 100644 index f1ce68a558546..0000000000000 --- a/components/trustpilot/sources/new-service-review-replies/new-service-review-replies.mjs +++ /dev/null @@ -1,71 +0,0 @@ -import { - SORT_OPTIONS, - SOURCE_TYPES, -} from "../../common/constants.mjs"; -import common from "../common/polling.mjs"; - -export default { - ...common, - key: "trustpilot-new-service-review-replies", - name: "New Service Review Replies", - description: "Emit new event when a business replies to a service review on Trustpilot. This source periodically polls the Trustpilot API to detect new replies to service reviews. Each event includes the reply text, creation timestamp, and associated review details (star rating, review title, consumer info). Essential for tracking business engagement with customer feedback, monitoring response times, and ensuring all service reviews receive appropriate attention.", - version: "0.0.4", - type: "source", - dedupe: "unique", - methods: { - ...common.methods, - getSourceType() { - return SOURCE_TYPES.NEW_REPLIES; - }, - getPollingMethod() { - return "getServiceReviews"; - }, - getPollingParams() { - return { - businessUnitId: this.businessUnitId, - limit: 100, - sortBy: SORT_OPTIONS.UPDATED_AT_DESC, // Use updated to catch new replies - offset: 0, - }; - }, - async fetchItems() { - const result = await this.trustpilot.getServiceReviews(this.getPollingParams()); - - // Filter for reviews that have replies and extract the replies - const repliesWithReviews = []; - - if (result.reviews) { - for (const review of result.reviews) { - if (review.company?.reply) { - // Create a pseudo-reply object that includes review context - repliesWithReviews.push({ - id: `reply_${review.id}`, - reviewId: review.id, - text: review.company.reply.text, - createdAt: review.company.reply.createdAt, - updatedAt: review.company.reply.createdAt, // Replies don't get updated - review: { - id: review.id, - title: review.title, - stars: review.stars, - consumer: review.consumer, - }, - }); - } - } - } - - return repliesWithReviews; - }, - generateSummary(item) { - const reviewTitle = item.review?.title || "Review"; - const consumerName = item.review?.consumer?.displayName || "Anonymous"; - const replyPreview = item.text?.substring(0, 50) || ""; - const preview = replyPreview.length > 50 - ? `${replyPreview}...` - : replyPreview; - - return `New reply to "${reviewTitle}" by ${consumerName}: "${preview}"`; - }, - }, -}; diff --git a/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs b/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs index 91b3e60fece49..8cf62ff3e1002 100644 --- a/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs +++ b/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs @@ -1,40 +1,94 @@ -import { - SORT_OPTIONS, - SOURCE_TYPES, -} from "../../common/constants.mjs"; -import common from "../common/polling.mjs"; +import trustpilot from "../../trustpilot.app.mjs"; export default { - ...common, key: "trustpilot-new-service-reviews", name: "New Service Reviews", - description: "Emit new event when a customer posts a new service review on Trustpilot. This source periodically polls the Trustpilot API to detect new service reviews, combining both public and private reviews for comprehensive coverage. Each event contains the complete review data including star rating, review text, consumer details, business unit info, and timestamps. Ideal for monitoring overall business reputation, tracking customer satisfaction metrics, and triggering workflows based on review ratings or content.", - version: "0.0.4", + description: "Emit new event when a customer posts a new service review on Trustpilot. This source periodically polls the Trustpilot API to detect new service reviews using the private reviews API for comprehensive coverage. Each event contains the complete review data including star rating, review text, consumer details, business unit info, customer email, and timestamps. Ideal for monitoring overall business reputation, tracking customer satisfaction metrics, and triggering workflows based on review ratings or content.", + version: "0.1.0", type: "source", dedupe: "unique", - methods: { - ...common.methods, - getSourceType() { - return SOURCE_TYPES.NEW_REVIEWS; + props: { + trustpilot, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: 15 * 60, // 15 minutes + }, }, - getPollingMethod() { - // Use private endpoint first as it has more data, fallback to public if needed - return "getServiceReviews"; + businessUnitId: { + propDefinition: [ + trustpilot, + "businessUnitId", + ], }, - getPollingParams() { - return { - businessUnitId: this.businessUnitId, - limit: 100, - sortBy: SORT_OPTIONS.CREATED_AT_DESC, - offset: 0, - }; + }, + methods: { + _getLastReviewTime() { + return this.db.get("lastReviewTime"); }, - generateSummary(item) { - const stars = item.stars || "N/A"; - const consumerName = item.consumer?.displayName || "Anonymous"; - const businessUnit = item.businessUnit?.displayName || this.businessUnitId || "Unknown"; + _setLastReviewTime(time) { + this.db.set("lastReviewTime", time); + }, + generateSummary(review) { + const stars = review.stars || "N/A"; + const consumerName = review.consumer?.displayName || "Anonymous"; + const businessUnit = review.businessUnit?.displayName || this.businessUnitId || "Unknown"; return `New ${stars}-star service review by ${consumerName} for ${businessUnit}`; }, }, + async run() { + try { + // Get the last review time for filtering new reviews + const lastReviewTime = this._getLastReviewTime(); + + // Use the fetch-service-reviews action to get reviews + const fetchParams = { + businessUnitId: this.businessUnitId, + perPage: 100, + orderBy: "createdat.desc", + }; + + // If we have a last review time, filter for reviews after that time + if (lastReviewTime) { + fetchParams.startDateTime = lastReviewTime; + } + + // Use the shared method from the app directly + const result = await this.trustpilot.fetchServiceReviews(fetchParams); + + const reviews = result.reviews || []; + + if (!reviews.length) { + console.log("No new service reviews found"); + return; + } + + // Emit reviews (already parsed by the action) + let latestReviewTime = lastReviewTime; + + for (const review of reviews) { + // Track the latest review time + if (!latestReviewTime || review.createdAt > latestReviewTime) { + latestReviewTime = review.createdAt; + } + + // Emit the review with unique ID and summary + this.$emit(review, { + id: review.id, + summary: this.generateSummary(review), + ts: new Date(review.createdAt).getTime(), + }); + } + + // Update the last review time for next poll + if (latestReviewTime && latestReviewTime !== lastReviewTime) { + this._setLastReviewTime(latestReviewTime); + } + + } catch (error) { + throw new Error(`Failed to fetch new service reviews: ${error.message}`); + } + }, }; diff --git a/components/trustpilot/sources/updated-conversations/updated-conversations.mjs b/components/trustpilot/sources/updated-conversations/updated-conversations.mjs deleted file mode 100644 index 24140a78c49b9..0000000000000 --- a/components/trustpilot/sources/updated-conversations/updated-conversations.mjs +++ /dev/null @@ -1,42 +0,0 @@ -import { - SORT_OPTIONS, - SOURCE_TYPES, -} from "../../common/constants.mjs"; -import common from "../common/polling.mjs"; - -export default { - ...common, - key: "trustpilot-updated-conversations", - name: "New Updated Conversations", - description: "Emit new event when an existing conversation is updated with new messages on Trustpilot. This source periodically polls the Trustpilot API to detect conversations that have received new messages. Each event contains updated conversation details including participants, subject, message count, and latest update timestamp. Useful for tracking ongoing customer interactions, ensuring timely responses to follow-up messages, and maintaining conversation continuity.", - version: "0.0.4", - type: "source", - dedupe: "unique", - methods: { - ...common.methods, - getSourceType() { - return SOURCE_TYPES.UPDATED_CONVERSATIONS; - }, - getPollingMethod() { - return "getConversations"; - }, - getPollingParams() { - return { - businessUnitId: this.businessUnitId, - limit: 100, - sortBy: SORT_OPTIONS.UPDATED_AT_DESC, - offset: 0, - }; - }, - generateSummary(item) { - const participantName = item.participants?.[0]?.displayName || - item.consumer?.displayName || - "Anonymous"; - const subject = item.subject || item.title || "Conversation"; - const businessUnit = item.businessUnit?.displayName || this.businessUnitId || "Unknown"; - const messageCount = item.messageCount || item.messages?.length || "Unknown"; - - return `Conversation "${subject}" updated by ${participantName} (${messageCount} messages) - ${businessUnit}`; - }, - }, -}; diff --git a/components/trustpilot/sources/updated-product-reviews/updated-product-reviews.mjs b/components/trustpilot/sources/updated-product-reviews/updated-product-reviews.mjs deleted file mode 100644 index dd0f0e0d3eb83..0000000000000 --- a/components/trustpilot/sources/updated-product-reviews/updated-product-reviews.mjs +++ /dev/null @@ -1,40 +0,0 @@ -import { - SORT_OPTIONS, - SOURCE_TYPES, -} from "../../common/constants.mjs"; -import common from "../common/polling.mjs"; - -export default { - ...common, - key: "trustpilot-updated-product-reviews", - name: "New Updated Product Reviews", - description: "Emit new event when an existing product review is updated or revised on Trustpilot. This source periodically polls the Trustpilot API to detect product reviews that have been modified. Each event contains the updated review data including any changes to star rating, review text, or other review attributes. Perfect for tracking review modifications, monitoring changes in customer sentiment, and ensuring product feedback accuracy over time.", - version: "0.0.4", - type: "source", - dedupe: "unique", - methods: { - ...common.methods, - getSourceType() { - return SOURCE_TYPES.UPDATED_REVIEWS; - }, - getPollingMethod() { - return "getProductReviews"; - }, - getPollingParams() { - return { - businessUnitId: this.businessUnitId, - limit: 100, - sortBy: SORT_OPTIONS.UPDATED_AT_DESC, - offset: 0, - }; - }, - generateSummary(item) { - const stars = item.stars || "N/A"; - const consumerName = item.consumer?.displayName || "Anonymous"; - const productName = item.product?.title || "Unknown Product"; - const businessUnit = item.businessUnit?.displayName || this.businessUnitId || "Unknown"; - - return `Product review updated by ${consumerName} (${stars} stars) for "${productName}" (${businessUnit})`; - }, - }, -}; diff --git a/components/trustpilot/sources/updated-service-reviews/updated-service-reviews.mjs b/components/trustpilot/sources/updated-service-reviews/updated-service-reviews.mjs deleted file mode 100644 index 9e9a1b5815701..0000000000000 --- a/components/trustpilot/sources/updated-service-reviews/updated-service-reviews.mjs +++ /dev/null @@ -1,39 +0,0 @@ -import { - SORT_OPTIONS, - SOURCE_TYPES, -} from "../../common/constants.mjs"; -import common from "../common/polling.mjs"; - -export default { - ...common, - key: "trustpilot-updated-service-reviews", - name: "New Updated Service Reviews", - description: "Emit new event when an existing service review is updated or revised on Trustpilot. This source periodically polls the Trustpilot API to detect service reviews that have been modified. Each event contains the updated review data including any changes to star rating, review text, or other review attributes. Essential for tracking review modifications, monitoring evolving customer feedback, and identifying patterns in review updates.", - version: "0.0.4", - type: "source", - dedupe: "unique", - methods: { - ...common.methods, - getSourceType() { - return SOURCE_TYPES.UPDATED_REVIEWS; - }, - getPollingMethod() { - return "getServiceReviews"; - }, - getPollingParams() { - return { - businessUnitId: this.businessUnitId, - limit: 100, - sortBy: SORT_OPTIONS.UPDATED_AT_DESC, - offset: 0, - }; - }, - generateSummary(item) { - const stars = item.stars || "N/A"; - const consumerName = item.consumer?.displayName || "Anonymous"; - const businessUnit = item.businessUnit?.displayName || this.businessUnitId || "Unknown"; - - return `Service review updated by ${consumerName} (${stars} stars) for ${businessUnit}`; - }, - }, -}; diff --git a/components/trustpilot/trustpilot.app.mjs b/components/trustpilot/trustpilot.app.mjs index 440d9e727c2ce..42a7df95af920 100644 --- a/components/trustpilot/trustpilot.app.mjs +++ b/components/trustpilot/trustpilot.app.mjs @@ -1,17 +1,11 @@ import { defineApp } from "@pipedream/types"; import { makeRequest } from "./common/api-client.mjs"; -import { - DEFAULT_LIMIT, - ENDPOINTS, - MAX_LIMIT, - SORT_OPTIONS, -} from "./common/constants.mjs"; +import { ENDPOINTS } from "./common/constants.mjs"; import { buildUrl, - parseReview, - sanitizeInput, + parseServiceReview, + parseProductReview, validateBusinessUnitId, - validateReviewId, } from "./common/utils.mjs"; export default defineApp({ @@ -65,40 +59,21 @@ export default defineApp({ description: "Filter by product URL", optional: true, }, - sortBy: { - type: "string", - label: "Sort By", - description: "How to sort the results", - options: Object.entries(SORT_OPTIONS).map(([ - key, - value, - ]) => ({ - label: key.replace(/_/g, " ").toLowerCase(), - value, - })), - optional: true, - default: SORT_OPTIONS.CREATED_AT_DESC, - }, - limit: { + page: { type: "integer", - label: "Limit", - description: "Maximum number of results to return", + label: "Page", + description: "The page to retrieve", min: 1, - max: MAX_LIMIT, - default: DEFAULT_LIMIT, + default: 1, optional: true, }, - includeReportedReviews: { - type: "boolean", - label: "Include Reported Reviews", - description: "Whether to include reviews that have been reported", - default: false, - optional: true, - }, - tags: { - type: "string[]", - label: "Tags", - description: "Filter reviews by tags", + perPage: { + type: "integer", + label: "Per Page", + description: "The number of items to retrieve per page", + min: 1, + max: 100, + default: 20, optional: true, }, language: { @@ -139,411 +114,151 @@ export default defineApp({ return response.businessUnits || []; }, - // Public Review methods (no auth required for basic info) - async getPublicServiceReviews({ - businessUnitId, - stars = null, - sortBy = SORT_OPTIONS.CREATED_AT_DESC, - limit = DEFAULT_LIMIT, - offset = 0, - tags = [], - language = null, - }) { - if (!validateBusinessUnitId(businessUnitId)) { - throw new Error("Invalid business unit ID"); - } - - const endpoint = buildUrl(ENDPOINTS.PUBLIC_REVIEWS, { + // Shared method for fetching service reviews - used by both actions and sources + async fetchServiceReviews(params = {}) { + const { businessUnitId, - }); - const params = { stars, - orderBy: sortBy, - perPage: limit, - page: Math.floor(offset / limit) + 1, language, - }; - - if (tags.length > 0) { - params.tags = tags.join(","); - } - - const response = await this._makeRequestWithRetry({ - endpoint, - params, - }); - - return { - reviews: response.reviews?.map(parseReview) || [], - pagination: { - total: response.pagination?.total || 0, - page: response.pagination?.page || 1, - perPage: response.pagination?.perPage || limit, - hasMore: response.pagination?.hasMore || false, - }, - }; - }, - - async getPublicServiceReviewById({ - businessUnitId, reviewId, - }) { - if (!validateBusinessUnitId(businessUnitId)) { - throw new Error("Invalid business unit ID"); + page = 1, + internalLocationId, + perPage = 20, + orderBy = "createdat.desc", + tagGroup, + tagValue, + ignoreTagValueCase = false, + responded, + referenceId, + referralEmail, + reported, + startDateTime, + endDateTime, + source, + username, + findReviewer, + } = params; + + // Validate required parameters + if (!businessUnitId) { + throw new Error("Business Unit ID is required"); } - if (!validateReviewId(reviewId)) { - throw new Error("Invalid review ID"); - } - - const endpoint = buildUrl(ENDPOINTS.PUBLIC_REVIEW_BY_ID, { - businessUnitId, - reviewId, - }); - const response = await this._makeRequest({ - endpoint, - }); - return parseReview(response); - }, - - // Service Review methods - simplified for sources - async getServiceReviews({ - businessUnitId, - stars = null, - sortBy = SORT_OPTIONS.CREATED_AT_DESC, - limit = DEFAULT_LIMIT, - offset = 0, - tags = [], - language = null, - } = {}) { if (!validateBusinessUnitId(businessUnitId)) { - throw new Error("Invalid business unit ID"); + throw new Error("Invalid business unit ID format"); } - const endpoint = buildUrl(ENDPOINTS.PUBLIC_REVIEWS, { + // Build the endpoint URL + const endpoint = buildUrl(ENDPOINTS.SERVICE_REVIEWS, { businessUnitId, }); - const params = { - stars, - orderBy: sortBy, - perPage: limit, - page: Math.floor(offset / limit) + 1, - language, - }; - - if (tags && tags.length > 0) { - params.tags = Array.isArray(tags) - ? tags.join(",") - : tags; - } - + // Prepare query parameters + const queryParams = {}; + + // Add optional parameters if provided + if (stars) queryParams.stars = stars; + if (language) queryParams.language = language; + if (page) queryParams.page = page; + if (internalLocationId) queryParams.internalLocationId = internalLocationId; + if (perPage) queryParams.perPage = perPage; + if (orderBy) queryParams.orderBy = orderBy; + if (tagGroup) queryParams.tagGroup = tagGroup; + if (tagValue) queryParams.tagValue = tagValue; + if (ignoreTagValueCase !== undefined) queryParams.ignoreTagValueCase = ignoreTagValueCase; + if (responded !== undefined) queryParams.responded = responded; + if (referenceId) queryParams.referenceId = referenceId; + if (referralEmail) queryParams.referralEmail = referralEmail; + if (reported !== undefined) queryParams.reported = reported; + if (startDateTime) queryParams.startDateTime = startDateTime; + if (endDateTime) queryParams.endDateTime = endDateTime; + if (source) queryParams.source = source; + if (username) queryParams.username = username; + if (findReviewer) queryParams.findReviewer = findReviewer; + + // Make the API request const response = await makeRequest(this, { endpoint, - params, + params: queryParams, }); + // Handle the correct response structure (reviews array) + const reviews = response.reviews?.map(parseServiceReview) || []; + const pagination = { + total: response.pagination?.total || 0, + page: response.pagination?.page || page, + perPage: response.pagination?.perPage || perPage, + hasMore: response.pagination?.hasMore || false, + }; + return { - reviews: response.reviews?.map(parseReview) || [], - pagination: { - total: response.pagination?.total || 0, - page: response.pagination?.page || 1, - perPage: response.pagination?.perPage || limit, - hasMore: response.pagination?.hasMore || false, + reviews, + pagination, + metadata: { + businessUnitId, + filters: queryParams, + requestTime: new Date().toISOString(), }, }; }, - async getServiceReviewById({ - businessUnitId, reviewId, - }) { - if (!validateBusinessUnitId(businessUnitId)) { - throw new Error("Invalid business unit ID"); - } - if (!validateReviewId(reviewId)) { - throw new Error("Invalid review ID"); - } - - const endpoint = buildUrl(ENDPOINTS.SERVICE_REVIEW_BY_ID, { + // Shared method for fetching product reviews - used by both actions and sources + async fetchProductReviews(params = {}) { + const { businessUnitId, - reviewId, - }); - const response = await this._makeRequest({ - endpoint, - }); - return parseReview(response); - }, - - async replyToServiceReview({ - businessUnitId, reviewId, message, - }) { - if (!validateBusinessUnitId(businessUnitId)) { - throw new Error("Invalid business unit ID"); - } - if (!validateReviewId(reviewId)) { - throw new Error("Invalid review ID"); - } - if (!message || typeof message !== "string") { - throw new Error("Reply message is required"); - } - - // Sanitize and validate message length (Trustpilot limit is 5000 characters) - const sanitizedMessage = sanitizeInput(message, 5000); - if (sanitizedMessage.length === 0) { - throw new Error("Reply message cannot be empty after sanitization"); - } - - const endpoint = buildUrl(ENDPOINTS.REPLY_TO_SERVICE_REVIEW, { - businessUnitId, - reviewId, - }); - const response = await this._makeRequest({ - endpoint, - method: "POST", - data: { - message: sanitizedMessage, - }, - }); - return response; - }, + page, + perPage, + sku, + language, + state, + locale, + } = params; - // Product Review methods - simplified for sources - async getProductReviews({ - businessUnitId, - sku = null, - productUrl = null, - stars = null, - sortBy = SORT_OPTIONS.CREATED_AT_DESC, - limit = DEFAULT_LIMIT, - offset = 0, - includeReportedReviews = false, - tags = [], - language = null, - } = {}) { - if (!validateBusinessUnitId(businessUnitId)) { - throw new Error("Invalid business unit ID"); + // Validate required parameters + if (!businessUnitId) { + throw new Error("Business Unit ID is required"); } + // Build the endpoint URL const endpoint = buildUrl(ENDPOINTS.PRIVATE_PRODUCT_REVIEWS, { businessUnitId, }); - const params = { + // Prepare query parameters + const queryParams = { sku, - productUrl, - stars, - orderBy: sortBy, - perPage: limit, - page: Math.floor(offset / limit) + 1, - includeReportedReviews, + state, + locale, + perPage, + page, + includeReportedReviews: false, language, }; - if (tags && tags.length > 0) { - params.tags = Array.isArray(tags) - ? tags.join(",") - : tags; - } - + // Make the API request const response = await makeRequest(this, { endpoint, - params, + params: queryParams, }); - return { - reviews: response.productReviews?.map(parseReview) || [], // Note: productReviews not reviews - pagination: { - total: response.links?.total || 0, - page: params.page, - perPage: params.perPage, - hasMore: response.links?.next - ? true - : false, - }, + // Handle the correct response structure (productReviews, not reviews) + const reviews = response.productReviews?.map(parseProductReview) || []; + const pagination = { + total: response.links?.total || 0, + page: queryParams.page || 1, + perPage: queryParams.perPage || 20, + hasMore: response.links?.next + ? true + : false, }; - }, - - async getProductReviewById({ reviewId }) { - if (!validateReviewId(reviewId)) { - throw new Error("Invalid review ID"); - } - - const endpoint = buildUrl(ENDPOINTS.PRIVATE_PRODUCT_REVIEW_BY_ID, { - reviewId, - }); - const response = await this._makeRequest({ - endpoint, - }); - return parseReview(response); - }, - - async replyToProductReview({ - reviewId, message, - }) { - if (!validateReviewId(reviewId)) { - throw new Error("Invalid review ID"); - } - if (!message || typeof message !== "string") { - throw new Error("Reply message is required"); - } - - // Sanitize and validate message length (Trustpilot limit is 5000 characters) - const sanitizedMessage = sanitizeInput(message, 5000); - if (sanitizedMessage.length === 0) { - throw new Error("Reply message cannot be empty after sanitization"); - } - - const review = await this.getProductReviewById({ - reviewId, - }); - - let conversationId = review.conversationId; - - if (!conversationId) { - const createConversationEndpoint = buildUrl(ENDPOINTS.CREATE_CONVERSATION_FOR_REVIEW, { - reviewId, - }); - const createConversationResponse = await this._makeRequest({ - endpoint: createConversationEndpoint, - method: "POST", - }); - conversationId = createConversationResponse.conversationId; - } - - const replyToConversationEndpoint = buildUrl(ENDPOINTS.REPLY_TO_CONVERSATION, { - conversationId, - }); - const response = await this._makeRequest({ - endpoint: replyToConversationEndpoint, - method: "POST", - data: { - message: sanitizedMessage, - }, - }); - return response; - }, - - // Conversation methods - async getConversations({ - limit = DEFAULT_LIMIT, - offset = 0, - sortBy = SORT_OPTIONS.CREATED_AT_DESC, - businessUnitId = null, - } = {}) { - const params = { - perPage: limit, - page: Math.floor(offset / limit) + 1, - orderBy: sortBy, - }; - - if (businessUnitId) { - params.businessUnitId = businessUnitId; - } - - const response = await this._makeRequestWithRetry({ - endpoint: ENDPOINTS.CONVERSATIONS, - params, - }); return { - conversations: response.conversations || [], - pagination: { - total: response.pagination?.total || 0, - page: response.pagination?.page || 1, - perPage: response.pagination?.perPage || limit, - hasMore: response.pagination?.hasMore || false, - }, - }; - }, - - async getConversationById({ conversationId }) { - if (!conversationId) { - throw new Error("Invalid conversation ID"); - } - - const endpoint = buildUrl(ENDPOINTS.CONVERSATION_BY_ID, { - conversationId, - }); - const response = await this._makeRequest({ - endpoint, - }); - return response; - }, - - async replyToConversation({ - conversationId, message, - }) { - if (!conversationId) { - throw new Error("Invalid conversation ID"); - } - if (!message || typeof message !== "string") { - throw new Error("Reply message is required"); - } - - // Sanitize and validate message length (Trustpilot limit is 5000 characters) - const sanitizedMessage = sanitizeInput(message, 5000); - if (sanitizedMessage.length === 0) { - throw new Error("Reply message cannot be empty after sanitization"); - } - - const endpoint = buildUrl(ENDPOINTS.REPLY_TO_CONVERSATION, { - conversationId, - }); - const response = await this._makeRequest({ - endpoint, - method: "POST", - data: { - message: sanitizedMessage, + reviews, + pagination, + metadata: { + businessUnitId, + filters: queryParams, + requestTime: new Date().toISOString(), }, - }); - return response; - }, - - // Webhook methods - async createWebhook({ - url, events = [], businessUnitId = null, - }) { - if (!url) { - throw new Error("Webhook URL is required"); - } - if (!Array.isArray(events) || events.length === 0) { - throw new Error("At least one event must be specified"); - } - - const data = { - url, - events, }; - - if (businessUnitId) { - data.businessUnitId = businessUnitId; - } - - const response = await this._makeRequest({ - endpoint: ENDPOINTS.WEBHOOKS, - method: "POST", - data, - }); - return response; - }, - - async deleteWebhook(webhookId) { - if (!webhookId) { - throw new Error("Webhook ID is required"); - } - - const endpoint = buildUrl(ENDPOINTS.WEBHOOK_BY_ID, { - webhookId, - }); - await this._makeRequest({ - endpoint, - method: "DELETE", - }); - }, - - async listWebhooks() { - const response = await this._makeRequest({ - endpoint: ENDPOINTS.WEBHOOKS, - }); - return response.webhooks || []; }, }, }); From 5feed610697fbd58170454e2dd2df0bf10a436db Mon Sep 17 00:00:00 2001 From: Job Nijenhuis Date: Wed, 3 Sep 2025 13:30:33 +0200 Subject: [PATCH 08/24] merge --- components/trustpilot/trustpilot.app.mjs | 1 - pnpm-lock.yaml | 144 ++++++++++++++--------- 2 files changed, 86 insertions(+), 59 deletions(-) diff --git a/components/trustpilot/trustpilot.app.mjs b/components/trustpilot/trustpilot.app.mjs index 0664dc52960c8..6c628bcfd47ba 100644 --- a/components/trustpilot/trustpilot.app.mjs +++ b/components/trustpilot/trustpilot.app.mjs @@ -1,4 +1,3 @@ -import { defineApp } from "@pipedream/types"; import { makeRequest } from "./common/api-client.mjs"; import { ENDPOINTS } from "./common/constants.mjs"; import { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 06af5c9380d3f..8ae7608403cf3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2044,8 +2044,7 @@ importers: specifier: ^3.0.0 version: 3.0.3 - components/buddee: - specifiers: {} + components/buddee: {} components/buddy: {} @@ -11010,8 +11009,7 @@ importers: components/postmaster: {} - components/postnl: - specifiers: {} + components/postnl: {} components/power_automate: {} @@ -11389,8 +11387,7 @@ importers: specifier: ^1.2.1 version: 1.6.6 - components/questdb: - specifiers: {} + components/questdb: {} components/questionpro: {} @@ -16764,7 +16761,7 @@ importers: version: 3.1.7 ts-jest: specifier: ^29.2.5 - version: 29.2.5(@babel/core@8.0.0-alpha.13)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@8.0.0-alpha.13))(jest@29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@5.7.2)))(typescript@5.7.2) + version: 29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@5.7.2)))(typescript@5.7.2) tsup: specifier: ^8.3.6 version: 8.3.6(@microsoft/api-extractor@7.47.12(@types/node@20.17.30))(jiti@2.4.2)(postcss@8.5.6)(tsx@4.19.4)(typescript@5.7.2)(yaml@2.8.0) @@ -16807,7 +16804,7 @@ importers: version: 3.1.0 jest: specifier: ^29.1.2 - version: 29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@3.9.10)) + version: 29.7.0(@types/node@24.0.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@24.0.10)(typescript@3.9.10)) type-fest: specifier: ^4.15.0 version: 4.27.0 @@ -37053,7 +37050,7 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@3.9.10))': + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@5.7.2))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -37067,7 +37064,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@3.9.10)) + jest-config: 29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@5.7.2)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -37088,7 +37085,7 @@ snapshots: - supports-color - ts-node - '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@5.7.2))': + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -37102,7 +37099,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@5.7.2)) + jest-config: 29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -37123,7 +37120,7 @@ snapshots: - supports-color - ts-node - '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3))': + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@24.0.10)(typescript@3.9.10))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -37137,7 +37134,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) + jest-config: 29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@24.0.10)(typescript@3.9.10)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -43100,13 +43097,13 @@ snapshots: safe-buffer: 5.2.1 sha.js: 2.4.11 - create-jest@29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@3.9.10)): + create-jest@29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@5.7.2)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@3.9.10)) + jest-config: 29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@5.7.2)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -43115,13 +43112,13 @@ snapshots: - supports-color - ts-node - create-jest@29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@5.7.2)): + create-jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@5.7.2)) + jest-config: 29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -43130,13 +43127,13 @@ snapshots: - supports-color - ts-node - create-jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)): + create-jest@29.7.0(@types/node@24.0.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@24.0.10)(typescript@3.9.10)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) + jest-config: 29.7.0(@types/node@24.0.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@24.0.10)(typescript@3.9.10)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -46838,16 +46835,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@3.9.10)): + jest-cli@29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@5.7.2)): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@3.9.10)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@5.7.2)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@3.9.10)) + create-jest: 29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@5.7.2)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@3.9.10)) + jest-config: 29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@5.7.2)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -46857,16 +46854,16 @@ snapshots: - supports-color - ts-node - jest-cli@29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@5.7.2)): + jest-cli@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@5.7.2)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@5.7.2)) + create-jest: 29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@5.7.2)) + jest-config: 29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -46876,16 +46873,16 @@ snapshots: - supports-color - ts-node - jest-cli@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)): + jest-cli@29.7.0(@types/node@24.0.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@24.0.10)(typescript@3.9.10)): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@24.0.10)(typescript@3.9.10)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) + create-jest: 29.7.0(@types/node@24.0.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@24.0.10)(typescript@3.9.10)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) + jest-config: 29.7.0(@types/node@24.0.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@24.0.10)(typescript@3.9.10)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -46895,7 +46892,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@3.9.10)): + jest-config@29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@5.7.2)): dependencies: '@babel/core': 7.26.0 '@jest/test-sequencer': 29.7.0 @@ -46921,12 +46918,12 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 20.17.30 - ts-node: 10.9.2(@types/node@20.17.30)(typescript@3.9.10) + ts-node: 10.9.2(@types/node@20.17.30)(typescript@5.7.2) transitivePeerDependencies: - babel-plugin-macros - supports-color - jest-config@29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@5.7.2)): + jest-config@29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)): dependencies: '@babel/core': 7.26.0 '@jest/test-sequencer': 29.7.0 @@ -46952,12 +46949,12 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 20.17.30 - ts-node: 10.9.2(@types/node@20.17.30)(typescript@5.7.2) + ts-node: 10.9.2(@types/node@20.17.6)(typescript@5.6.3) transitivePeerDependencies: - babel-plugin-macros - supports-color - jest-config@29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)): + jest-config@29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@24.0.10)(typescript@3.9.10)): dependencies: '@babel/core': 7.26.0 '@jest/test-sequencer': 29.7.0 @@ -46983,7 +46980,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 20.17.30 - ts-node: 10.9.2(@types/node@20.17.6)(typescript@5.6.3) + ts-node: 10.9.2(@types/node@24.0.10)(typescript@3.9.10) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -47019,6 +47016,37 @@ snapshots: - babel-plugin-macros - supports-color + jest-config@29.7.0(@types/node@24.0.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@24.0.10)(typescript@3.9.10)): + dependencies: + '@babel/core': 7.26.0 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.26.0) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0(babel-plugin-macros@3.1.0) + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 24.0.10 + ts-node: 10.9.2(@types/node@24.0.10)(typescript@3.9.10) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-diff@27.5.1: dependencies: chalk: 4.1.2 @@ -47250,36 +47278,36 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@3.9.10)): + jest@29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@5.7.2)): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@3.9.10)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@5.7.2)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@3.9.10)) + jest-cli: 29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@5.7.2)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros - supports-color - ts-node - jest@29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@5.7.2)): + jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@5.7.2)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@5.7.2)) + jest-cli: 29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros - supports-color - ts-node - jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)): + jest@29.7.0(@types/node@24.0.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@24.0.10)(typescript@3.9.10)): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@24.0.10)(typescript@3.9.10)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) + jest-cli: 29.7.0(@types/node@24.0.10)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@24.0.10)(typescript@3.9.10)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -52756,7 +52784,7 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.2.5(@babel/core@8.0.0-alpha.13)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@8.0.0-alpha.13))(jest@29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@5.7.2)))(typescript@5.7.2): + ts-jest@29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.30)(typescript@5.7.2)))(typescript@5.7.2): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 @@ -52770,10 +52798,10 @@ snapshots: typescript: 5.7.2 yargs-parser: 21.1.1 optionalDependencies: - '@babel/core': 8.0.0-alpha.13 + '@babel/core': 7.26.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@8.0.0-alpha.13) + babel-jest: 29.7.0(@babel/core@7.26.0) ts-jest@29.2.5(@babel/core@8.0.0-alpha.13)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@8.0.0-alpha.13))(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)))(typescript@5.6.3): dependencies: @@ -52794,7 +52822,7 @@ snapshots: '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@8.0.0-alpha.13) - ts-node@10.9.2(@types/node@20.17.30)(typescript@3.9.10): + ts-node@10.9.2(@types/node@20.17.30)(typescript@5.7.2): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 @@ -52808,45 +52836,45 @@ snapshots: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 3.9.10 + typescript: 5.7.2 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 optional: true - ts-node@10.9.2(@types/node@20.17.30)(typescript@5.7.2): + ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 20.17.30 + '@types/node': 20.17.6 acorn: 8.14.0 acorn-walk: 8.3.4 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.7.2 + typescript: 5.6.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 optional: true - ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3): + ts-node@10.9.2(@types/node@24.0.10)(typescript@3.9.10): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 20.17.6 + '@types/node': 24.0.10 acorn: 8.14.0 acorn-walk: 8.3.4 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.6.3 + typescript: 3.9.10 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 optional: true From d5f6d9e1af3a6ef9f2078047375dd77a4855d7e6 Mon Sep 17 00:00:00 2001 From: Job Nijenhuis Date: Wed, 3 Sep 2025 16:06:42 +0200 Subject: [PATCH 09/24] fix business ID --- components/trustpilot/trustpilot.app.mjs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/components/trustpilot/trustpilot.app.mjs b/components/trustpilot/trustpilot.app.mjs index 6c628bcfd47ba..2e101b9bd68b9 100644 --- a/components/trustpilot/trustpilot.app.mjs +++ b/components/trustpilot/trustpilot.app.mjs @@ -20,8 +20,15 @@ export default { page, query, }) { try { + if (query === "") { + // Trustpilot requires a query to be passed in, default to "a" if empty + query = "a"; + } + const businessUnits = await this.searchBusinessUnits({ - page, + // Trustpilot requires the page to be 1-indexed + // whereas pipedream is 0-indexed + page: page + 1, query, }); From 2b7761255be637c55538e1abd8f95174a012302c Mon Sep 17 00:00:00 2001 From: Job Nijenhuis Date: Wed, 3 Sep 2025 16:30:20 +0200 Subject: [PATCH 10/24] delete temp action --- .../search-business-units-test.mjs | 66 ------------------- 1 file changed, 66 deletions(-) delete mode 100644 components/trustpilot/actions/search-business-units-test/search-business-units-test.mjs diff --git a/components/trustpilot/actions/search-business-units-test/search-business-units-test.mjs b/components/trustpilot/actions/search-business-units-test/search-business-units-test.mjs deleted file mode 100644 index 2e846fb7ecdf1..0000000000000 --- a/components/trustpilot/actions/search-business-units-test/search-business-units-test.mjs +++ /dev/null @@ -1,66 +0,0 @@ -import trustpilot from "../../trustpilot.app.mjs"; - -export default { - key: "trustpilot-search-business-units-test", - name: "Search Business Units (Test)", - description: "Temporary action to test the searchBusinessUnits implementation. Searches for business units by name or domain.", - version: "0.0.4", - type: "action", - props: { - trustpilot, - query: { - type: "string", - label: "Search Query", - description: "Search term to find business units (name or domain)", - default: "stoov", - optional: true, - }, - page: { - type: "integer", - label: "Page", - description: "Page number for pagination", - min: 1, - default: 1, - optional: true, - }, - }, - async run({ $ }) { - const { - query, - page, - } = this; - - try { - const businessUnits = await this.trustpilot.searchBusinessUnits({ - query, - page, - }); - - const options = businessUnits.map((businessUnit) => { - const { - id, displayName, - } = businessUnit; - - return { - label: displayName, - value: id, - }; - }); - - $.export("$summary", `Found ${businessUnits.length} business unit(s) for query: "${query}"`); - - return { - businessUnits, - options, - metadata: { - query, - page, - count: businessUnits.length, - requestTime: new Date().toISOString(), - }, - }; - } catch (error) { - throw new Error(`Failed to search business units: ${error.message}`); - } - }, -}; From 0e5ec6f3d774187f91cfac5e9a0463ef86ae2a74 Mon Sep 17 00:00:00 2001 From: Job <9075380+Afstkla@users.noreply.github.com> Date: Wed, 3 Sep 2025 16:53:27 +0200 Subject: [PATCH 11/24] Update components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../new-product-reviews/new-product-reviews.mjs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs b/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs index 7027ee9fac784..f568f35c98f6b 100644 --- a/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs +++ b/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs @@ -64,11 +64,11 @@ export default { } // Filter for new reviews since last poll (client-side filtering) - let newReviews = reviews; - if (lastReviewTime) { - newReviews = reviews.filter((review) => - new Date(review.createdAt).getTime() > new Date(lastReviewTime).getTime()); - } + const lastTs = Number(lastReviewTime) || 0; + const toMs = (d) => new Date(d).getTime(); + let newReviews = lastTs + ? reviews.filter((r) => toMs(r.createdAt) > lastTs) + : reviews; if (!newReviews.length) { console.log("No new product reviews since last poll"); From 1672ad5af6882f59790dfd3551314862226f8c73 Mon Sep 17 00:00:00 2001 From: Job <9075380+Afstkla@users.noreply.github.com> Date: Wed, 3 Sep 2025 16:53:58 +0200 Subject: [PATCH 12/24] Update components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../sources/new-product-reviews/new-product-reviews.mjs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs b/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs index f568f35c98f6b..b352e5e362742 100644 --- a/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs +++ b/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs @@ -76,14 +76,13 @@ export default { } // Track the latest review time - let latestReviewTime = lastReviewTime; + // Initialize latestReviewTime as a numeric timestamp (ms) + let latestReviewTime = Number(lastReviewTime) || 0; for (const review of newReviews) { // Track the latest review time - if (!latestReviewTime || review.createdAt > latestReviewTime) { - latestReviewTime = review.createdAt; - } - + const createdTs = new Date(review.createdAt).getTime(); + if (createdTs > latestReviewTime) latestReviewTime = createdTs; // Emit the review with unique ID and summary this.$emit(review, { id: review.id, From e77c5ab4c639b763b192d556fc2eef74d967aa9d Mon Sep 17 00:00:00 2001 From: Job <9075380+Afstkla@users.noreply.github.com> Date: Wed, 3 Sep 2025 16:54:11 +0200 Subject: [PATCH 13/24] Update components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../sources/new-product-reviews/new-product-reviews.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs b/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs index b352e5e362742..3d88901846ba0 100644 --- a/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs +++ b/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs @@ -92,7 +92,7 @@ export default { } // Update the last review time for next poll - if (latestReviewTime && latestReviewTime !== lastReviewTime) { + if (latestReviewTime && latestReviewTime !== Number(lastReviewTime)) { this._setLastReviewTime(latestReviewTime); } From a57fafd3a2543291ccd24b1348b2d4af3fe2031d Mon Sep 17 00:00:00 2001 From: Job <9075380+Afstkla@users.noreply.github.com> Date: Wed, 3 Sep 2025 16:58:05 +0200 Subject: [PATCH 14/24] Update components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../new-service-reviews/new-service-reviews.mjs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs b/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs index 8cf62ff3e1002..e3e8f6019c811 100644 --- a/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs +++ b/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs @@ -66,13 +66,15 @@ export default { } // Emit reviews (already parsed by the action) - let latestReviewTime = lastReviewTime; +let latestReviewTime = lastReviewTime; - for (const review of reviews) { - // Track the latest review time - if (!latestReviewTime || review.createdAt > latestReviewTime) { - latestReviewTime = review.createdAt; - } +for (const review of reviews) { + // Track the latest review time + const reviewTime = new Date(review.createdAt).toISOString(); + if (!latestReviewTime || new Date(reviewTime) > new Date(latestReviewTime)) { + latestReviewTime = reviewTime; + } +} // Emit the review with unique ID and summary this.$emit(review, { From 34b212f3cb05c292c79e5d10536c5fb24797623a Mon Sep 17 00:00:00 2001 From: Job Nijenhuis Date: Wed, 3 Sep 2025 16:52:20 +0200 Subject: [PATCH 15/24] comments --- .../actions/fetch-product-reviews/fetch-product-reviews.mjs | 2 +- .../reply-to-service-review/reply-to-service-review.mjs | 2 +- components/trustpilot/common/api-client.mjs | 1 + .../sources/new-product-reviews/new-product-reviews.mjs | 6 +++--- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs b/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs index 637297268c099..79ad4f619b62e 100644 --- a/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs +++ b/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs @@ -3,7 +3,7 @@ import trustpilot from "../../trustpilot.app.mjs"; export default { key: "trustpilot-fetch-product-reviews", name: "Fetch Product Reviews", - description: "Retrieves a list of product reviews for a specific business unit on Trustpilot. This action enables you to fetch multiple product reviews with powerful filtering options including star ratings, language, tags, and sorting preferences. Ideal for monitoring product feedback trends, generating reports, analyzing customer sentiment across your product catalog, or building review dashboards. Supports pagination for handling large review volumes. [See the documentation](https://developers.trustpilot.com/product-reviews-api#get-private-product-reviews)", + description: "Retrieves a list of product reviews for a specific business unit.", version: "0.1.0", type: "action", props: { diff --git a/components/trustpilot/actions/reply-to-service-review/reply-to-service-review.mjs b/components/trustpilot/actions/reply-to-service-review/reply-to-service-review.mjs index 747d67dfdb861..fe484c751502f 100644 --- a/components/trustpilot/actions/reply-to-service-review/reply-to-service-review.mjs +++ b/components/trustpilot/actions/reply-to-service-review/reply-to-service-review.mjs @@ -10,7 +10,7 @@ import { export default { key: "trustpilot-reply-to-service-review", name: "Reply to Service Review", - description: "Reply to a service review on Trustpilot. Posts a public reply on behalf of your business using the private reviews API. [See the documentation](https://developers.trustpilot.com/private-reviews-api#reply-to-review)", + description: "Reply to a service review on Trustpilot.", version: "0.1.0", type: "action", props: { diff --git a/components/trustpilot/common/api-client.mjs b/components/trustpilot/common/api-client.mjs index 3275ad9b6dcfc..6e5efce531e97 100644 --- a/components/trustpilot/common/api-client.mjs +++ b/components/trustpilot/common/api-client.mjs @@ -63,6 +63,7 @@ export async function makeRequest(trustpilotApp, { params, data, timeout, + additionalHeaders, ...args, }, retries - 1); } diff --git a/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs b/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs index 3d88901846ba0..3f1a541e22efd 100644 --- a/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs +++ b/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs @@ -32,9 +32,9 @@ export default { }, generateSummary(review) { const stars = review.stars || "N/A"; - const consumerName = review.consumer?.displayName || "Anonymous"; - const productName = review.product?.title || "Unknown Product"; - const businessUnit = review.businessUnit?.displayName || this.businessUnitId || "Unknown"; + const consumerName = review.consumer?.name || "Anonymous"; + const productName = review.product?.name || "Unknown Product"; + const businessUnit = this.businessUnitId || "Unknown"; return `New ${stars}-star product review by ${consumerName} for "${productName}" (${businessUnit})`; }, From 76ed734bfd812eff09aab54b8524bc8e4427ce01 Mon Sep 17 00:00:00 2001 From: Job Nijenhuis Date: Wed, 3 Sep 2025 16:57:33 +0200 Subject: [PATCH 16/24] Pagination --- .../sources/new-service-reviews/new-service-reviews.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs b/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs index e3e8f6019c811..b8b78b658a99e 100644 --- a/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs +++ b/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs @@ -56,7 +56,12 @@ export default { } // Use the shared method from the app directly - const result = await this.trustpilot.fetchServiceReviews(fetchParams); + let result = await this.trustpilot.fetchServiceReviews(fetchParams); + + while (result.length === 100) { + fetchParams.page += 1; + result = result.concat(await this.trustpilot.fetchServiceReviews(fetchParams)); + } const reviews = result.reviews || []; From 5f34833d968de848396252d4ba92472750e28594 Mon Sep 17 00:00:00 2001 From: Job Nijenhuis Date: Wed, 3 Sep 2025 17:01:34 +0200 Subject: [PATCH 17/24] fixes --- .../new-service-reviews.mjs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs b/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs index b8b78b658a99e..049f4c21c3f70 100644 --- a/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs +++ b/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs @@ -58,9 +58,10 @@ export default { // Use the shared method from the app directly let result = await this.trustpilot.fetchServiceReviews(fetchParams); - while (result.length === 100) { - fetchParams.page += 1; - result = result.concat(await this.trustpilot.fetchServiceReviews(fetchParams)); + while (result.reviews && result.reviews.length === 100) { + fetchParams.page = (fetchParams.page || 1) + 1; + const nextResult = await this.trustpilot.fetchServiceReviews(fetchParams); + result.reviews = result.reviews.concat(nextResult.reviews || []); } const reviews = result.reviews || []; @@ -71,15 +72,14 @@ export default { } // Emit reviews (already parsed by the action) -let latestReviewTime = lastReviewTime; + let latestReviewTime = lastReviewTime; -for (const review of reviews) { - // Track the latest review time - const reviewTime = new Date(review.createdAt).toISOString(); - if (!latestReviewTime || new Date(reviewTime) > new Date(latestReviewTime)) { - latestReviewTime = reviewTime; - } -} + for (const review of reviews) { + // Track the latest review time + const reviewTime = new Date(review.createdAt).toISOString(); + if (!latestReviewTime || new Date(reviewTime) > new Date(latestReviewTime)) { + latestReviewTime = reviewTime; + } // Emit the review with unique ID and summary this.$emit(review, { From acf2b0f9bf7807bfb7fb97dbbc5f76b1acd9732d Mon Sep 17 00:00:00 2001 From: Job Nijenhuis Date: Fri, 5 Sep 2025 09:44:41 +0200 Subject: [PATCH 18/24] comments --- .../fetch-product-review-by-id.mjs | 2 +- .../fetch-product-reviews.mjs | 4 +- .../fetch-service-review-by-id.mjs | 2 +- .../fetch-service-reviews.mjs | 6 +- .../get-conversation-from-product-review.mjs | 90 +++++++++---------- .../reply-to-product-review.mjs | 6 +- .../reply-to-service-review.mjs | 2 +- components/trustpilot/common/api-client.mjs | 37 ++------ components/trustpilot/common/constants.mjs | 18 ---- components/trustpilot/common/utils.mjs | 9 -- components/trustpilot/trustpilot.app.mjs | 64 ++++--------- 11 files changed, 75 insertions(+), 165 deletions(-) diff --git a/components/trustpilot/actions/fetch-product-review-by-id/fetch-product-review-by-id.mjs b/components/trustpilot/actions/fetch-product-review-by-id/fetch-product-review-by-id.mjs index 62d8457887b9b..9b5f1994b7cd1 100644 --- a/components/trustpilot/actions/fetch-product-review-by-id/fetch-product-review-by-id.mjs +++ b/components/trustpilot/actions/fetch-product-review-by-id/fetch-product-review-by-id.mjs @@ -40,7 +40,7 @@ export default { }); // Make the API request - const response = await makeRequest(this.trustpilot, { + const response = await makeRequest($, this.trustpilot, { endpoint, }); diff --git a/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs b/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs index 79ad4f619b62e..aedc485bd4193 100644 --- a/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs +++ b/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs @@ -3,8 +3,8 @@ import trustpilot from "../../trustpilot.app.mjs"; export default { key: "trustpilot-fetch-product-reviews", name: "Fetch Product Reviews", - description: "Retrieves a list of product reviews for a specific business unit.", - version: "0.1.0", + description: "Retrieves a list of product reviews for a specific business unit. See documentation [here](https://developers.trustpilot.com/product-reviews-api/#get-private-product-reviews)", + version: "1.0.0", type: "action", props: { trustpilot, diff --git a/components/trustpilot/actions/fetch-service-review-by-id/fetch-service-review-by-id.mjs b/components/trustpilot/actions/fetch-service-review-by-id/fetch-service-review-by-id.mjs index e382e8b29629d..0289bab9e6deb 100644 --- a/components/trustpilot/actions/fetch-service-review-by-id/fetch-service-review-by-id.mjs +++ b/components/trustpilot/actions/fetch-service-review-by-id/fetch-service-review-by-id.mjs @@ -40,7 +40,7 @@ export default { }); // Make the API request - const response = await makeRequest(this.trustpilot, { + const response = await makeRequest($, this.trustpilot, { endpoint, }); diff --git a/components/trustpilot/actions/fetch-service-reviews/fetch-service-reviews.mjs b/components/trustpilot/actions/fetch-service-reviews/fetch-service-reviews.mjs index de27cc8a1bca6..0ded6e6d8a237 100644 --- a/components/trustpilot/actions/fetch-service-reviews/fetch-service-reviews.mjs +++ b/components/trustpilot/actions/fetch-service-reviews/fetch-service-reviews.mjs @@ -23,7 +23,7 @@ export default { page: { type: "integer", label: "Page", - description: "The page to retrieve. If the page number requested is higher than the available number of pages an empty array will be returned.", + description: "The page to retrieve. If the page number requested is higher than the available number of pages, an empty array will be returned.", min: 1, default: 1, optional: true, @@ -120,13 +120,13 @@ export default { startDateTime: { type: "string", label: "Start Date Time", - description: "Filter reviews by datetime range. If no time is specified than time is implicit 00:00:00. Format: 2013-09-07T13:37:00", + description: "Filter reviews by datetime range. If no time is specified, then time is implicitly `00:00:00`. Format: `2013-09-07T13:37:00`", optional: true, }, endDateTime: { type: "string", label: "End Date Time", - description: "Filter reviews by datetime range. If no time is specified than time is implicit 00:00:00. Format: 2013-09-07T13:37:00", + description: "Filter reviews by datetime range. If no time is specified, then time is implicitly `00:00:00`. Format: `2013-09-07T13:37:00`", optional: true, }, source: { diff --git a/components/trustpilot/actions/get-conversation-from-product-review/get-conversation-from-product-review.mjs b/components/trustpilot/actions/get-conversation-from-product-review/get-conversation-from-product-review.mjs index 0b347f6671ab4..a71fadb03802c 100644 --- a/components/trustpilot/actions/get-conversation-from-product-review/get-conversation-from-product-review.mjs +++ b/components/trustpilot/actions/get-conversation-from-product-review/get-conversation-from-product-review.mjs @@ -11,7 +11,7 @@ export default { key: "trustpilot-get-conversation-from-product-review", name: "Get Conversation from Product Review", description: "Get conversation and related comments from a product review. First fetches the review to get the conversationId, then retrieves the full conversation details. [See the documentation](https://developers.trustpilot.com/conversations-api#get-conversation)", - version: "0.1.0", + version: "0.0.1", type: "action", props: { trustpilot, @@ -33,62 +33,58 @@ export default { throw new ConfigurationError("Invalid review ID format"); } - try { - // Step 1: Get the product review to get the conversationId - $.export("$summary", "Fetching product review details..."); + // Step 1: Get the product review to get the conversationId + $.export("$summary", "Fetching product review details..."); - const getReviewEndpoint = buildUrl(ENDPOINTS.PRIVATE_PRODUCT_REVIEW_BY_ID, { - reviewId, - }); - - const review = await makeRequest(this.trustpilot, { - endpoint: getReviewEndpoint, - }); - - const conversationId = review.conversationId; - - if (!conversationId) { - return { - success: false, - message: "No conversation found for this product review", - review: { - id: reviewId, - hasConversation: false, - }, - metadata: { - reviewId, - requestTime: new Date().toISOString(), - }, - }; - } + const getReviewEndpoint = buildUrl(ENDPOINTS.PRIVATE_PRODUCT_REVIEW_BY_ID, { + reviewId, + }); - // Step 2: Get the conversation details - $.export("$summary", "Fetching conversation details..."); + const review = await makeRequest($, this.trustpilot, { + endpoint: getReviewEndpoint, + }); - const getConversationEndpoint = buildUrl(ENDPOINTS.CONVERSATION_BY_ID, { - conversationId, - }); - - const conversation = await makeRequest(this.trustpilot, { - endpoint: getConversationEndpoint, - }); - - $.export("$summary", `Successfully retrieved conversation ${conversationId} for product review ${reviewId}`); + const conversationId = review.conversationId; + if (!conversationId) { return { - success: true, - conversation, + success: false, + message: "No conversation found for this product review", + review: { + id: reviewId, + hasConversation: false, + }, metadata: { reviewId, - conversationId, - commentCount: conversation.comments?.length || 0, - conversationState: conversation.state, - source: conversation.source, requestTime: new Date().toISOString(), }, }; - } catch (error) { - throw new ConfigurationError(`Failed to get conversation from product review: ${error.message}`); } + + // Step 2: Get the conversation details + $.export("$summary", "Fetching conversation details..."); + + const getConversationEndpoint = buildUrl(ENDPOINTS.CONVERSATION_BY_ID, { + conversationId, + }); + + const conversation = await makeRequest($, this.trustpilot, { + endpoint: getConversationEndpoint, + }); + + $.export("$summary", `Successfully retrieved conversation ${conversationId} for product review ${reviewId}`); + + return { + success: true, + conversation, + metadata: { + reviewId, + conversationId, + commentCount: conversation.comments?.length || 0, + conversationState: conversation.state, + source: conversation.source, + requestTime: new Date().toISOString(), + }, + }; }, }; diff --git a/components/trustpilot/actions/reply-to-product-review/reply-to-product-review.mjs b/components/trustpilot/actions/reply-to-product-review/reply-to-product-review.mjs index 68c9f9a66cd07..1bbf3e34150b9 100644 --- a/components/trustpilot/actions/reply-to-product-review/reply-to-product-review.mjs +++ b/components/trustpilot/actions/reply-to-product-review/reply-to-product-review.mjs @@ -70,7 +70,7 @@ export default { reviewId, }); - const review = await makeRequest(this.trustpilot, { + const review = await makeRequest($, this.trustpilot, { endpoint: getReviewEndpoint, }); @@ -84,7 +84,7 @@ export default { reviewId, }); - const createConversationResponse = await makeRequest(this.trustpilot, { + const createConversationResponse = await makeRequest($, this.trustpilot, { endpoint: createConversationEndpoint, method: "POST", }); @@ -113,7 +113,7 @@ export default { requestData.integrationId = integrationId; } - const replyResponse = await makeRequest(this.trustpilot, { + const replyResponse = await makeRequest($, this.trustpilot, { endpoint: replyEndpoint, method: "POST", data: requestData, diff --git a/components/trustpilot/actions/reply-to-service-review/reply-to-service-review.mjs b/components/trustpilot/actions/reply-to-service-review/reply-to-service-review.mjs index fe484c751502f..f9a082ef815cf 100644 --- a/components/trustpilot/actions/reply-to-service-review/reply-to-service-review.mjs +++ b/components/trustpilot/actions/reply-to-service-review/reply-to-service-review.mjs @@ -68,7 +68,7 @@ export default { }; // Make the API request - await makeRequest(this.trustpilot, { + await makeRequest($, this.trustpilot, { endpoint, method: "POST", data: requestData, diff --git a/components/trustpilot/common/api-client.mjs b/components/trustpilot/common/api-client.mjs index 6e5efce531e97..74d357963bb5c 100644 --- a/components/trustpilot/common/api-client.mjs +++ b/components/trustpilot/common/api-client.mjs @@ -1,10 +1,6 @@ import { axios } from "@pipedream/platform"; -import { - BASE_URL, HTTP_STATUS, RETRY_CONFIG, -} from "./constants.mjs"; -import { - formatQueryParams, sleep, -} from "./utils.mjs"; +import { BASE_URL } from "./constants.mjs"; +import { formatQueryParams } from "./utils.mjs"; /** * Make an authenticated request to the Trustpilot API @@ -16,10 +12,9 @@ import { * @param {object} [options.data] - Request body data * @param {object} [options.additionalHeaders={}] - Additional headers to include in the request * @param {number} [options.timeout=30000] - Request timeout - * @param {number} [retries=RETRY_CONFIG.MAX_RETRIES] - Number of retries for rate limiting * @returns {Promise} API response data */ -export async function makeRequest(trustpilotApp, { +export async function makeRequest($, trustpilotApp, { endpoint, method = "GET", params = {}, @@ -27,7 +22,7 @@ export async function makeRequest(trustpilotApp, { timeout = 30000, additionalHeaders = {}, ...args -}, retries = RETRY_CONFIG.MAX_RETRIES) { +}) { const url = `${BASE_URL}${endpoint}`; const headers = { ...getAuthHeaders(trustpilotApp, url), @@ -47,28 +42,8 @@ export async function makeRequest(trustpilotApp, { config.data = data; } - try { - const response = await axios(trustpilotApp, config); - return response.data || response; - } catch (error) { - if (retries > 0 && error.response?.status === HTTP_STATUS.TOO_MANY_REQUESTS) { - const delay = Math.min( - RETRY_CONFIG.INITIAL_DELAY * (RETRY_CONFIG.MAX_RETRIES - retries + 1), - RETRY_CONFIG.MAX_DELAY, - ); - await sleep(delay); - return makeRequest(trustpilotApp, { - endpoint, - method, - params, - data, - timeout, - additionalHeaders, - ...args, - }, retries - 1); - } - throw error; - } + const response = await axios($ ?? trustpilotApp, config); + return response.data || response; } /** diff --git a/components/trustpilot/common/constants.mjs b/components/trustpilot/common/constants.mjs index 576dccb2c2c02..0fbf1a4613e3a 100644 --- a/components/trustpilot/common/constants.mjs +++ b/components/trustpilot/common/constants.mjs @@ -64,24 +64,6 @@ export const RATING_SCALE = [ export const DEFAULT_LIMIT = 20; export const MAX_LIMIT = 100; -export const HTTP_STATUS = { - OK: 200, - CREATED: 201, - NO_CONTENT: 204, - BAD_REQUEST: 400, - UNAUTHORIZED: 401, - FORBIDDEN: 403, - NOT_FOUND: 404, - TOO_MANY_REQUESTS: 429, - INTERNAL_SERVER_ERROR: 500, -}; - -export const RETRY_CONFIG = { - MAX_RETRIES: 3, - INITIAL_DELAY: 1000, - MAX_DELAY: 10000, -}; - export const POLLING_CONFIG = { DEFAULT_TIMER_INTERVAL_SECONDS: 15 * 60, // 15 minutes MAX_ITEMS_PER_POLL: 100, diff --git a/components/trustpilot/common/utils.mjs b/components/trustpilot/common/utils.mjs index 93c0b4a18b1d2..89c3e059f1139 100644 --- a/components/trustpilot/common/utils.mjs +++ b/components/trustpilot/common/utils.mjs @@ -281,12 +281,3 @@ export function parseApiError(error) { code: "UNKNOWN_ERROR", }; } - -/** - * Sleep function for retry logic - * @param {number} ms - Milliseconds to sleep - * @returns {Promise} - Promise that resolves after delay - */ -export function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} diff --git a/components/trustpilot/trustpilot.app.mjs b/components/trustpilot/trustpilot.app.mjs index 2e101b9bd68b9..cac5df5116576 100644 --- a/components/trustpilot/trustpilot.app.mjs +++ b/components/trustpilot/trustpilot.app.mjs @@ -109,7 +109,7 @@ export default { async searchBusinessUnits({ query = "a", page = 1, } = {}) { - const response = await makeRequest(this, { + const response = await makeRequest(this, this, { endpoint: ENDPOINTS.BUSINESS_UNITS, params: { query, @@ -121,28 +121,8 @@ export default { }, // Shared method for fetching service reviews - used by both actions and sources - async fetchServiceReviews(params = {}) { - const { - businessUnitId, - stars, - language, - page = 1, - internalLocationId, - perPage = 20, - orderBy = "createdat.desc", - tagGroup, - tagValue, - ignoreTagValueCase = false, - responded, - referenceId, - referralEmail, - reported, - startDateTime, - endDateTime, - source, - username, - findReviewer, - } = params; + async fetchServiceReviews($, params = {}) { + const { businessUnitId } = params; // Validate required parameters if (!businessUnitId) { @@ -158,30 +138,16 @@ export default { }); // Prepare query parameters - const queryParams = {}; - - // Add optional parameters if provided - if (stars) queryParams.stars = stars; - if (language) queryParams.language = language; - if (page) queryParams.page = page; - if (internalLocationId) queryParams.internalLocationId = internalLocationId; - if (perPage) queryParams.perPage = perPage; - if (orderBy) queryParams.orderBy = orderBy; - if (tagGroup) queryParams.tagGroup = tagGroup; - if (tagValue) queryParams.tagValue = tagValue; - if (ignoreTagValueCase !== undefined) queryParams.ignoreTagValueCase = ignoreTagValueCase; - if (responded !== undefined) queryParams.responded = responded; - if (referenceId) queryParams.referenceId = referenceId; - if (referralEmail) queryParams.referralEmail = referralEmail; - if (reported !== undefined) queryParams.reported = reported; - if (startDateTime) queryParams.startDateTime = startDateTime; - if (endDateTime) queryParams.endDateTime = endDateTime; - if (source) queryParams.source = source; - if (username) queryParams.username = username; - if (findReviewer) queryParams.findReviewer = findReviewer; + const queryParams = { + ...params, + page: 1, + perPage: 20, + orderBy: "createdat.desc", + ignoreTagValueCase: false, + }; // Make the API request - const response = await makeRequest(this, { + const response = await makeRequest($, this, { endpoint, params: queryParams, }); @@ -190,8 +156,8 @@ export default { const reviews = response.reviews?.map(parseServiceReview) || []; const pagination = { total: response.pagination?.total || 0, - page: response.pagination?.page || page, - perPage: response.pagination?.perPage || perPage, + page: response.pagination?.page || queryParams.page, + perPage: response.pagination?.perPage || queryParams.perPage, hasMore: response.pagination?.hasMore || false, }; @@ -207,7 +173,7 @@ export default { }, // Shared method for fetching product reviews - used by both actions and sources - async fetchProductReviews(params = {}) { + async fetchProductReviews($, params = {}) { const { businessUnitId, page, @@ -240,7 +206,7 @@ export default { }; // Make the API request - const response = await makeRequest(this, { + const response = await makeRequest($, this, { endpoint, params: queryParams, }); From c2d5ffa744dd28c725f09c2347f08033aca590e9 Mon Sep 17 00:00:00 2001 From: Job Nijenhuis Date: Fri, 5 Sep 2025 09:49:18 +0200 Subject: [PATCH 19/24] missed some `$`'s --- .../actions/fetch-product-reviews/fetch-product-reviews.mjs | 2 +- .../actions/fetch-service-reviews/fetch-service-reviews.mjs | 2 +- .../sources/new-product-reviews/new-product-reviews.mjs | 4 ++-- .../sources/new-service-reviews/new-service-reviews.mjs | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs b/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs index aedc485bd4193..2acc51c2905e1 100644 --- a/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs +++ b/components/trustpilot/actions/fetch-product-reviews/fetch-product-reviews.mjs @@ -64,7 +64,7 @@ export default { try { // Use the shared method from the app - const result = await this.trustpilot.fetchProductReviews({ + const result = await this.trustpilot.fetchProductReviews($, { businessUnitId, page, perPage, diff --git a/components/trustpilot/actions/fetch-service-reviews/fetch-service-reviews.mjs b/components/trustpilot/actions/fetch-service-reviews/fetch-service-reviews.mjs index 0ded6e6d8a237..7d3cd97e89cf8 100644 --- a/components/trustpilot/actions/fetch-service-reviews/fetch-service-reviews.mjs +++ b/components/trustpilot/actions/fetch-service-reviews/fetch-service-reviews.mjs @@ -177,7 +177,7 @@ export default { try { // Use the shared method from the app - const result = await this.trustpilot.fetchServiceReviews({ + const result = await this.trustpilot.fetchServiceReviews($, { businessUnitId, stars, language, diff --git a/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs b/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs index 3f1a541e22efd..6bd19c0e45d23 100644 --- a/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs +++ b/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs @@ -39,7 +39,7 @@ export default { return `New ${stars}-star product review by ${consumerName} for "${productName}" (${businessUnit})`; }, }, - async run() { + async run({ $ }) { try { // Get the last review time for filtering new reviews const lastReviewTime = this._getLastReviewTime(); @@ -54,7 +54,7 @@ export default { }; // Use the shared method from the app directly - const result = await this.trustpilot.fetchProductReviews(fetchParams); + const result = await this.trustpilot.fetchProductReviews($, fetchParams); const reviews = result.reviews || []; diff --git a/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs b/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs index 049f4c21c3f70..1b9b7c5653dd6 100644 --- a/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs +++ b/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs @@ -38,7 +38,7 @@ export default { return `New ${stars}-star service review by ${consumerName} for ${businessUnit}`; }, }, - async run() { + async run({ $ }) { try { // Get the last review time for filtering new reviews const lastReviewTime = this._getLastReviewTime(); @@ -56,11 +56,11 @@ export default { } // Use the shared method from the app directly - let result = await this.trustpilot.fetchServiceReviews(fetchParams); + let result = await this.trustpilot.fetchServiceReviews($, fetchParams); while (result.reviews && result.reviews.length === 100) { fetchParams.page = (fetchParams.page || 1) + 1; - const nextResult = await this.trustpilot.fetchServiceReviews(fetchParams); + const nextResult = await this.trustpilot.fetchServiceReviews($, fetchParams); result.reviews = result.reviews.concat(nextResult.reviews || []); } From 62924ccdfeab221535092efa57773ae4993afa02 Mon Sep 17 00:00:00 2001 From: Job Nijenhuis Date: Fri, 5 Sep 2025 11:18:02 +0200 Subject: [PATCH 20/24] unduplicated --- .../trustpilot/sources/common/polling.mjs | 128 ++++++++++++++++++ .../new-product-reviews.mjs | 99 ++++---------- .../new-service-reviews.mjs | 85 +++--------- 3 files changed, 170 insertions(+), 142 deletions(-) create mode 100644 components/trustpilot/sources/common/polling.mjs diff --git a/components/trustpilot/sources/common/polling.mjs b/components/trustpilot/sources/common/polling.mjs new file mode 100644 index 0000000000000..a3d689dbf700d --- /dev/null +++ b/components/trustpilot/sources/common/polling.mjs @@ -0,0 +1,128 @@ +import trustpilot from "../../trustpilot.app.mjs"; + +/** + * Base polling source for Trustpilot integration + * + * Provides common functionality for polling Trustpilot API endpoints + * and emitting new events with deduplication. + */ +export default { + props: { + trustpilot, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: 15 * 60, // 15 minutes + }, + }, + businessUnitId: { + propDefinition: [ + trustpilot, + "businessUnitId", + ], + }, + }, + methods: { + _getLastReviewTime() { + return this.db.get("lastReviewTime"); + }, + _setLastReviewTime(time) { + this.db.set("lastReviewTime", time); + }, + /** + * Override in child classes to provide review type-specific summary + * @param {Object} _review - The review object + * @returns {string} - Human-readable summary + */ + // eslint-disable-next-line no-unused-vars + generateSummary(_review) { + throw new Error("generateSummary must be implemented in child class"); + }, + /** + * Override in child classes to fetch reviews using appropriate method + * @param {Object} _$ - Pipedream step context + * @param {Object} _params - Fetch parameters + * @returns {Object} - API response with reviews array + */ + // eslint-disable-next-line no-unused-vars + async fetchReviews(_$, _params) { + throw new Error("fetchReviews must be implemented in child class"); + }, + /** + * Override in child classes to provide fetch parameters + * @param {string} _lastReviewTime - ISO timestamp of last review + * @returns {Object} - Parameters for fetchReviews call + */ + // eslint-disable-next-line no-unused-vars + getFetchParams(_lastReviewTime) { + return { + businessUnitId: this.businessUnitId, + perPage: 100, + }; + }, + /** + * Override in child classes to filter reviews (for APIs without time filtering) + * @param {Array} reviews - Array of reviews from API + * @param {string} _lastReviewTime - ISO timestamp of last review + * @returns {Array} - Filtered array of new reviews + */ + // eslint-disable-next-line no-unused-vars + filterNewReviews(reviews, _lastReviewTime) { + // Default: return all reviews (for APIs with server-side time filtering) + return reviews; + }, + }, + async run({ $ }) { + try { + // Get the last review time for filtering new reviews + const lastReviewTime = this._getLastReviewTime(); + + // Get fetch parameters from child class + const fetchParams = this.getFetchParams(lastReviewTime); + + // Fetch reviews using child class method + const result = await this.fetchReviews($, fetchParams); + const reviews = result.reviews || []; + + if (!reviews.length) { + console.log("No reviews found"); + return; + } + + // Filter for new reviews (child class may override) + const newReviews = this.filterNewReviews(reviews, lastReviewTime); + + if (!newReviews.length) { + console.log("No new reviews since last poll"); + return; + } + + // Track the latest review time + let latestReviewTime = lastReviewTime; + + for (const review of newReviews) { + // Track the latest review time + const reviewTime = new Date(review.createdAt).toISOString(); + if (!latestReviewTime || new Date(reviewTime) > new Date(latestReviewTime)) { + latestReviewTime = reviewTime; + } + + // Emit the review with unique ID and summary + this.$emit(review, { + id: review.id, + summary: this.generateSummary(review), + ts: new Date(review.createdAt).getTime(), + }); + } + + // Update the last review time for next poll + if (latestReviewTime && latestReviewTime !== lastReviewTime) { + this._setLastReviewTime(latestReviewTime); + } + + } catch (error) { + throw new Error(`Failed to fetch reviews: ${error.message}`); + } + }, +}; diff --git a/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs b/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs index 6bd19c0e45d23..4351dadc97297 100644 --- a/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs +++ b/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs @@ -1,35 +1,15 @@ -import trustpilot from "../../trustpilot.app.mjs"; +import common from "../common/polling.mjs"; export default { + ...common, key: "trustpilot-new-product-reviews", name: "New Product Reviews", description: "Emit new event when a customer posts a new product review on Trustpilot. This source periodically polls the Trustpilot API to detect new product reviews. Each event contains the complete review data including star rating, review text, product information, consumer details, and timestamps. Perfect for monitoring product feedback, analyzing customer satisfaction trends, and triggering automated responses or alerts for specific products.", version: "0.1.0", type: "source", dedupe: "unique", - props: { - trustpilot, - db: "$.service.db", - timer: { - type: "$.interface.timer", - default: { - intervalSeconds: 15 * 60, // 15 minutes - }, - }, - businessUnitId: { - propDefinition: [ - trustpilot, - "businessUnitId", - ], - }, - }, methods: { - _getLastReviewTime() { - return this.db.get("lastReviewTime"); - }, - _setLastReviewTime(time) { - this.db.set("lastReviewTime", time); - }, + ...common.methods, generateSummary(review) { const stars = review.stars || "N/A"; const consumerName = review.consumer?.name || "Anonymous"; @@ -38,66 +18,39 @@ export default { return `New ${stars}-star product review by ${consumerName} for "${productName}" (${businessUnit})`; }, - }, - async run({ $ }) { - try { - // Get the last review time for filtering new reviews - const lastReviewTime = this._getLastReviewTime(); - - // Use the fetch-product-reviews action to get reviews + getFetchParams() { // Note: Product reviews API doesn't support time-based filtering, // so we'll rely on pagination and client-side filtering - const fetchParams = { + return { businessUnitId: this.businessUnitId, perPage: 100, page: 1, }; - + }, + async fetchReviews($, params) { // Use the shared method from the app directly - const result = await this.trustpilot.fetchProductReviews($, fetchParams); - - const reviews = result.reviews || []; - - if (!reviews.length) { - console.log("No new product reviews found"); - return; - } - - // Filter for new reviews since last poll (client-side filtering) + return await this.trustpilot.fetchProductReviews($, params); + }, + filterNewReviews(reviews, lastReviewTime) { + // Product reviews require client-side filtering since API doesn't support + // time-based filtering const lastTs = Number(lastReviewTime) || 0; const toMs = (d) => new Date(d).getTime(); - let newReviews = lastTs + + return lastTs ? reviews.filter((r) => toMs(r.createdAt) > lastTs) : reviews; - - if (!newReviews.length) { - console.log("No new product reviews since last poll"); - return; - } - - // Track the latest review time - // Initialize latestReviewTime as a numeric timestamp (ms) - let latestReviewTime = Number(lastReviewTime) || 0; - - for (const review of newReviews) { - // Track the latest review time - const createdTs = new Date(review.createdAt).getTime(); - if (createdTs > latestReviewTime) latestReviewTime = createdTs; - // Emit the review with unique ID and summary - this.$emit(review, { - id: review.id, - summary: this.generateSummary(review), - ts: new Date(review.createdAt).getTime(), - }); - } - - // Update the last review time for next poll - if (latestReviewTime && latestReviewTime !== Number(lastReviewTime)) { - this._setLastReviewTime(latestReviewTime); - } - - } catch (error) { - throw new Error(`Failed to fetch new product reviews: ${error.message}`); - } + }, + _getLastReviewTime() { + // Product reviews store timestamp as number (ms), others store as ISO string + return this.db.get("lastReviewTime"); + }, + _setLastReviewTime(time) { + // Store as number for product reviews to match existing behavior + const timeMs = typeof time === "string" + ? new Date(time).getTime() + : time; + this.db.set("lastReviewTime", timeMs); + }, }, }; diff --git a/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs b/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs index 1b9b7c5653dd6..bbcb7ad01508f 100644 --- a/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs +++ b/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs @@ -1,35 +1,15 @@ -import trustpilot from "../../trustpilot.app.mjs"; +import common from "../common/polling.mjs"; export default { + ...common, key: "trustpilot-new-service-reviews", name: "New Service Reviews", description: "Emit new event when a customer posts a new service review on Trustpilot. This source periodically polls the Trustpilot API to detect new service reviews using the private reviews API for comprehensive coverage. Each event contains the complete review data including star rating, review text, consumer details, business unit info, customer email, and timestamps. Ideal for monitoring overall business reputation, tracking customer satisfaction metrics, and triggering workflows based on review ratings or content.", version: "0.1.0", type: "source", dedupe: "unique", - props: { - trustpilot, - db: "$.service.db", - timer: { - type: "$.interface.timer", - default: { - intervalSeconds: 15 * 60, // 15 minutes - }, - }, - businessUnitId: { - propDefinition: [ - trustpilot, - "businessUnitId", - ], - }, - }, methods: { - _getLastReviewTime() { - return this.db.get("lastReviewTime"); - }, - _setLastReviewTime(time) { - this.db.set("lastReviewTime", time); - }, + ...common.methods, generateSummary(review) { const stars = review.stars || "N/A"; const consumerName = review.consumer?.displayName || "Anonymous"; @@ -37,14 +17,8 @@ export default { return `New ${stars}-star service review by ${consumerName} for ${businessUnit}`; }, - }, - async run({ $ }) { - try { - // Get the last review time for filtering new reviews - const lastReviewTime = this._getLastReviewTime(); - - // Use the fetch-service-reviews action to get reviews - const fetchParams = { + getFetchParams(lastReviewTime) { + const params = { businessUnitId: this.businessUnitId, perPage: 100, orderBy: "createdat.desc", @@ -52,50 +26,23 @@ export default { // If we have a last review time, filter for reviews after that time if (lastReviewTime) { - fetchParams.startDateTime = lastReviewTime; + params.startDateTime = lastReviewTime; } - // Use the shared method from the app directly - let result = await this.trustpilot.fetchServiceReviews($, fetchParams); + return params; + }, + async fetchReviews($, params) { + // Use the shared method from the app directly with pagination support + let result = await this.trustpilot.fetchServiceReviews($, params); + // Handle pagination for service reviews while (result.reviews && result.reviews.length === 100) { - fetchParams.page = (fetchParams.page || 1) + 1; - const nextResult = await this.trustpilot.fetchServiceReviews($, fetchParams); + params.page = (params.page || 1) + 1; + const nextResult = await this.trustpilot.fetchServiceReviews($, params); result.reviews = result.reviews.concat(nextResult.reviews || []); } - const reviews = result.reviews || []; - - if (!reviews.length) { - console.log("No new service reviews found"); - return; - } - - // Emit reviews (already parsed by the action) - let latestReviewTime = lastReviewTime; - - for (const review of reviews) { - // Track the latest review time - const reviewTime = new Date(review.createdAt).toISOString(); - if (!latestReviewTime || new Date(reviewTime) > new Date(latestReviewTime)) { - latestReviewTime = reviewTime; - } - - // Emit the review with unique ID and summary - this.$emit(review, { - id: review.id, - summary: this.generateSummary(review), - ts: new Date(review.createdAt).getTime(), - }); - } - - // Update the last review time for next poll - if (latestReviewTime && latestReviewTime !== lastReviewTime) { - this._setLastReviewTime(latestReviewTime); - } - - } catch (error) { - throw new Error(`Failed to fetch new service reviews: ${error.message}`); - } + return result; + }, }, }; From d84333b420c9348a88c1561a0661b77ac22aba30 Mon Sep 17 00:00:00 2001 From: Job Nijenhuis Date: Fri, 5 Sep 2025 11:29:55 +0200 Subject: [PATCH 21/24] more fixes --- .../fetch-service-reviews.mjs | 2 +- components/trustpilot/common/constants.mjs | 56 ------------------- .../new-product-reviews.mjs | 3 +- .../new-service-reviews.mjs | 21 +++++-- components/trustpilot/trustpilot.app.mjs | 19 +++++-- 5 files changed, 32 insertions(+), 69 deletions(-) diff --git a/components/trustpilot/actions/fetch-service-reviews/fetch-service-reviews.mjs b/components/trustpilot/actions/fetch-service-reviews/fetch-service-reviews.mjs index 7d3cd97e89cf8..7cfa4e5ce3920 100644 --- a/components/trustpilot/actions/fetch-service-reviews/fetch-service-reviews.mjs +++ b/components/trustpilot/actions/fetch-service-reviews/fetch-service-reviews.mjs @@ -3,7 +3,7 @@ import trustpilot from "../../trustpilot.app.mjs"; export default { key: "trustpilot-fetch-service-reviews", name: "Fetch Service Reviews", - description: "Get private reviews for a business unit, limited to 100,000 records. Response includes customer email and order ID. [See the documentation](https://developers.trustpilot.com/business-units-api#get-private-reviews-for-business-unit)", + description: "Get private reviews for a business unit. Response includes customer email and order ID. [See the documentation](https://developers.trustpilot.com/business-units-api#get-private-reviews-for-business-unit)", version: "0.1.0", type: "action", props: { diff --git a/components/trustpilot/common/constants.mjs b/components/trustpilot/common/constants.mjs index 0fbf1a4613e3a..e2d5a8aedb50a 100644 --- a/components/trustpilot/common/constants.mjs +++ b/components/trustpilot/common/constants.mjs @@ -1,22 +1,8 @@ export const BASE_URL = "https://api.trustpilot.com/v1"; -export const WEBHOOK_EVENTS = { - REVIEW_CREATED: "review.created", - REVIEW_REVISED: "review.revised", - REVIEW_DELETED: "review.deleted", - REPLY_CREATED: "reply.created", - INVITATION_SENT: "invitation.sent", - INVITATION_FAILED: "invitation.failed", -}; - export const ENDPOINTS = { // Business Units BUSINESS_UNITS: "/business-units/search", - BUSINESS_UNIT_BY_ID: "/business-units/{businessUnitId}", - - // Public Reviews - PUBLIC_REVIEWS: "/business-units/{businessUnitId}/reviews", - PUBLIC_REVIEW_BY_ID: "/reviews/{reviewId}", // Service Reviews SERVICE_REVIEWS: "/private/business-units/{businessUnitId}/reviews", @@ -29,51 +15,9 @@ export const ENDPOINTS = { CREATE_CONVERSATION_FOR_REVIEW: "/private/product-reviews/{reviewId}/create-conversation", // Conversations - CONVERSATIONS: "/private/conversations", CONVERSATION_BY_ID: "/private/conversations/{conversationId}", REPLY_TO_CONVERSATION: "/private/conversations/{conversationId}/comments", }; -export const REVIEW_TYPES = { - SERVICE: "service", - PRODUCT: "product", -}; - -export const INVITATION_TYPES = { - REVIEW: "review", - PRODUCT_REVIEW: "product-review", -}; - -export const SORT_OPTIONS = { - CREATED_AT_ASC: "createdat.asc", - CREATED_AT_DESC: "createdat.desc", - STARS_ASC: "stars.asc", - STARS_DESC: "stars.desc", - UPDATED_AT_ASC: "updatedat.asc", - UPDATED_AT_DESC: "updatedat.desc", -}; - -export const RATING_SCALE = [ - 1, - 2, - 3, - 4, - 5, -]; - export const DEFAULT_LIMIT = 20; export const MAX_LIMIT = 100; - -export const POLLING_CONFIG = { - DEFAULT_TIMER_INTERVAL_SECONDS: 15 * 60, // 15 minutes - MAX_ITEMS_PER_POLL: 100, - LOOKBACK_HOURS: 24, // How far back to look on first run -}; - -export const SOURCE_TYPES = { - NEW_REVIEWS: "new_reviews", - UPDATED_REVIEWS: "updated_reviews", - NEW_REPLIES: "new_replies", - NEW_CONVERSATIONS: "new_conversations", - UPDATED_CONVERSATIONS: "updated_conversations", -}; diff --git a/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs b/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs index 4351dadc97297..1ffcad96f729e 100644 --- a/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs +++ b/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs @@ -1,4 +1,5 @@ import common from "../common/polling.mjs"; +import { DEFAULT_LIMIT } from "../../common/constants.mjs"; export default { ...common, @@ -23,7 +24,7 @@ export default { // so we'll rely on pagination and client-side filtering return { businessUnitId: this.businessUnitId, - perPage: 100, + perPage: DEFAULT_LIMIT, page: 1, }; }, diff --git a/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs b/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs index bbcb7ad01508f..f72c794001d07 100644 --- a/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs +++ b/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs @@ -1,4 +1,8 @@ import common from "../common/polling.mjs"; +import { + DEFAULT_LIMIT, + MAX_LIMIT, +} from "../../common/constants.mjs"; export default { ...common, @@ -20,7 +24,7 @@ export default { getFetchParams(lastReviewTime) { const params = { businessUnitId: this.businessUnitId, - perPage: 100, + perPage: DEFAULT_LIMIT, orderBy: "createdat.desc", }; @@ -36,10 +40,17 @@ export default { let result = await this.trustpilot.fetchServiceReviews($, params); // Handle pagination for service reviews - while (result.reviews && result.reviews.length === 100) { - params.page = (params.page || 1) + 1; - const nextResult = await this.trustpilot.fetchServiceReviews($, params); - result.reviews = result.reviews.concat(nextResult.reviews || []); + if (result.reviews && result.reviews.length === DEFAULT_LIMIT) { + while (true) { + params.page = (params.page || 1) + 1; + const nextResult = await this.trustpilot.fetchServiceReviews($, params); + result.reviews = result.reviews.concat(nextResult.reviews || []); + + if ((nextResult.reviews && nextResult.reviews.length < DEFAULT_LIMIT) + || result.reviews.length >= MAX_LIMIT) { + break; + } + } } return result; diff --git a/components/trustpilot/trustpilot.app.mjs b/components/trustpilot/trustpilot.app.mjs index cac5df5116576..e37fb575e0472 100644 --- a/components/trustpilot/trustpilot.app.mjs +++ b/components/trustpilot/trustpilot.app.mjs @@ -122,7 +122,14 @@ export default { // Shared method for fetching service reviews - used by both actions and sources async fetchServiceReviews($, params = {}) { - const { businessUnitId } = params; + const { + businessUnitId, + page = 1, + perPage = 20, + orderBy = "createdat.desc", + ignoreTagValueCase = false, + ...filters + } = params; // Validate required parameters if (!businessUnitId) { @@ -139,11 +146,11 @@ export default { // Prepare query parameters const queryParams = { - ...params, - page: 1, - perPage: 20, - orderBy: "createdat.desc", - ignoreTagValueCase: false, + ...filters, + page, + perPage, + orderBy, + ignoreTagValueCase, }; // Make the API request From b55a70ba5cb5a4d321c62485bc4ed3bdaa602b1e Mon Sep 17 00:00:00 2001 From: Job Nijenhuis Date: Fri, 5 Sep 2025 11:35:12 +0200 Subject: [PATCH 22/24] final comments --- .../trustpilot/sources/common/polling.mjs | 73 +++++++++---------- components/trustpilot/trustpilot.app.mjs | 6 +- 2 files changed, 36 insertions(+), 43 deletions(-) diff --git a/components/trustpilot/sources/common/polling.mjs b/components/trustpilot/sources/common/polling.mjs index a3d689dbf700d..2fd5febe9377a 100644 --- a/components/trustpilot/sources/common/polling.mjs +++ b/components/trustpilot/sources/common/polling.mjs @@ -74,55 +74,50 @@ export default { }, }, async run({ $ }) { - try { - // Get the last review time for filtering new reviews - const lastReviewTime = this._getLastReviewTime(); + // Get the last review time for filtering new reviews + const lastReviewTime = this._getLastReviewTime(); - // Get fetch parameters from child class - const fetchParams = this.getFetchParams(lastReviewTime); + // Get fetch parameters from child class + const fetchParams = this.getFetchParams(lastReviewTime); - // Fetch reviews using child class method - const result = await this.fetchReviews($, fetchParams); - const reviews = result.reviews || []; + // Fetch reviews using child class method + const result = await this.fetchReviews($, fetchParams); + const reviews = result.reviews || []; - if (!reviews.length) { - console.log("No reviews found"); - return; - } + if (!reviews.length) { + console.log("No reviews found"); + return; + } - // Filter for new reviews (child class may override) - const newReviews = this.filterNewReviews(reviews, lastReviewTime); + // Filter for new reviews (child class may override) + const newReviews = this.filterNewReviews(reviews, lastReviewTime); - if (!newReviews.length) { - console.log("No new reviews since last poll"); - return; - } - - // Track the latest review time - let latestReviewTime = lastReviewTime; + if (!newReviews.length) { + console.log("No new reviews since last poll"); + return; + } - for (const review of newReviews) { - // Track the latest review time - const reviewTime = new Date(review.createdAt).toISOString(); - if (!latestReviewTime || new Date(reviewTime) > new Date(latestReviewTime)) { - latestReviewTime = reviewTime; - } + // Track the latest review time + let latestReviewTime = lastReviewTime; - // Emit the review with unique ID and summary - this.$emit(review, { - id: review.id, - summary: this.generateSummary(review), - ts: new Date(review.createdAt).getTime(), - }); + for (const review of newReviews) { + // Track the latest review time + const reviewTime = new Date(review.createdAt).toISOString(); + if (!latestReviewTime || new Date(reviewTime) > new Date(latestReviewTime)) { + latestReviewTime = reviewTime; } - // Update the last review time for next poll - if (latestReviewTime && latestReviewTime !== lastReviewTime) { - this._setLastReviewTime(latestReviewTime); - } + // Emit the review with unique ID and summary + this.$emit(review, { + id: review.id, + summary: this.generateSummary(review), + ts: new Date(review.createdAt).getTime(), + }); + } - } catch (error) { - throw new Error(`Failed to fetch reviews: ${error.message}`); + // Update the last review time for next poll + if (latestReviewTime && latestReviewTime !== lastReviewTime) { + this._setLastReviewTime(latestReviewTime); } }, }; diff --git a/components/trustpilot/trustpilot.app.mjs b/components/trustpilot/trustpilot.app.mjs index e37fb575e0472..0ff3d01f57526 100644 --- a/components/trustpilot/trustpilot.app.mjs +++ b/components/trustpilot/trustpilot.app.mjs @@ -221,12 +221,10 @@ export default { // Handle the correct response structure (productReviews, not reviews) const reviews = response.productReviews?.map(parseProductReview) || []; const pagination = { - total: response.links?.total || 0, + total: response.total || 0, page: queryParams.page || 1, perPage: queryParams.perPage || 20, - hasMore: response.links?.next - ? true - : false, + hasMore: response.links?.some((l) => l.rel === "next") || false, }; return { From 6bb8e541656e902f8c15dc80a9e471f246cb14af Mon Sep 17 00:00:00 2001 From: Job Nijenhuis Date: Fri, 5 Sep 2025 12:05:07 +0200 Subject: [PATCH 23/24] more comments --- .../trustpilot/sources/common/polling.mjs | 40 ++++++++++++++----- .../new-product-reviews.mjs | 40 +++++++++++++++++-- .../new-service-reviews.mjs | 7 ++-- components/trustpilot/trustpilot.app.mjs | 24 ++++++----- 4 files changed, 83 insertions(+), 28 deletions(-) diff --git a/components/trustpilot/sources/common/polling.mjs b/components/trustpilot/sources/common/polling.mjs index 2fd5febe9377a..112ded8cb876a 100644 --- a/components/trustpilot/sources/common/polling.mjs +++ b/components/trustpilot/sources/common/polling.mjs @@ -1,4 +1,5 @@ import trustpilot from "../../trustpilot.app.mjs"; +import { DEFAULT_LIMIT } from "../../common/constants.mjs"; /** * Base polling source for Trustpilot integration @@ -25,10 +26,10 @@ export default { }, methods: { _getLastReviewTime() { - return this.db.get("lastReviewTime"); + return this.db.get(`lastReviewTime:${this.businessUnitId}`); }, _setLastReviewTime(time) { - this.db.set("lastReviewTime", time); + this.db.set(`lastReviewTime:${this.businessUnitId}`, time); }, /** * Override in child classes to provide review type-specific summary @@ -40,10 +41,13 @@ export default { throw new Error("generateSummary must be implemented in child class"); }, /** - * Override in child classes to fetch reviews using appropriate method + * Override in child classes to fetch reviews. + * Requirements: + * - Must return ALL reviews newer than `lastReviewTime` (handle pagination internally), or + * - Return the first page AND expose a pagination cursor so the base can iterate (future). * @param {Object} _$ - Pipedream step context - * @param {Object} _params - Fetch parameters - * @returns {Object} - API response with reviews array + * @param {Object} _params - Fetch parameters produced by `getFetchParams(lastReviewTime)` + * @returns {{ reviews: Array }} - Array of normalized reviews */ // eslint-disable-next-line no-unused-vars async fetchReviews(_$, _params) { @@ -58,7 +62,7 @@ export default { getFetchParams(_lastReviewTime) { return { businessUnitId: this.businessUnitId, - perPage: 100, + perPage: DEFAULT_LIMIT, }; }, /** @@ -69,8 +73,14 @@ export default { */ // eslint-disable-next-line no-unused-vars filterNewReviews(reviews, _lastReviewTime) { - // Default: return all reviews (for APIs with server-side time filtering) - return reviews; + if (!_lastReviewTime) return reviews; + const lastMs = Date.parse(_lastReviewTime); + if (Number.isNaN(lastMs)) return reviews; + + return reviews.filter((r) => { + const ms = Date.parse(r?.createdAt); + return Number.isFinite(ms) && ms > lastMs; + }); }, }, async run({ $ }) { @@ -102,8 +112,16 @@ export default { for (const review of newReviews) { // Track the latest review time - const reviewTime = new Date(review.createdAt).toISOString(); - if (!latestReviewTime || new Date(reviewTime) > new Date(latestReviewTime)) { + const createdMs = Date.parse(review?.createdAt); + if (!Number.isFinite(createdMs)) { + ($.logger?.warn ?? console.warn)("Skipping review with invalid createdAt", { + id: review?.id, + createdAt: review?.createdAt, + }); + continue; + } + const reviewTime = new Date(createdMs).toISOString(); + if (!latestReviewTime || createdMs > Date.parse(latestReviewTime)) { latestReviewTime = reviewTime; } @@ -111,7 +129,7 @@ export default { this.$emit(review, { id: review.id, summary: this.generateSummary(review), - ts: new Date(review.createdAt).getTime(), + ts: createdMs, }); } diff --git a/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs b/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs index 1ffcad96f729e..d04132e3c7f92 100644 --- a/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs +++ b/components/trustpilot/sources/new-product-reviews/new-product-reviews.mjs @@ -1,5 +1,8 @@ import common from "../common/polling.mjs"; -import { DEFAULT_LIMIT } from "../../common/constants.mjs"; +import { + DEFAULT_LIMIT, + MAX_LIMIT, +} from "../../common/constants.mjs"; export default { ...common, @@ -29,8 +32,39 @@ export default { }; }, async fetchReviews($, params) { - // Use the shared method from the app directly - return await this.trustpilot.fetchProductReviews($, params); + const perPage = params.perPage ?? DEFAULT_LIMIT; + let page = params.page ?? 1; + + // fetch first page + let result = await this.trustpilot.fetchProductReviews($, { + ...params, + page, + }); + let all = Array.isArray(result.reviews) + ? result.reviews + : []; + let lastPageSize = all.length; + + // keep paging while we get a full page and stay under MAX_LIMIT + while (lastPageSize === perPage && all.length < MAX_LIMIT) { + page += 1; + const next = await this.trustpilot.fetchProductReviews($, { + ...params, + page, + }); + const chunk = Array.isArray(next.reviews) ? + next.reviews : + []; + if (chunk.length === 0) break; + + all = all.concat(chunk); + lastPageSize = chunk.length; + result = next; // preserve any metadata from the latest fetch + } + + // truncate to MAX_LIMIT in case there are more than allowed + result.reviews = all.slice(0, MAX_LIMIT); + return result; }, filterNewReviews(reviews, lastReviewTime) { // Product reviews require client-side filtering since API doesn't support diff --git a/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs b/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs index f72c794001d07..e58cce907fe95 100644 --- a/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs +++ b/components/trustpilot/sources/new-service-reviews/new-service-reviews.mjs @@ -8,7 +8,7 @@ export default { ...common, key: "trustpilot-new-service-reviews", name: "New Service Reviews", - description: "Emit new event when a customer posts a new service review on Trustpilot. This source periodically polls the Trustpilot API to detect new service reviews using the private reviews API for comprehensive coverage. Each event contains the complete review data including star rating, review text, consumer details, business unit info, customer email, and timestamps. Ideal for monitoring overall business reputation, tracking customer satisfaction metrics, and triggering workflows based on review ratings or content.", + description: "Emit new event when a customer posts a new service review on Trustpilot. This source periodically polls the Trustpilot API to detect new service reviews using the private reviews API for comprehensive coverage.", version: "0.1.0", type: "source", dedupe: "unique", @@ -46,8 +46,9 @@ export default { const nextResult = await this.trustpilot.fetchServiceReviews($, params); result.reviews = result.reviews.concat(nextResult.reviews || []); - if ((nextResult.reviews && nextResult.reviews.length < DEFAULT_LIMIT) - || result.reviews.length >= MAX_LIMIT) { + if (!nextResult.reviews || + nextResult.reviews.length < DEFAULT_LIMIT || + result.reviews.length >= MAX_LIMIT) { break; } } diff --git a/components/trustpilot/trustpilot.app.mjs b/components/trustpilot/trustpilot.app.mjs index 0ff3d01f57526..31e6f84f23d8d 100644 --- a/components/trustpilot/trustpilot.app.mjs +++ b/components/trustpilot/trustpilot.app.mjs @@ -59,12 +59,6 @@ export default { description: "Filter by SKU", optional: true, }, - productUrl: { - type: "string", - label: "Product URL", - description: "Filter by product URL", - optional: true, - }, page: { type: "integer", label: "Page", @@ -162,10 +156,16 @@ export default { // Handle the correct response structure (reviews array) const reviews = response.reviews?.map(parseServiceReview) || []; const pagination = { - total: response.pagination?.total || 0, - page: response.pagination?.page || queryParams.page, - perPage: response.pagination?.perPage || queryParams.perPage, - hasMore: response.pagination?.hasMore || false, + total: typeof response.total === "number" + ? response.total : + null, + // Preserve the page and perPage we requested + page: queryParams.page, + perPage: queryParams.perPage, + // Determine if there’s a next page by checking for a "next" link + hasMore: Array.isArray(response.links) + ? response.links.some((l) => l?.rel === "next-page") + : false, }; return { @@ -195,6 +195,9 @@ export default { if (!businessUnitId) { throw new Error("Business Unit ID is required"); } + if (!validateBusinessUnitId(businessUnitId)) { + throw new Error("Invalid business unit ID format"); + } // Build the endpoint URL const endpoint = buildUrl(ENDPOINTS.PRIVATE_PRODUCT_REVIEWS, { @@ -208,7 +211,6 @@ export default { locale, perPage, page, - includeReportedReviews: false, language, }; From f306eda6cfca584cf34d498375d3a2d1e8e4a110 Mon Sep 17 00:00:00 2001 From: Job Nijenhuis Date: Fri, 5 Sep 2025 12:08:34 +0200 Subject: [PATCH 24/24] . --- pnpm-lock.yaml | 573 +------------------------------------------------ 1 file changed, 6 insertions(+), 567 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9796299477691..511b002848b36 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -976,8 +976,7 @@ importers: components/apilio: {} - components/apimage: - specifiers: {} + components/apimage: {} components/apipie_ai: {} @@ -3718,8 +3717,7 @@ importers: components/device_magic: {} - components/devin: - specifiers: {} + components/devin: {} components/devrev: dependencies: @@ -16696,37 +16694,6 @@ importers: specifier: ^4.3.0 version: 4.3.0(@types/node@24.0.10)(rollup@4.27.3)(typescript@5.9.2)(vite@5.4.11(@types/node@24.0.10)) - packages/connect-react/examples/nextjs: - dependencies: - '@pipedream/connect-react': - specifier: file:../.. - version: file:packages/connect-react(@types/react@18.3.12)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) - '@pipedream/sdk': - specifier: ^1.8.0 - version: 1.8.0 - next: - specifier: 15.0.3 - version: 15.0.3(@babel/core@8.0.0-alpha.13)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) - react: - specifier: 19.0.0-rc-66855b96-20241106 - version: 19.0.0-rc-66855b96-20241106 - react-dom: - specifier: 19.0.0-rc-66855b96-20241106 - version: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) - devDependencies: - '@types/node': - specifier: ^20 - version: 20.17.6 - '@types/react': - specifier: ^18 - version: 18.3.12 - '@types/react-dom': - specifier: ^18 - version: 18.3.1 - typescript: - specifier: ^5 - version: 5.7.2 - packages/prompts: dependencies: typescript: @@ -18245,9 +18212,6 @@ packages: '@emnapi/core@1.4.3': resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==} - '@emnapi/runtime@1.3.1': - resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} - '@emnapi/runtime@1.4.3': resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==} @@ -19199,65 +19163,33 @@ packages: '@iarna/toml@2.2.5': resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==} - '@img/sharp-darwin-arm64@0.33.5': - resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [darwin] - '@img/sharp-darwin-arm64@0.34.3': resolution: {integrity: sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.33.5': - resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [darwin] - '@img/sharp-darwin-x64@0.34.3': resolution: {integrity: sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.0.4': - resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} - cpu: [arm64] - os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.2.0': resolution: {integrity: sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.0.4': - resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} - cpu: [x64] - os: [darwin] - '@img/sharp-libvips-darwin-x64@1.2.0': resolution: {integrity: sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.0.4': - resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} - cpu: [arm64] - os: [linux] - '@img/sharp-libvips-linux-arm64@1.2.0': resolution: {integrity: sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linux-arm@1.0.5': - resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} - cpu: [arm] - os: [linux] - '@img/sharp-libvips-linux-arm@1.2.0': resolution: {integrity: sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==} cpu: [arm] @@ -19268,64 +19200,32 @@ packages: cpu: [ppc64] os: [linux] - '@img/sharp-libvips-linux-s390x@1.0.4': - resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} - cpu: [s390x] - os: [linux] - '@img/sharp-libvips-linux-s390x@1.2.0': resolution: {integrity: sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==} cpu: [s390x] os: [linux] - '@img/sharp-libvips-linux-x64@1.0.4': - resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} - cpu: [x64] - os: [linux] - '@img/sharp-libvips-linux-x64@1.2.0': resolution: {integrity: sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==} cpu: [x64] os: [linux] - '@img/sharp-libvips-linuxmusl-arm64@1.0.4': - resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} - cpu: [arm64] - os: [linux] - '@img/sharp-libvips-linuxmusl-arm64@1.2.0': resolution: {integrity: sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linuxmusl-x64@1.0.4': - resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} - cpu: [x64] - os: [linux] - '@img/sharp-libvips-linuxmusl-x64@1.2.0': resolution: {integrity: sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==} cpu: [x64] os: [linux] - '@img/sharp-linux-arm64@0.33.5': - resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - '@img/sharp-linux-arm64@0.34.3': resolution: {integrity: sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linux-arm@0.33.5': - resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm] - os: [linux] - '@img/sharp-linux-arm@0.34.3': resolution: {integrity: sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -19338,59 +19238,30 @@ packages: cpu: [ppc64] os: [linux] - '@img/sharp-linux-s390x@0.33.5': - resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [s390x] - os: [linux] - '@img/sharp-linux-s390x@0.34.3': resolution: {integrity: sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - '@img/sharp-linux-x64@0.33.5': - resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - '@img/sharp-linux-x64@0.34.3': resolution: {integrity: sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-linuxmusl-arm64@0.33.5': - resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - '@img/sharp-linuxmusl-arm64@0.34.3': resolution: {integrity: sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linuxmusl-x64@0.33.5': - resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - '@img/sharp-linuxmusl-x64@0.34.3': resolution: {integrity: sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-wasm32@0.33.5': - resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [wasm32] - '@img/sharp-wasm32@0.34.3': resolution: {integrity: sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -19402,24 +19273,12 @@ packages: cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.33.5': - resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ia32] - os: [win32] - '@img/sharp-win32-ia32@0.34.3': resolution: {integrity: sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.33.5': - resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [win32] - '@img/sharp-win32-x64@0.34.3': resolution: {integrity: sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -19821,63 +19680,12 @@ packages: engines: {node: '>=18.14.0'} hasBin: true - '@next/env@15.0.3': - resolution: {integrity: sha512-t9Xy32pjNOvVn2AS+Utt6VmyrshbpfUMhIjFO60gI58deSo/KgLOp31XZ4O+kY/Is8WAGYwA5gR7kOb1eORDBA==} - '@next/eslint-plugin-next@14.2.19': resolution: {integrity: sha512-S/vJfvcmAh3BK0xn//EolX6mmuF7Os0PJk3pThn4IwVmfl31HABLRb5qkmS0Gn/OwogiLZ1iCQnFFtyJqs5ROw==} '@next/eslint-plugin-next@15.0.3': resolution: {integrity: sha512-3Ln/nHq2V+v8uIaxCR6YfYo7ceRgZNXfTd3yW1ukTaFbO+/I8jNakrjYWODvG9BuR2v5kgVtH/C8r0i11quOgw==} - '@next/swc-darwin-arm64@15.0.3': - resolution: {integrity: sha512-s3Q/NOorCsLYdCKvQlWU+a+GeAd3C8Rb3L1YnetsgwXzhc3UTWrtQpB/3eCjFOdGUj5QmXfRak12uocd1ZiiQw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - - '@next/swc-darwin-x64@15.0.3': - resolution: {integrity: sha512-Zxl/TwyXVZPCFSf0u2BNj5sE0F2uR6iSKxWpq4Wlk/Sv9Ob6YCKByQTkV2y6BCic+fkabp9190hyrDdPA/dNrw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - - '@next/swc-linux-arm64-gnu@15.0.3': - resolution: {integrity: sha512-T5+gg2EwpsY3OoaLxUIofmMb7ohAUlcNZW0fPQ6YAutaWJaxt1Z1h+8zdl4FRIOr5ABAAhXtBcpkZNwUcKI2fw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@next/swc-linux-arm64-musl@15.0.3': - resolution: {integrity: sha512-WkAk6R60mwDjH4lG/JBpb2xHl2/0Vj0ZRu1TIzWuOYfQ9tt9NFsIinI1Epma77JVgy81F32X/AeD+B2cBu/YQA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@next/swc-linux-x64-gnu@15.0.3': - resolution: {integrity: sha512-gWL/Cta1aPVqIGgDb6nxkqy06DkwJ9gAnKORdHWX1QBbSZZB+biFYPFti8aKIQL7otCE1pjyPaXpFzGeG2OS2w==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@next/swc-linux-x64-musl@15.0.3': - resolution: {integrity: sha512-QQEMwFd8r7C0GxQS62Zcdy6GKx999I/rTO2ubdXEe+MlZk9ZiinsrjwoiBL5/57tfyjikgh6GOU2WRQVUej3UA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@next/swc-win32-arm64-msvc@15.0.3': - resolution: {integrity: sha512-9TEp47AAd/ms9fPNgtgnT7F3M1Hf7koIYYWCMQ9neOwjbVWJsHZxrFbI3iEDJ8rf1TDGpmHbKxXf2IFpAvheIQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - - '@next/swc-win32-x64-msvc@15.0.3': - resolution: {integrity: sha512-VNAz+HN4OGgvZs6MOoVfnn41kBzT+M+tB+OK4cww6DNyWS6wKaDpaAm/qLeOUbnMh0oVx1+mg0uoYARF69dJyA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - '@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3': resolution: {integrity: sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==} @@ -20221,12 +20029,6 @@ packages: '@pipedream/clio@0.1.1': resolution: {integrity: sha512-IbnvVIWiAjS6P03WmDkLLnKxFwM+WgjwZZ6TmEbYmyX9zUDfVbohnniqdAcJh++pH2GcCS8aCOVNBosjwKzJbQ==} - '@pipedream/connect-react@file:packages/connect-react': - resolution: {directory: packages/connect-react, type: directory} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - '@pipedream/docusign@0.2.1': resolution: {integrity: sha512-4NxQNHAWOZ/eFvj/PkmZ4LY9ZwsXZdCh363scbNlJfXqeMGCxnNrfRrEgYIFWvnB8pJyzHtSdxsDLL2axIMx4g==} @@ -22164,12 +21966,6 @@ packages: '@supabase/supabase-js@2.49.4': resolution: {integrity: sha512-jUF0uRUmS8BKt37t01qaZ88H9yV1mbGYnqLeuFWLcdV+x1P4fl0yP9DGtaEhFPZcwSom7u16GkLEH9QJZOqOkw==} - '@swc/counter@0.1.3': - resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - - '@swc/helpers@0.5.13': - resolution: {integrity: sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==} - '@szmarczak/http-timer@4.0.6': resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} engines: {node: '>=10'} @@ -22442,9 +22238,6 @@ packages: '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - '@types/react-dom@18.3.1': - resolution: {integrity: sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==} - '@types/react-transition-group@4.4.11': resolution: {integrity: sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==} @@ -23460,10 +23253,6 @@ packages: engines: {'0': node >=0.10.0} hasBin: true - busboy@1.6.0: - resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} - engines: {node: '>=10.16.0'} - byline@5.0.0: resolution: {integrity: sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==} engines: {node: '>=0.10.0'} @@ -23746,9 +23535,6 @@ packages: resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} engines: {node: '>= 10'} - client-only@0.0.1: - resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} - clipboardy@4.0.0: resolution: {integrity: sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w==} engines: {node: '>=18'} @@ -28476,27 +28262,6 @@ packages: next-tick@1.1.0: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} - next@15.0.3: - resolution: {integrity: sha512-ontCbCRKJUIoivAdGB34yCaOcPgYXr9AAkV/IwqFfWWTXEPUgLYkSkqBhIk9KK7gGmgjc64B+RdoeIDM13Irnw==} - engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} - hasBin: true - peerDependencies: - '@opentelemetry/api': ^1.1.0 - '@playwright/test': ^1.41.2 - babel-plugin-react-compiler: '*' - react: ^18.2.0 || 19.0.0-rc-66855b96-20241106 - react-dom: ^18.2.0 || 19.0.0-rc-66855b96-20241106 - sass: ^1.3.0 - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - '@playwright/test': - optional: true - babel-plugin-react-compiler: - optional: true - sass: - optional: true - nice-try@1.0.5: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} @@ -29405,10 +29170,6 @@ packages: peerDependencies: postcss: ^8.2.9 - postcss@8.4.31: - resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.4.49: resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} engines: {node: ^10 || ^12 || >=14} @@ -29758,11 +29519,6 @@ packages: peerDependencies: react: ^18.3.1 - react-dom@19.0.0-rc-66855b96-20241106: - resolution: {integrity: sha512-D25vdaytZ1wFIRiwNU98NPQ/upS2P8Co4/oNoa02PzHbh8deWdepjm5qwZM/46OdSiGv4WSWwxP55RO9obqJEQ==} - peerDependencies: - react: 19.0.0-rc-66855b96-20241106 - react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -29794,10 +29550,6 @@ packages: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} - react@19.0.0-rc-66855b96-20241106: - resolution: {integrity: sha512-klH7xkT71SxRCx4hb1hly5FJB21Hz0ACyxbXYAECEqssUjtJeFUAaI2U1DgJAzkGEnvEm3DkxuBchMC/9K4ipg==} - engines: {node: '>=0.10.0'} - read-package-up@11.0.0: resolution: {integrity: sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==} engines: {node: '>=18'} @@ -30269,9 +30021,6 @@ packages: scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} - scheduler@0.25.0-rc-66855b96-20241106: - resolution: {integrity: sha512-HQXp/Mnp/MMRSXMQF7urNFla+gmtXW/Gr1KliuR0iboTit4KvZRY8KYaq5ccCTAOJiUqQh2rE2F3wgUekmgdlA==} - scmp@2.1.0: resolution: {integrity: sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==} @@ -30377,10 +30126,6 @@ packages: resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} engines: {node: '>=8'} - sharp@0.33.5: - resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - sharp@0.34.3: resolution: {integrity: sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -30734,10 +30479,6 @@ packages: resolution: {integrity: sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==} engines: {node: '>=8.0'} - streamsearch@1.1.0: - resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} - engines: {node: '>=10.0.0'} - streamx@2.22.1: resolution: {integrity: sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==} @@ -30885,19 +30626,6 @@ packages: style-to-object@1.0.8: resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==} - styled-jsx@5.1.6: - resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} - engines: {node: '>= 12.0.0'} - peerDependencies: - '@babel/core': '*' - babel-plugin-macros: '*' - react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' - peerDependenciesMeta: - '@babel/core': - optional: true - babel-plugin-macros: - optional: true - stylelint-config-recommended@14.0.1: resolution: {integrity: sha512-bLvc1WOz/14aPImu/cufKAZYfXs/A/owZfSMZ4N+16WGXLoX5lOir53M6odBxvhgmgdxCVnNySJmZKx73T93cg==} engines: {node: '>=18.12.0'} @@ -30947,22 +30675,22 @@ packages: superagent@3.8.1: resolution: {integrity: sha512-VMBFLYgFuRdfeNQSMLbxGSLfmXL/xc+OO+BZp41Za/NRDBet/BNbkRJrYzCUu0u4GU0i/ml2dtT8b9qgkw9z6Q==} engines: {node: '>= 4.0'} - deprecated: Please upgrade to v7.0.2+ of superagent. We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing. See the releases tab for more information at . + deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net superagent@4.1.0: resolution: {integrity: sha512-FT3QLMasz0YyCd4uIi5HNe+3t/onxMyEho7C3PSqmti3Twgy2rXT4fmkTz6wRL6bTF4uzPcfkUCa8u4JWHw8Ag==} engines: {node: '>= 6.0'} - deprecated: Please upgrade to v7.0.2+ of superagent. We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing. See the releases tab for more information at . + deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net superagent@5.3.1: resolution: {integrity: sha512-wjJ/MoTid2/RuGCOFtlacyGNxN9QLMgcpYLDQlWFIhhdJ93kNscFonGvrpAHSCVjRVj++DGCglocF7Aej1KHvQ==} engines: {node: '>= 7.0.0'} - deprecated: Please upgrade to v7.0.2+ of superagent. We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing. See the releases tab for more information at . + deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net superagent@7.1.6: resolution: {integrity: sha512-gZkVCQR1gy/oUXr+kxJMLDjla434KmSOKbx5iGD30Ql+AkJQ/YlPKECJy2nhqOsHLjGHzoDTXNSjhnvWhzKk7g==} engines: {node: '>=6.4.0 <13 || >=14'} - deprecated: Please downgrade to v7.1.5 if you need IE/ActiveXObject support OR upgrade to v8.0.0 as we no longer support IE and published an incorrect patch version (see https://github.com/visionmedia/superagent/issues/1731) + deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net supports-color@10.0.0: resolution: {integrity: sha512-HRVVSbCCMbj7/kdWF9Q+bbckjBHLtHMEoJWlkmYzzdwhYMkjkOwubLM6t7NbWKjgKamGDrWL1++KrjUO1t9oAQ==} @@ -35940,11 +35668,6 @@ snapshots: tslib: 2.8.1 optional: true - '@emnapi/runtime@1.3.1': - dependencies: - tslib: 2.8.1 - optional: true - '@emnapi/runtime@1.4.3': dependencies: tslib: 2.8.1 @@ -36004,22 +35727,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@emotion/react@11.13.5(@types/react@18.3.12)(react@19.0.0-rc-66855b96-20241106)': - dependencies: - '@babel/runtime': 7.26.0 - '@emotion/babel-plugin': 11.13.5 - '@emotion/cache': 11.13.5 - '@emotion/serialize': 1.3.3 - '@emotion/use-insertion-effect-with-fallbacks': 1.1.0(react@19.0.0-rc-66855b96-20241106) - '@emotion/utils': 1.4.2 - '@emotion/weak-memoize': 0.4.0 - hoist-non-react-statics: 3.3.2 - react: 19.0.0-rc-66855b96-20241106 - optionalDependencies: - '@types/react': 18.3.12 - transitivePeerDependencies: - - supports-color - '@emotion/serialize@1.3.3': dependencies: '@emotion/hash': 0.9.2 @@ -36036,10 +35743,6 @@ snapshots: dependencies: react: 18.3.1 - '@emotion/use-insertion-effect-with-fallbacks@1.1.0(react@19.0.0-rc-66855b96-20241106)': - dependencies: - react: 19.0.0-rc-66855b96-20241106 - '@emotion/utils@1.4.2': {} '@emotion/weak-memoize@0.4.0': {} @@ -36865,92 +36568,48 @@ snapshots: '@iarna/toml@2.2.5': {} - '@img/sharp-darwin-arm64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.0.4 - optional: true - '@img/sharp-darwin-arm64@0.34.3': optionalDependencies: '@img/sharp-libvips-darwin-arm64': 1.2.0 optional: true - '@img/sharp-darwin-x64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.0.4 - optional: true - '@img/sharp-darwin-x64@0.34.3': optionalDependencies: '@img/sharp-libvips-darwin-x64': 1.2.0 optional: true - '@img/sharp-libvips-darwin-arm64@1.0.4': - optional: true - '@img/sharp-libvips-darwin-arm64@1.2.0': optional: true - '@img/sharp-libvips-darwin-x64@1.0.4': - optional: true - '@img/sharp-libvips-darwin-x64@1.2.0': optional: true - '@img/sharp-libvips-linux-arm64@1.0.4': - optional: true - '@img/sharp-libvips-linux-arm64@1.2.0': optional: true - '@img/sharp-libvips-linux-arm@1.0.5': - optional: true - '@img/sharp-libvips-linux-arm@1.2.0': optional: true '@img/sharp-libvips-linux-ppc64@1.2.0': optional: true - '@img/sharp-libvips-linux-s390x@1.0.4': - optional: true - '@img/sharp-libvips-linux-s390x@1.2.0': optional: true - '@img/sharp-libvips-linux-x64@1.0.4': - optional: true - '@img/sharp-libvips-linux-x64@1.2.0': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.0.4': - optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.2.0': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.0.4': - optional: true - '@img/sharp-libvips-linuxmusl-x64@1.2.0': optional: true - '@img/sharp-linux-arm64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.0.4 - optional: true - '@img/sharp-linux-arm64@0.34.3': optionalDependencies: '@img/sharp-libvips-linux-arm64': 1.2.0 optional: true - '@img/sharp-linux-arm@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.0.5 - optional: true - '@img/sharp-linux-arm@0.34.3': optionalDependencies: '@img/sharp-libvips-linux-arm': 1.2.0 @@ -36961,51 +36620,26 @@ snapshots: '@img/sharp-libvips-linux-ppc64': 1.2.0 optional: true - '@img/sharp-linux-s390x@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.0.4 - optional: true - '@img/sharp-linux-s390x@0.34.3': optionalDependencies: '@img/sharp-libvips-linux-s390x': 1.2.0 optional: true - '@img/sharp-linux-x64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.0.4 - optional: true - '@img/sharp-linux-x64@0.34.3': optionalDependencies: '@img/sharp-libvips-linux-x64': 1.2.0 optional: true - '@img/sharp-linuxmusl-arm64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 - optional: true - '@img/sharp-linuxmusl-arm64@0.34.3': optionalDependencies: '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 optional: true - '@img/sharp-linuxmusl-x64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.0.4 - optional: true - '@img/sharp-linuxmusl-x64@0.34.3': optionalDependencies: '@img/sharp-libvips-linuxmusl-x64': 1.2.0 optional: true - '@img/sharp-wasm32@0.33.5': - dependencies: - '@emnapi/runtime': 1.3.1 - optional: true - '@img/sharp-wasm32@0.34.3': dependencies: '@emnapi/runtime': 1.4.5 @@ -37014,15 +36648,9 @@ snapshots: '@img/sharp-win32-arm64@0.34.3': optional: true - '@img/sharp-win32-ia32@0.33.5': - optional: true - '@img/sharp-win32-ia32@0.34.3': optional: true - '@img/sharp-win32-x64@0.33.5': - optional: true - '@img/sharp-win32-x64@0.34.3': optional: true @@ -37835,8 +37463,6 @@ snapshots: - rollup - supports-color - '@next/env@15.0.3': {} - '@next/eslint-plugin-next@14.2.19': dependencies: glob: 10.3.10 @@ -37845,30 +37471,6 @@ snapshots: dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@15.0.3': - optional: true - - '@next/swc-darwin-x64@15.0.3': - optional: true - - '@next/swc-linux-arm64-gnu@15.0.3': - optional: true - - '@next/swc-linux-arm64-musl@15.0.3': - optional: true - - '@next/swc-linux-x64-gnu@15.0.3': - optional: true - - '@next/swc-linux-x64-musl@15.0.3': - optional: true - - '@next/swc-win32-arm64-msvc@15.0.3': - optional: true - - '@next/swc-win32-x64-msvc@15.0.3': - optional: true - '@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3': optional: true @@ -38267,21 +37869,6 @@ snapshots: transitivePeerDependencies: - debug - '@pipedream/connect-react@file:packages/connect-react(@types/react@18.3.12)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': - dependencies: - '@pipedream/sdk': 1.8.0 - '@tanstack/react-query': 5.61.0(react@19.0.0-rc-66855b96-20241106) - lodash.isequal: 4.5.0 - react: 19.0.0-rc-66855b96-20241106 - react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) - react-markdown: 9.0.1(@types/react@18.3.12)(react@19.0.0-rc-66855b96-20241106) - react-select: 5.8.3(@types/react@18.3.12)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) - transitivePeerDependencies: - - '@types/react' - - bufferutil - - supports-color - - utf-8-validate - '@pipedream/docusign@0.2.1': dependencies: '@pipedream/platform': 3.1.0 @@ -40762,12 +40349,6 @@ snapshots: - bufferutil - utf-8-validate - '@swc/counter@0.1.3': {} - - '@swc/helpers@0.5.13': - dependencies: - tslib: 2.8.1 - '@szmarczak/http-timer@4.0.6': dependencies: defer-to-connect: 2.0.1 @@ -40783,11 +40364,6 @@ snapshots: '@tanstack/query-core': 5.60.6 react: 18.3.1 - '@tanstack/react-query@5.61.0(react@19.0.0-rc-66855b96-20241106)': - dependencies: - '@tanstack/query-core': 5.60.6 - react: 19.0.0-rc-66855b96-20241106 - '@techteamer/ocsp@1.0.0': dependencies: asn1.js: 5.4.1 @@ -41095,10 +40671,6 @@ snapshots: '@types/range-parser@1.2.7': {} - '@types/react-dom@18.3.1': - dependencies: - '@types/react': 18.3.12 - '@types/react-transition-group@4.4.11': dependencies: '@types/react': 18.3.12 @@ -42462,10 +42034,6 @@ snapshots: mv: 2.1.1 safe-json-stringify: 1.2.0 - busboy@1.6.0: - dependencies: - streamsearch: 1.1.0 - byline@5.0.0: {} bytes@3.1.2: {} @@ -42733,8 +42301,6 @@ snapshots: cli-width@3.0.0: {} - client-only@0.0.1: {} - clipboardy@4.0.0: dependencies: execa: 8.0.1 @@ -49049,32 +48615,6 @@ snapshots: next-tick@1.1.0: {} - next@15.0.3(@babel/core@8.0.0-alpha.13)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106): - dependencies: - '@next/env': 15.0.3 - '@swc/counter': 0.1.3 - '@swc/helpers': 0.5.13 - busboy: 1.6.0 - caniuse-lite: 1.0.30001683 - postcss: 8.4.31 - react: 19.0.0-rc-66855b96-20241106 - react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) - styled-jsx: 5.1.6(@babel/core@8.0.0-alpha.13)(babel-plugin-macros@3.1.0)(react@19.0.0-rc-66855b96-20241106) - optionalDependencies: - '@next/swc-darwin-arm64': 15.0.3 - '@next/swc-darwin-x64': 15.0.3 - '@next/swc-linux-arm64-gnu': 15.0.3 - '@next/swc-linux-arm64-musl': 15.0.3 - '@next/swc-linux-x64-gnu': 15.0.3 - '@next/swc-linux-x64-musl': 15.0.3 - '@next/swc-win32-arm64-msvc': 15.0.3 - '@next/swc-win32-x64-msvc': 15.0.3 - '@opentelemetry/api': 1.9.0 - sharp: 0.33.5 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - nice-try@1.0.5: {} node-abort-controller@3.1.1: {} @@ -50106,12 +49646,6 @@ snapshots: postcss: 8.5.6 quote-unquote: 1.0.0 - postcss@8.4.31: - dependencies: - nanoid: 3.3.7 - picocolors: 1.1.1 - source-map-js: 1.2.1 - postcss@8.4.49: dependencies: nanoid: 3.3.7 @@ -50709,11 +50243,6 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 - react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106): - dependencies: - react: 19.0.0-rc-66855b96-20241106 - scheduler: 0.25.0-rc-66855b96-20241106 - react-is@16.13.1: {} react-is@17.0.2: {} @@ -50737,23 +50266,6 @@ snapshots: transitivePeerDependencies: - supports-color - react-markdown@9.0.1(@types/react@18.3.12)(react@19.0.0-rc-66855b96-20241106): - dependencies: - '@types/hast': 3.0.4 - '@types/react': 18.3.12 - devlop: 1.1.0 - hast-util-to-jsx-runtime: 2.3.2 - html-url-attributes: 3.0.1 - mdast-util-to-hast: 13.2.0 - react: 19.0.0-rc-66855b96-20241106 - remark-parse: 11.0.0 - remark-rehype: 11.1.1 - unified: 11.0.5 - unist-util-visit: 5.0.0 - vfile: 6.0.3 - transitivePeerDependencies: - - supports-color - react-select@5.8.3(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.26.0 @@ -50771,23 +50283,6 @@ snapshots: - '@types/react' - supports-color - react-select@5.8.3(@types/react@18.3.12)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106): - dependencies: - '@babel/runtime': 7.26.0 - '@emotion/cache': 11.13.5 - '@emotion/react': 11.13.5(@types/react@18.3.12)(react@19.0.0-rc-66855b96-20241106) - '@floating-ui/dom': 1.6.12 - '@types/react-transition-group': 4.4.11 - memoize-one: 6.0.0 - prop-types: 15.8.1 - react: 19.0.0-rc-66855b96-20241106 - react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) - react-transition-group: 4.4.5(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) - use-isomorphic-layout-effect: 1.1.2(@types/react@18.3.12)(react@19.0.0-rc-66855b96-20241106) - transitivePeerDependencies: - - '@types/react' - - supports-color - react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.26.0 @@ -50797,21 +50292,10 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-transition-group@4.4.5(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106): - dependencies: - '@babel/runtime': 7.26.0 - dom-helpers: 5.2.1 - loose-envify: 1.4.0 - prop-types: 15.8.1 - react: 19.0.0-rc-66855b96-20241106 - react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) - react@18.3.1: dependencies: loose-envify: 1.4.0 - react@19.0.0-rc-66855b96-20241106: {} - read-package-up@11.0.0: dependencies: find-up-simple: 1.0.1 @@ -51517,8 +51001,6 @@ snapshots: dependencies: loose-envify: 1.4.0 - scheduler@0.25.0-rc-66855b96-20241106: {} - scmp@2.1.0: {} secure-json-parse@2.7.0: {} @@ -51652,33 +51134,6 @@ snapshots: dependencies: kind-of: 6.0.3 - sharp@0.33.5: - dependencies: - color: 4.2.3 - detect-libc: 2.0.3 - semver: 7.6.3 - optionalDependencies: - '@img/sharp-darwin-arm64': 0.33.5 - '@img/sharp-darwin-x64': 0.33.5 - '@img/sharp-libvips-darwin-arm64': 1.0.4 - '@img/sharp-libvips-darwin-x64': 1.0.4 - '@img/sharp-libvips-linux-arm': 1.0.5 - '@img/sharp-libvips-linux-arm64': 1.0.4 - '@img/sharp-libvips-linux-s390x': 1.0.4 - '@img/sharp-libvips-linux-x64': 1.0.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 - '@img/sharp-libvips-linuxmusl-x64': 1.0.4 - '@img/sharp-linux-arm': 0.33.5 - '@img/sharp-linux-arm64': 0.33.5 - '@img/sharp-linux-s390x': 0.33.5 - '@img/sharp-linux-x64': 0.33.5 - '@img/sharp-linuxmusl-arm64': 0.33.5 - '@img/sharp-linuxmusl-x64': 0.33.5 - '@img/sharp-wasm32': 0.33.5 - '@img/sharp-win32-ia32': 0.33.5 - '@img/sharp-win32-x64': 0.33.5 - optional: true - sharp@0.34.3: dependencies: color: 4.2.3 @@ -52115,8 +51570,6 @@ snapshots: transitivePeerDependencies: - supports-color - streamsearch@1.1.0: {} - streamx@2.22.1: dependencies: fast-fifo: 1.3.2 @@ -52284,14 +51737,6 @@ snapshots: dependencies: inline-style-parser: 0.2.4 - styled-jsx@5.1.6(@babel/core@8.0.0-alpha.13)(babel-plugin-macros@3.1.0)(react@19.0.0-rc-66855b96-20241106): - dependencies: - client-only: 0.0.1 - react: 19.0.0-rc-66855b96-20241106 - optionalDependencies: - '@babel/core': 8.0.0-alpha.13 - babel-plugin-macros: 3.1.0 - stylelint-config-recommended@14.0.1(stylelint@16.10.0(typescript@5.6.3)): dependencies: stylelint: 16.10.0(typescript@5.6.3) @@ -53467,12 +52912,6 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 - use-isomorphic-layout-effect@1.1.2(@types/react@18.3.12)(react@19.0.0-rc-66855b96-20241106): - dependencies: - react: 19.0.0-rc-66855b96-20241106 - optionalDependencies: - '@types/react': 18.3.12 - utf7@1.0.2: dependencies: semver: 5.3.0