@pipedream/trustpilot
Version:
Pipedream Trustpilot Components
582 lines (521 loc) • 15.3 kB
JavaScript
import { axios } from "@pipedream/platform";
import { defineApp } from "@pipedream/types";
import * as crypto from "crypto";
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";
export default defineApp({
type: "app",
app: "trustpilot",
propDefinitions: {
businessUnitId: {
type: "string",
label: "Business Unit ID",
description: "The unique identifier for your business unit on Trustpilot",
async options() {
try {
const businessUnits = await this.searchBusinessUnits({
query: "",
limit: 20,
});
return businessUnits.map(({
id, displayName, name: { identifying },
}) => ({
label: `${identifying || displayName}`,
value: id,
}));
} catch (error) {
console.error("Error fetching business units:", error);
return [];
}
},
},
reviewId: {
type: "string",
label: "Review ID",
description: "The unique identifier for a review",
},
stars: {
type: "integer",
label: "Star Rating",
description: "Filter by star rating (1-5)",
options: RATING_SCALE,
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: {
type: "integer",
label: "Limit",
description: "Maximum number of results to return",
min: 1,
max: MAX_LIMIT,
default: DEFAULT_LIMIT,
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",
optional: true,
},
language: {
type: "string",
label: "Language",
description: "Filter reviews by language (ISO 639-1 code)",
optional: true,
},
},
methods: {
// Authentication and base request methods
_getAuthHeaders() {
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;
}
if (this.$auth?.oauth_access_token) {
headers["Authorization"] = `Bearer ${this.$auth.oauth_access_token}`;
}
return headers;
},
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;
}
},
// 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);
},
async searchBusinessUnits({
query = "", limit = DEFAULT_LIMIT, offset = 0,
} = {}) {
const response = await this._makeRequest({
endpoint: ENDPOINTS.BUSINESS_UNITS,
params: {
query,
limit,
offset,
},
});
return response.businessUnits?.map(parseBusinessUnit) || [];
},
// 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, {
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");
}
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);
},
// Private helper for fetching reviews
async _getReviews({
endpoint,
businessUnitId,
stars = null,
sortBy = SORT_OPTIONS.CREATED_AT_DESC,
limit = DEFAULT_LIMIT,
offset = 0,
includeReportedReviews = false,
tags = [],
language = null,
}) {
if (businessUnitId && !validateBusinessUnitId(businessUnitId)) {
throw new Error("Invalid business unit ID");
}
const params = {
stars,
orderBy: sortBy,
perPage: limit,
page: Math.floor(offset / limit) + 1,
includeReportedReviews,
language,
};
if (tags.length > 0) {
params.tags = tags.join(",");
}
const response = await this._makeRequestWithRetry({
endpoint: endpoint || ENDPOINTS.PRIVATE_SERVICE_REVIEWS,
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,
},
};
},
// Private Service Review methods
async getServiceReviews(options = {}) {
const endpoint = buildUrl(ENDPOINTS.PRIVATE_SERVICE_REVIEWS, {
businessUnitId: options.businessUnitId,
});
return this._getReviews({
endpoint,
...options,
});
},
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.PRIVATE_SERVICE_REVIEW_BY_ID, {
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;
},
// Product Review methods
async getProductReviews(options = {}) {
const endpoint = buildUrl(ENDPOINTS.PRIVATE_PRODUCT_REVIEWS, {
businessUnitId: options.businessUnitId,
});
return this._getReviews({
endpoint,
...options,
});
},
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 endpoint = buildUrl(ENDPOINTS.REPLY_TO_PRODUCT_REVIEW, {
reviewId,
});
const response = await this._makeRequest({
endpoint,
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,
},
});
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 || [];
},
// 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),
);
},
},
});