UNPKG

@pipedream/trustpilot

Version:

Pipedream Trustpilot Components

189 lines (171 loc) 5.83 kB
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(); }, };