diff --git a/components/ortto/.gitignore b/components/ortto/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/ortto/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/ortto/actions/create-custom-activity/create-custom-activity.mjs b/components/ortto/actions/create-custom-activity/create-custom-activity.mjs new file mode 100644 index 0000000000000..fe7753fa909ee --- /dev/null +++ b/components/ortto/actions/create-custom-activity/create-custom-activity.mjs @@ -0,0 +1,51 @@ +import { parseObject } from "../../common/utils.mjs"; +import ortto from "../../ortto.app.mjs"; + +export default { + key: "ortto-create-custom-activity", + name: "Create Custom Activity", + description: "Creates a unique activity for a person. Can optionally initialize a new record beforehand. [See the documentation](https://help.ortto.com/a-271-create-a-custom-activity-event-create)", + version: "0.0.1", + type: "action", + props: { + ortto, + activityId: { + propDefinition: [ + ortto, + "activityId", + ], + }, + attributes: { + propDefinition: [ + ortto, + "attributes", + ], + }, + fields: { + propDefinition: [ + ortto, + "fields", + ], + }, + }, + async run({ $ }) { + const response = await this.ortto.createCustomActivity({ + $, + data: { + "activities": [ + { + activity_id: this.activityId, + attributes: parseObject(this.attributes), + fields: parseObject(this.fields), + }, + ], + "merge_by": [ + "str::email", + ], + }, + }); + + $.export("$summary", "Successfully created activity"); + return response; + }, +}; diff --git a/components/ortto/actions/create-person/create-person.mjs b/components/ortto/actions/create-person/create-person.mjs new file mode 100644 index 0000000000000..e9962f3402959 --- /dev/null +++ b/components/ortto/actions/create-person/create-person.mjs @@ -0,0 +1,108 @@ +import { clearObj } from "../../common/utils.mjs"; +import ortto from "../../ortto.app.mjs"; + +export default { + key: "ortto-create-person", + name: "Create or Update a Person", + description: "Create or update a preexisting person in the Ortto account. [See the documentation](https://help.ortto.com/a-250-api-reference)", + version: "0.0.1", + type: "action", + props: { + ortto, + firstName: { + type: "string", + label: "First Name", + description: "The person's first name.", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "The person's last name.", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "The person's phone number.", + }, + email: { + type: "string", + label: "Email", + description: "The person's email address.", + }, + city: { + type: "string", + label: "City", + description: "The person's address city.", + optional: true, + }, + country: { + type: "string", + label: "Country", + description: "The person's address country.", + optional: true, + }, + region: { + type: "string", + label: "Region", + description: "The person's address region.", + optional: true, + }, + birthday: { + type: "string", + label: "Birthday", + description: "The person's birth date.", + optional: true, + }, + }, + async run({ $ }) { + const { + ortto, + ...props + } = this; + + const birthday = {}; + if (props.birthday) { + const date = new Date(props.birthday); + birthday.day = date.getDate(); + birthday.month = date.getMonth() + 1; + birthday.year = date.getFullYear(); + } + + const response = await ortto.createPerson({ + data: { + people: [ + { + fields: clearObj({ + "str::first": props.firstName, + "str::last": props.lastName, + "phn::phone": { + "phone": props.phone, + "parse_with_country_code": true, + }, + "str::email": props.email, + "geo::city": { + name: props.city, + }, + "geo::country": { + name: props.country, + }, + "geo::region": { + name: props.region, + }, + "dtz::b": birthday, + }), + }, + ], + async: false, + merge_by: [ + "str::email", + ], + find_strategy: 0, + }, + }); + $.export("$summary", "Person successfully initialized or updated!"); + return response; + }, +}; diff --git a/components/ortto/actions/opt-out-sms/opt-out-sms.mjs b/components/ortto/actions/opt-out-sms/opt-out-sms.mjs new file mode 100644 index 0000000000000..23493003267db --- /dev/null +++ b/components/ortto/actions/opt-out-sms/opt-out-sms.mjs @@ -0,0 +1,39 @@ +import ortto from "../../ortto.app.mjs"; + +export default { + key: "ortto-opt-out-sms", + name: "Opt Out of SMS", + description: "Allows a user to opt-out from all SMS communications. [See the documentation](https://help.ortto.com/a-250-api-reference)", + version: "0.0.1", + type: "action", + props: { + ortto, + userEmail: { + propDefinition: [ + ortto, + "userEmail", + ], + }, + }, + async run({ $ }) { + const response = await this.ortto.updatePerson({ + data: { + people: [ + { + fields: { + "str::email": this.userEmail, + "bol::sp": false, + }, + }, + ], + async: false, + merge_by: [ + "str::email", + ], + }, + }); + + $.export("$summary", `Successfully opted out SMS for User ID: ${this.userEmail}`); + return response; + }, +}; diff --git a/components/ortto/app/ortto.app.ts b/components/ortto/app/ortto.app.ts deleted file mode 100644 index 3cf734f0e1e4c..0000000000000 --- a/components/ortto/app/ortto.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "ortto", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); \ No newline at end of file diff --git a/components/ortto/common/constants.mjs b/components/ortto/common/constants.mjs new file mode 100644 index 0000000000000..ea830c15a04cb --- /dev/null +++ b/components/ortto/common/constants.mjs @@ -0,0 +1 @@ +export const LIMIT = 100; diff --git a/components/ortto/common/utils.mjs b/components/ortto/common/utils.mjs new file mode 100644 index 0000000000000..adf012089ff85 --- /dev/null +++ b/components/ortto/common/utils.mjs @@ -0,0 +1,41 @@ +export const clearObj = (obj) => { + return Object.entries(obj) + .filter(([ + , + v, + ]) => (v != null && v != "" && JSON.stringify(v) != "{}")) + .reduce((acc, [ + k, + v, + ]) => ({ + ...acc, + [k]: (!Array.isArray(v) && v === Object(v)) + ? clearObj(v) + : v, + }), {}); +}; + +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/ortto/ortto.app.mjs b/components/ortto/ortto.app.mjs new file mode 100644 index 0000000000000..fb899b14a3dd4 --- /dev/null +++ b/components/ortto/ortto.app.mjs @@ -0,0 +1,115 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + +export default { + type: "app", + app: "ortto", + propDefinitions: { + userEmail: { + type: "string", + label: "User Email", + description: "Specify the user email to opt out from all SMS communications.", + async options({ page }) { + const { contacts } = await this.listPeople({ + data: { + limit: LIMIT, + offset: LIMIT * page, + fields: [ + "str::first", + "str::last", + "str::email", + ], + }, + }); + + return contacts.map(({ fields }) => ({ + label: `${fields["str::first"]} ${fields["str::last"]} (${fields["str::email"]})`, + value: fields["str::email"], + })); + }, + }, + activityId: { + type: "string", + label: "Activity Id", + description: "The Id of the activity definition. To find Activity ID, login into your Ortto app > CDP > Activities > Select an activity, then you can find the Activity ID in the browser URL as `https://ortto.app/{Org}/activities/{Activity_ID}/overview`. For example, if your Activity URL is `https://ortto.app/pipedreamtest/activities/act::s/overview`, then your Activity ID is `act::s`", + }, + fields: { + type: "object", + label: "Fields", + description: "The object containing the fields for a person associated with the event.", + }, + attributes: { + type: "object", + label: "Attributes", + description: "An object with the attributes. To find Activity attributes, login into your Ortto app > CDP > Activities > Select an activity > Developer.", + }, + }, + methods: { + _baseUrl() { + return `https://${this.$auth.region}/v1`; + }, + _headers() { + return { + "X-Api-Key": `${this.$auth.api_key}`, + "Content-Type": "application/json", + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + method: "POST", + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + listPeople(opts = {}) { + return this._makeRequest({ + path: "/person/get", + ...opts, + }); + }, + updatePerson(opts = {}) { + return this._makeRequest({ + path: "/person/merge", + ...opts, + }); + }, + createPerson(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/person/merge", + ...opts, + }); + }, + createCustomActivity(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/activities/create", + ...opts, + }); + }, + async *paginate({ + fn, data = {}, fieldName, ...opts + }) { + let hasMore = false; + let page = 0; + + do { + data.limit = LIMIT; + data.offset = LIMIT * page++; + const response = await fn({ + data, + ...opts, + }); + for (const d of response[fieldName]) { + yield d; + } + + hasMore = response.has_more; + + } while (hasMore); + }, + }, +}; diff --git a/components/ortto/package.json b/components/ortto/package.json index 791493cd88960..3411ff2897a21 100644 --- a/components/ortto/package.json +++ b/components/ortto/package.json @@ -1,16 +1,18 @@ { "name": "@pipedream/ortto", - "version": "0.0.2", + "version": "0.1.0", "description": "Pipedream Ortto Components", - "main": "dist/app/ortto.app.mjs", + "main": "ortto.app.mjs", "keywords": [ "pipedream", "ortto" ], - "files": ["dist"], "homepage": "https://pipedream.com/apps/ortto", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } diff --git a/components/ortto/sources/common/base.mjs b/components/ortto/sources/common/base.mjs new file mode 100644 index 0000000000000..0479c45dd01af --- /dev/null +++ b/components/ortto/sources/common/base.mjs @@ -0,0 +1,65 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import ortto from "../../ortto.app.mjs"; + +export default { + props: { + ortto, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || 0; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + + const response = this.ortto.paginate({ + fn: this.getFunction(), + fieldName: this.getFieldName(), + data: { + "fields": this.getFields(), + "sort_by_field_id": "c", + "sort_order": "desc", + }, + }); + + let responseArray = []; + for await (const item of response) { + if (Date.parse(item.fields.c) <= lastDate) break; + responseArray.push(item); + } + + if (responseArray.length) { + if (maxResults && (responseArray.length > maxResults)) { + responseArray.length = maxResults; + } + this._setLastDate(responseArray[0].id); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item.id, + summary: this.getSummary(item), + ts: Date.parse(item.fields.c || new Date()), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/ortto/sources/new-contact-created/new-contact-created.mjs b/components/ortto/sources/new-contact-created/new-contact-created.mjs new file mode 100644 index 0000000000000..e4d5235c9f673 --- /dev/null +++ b/components/ortto/sources/new-contact-created/new-contact-created.mjs @@ -0,0 +1,48 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "ortto-new-contact-created", + name: "New Contact Created", + description: "Emit new event when a contact is created in your Ortto account.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.ortto.listPeople; + }, + getFieldName() { + return "contacts"; + }, + getFields() { + return [ + "str::first", + "str::last", + "phn::phone", + "str::email", + "geo::city", + "geo::country", + "dtz::b", + "geo::region", + "str::postal", + "tags", + "u4s::t", + "bol::gdpr", + "str::ei", + "bol::p", + "str::u-ctx", + "str::s-ctx", + "bol::sp", + "str::soi-ctx", + "str::soo-ctx", + ]; + }, + getSummary(item) { + return `New Contact: ${item.fields["str::first"] || ""} ${item.fields["str::last"] || ""} ${item.fields["str::email"] || ""} `; + }, + }, + sampleEmit, +}; diff --git a/components/ortto/sources/new-contact-created/test-event.mjs b/components/ortto/sources/new-contact-created/test-event.mjs new file mode 100644 index 0000000000000..c57c01771ccb2 --- /dev/null +++ b/components/ortto/sources/new-contact-created/test-event.mjs @@ -0,0 +1,8 @@ +export default { + "id":"0061b02b24f9b6f85dcb1e00", + "fields":{ + "str::ei":"c533532fe5d16c7d4fa4c7f0", + "str::email":"alex@example.com", + "str::first":"Alex" + } +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a530799bd550..e773dd20c7542 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7149,7 +7149,11 @@ importers: specifier: ^1.6.2 version: 1.6.6 - components/ortto: {} + components/ortto: + dependencies: + '@pipedream/platform': + specifier: ^3.0.3 + version: 3.0.3 components/otter_waiver: dependencies: