Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion components/etrusted/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pipedream/etrusted",
"version": "0.2.0",
"version": "0.3.0",
"description": "Pipedream eTrusted Components",
"main": "etrusted.app.mjs",
"keywords": [
Expand Down
225 changes: 225 additions & 0 deletions components/etrusted/sources/new-review/new-review.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform";
import { parseObject } from "../../common/utils.mjs";
import etrusted from "../../etrusted.app.mjs";
import sampleEmit from "./test-event.mjs";

export default {
key: "etrusted-new-review",
name: "New Review",
description: "Emit new event when a new review is submitted on an eTrusted channel. [See the documentation](https://developers.etrusted.com/reference/getreviews)",
version: "0.0.1",
type: "source",
dedupe: "unique",
props: {
etrusted,
db: "$.service.db",
timer: {
type: "$.interface.timer",
label: "Polling Schedule",
description: "How often to poll the eTrusted Reviews API.",
default: {
intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL,
},
},
channelId: {
propDefinition: [
etrusted,
"channelId",
],
type: "string[]",
optional: true,
},
type: {
propDefinition: [
etrusted,
"type",
],
optional: true,
},
status: {
propDefinition: [
etrusted,
"status",
],
optional: true,
},
rating: {
propDefinition: [
etrusted,
"rating",
],
optional: true,
},
ignoreStatements: {
propDefinition: [
etrusted,
"ignoreStatements",
],
optional: true,
},
},
methods: {
_getCursor() {
return this.db.get("cursor") ?? {
submittedAt: this.db.get("lastSubmittedAt"),
ids: [],
};
},
_setCursor(cursor) {
this.db.set("cursor", cursor);
},
_toCsv(value) {
const parsed = parseObject(value);
if (parsed == null || parsed === "") {
return undefined;
}
return Array.isArray(parsed)
? parsed.join(",")
: String(parsed);
},
_getParams() {
return {
channels: this._toCsv(this.channelId),
type: this._toCsv(this.type),
status: this._toCsv(this.status),
rating: this._toCsv(this.rating),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
ignoreStatements: this.ignoreStatements,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
orderBy: "submittedAt",
};
},
_getSubmittedAt(review) {
return review.submittedAt ?? review.submitted_at ?? review.createdAt ?? review.created_at;
},
_getTs(review) {
const submittedAt = this._getSubmittedAt(review);
return submittedAt
? Date.parse(submittedAt)
: Date.now();
},
_getSummary(review) {
const rating = review.rating
? ` (${review.rating} star${review.rating === 1
? ""
: "s"})`
: "";
return `New review${rating}: ${review.title ?? review.id}`;
},
_emitReview(review) {
this.$emit(review, {
id: review.id,
summary: this._getSummary(review),
ts: this._getTs(review),
});
},
_getSubmittedAfter(cursor) {
if (!cursor?.submittedAt) {
return undefined;
}

return new Date(Date.parse(cursor.submittedAt) - 1).toISOString();
},
async _getReviews({
maxResults,
submittedAfter,
} = {}) {
const response = this.etrusted.paginate({
fn: this.etrusted.getListOfReviews,
params: {
...this._getParams(),
submittedAfter,
},
maxResults,
});

const reviews = [];
for await (const review of response) {
reviews.push(review);
}
return reviews;
},
_isNewReview(review, cursor) {
if (!cursor?.submittedAt) {
return true;
}

const submittedAt = this._getSubmittedAt(review);
if (!submittedAt) {
return true;
}

const reviewTs = Date.parse(submittedAt);
const cursorTs = Date.parse(cursor.submittedAt);
if (reviewTs > cursorTs) {
return true;
}
if (reviewTs < cursorTs) {
return false;
}

return !cursor.ids?.includes(review.id);
},
_sortBySubmittedAt(reviews) {
return reviews.sort((a, b) => this._getTs(a) - this._getTs(b));
},
_getNextCursor(reviews, cursor) {
const withSubmittedAt = reviews.filter((review) => this._getSubmittedAt(review));
if (!withSubmittedAt.length) {
return cursor;
}

const newestSubmittedAt = withSubmittedAt.reduce((max, review) => {
const submittedAt = this._getSubmittedAt(review);
return !max || Date.parse(submittedAt) > Date.parse(max)
? submittedAt
: max;
}, null);
const ids = withSubmittedAt
.filter((review) => this._getSubmittedAt(review) === newestSubmittedAt)
.map((review) => review.id);

if (cursor?.submittedAt === newestSubmittedAt) {
ids.push(...(cursor.ids ?? []));
}

return {
submittedAt: newestSubmittedAt,
ids: [
...new Set(ids),
],
};
},
_processReviews(reviews) {
if (!reviews.length) {
return;
}

const cursor = this._getCursor();
const newReviews = reviews.filter((review) => this._isNewReview(review, cursor));
const nextCursor = this._getNextCursor(reviews, cursor);

for (const review of this._sortBySubmittedAt(newReviews)) {
this._emitReview(review);
}

if (nextCursor?.submittedAt) {
this._setCursor(nextCursor);
}
},
},
hooks: {
async deploy() {
const reviews = await this._getReviews({
maxResults: 10,
});
this._processReviews(reviews);
},
},
async run() {
const cursor = this._getCursor();
const reviews = await this._getReviews({
submittedAfter: this._getSubmittedAfter(cursor),
});
this._processReviews(reviews);
},
sampleEmit,
};
16 changes: 16 additions & 0 deletions components/etrusted/sources/new-review/test-event.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export default {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sample event doesn't match the real CustomerReview payload. I deployed this source against a live eTrusted account and captured a real emitted event. The current fixture uses field names that don't exist in the API response (status -> actually state, channel: {id, name} -> actually channelRef string, consumer: {email} -> actually customer: {}), and omits ~11 real fields. Since sampleEmit drives downstream autocomplete, builders referencing steps.trigger.event.channel.name or .status would be building against fields that never arrive.

Suggest replacing the fixture with this real event shape (captured live, PII neutralized):

export default {
    _object: "CustomerReview",
    accountRef: "acc-5b57249d-9b51-49ef-aa8c-02b8dbad6722",
    channelRef: "chl-3de8e569-400f-4d31-9fdf-0d9b852748aa",
    comment: "Test",
    createdAt: "2026-06-25T12:47:20.793Z",
    customer: {
      email: "customer@example.com",
      firstName: "John",
      fullName: "John Doe",
      lastName: "Doe",
    },
    event: {
      id: "evt-de79c52f-f6eb-43e5-9b3b-54f548dfbb6c",
      type: "test",
    },
    feedbackType: "REVIEW",
    hasAttachments: false,
    id: "rev-c6411ca3-5e24-4913-be18-6c1c45777516",
    metadata: {
      OriginalCarModel: "Golf GTI",
      OriginalNameTP: "John Smith",
      title: "Mr.",
      nameAgent: "VW Demo Dealer",
    },
    questionnaire: {
      id: "qre-19f0315e-df85-4a39-80f1-e8a139cee10c",
      locale: "en_150",
      templateRef: "tpl-qst-b59263c2-89ef-4b4c-bbc8-cf4b5ae9725f",
    },
    rating: 5,
    state: "APPROVED",
    submittedAt: "2026-06-25T12:47:20.000Z",
    surveyData: [
      {
        questionRef: "coreStarRating",
        type: "CORE_STAR_RATING",
        userDefinedId: "coreStarRating",
        properties: {},
        value: {},
      },
      {
        questionRef: "coreReviewGood",
        type: "CORE_REVIEW",
        userDefinedId: "coreReviewGood",
        properties: {},
        value: {},
      },
      {
        questionRef: "opinionScale",
        type: "DIMENSIONS",
        userDefinedId: "opinionScale",
        properties: {},
        value: {},
      },
    ],
    title: "Test",
    transaction: {
      date: "2026-06-24T12:35:33.887Z",
      reference: "Test250626",
    },
    type: "SERVICE_REVIEW",
    updatedAt: "2026-06-25T12:47:20.859Z",
    verificationType: "VERIFIED",
  };

id: "rev-1234567890",
title: "Great service",
comment: "The order arrived quickly and support was helpful.",
rating: 5,
type: "SERVICE_REVIEW",
status: "APPROVED",
submittedAt: "2026-06-23T09:40:50.000Z",
channel: {
id: "chl-1234567890",
name: "Example channel",
},
consumer: {
email: "customer@example.com",
},
};