diff --git a/components/smashsend/actions/create-or-update-contact/create-or-update-contact.mjs b/components/smashsend/actions/create-or-update-contact/create-or-update-contact.mjs new file mode 100644 index 0000000000000..0b86f197e22f3 --- /dev/null +++ b/components/smashsend/actions/create-or-update-contact/create-or-update-contact.mjs @@ -0,0 +1,76 @@ +import smashsend from "../../smashsend.app.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "smashsend-create-or-update-contact", + name: "Create or Update Contact", + description: "Create a new contact or update an existing contact. [See the documentation](https://smashsend.com/docs/api/contacts)", + version: "0.0.1", + type: "action", + props: { + smashsend, + email: { + type: "string", + label: "Email", + description: "The email address of the contact. If the contact already exists, this will update the contact.", + }, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the contact", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the contact", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "The phone number of the contact", + optional: true, + }, + avatarUrl: { + type: "string", + label: "Avatar URL", + description: "The URL of the contact's avatar", + optional: true, + }, + countryCode: { + type: "string", + label: "Country Code", + description: "Two-letter ISO country code like `US`", + optional: true, + }, + properties: { + type: "object", + label: "Properties", + description: "Additional properties of the contact. Properties must exist in your workspace.", + optional: true, + }, + }, + async run({ $ }) { + const { contact } = await this.smashsend.createContact({ + $, + data: { + properties: { + email: this.email, + firstName: this.firstName, + lastName: this.lastName, + phone: this.phone, + avatarUrl: this.avatarUrl, + countryCode: this.countryCode, + ...parseObject(this.properties), + }, + }, + }); + if (contact?.id) { + $.export("$summary", `Successfully ${contact.updatedAt + ? "updated" + : "created"} contact ${contact.id}`); + } + return contact; + }, +}; diff --git a/components/smashsend/actions/delete-contact/delete-contact.mjs b/components/smashsend/actions/delete-contact/delete-contact.mjs new file mode 100644 index 0000000000000..f479615feadf3 --- /dev/null +++ b/components/smashsend/actions/delete-contact/delete-contact.mjs @@ -0,0 +1,26 @@ +import smashsend from "../../smashsend.app.mjs"; + +export default { + key: "smashsend-delete-contact", + name: "Delete Contact", + description: "Delete a contact. [See the documentation](https://smashsend.com/docs/api/contacts)", + version: "0.0.1", + type: "action", + props: { + smashsend, + contactId: { + propDefinition: [ + smashsend, + "contactId", + ], + }, + }, + async run({ $ }) { + const response = await this.smashsend.deleteContact({ + $, + contactId: this.contactId, + }); + $.export("$summary", `Successfully deleted contact ${this.contactId}`); + return response; + }, +}; diff --git a/components/smashsend/actions/list-contacts/list-contacts.mjs b/components/smashsend/actions/list-contacts/list-contacts.mjs new file mode 100644 index 0000000000000..5b07684d6ec4e --- /dev/null +++ b/components/smashsend/actions/list-contacts/list-contacts.mjs @@ -0,0 +1,62 @@ +import smashsend from "../../smashsend.app.mjs"; + +export default { + key: "smashsend-list-contacts", + name: "List Contacts", + description: "List all contacts. [See the documentation](https://smashsend.com/docs/api/contacts)", + version: "0.0.1", + type: "action", + props: { + smashsend, + sort: { + type: "string", + label: "Sort", + description: "Sort order: “createdAt.desc” or “createdAt.asc” (default: “createdAt.desc”)", + options: [ + "createdAt.desc", + "createdAt.asc", + ], + optional: true, + }, + search: { + type: "string", + label: "Search", + description: "Search for contacts by name, email, or phone number", + optional: true, + }, + status: { + type: "string", + label: "Status", + description: "Filter contacts by status", + options: [ + "SUBSCRIBED", + "UNSUBSCRIBED", + "BANNED", + ], + optional: true, + }, + maxResults: { + type: "integer", + label: "Max Results", + description: "The maximum number of results to return", + default: 100, + optional: true, + }, + }, + async run({ $ }) { + const contacts = await this.smashsend.getPaginatedResources({ + fn: this.smashsend.listContacts, + params: { + sort: this.sort, + search: this.search, + status: this.status, + }, + resourceKey: "contacts", + max: this.maxResults, + }); + $.export("$summary", `Successfully fetched ${contacts.length} contact${contacts.length === 1 + ? "" + : "s"}`); + return contacts; + }, +}; diff --git a/components/smashsend/actions/search-contacts/search-contacts.mjs b/components/smashsend/actions/search-contacts/search-contacts.mjs new file mode 100644 index 0000000000000..46a70d169bc01 --- /dev/null +++ b/components/smashsend/actions/search-contacts/search-contacts.mjs @@ -0,0 +1,29 @@ +import smashsend from "../../smashsend.app.mjs"; + +export default { + key: "smashsend-search-contacts", + name: "Search Contacts", + description: "Search for contacts by email address. [See the documentation](https://smashsend.com/docs/api/contacts)", + version: "0.0.1", + type: "action", + props: { + smashsend, + email: { + type: "string", + label: "Email", + description: "The email address to search for", + }, + }, + async run({ $ }) { + const { contact } = await this.smashsend.searchContacts({ + $, + params: { + email: this.email, + }, + }); + if (contact?.id) { + $.export("$summary", `Successfully fetched contact ${contact.id}`); + } + return contact; + }, +}; diff --git a/components/smashsend/common/utils.mjs b/components/smashsend/common/utils.mjs new file mode 100644 index 0000000000000..c2b75bdc360c0 --- /dev/null +++ b/components/smashsend/common/utils.mjs @@ -0,0 +1,25 @@ +export const parseObject = (obj) => { + if (!obj) { + return {}; + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch { + return obj; + } + } + if (Array.isArray(obj)) { + return obj.map(parseObject); + } + if (typeof obj === "object") { + return Object.fromEntries(Object.entries(obj).map(([ + key, + value, + ]) => [ + key, + parseObject(value), + ])); + } + return obj; +}; diff --git a/components/smashsend/package.json b/components/smashsend/package.json index 81fee398224e0..3842c30c4a907 100644 --- a/components/smashsend/package.json +++ b/components/smashsend/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/smashsend", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream SmashSend Components", "main": "smashsend.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.1.0" } -} \ No newline at end of file +} diff --git a/components/smashsend/smashsend.app.mjs b/components/smashsend/smashsend.app.mjs index 014613ec92db9..f4354ddb84e4c 100644 --- a/components/smashsend/smashsend.app.mjs +++ b/components/smashsend/smashsend.app.mjs @@ -1,11 +1,132 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "smashsend", - propDefinitions: {}, + propDefinitions: { + contactId: { + type: "string", + label: "Contact ID", + description: "The ID of the contact to update", + async options({ prevContext }) { + const params = prevContext?.cursor + ? { + cursor: prevContext.cursor, + } + : {}; + const { contacts } = await this.listContacts(params); + const { + items, cursor, + } = contacts; + return { + options: items?.map(({ + id: value, properties: { + firstName, lastName, email, + }, + }) => ({ + label: (firstName || lastName) + ? (`${firstName} ${lastName}`).trim() + " - " + email + : email, + value, + })) || [], + context: { + cursor, + }, + }; + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.smashsend.com/v1"; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `Bearer ${this.$auth.api_key}`, + }, + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks", + ...opts, + }); + }, + deleteWebhook({ + hookId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/webhooks/${hookId}`, + ...opts, + }); + }, + listContacts(opts = {}) { + return this._makeRequest({ + path: "/contacts", + ...opts, + }); + }, + searchContacts(opts = {}) { + return this._makeRequest({ + path: "/contacts/search", + ...opts, + }); + }, + createContact(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/contacts", + ...opts, + }); + }, + deleteContact({ + contactId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/contacts/${contactId}`, + ...opts, + }); + }, + async *paginate({ + fn, params = {}, resourceKey, max, + }) { + let count = 0; + do { + const response = await fn({ + params, + }); + const { + cursor, hasMore, items, + } = response[resourceKey]; + if (!items?.length) { + return; + } + for (const item of items) { + yield item; + if (max && ++count >= max) { + return; + } + } + if (!hasMore) { + return; + } + params.cursor = cursor; + } while (true); + }, + async getPaginatedResources(opts) { + const results = []; + for await (const item of this.paginate(opts)) { + results.push(item); + } + return results; }, }, }; diff --git a/components/smashsend/sources/common/base.mjs b/components/smashsend/sources/common/base.mjs new file mode 100644 index 0000000000000..b53ee317cb0fc --- /dev/null +++ b/components/smashsend/sources/common/base.mjs @@ -0,0 +1,66 @@ +import smashsend from "../../smashsend.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + props: { + smashsend, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + }, + hooks: { + async activate() { + const { webhook } = await this.smashsend.createWebhook({ + data: { + url: this.http.endpoint, + events: this.getEvents(), + }, + }); + this._setHookId(webhook?.id); + }, + async deactivate() { + const hookId = this._getHookId(); + if (hookId) { + await this.smashsend.deleteWebhook({ + hookId, + }); + } + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + generateMeta(event) { + const ts = Date.parse(event.timestamp); + return { + id: `${event.event}-${ts}`, + summary: this.getSummary(event), + ts, + }; + }, + getEvents() { + throw new ConfigurationError("getEvents must be implemented"); + }, + getSummary() { + throw new ConfigurationError("getSummary must be implemented"); + }, + }, + async run(event) { + this.http.respond({ + status: 200, + }); + + const { body } = event; + if (!body || body.event === "TESTING_CONNECTION") { + return; + } + const meta = this.generateMeta(body); + this.$emit(body, meta); + }, +}; diff --git a/components/smashsend/sources/contact-deleted/contact-deleted.mjs b/components/smashsend/sources/contact-deleted/contact-deleted.mjs new file mode 100644 index 0000000000000..f6785ecdf6f82 --- /dev/null +++ b/components/smashsend/sources/contact-deleted/contact-deleted.mjs @@ -0,0 +1,24 @@ +import base from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...base, + key: "smashsend-contact-deleted", + name: "Contact Deleted (Instant)", + description: "Emit new event when a contact is deleted", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...base.methods, + getEvents() { + return [ + "CONTACT_DELETED", + ]; + }, + getSummary(event) { + return `Contact Deleted: ${event.payload.contact.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/smashsend/sources/contact-deleted/test-event.mjs b/components/smashsend/sources/contact-deleted/test-event.mjs new file mode 100644 index 0000000000000..c70c775ffaf7b --- /dev/null +++ b/components/smashsend/sources/contact-deleted/test-event.mjs @@ -0,0 +1,26 @@ +export default { + "event": "CONTACT_DELETED", + "payload": { + "contact": { + "createdAt": "2025-07-07T20:19:09.661Z", + "id": "ctc_du4oDXsppLrEhQh5UoN1fUyK", + "properties": { + "avatarUrl": null, + "birthday": null, + "city": null, + "countryCode": null, + "email": "contact@example.com", + "firstName": "John", + "language": null, + "lastName": "Doe", + "phone": null, + "status": "SUBSCRIBED", + "tags": [] + }, + "updatedAt": "2025-07-07T20:21:12.873Z", + "workspaceId": "wrk_lKDIOgIaDqfDcfUnuCKG9C4S" + } + }, + "timestamp": "2025-07-07T20:34:54.906Z", + "version": 1 +} \ No newline at end of file diff --git a/components/smashsend/sources/contact-resubscribed/contact-resubscribed.mjs b/components/smashsend/sources/contact-resubscribed/contact-resubscribed.mjs new file mode 100644 index 0000000000000..9a38a288327ed --- /dev/null +++ b/components/smashsend/sources/contact-resubscribed/contact-resubscribed.mjs @@ -0,0 +1,24 @@ +import base from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...base, + key: "smashsend-contact-resubscribed", + name: "Contact Resubscribed (Instant)", + description: "Emit new event when a contact is resubscribed", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...base.methods, + getEvents() { + return [ + "CONTACT_RESUBSCRIBED", + ]; + }, + getSummary(event) { + return `Contact Resubscribed: ${event.payload.contact.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/smashsend/sources/contact-resubscribed/test-event.mjs b/components/smashsend/sources/contact-resubscribed/test-event.mjs new file mode 100644 index 0000000000000..46d19e0682bb5 --- /dev/null +++ b/components/smashsend/sources/contact-resubscribed/test-event.mjs @@ -0,0 +1,25 @@ +export default { + "event": "CONTACT_RESUBSCRIBED", + "payload": { + "contact": { + "createdAt": "2025-07-07T20:22:49.263Z", + "id": "ctc_FqzSFyFXRD9QrVGsm5ge9XnT", + "properties": { + "avatarUrl": null, + "birthday": null, + "city": null, + "countryCode": null, + "email": "contact@example.com", + "firstName": "John", + "language": null, + "lastName": "Doe", + "phone": null, + "status": "SUBSCRIBED" + }, + "updatedAt": "2025-07-07T20:32:15.654Z", + "workspaceId": "wrk_lKDIOgIaDqfDcfUnuCKG9C4S" + } + }, + "timestamp": "2025-07-07T20:32:15.737Z", + "version": 1 +} \ No newline at end of file diff --git a/components/smashsend/sources/contact-unsubscribed/contact-unsubscribed.mjs b/components/smashsend/sources/contact-unsubscribed/contact-unsubscribed.mjs new file mode 100644 index 0000000000000..a5d2d028000d9 --- /dev/null +++ b/components/smashsend/sources/contact-unsubscribed/contact-unsubscribed.mjs @@ -0,0 +1,24 @@ +import base from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...base, + key: "smashsend-contact-unsubscribed", + name: "Contact Unsubscribed (Instant)", + description: "Emit new event when a contact is unsubscribed", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...base.methods, + getEvents() { + return [ + "CONTACT_UNSUBSCRIBED", + ]; + }, + getSummary(event) { + return `Contact Unsubscribed: ${event.payload.contact.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/smashsend/sources/contact-unsubscribed/test-event.mjs b/components/smashsend/sources/contact-unsubscribed/test-event.mjs new file mode 100644 index 0000000000000..ee07b5992a340 --- /dev/null +++ b/components/smashsend/sources/contact-unsubscribed/test-event.mjs @@ -0,0 +1,25 @@ +export default { + "event": "CONTACT_UNSUBSCRIBED", + "payload": { + "contact": { + "createdAt": "2025-07-07T20:22:49.263Z", + "id": "ctc_FqzSFyFXRD9QrVGsm5ge9XnT", + "properties": { + "avatarUrl": null, + "birthday": null, + "city": null, + "countryCode": null, + "email": "contact@example.com", + "firstName": "John", + "language": null, + "lastName": "Doe", + "phone": null, + "status": "UNSUBSCRIBED" + }, + "updatedAt": "2025-07-07T20:29:33.222Z", + "workspaceId": "wrk_lKDIOgIaDqfDcfUnuCKG9C4S" + } + }, + "timestamp": "2025-07-07T20:29:33.306Z", + "version": 1 +} \ No newline at end of file diff --git a/components/smashsend/sources/contact-updated/contact-updated.mjs b/components/smashsend/sources/contact-updated/contact-updated.mjs new file mode 100644 index 0000000000000..404f3a651fd73 --- /dev/null +++ b/components/smashsend/sources/contact-updated/contact-updated.mjs @@ -0,0 +1,24 @@ +import base from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...base, + key: "smashsend-contact-updated", + name: "Contact Updated (Instant)", + description: "Emit new event when a contact is updated", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...base.methods, + getEvents() { + return [ + "CONTACT_UPDATED", + ]; + }, + getSummary(event) { + return `Contact Updated: ${event.payload.contact.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/smashsend/sources/contact-updated/test-event.mjs b/components/smashsend/sources/contact-updated/test-event.mjs new file mode 100644 index 0000000000000..774527cef4aa0 --- /dev/null +++ b/components/smashsend/sources/contact-updated/test-event.mjs @@ -0,0 +1,25 @@ +export default { + "event": "CONTACT_UPDATED", + "payload": { + "contact": { + "createdAt": "2025-07-07T20:22:49.263Z", + "id": "ctc_FqzSFyFXRD9QrVGsm5ge9XnT", + "properties": { + "avatarUrl": null, + "birthday": null, + "city": null, + "countryCode": null, + "email": "contact@example.com", + "firstName": "John", + "language": null, + "lastName": "Doe", + "phone": null, + "status": "SUBSCRIBED" + }, + "updatedAt": "2025-07-07T20:27:12.419Z", + "workspaceId": "wrk_lKDIOgIaDqfDcfUnuCKG9C4S" + } + }, + "timestamp": "2025-07-07T20:27:12.502Z", + "version": 1 +} \ No newline at end of file diff --git a/components/smashsend/sources/new-contact-created/new-contact-created.mjs b/components/smashsend/sources/new-contact-created/new-contact-created.mjs new file mode 100644 index 0000000000000..9af551ba59efa --- /dev/null +++ b/components/smashsend/sources/new-contact-created/new-contact-created.mjs @@ -0,0 +1,24 @@ +import base from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...base, + key: "smashsend-new-contact-created", + name: "New Contact Created (Instant)", + description: "Emit new event when a contact is created", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...base.methods, + getEvents() { + return [ + "CONTACT_CREATED", + ]; + }, + getSummary(event) { + return `New Contact Created: ${event.payload.contact.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/smashsend/sources/new-contact-created/test-event.mjs b/components/smashsend/sources/new-contact-created/test-event.mjs new file mode 100644 index 0000000000000..2017bdc06d204 --- /dev/null +++ b/components/smashsend/sources/new-contact-created/test-event.mjs @@ -0,0 +1,25 @@ +export default { + "event": "CONTACT_CREATED", + "payload": { + "contact": { + "createdAt": "2025-07-07T20:22:49.263Z", + "id": "ctc_FqzSFyFXRD9QrVGsm5ge9XnT", + "properties": { + "avatarUrl": null, + "birthday": null, + "city": null, + "countryCode": null, + "email": "contact@example.com", + "firstName": "", + "language": null, + "lastName": "", + "phone": null, + "status": "SUBSCRIBED" + }, + "updatedAt": null, + "workspaceId": "wrk_lKDIOgIaDqfDcfUnuCKG9C4S" + } + }, + "timestamp": "2025-07-07T20:22:49.350Z", + "version": 1 +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e1b833f216ee4..2334e83ebf464 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12635,7 +12635,11 @@ importers: components/smartymeet: {} - components/smashsend: {} + components/smashsend: + dependencies: + '@pipedream/platform': + specifier: ^3.1.0 + version: 3.1.0 components/smiirl: dependencies: @@ -15832,14 +15836,6 @@ importers: specifier: ^6.0.0 version: 6.2.0 - modelcontextprotocol/node_modules2/@modelcontextprotocol/sdk/dist/cjs: {} - - modelcontextprotocol/node_modules2/@modelcontextprotocol/sdk/dist/esm: {} - - modelcontextprotocol/node_modules2/zod-to-json-schema/dist/cjs: {} - - modelcontextprotocol/node_modules2/zod-to-json-schema/dist/esm: {} - packages/ai: dependencies: '@pipedream/sdk':