From 0255315490a34c2fa96835835763f5c92620a011 Mon Sep 17 00:00:00 2001 From: Jorge Cortes Date: Fri, 29 Nov 2024 17:04:36 -0500 Subject: [PATCH] [Components] attractwell - New components --- .../create-update-contact.mjs | 353 ++++++++++++++++++ .../lesson-approval/lesson-approval.mjs | 81 ++++ components/attractwell/attractwell.app.mjs | 190 +++++++++- components/attractwell/common/constants.mjs | 9 + components/attractwell/common/utils.mjs | 28 ++ components/attractwell/package.json | 7 +- .../sources/common/trigger-contexts.mjs | 9 + .../sources/common/trigger-names.mjs | 7 + .../attractwell/sources/common/webhook.mjs | 99 +++++ .../contact-joins-vault-instant.mjs | 29 ++ .../new-event-registration-instant.mjs | 30 ++ .../new-lead-from-landing-page-instant.mjs | 30 ++ pnpm-lock.yaml | 6 +- 13 files changed, 871 insertions(+), 7 deletions(-) create mode 100644 components/attractwell/actions/create-update-contact/create-update-contact.mjs create mode 100644 components/attractwell/actions/lesson-approval/lesson-approval.mjs create mode 100644 components/attractwell/common/constants.mjs create mode 100644 components/attractwell/common/utils.mjs create mode 100644 components/attractwell/sources/common/trigger-contexts.mjs create mode 100644 components/attractwell/sources/common/trigger-names.mjs create mode 100644 components/attractwell/sources/common/webhook.mjs create mode 100644 components/attractwell/sources/contact-joins-vault-instant/contact-joins-vault-instant.mjs create mode 100644 components/attractwell/sources/new-event-registration-instant/new-event-registration-instant.mjs create mode 100644 components/attractwell/sources/new-lead-from-landing-page-instant/new-lead-from-landing-page-instant.mjs diff --git a/components/attractwell/actions/create-update-contact/create-update-contact.mjs b/components/attractwell/actions/create-update-contact/create-update-contact.mjs new file mode 100644 index 0000000000000..9c5301430cbc7 --- /dev/null +++ b/components/attractwell/actions/create-update-contact/create-update-contact.mjs @@ -0,0 +1,353 @@ +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../attractwell.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "attractwell-create-update-contact", + name: "Create or Update Contact", + description: "Creates or updates a contact with the provided identification and contact details.", + version: "0.0.1", + type: "action", + props: { + app, + email: { + type: "string", + label: "Email", + description: "The email address of the contact.", + optional: true, + }, + mobilePhone: { + type: "string", + label: "Mobile Phone", + description: "The mobile phone number of the contact.", + optional: true, + }, + 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, + }, + contactType: { + type: "string", + label: "Contact Type", + description: "The type of the contact.", + optional: true, + }, + rating: { + type: "string", + label: "Rating", + description: "The rating of the contact. From `0` (coldest) to `5` (hottest). You'll get periodic reminders of which contacts to reach out to more often if you choose a higher rating, or not at all if you pick `0`.", + default: "0", + options: [ + { + value: "0", + label: "No reminders", + }, + { + value: "1", + label: "Annual reminders", + }, + { + value: "2", + label: "Quarterly reminders", + }, + { + value: "3", + label: "Monthly reminders", + }, + { + value: "4", + label: "Weekly reminders", + }, + { + value: "5", + label: "Reminders every 3 days", + }, + ], + }, + workPhone: { + type: "string", + label: "Work Phone", + description: "The work phone number of the contact.", + optional: true, + }, + homePhone: { + type: "string", + label: "Home Phone", + description: "The home phone number of the contact.", + optional: true, + }, + address1: { + type: "string", + label: "Street Address", + description: "The street address of the contact.", + optional: true, + }, + city: { + type: "string", + label: "City", + description: "The city of the contact.", + optional: true, + }, + state: { + type: "string", + label: "State", + description: "The state of the contact.", + optional: true, + }, + postalCode: { + type: "string", + label: "Postal Code", + description: "The postal code of the contact.", + optional: true, + }, + country: { + type: "string", + label: "Country", + description: "The country of the contact.", + optional: true, + }, + companyName: { + type: "string", + label: "Company Name", + description: "The company name of the contact.", + optional: true, + }, + title: { + type: "string", + label: "Title", + description: "The title of the contact.", + optional: true, + }, + campaignContactEmail: { + type: "boolean", + label: "Send Campaigns By Email", + description: "The campaign contact email setting.", + default: true, + }, + campaignContactText: { + type: "boolean", + label: "Send Campaigns By Text", + description: "The campaign contact text setting.", + default: false, + }, + receiveMarketingEmail: { + type: "boolean", + label: "Opted Into Email", + description: "The receive marketing email setting.", + default: true, + }, + receiveMarketingText: { + type: "boolean", + label: "Opted Into Text", + description: "The receive marketing text setting.", + default: true, + }, + tagsToAdd: { + type: "string[]", + label: "Tags to Add", + description: "Tags to add to the contact.", + propDefinition: [ + app, + "tag", + ], + }, + tagsToRemove: { + type: "string[]", + label: "Tags to Remove", + description: "Tags to remove from the contact.", + propDefinition: [ + app, + "tag", + ], + }, + campaignsToAdd: { + type: "string[]", + label: "Campaigns to Add", + description: "If a contact isn't already receiving a campaign, start sending these campaigns to them.", + propDefinition: [ + app, + "campaignId", + ], + }, + campaignsToAddOrRestart: { + type: "string[]", + label: "Campaigns to Add or Restart", + description: "If a contact is already receiving a campaign, restart these campaigns. If a contact is not receiving a campaign, start sending these campaigns to them.", + propDefinition: [ + app, + "campaignId", + ], + }, + campaignsToRemove: { + type: "string[]", + label: "Campaigns to Remove", + description: "Campaigns to remove from the contact.", + propDefinition: [ + app, + "campaignId", + ], + }, + offlineCampaignsToAdd: { + type: "string[]", + label: "Offline Campaigns To Add", + description: "Offline campaigns to add to the contact.", + propDefinition: [ + app, + "campaignId", + ], + }, + offlineCampaignsToRemove: { + type: "string[]", + label: "Offline Campaigns To Remove", + description: "Offline campaigns to remove from the contact.", + propDefinition: [ + app, + "campaignId", + ], + }, + addToVaults: { + type: "string[]", + label: "Add To Vaults", + description: "Give Access To Vault (Contact Still Must Pay For Paid Vaults).", + propDefinition: [ + app, + "vaultId", + ], + }, + addToVaultsForFree: { + type: "string[]", + label: "Add To Vaults For Free", + description: "Give Access To Vault For Free (Contact Gets Free Access To Paid Vaults).", + propDefinition: [ + app, + "vaultId", + ], + }, + removeFromVaults: { + type: "string[]", + label: "Remove from Vaults", + description: "Vaults to remove the contact from.", + propDefinition: [ + app, + "vaultId", + ], + }, + automationsToRun: { + type: "string[]", + label: "Automations To Run", + description: "Automations to run for the contact.", + propDefinition: [ + app, + "automationId", + ], + }, + mayAccessMemberArea: { + type: "boolean", + label: "May Access Member Area", + description: "Whether the user may access or is banned from the member area. If this is set to `true`, they only are able to access the member area if they are also assigned to one or more vaults.", + default: true, + }, + }, + methods: { + fromBooleanToInt(value) { + return value === true + ? 1 + : 0; + }, + createOrUpdateContact(args = {}) { + return this.app.post({ + path: "/contacts", + ...args, + }); + }, + }, + async run({ $ }) { + const { + fromBooleanToInt, + createOrUpdateContact, + email, + mobilePhone, + firstName, + lastName, + contactType, + rating, + workPhone, + homePhone, + address1, + city, + state, + postalCode, + country, + companyName, + title, + campaignContactEmail, + campaignContactText, + receiveMarketingEmail, + receiveMarketingText, + tagsToAdd, + tagsToRemove, + campaignsToAdd, + campaignsToRemove, + offlineCampaignsToAdd, + offlineCampaignsToRemove, + addToVaults, + addToVaultsForFree, + removeFromVaults, + automationsToRun, + campaignsToAddOrRestart, + mayAccessMemberArea, + } = this; + + if (!email && !mobilePhone) { + throw new ConfigurationError("Either **Email** or **Mobile Phone** is required."); + } + + const response = await createOrUpdateContact({ + $, + data: { + contact_source: "API", + email, + mobile_phone: mobilePhone, + first_name: firstName, + last_name: lastName, + contact_type: contactType, + rating: parseInt(rating, 10), + work_phone: workPhone, + home_phone: homePhone, + address1, + city, + state, + postal_code: postalCode, + country, + company_name: companyName, + title, + campaign_contact_email: fromBooleanToInt(campaignContactEmail), + campaign_contact_text: fromBooleanToInt(campaignContactText), + receive_marketing_email: fromBooleanToInt(receiveMarketingEmail), + receive_marketing_text: fromBooleanToInt(receiveMarketingText), + tags_to_add: utils.parseArray(tagsToAdd), + tags_to_remove: utils.parseArray(tagsToRemove), + campaigns_to_add: campaignsToAdd, + campaigns_to_remove: campaignsToRemove, + offline_campaigns_to_add: offlineCampaignsToAdd, + offline_campaigns_to_remove: offlineCampaignsToRemove, + add_to_vaults: addToVaults, + add_to_vaults_for_free: addToVaultsForFree, + remove_from_vaults: removeFromVaults, + automations_to_run: automationsToRun, + campaigns_to_add_or_restart: campaignsToAddOrRestart, + may_access_member_area: fromBooleanToInt(mayAccessMemberArea), + }, + }); + $.export("$summary", `Successfully created or updated contact with ID \`${response.results.contact_id}\`.`); + return response; + }, +}; diff --git a/components/attractwell/actions/lesson-approval/lesson-approval.mjs b/components/attractwell/actions/lesson-approval/lesson-approval.mjs new file mode 100644 index 0000000000000..cc812fdc61428 --- /dev/null +++ b/components/attractwell/actions/lesson-approval/lesson-approval.mjs @@ -0,0 +1,81 @@ +import app from "../../attractwell.app.mjs"; + +export default { + key: "attractwell-lesson-approval", + name: "Lesson Approval", + description: "Approves, rejects, or unapproves a lesson in the AttractWell system based on the selected status.", + version: "0.0.1", + type: "action", + props: { + app, + approvalStatus: { + type: "string", + label: "Approval Status", + description: "The approval status to set for the lesson.", + options: [ + "approved", + "rejected", + "unapproved", + ], + }, + contactEmail: { + type: "string", + label: "Contact Email", + description: "The email address of the contact.", + }, + vaultId: { + optional: false, + propDefinition: [ + app, + "vaultId", + ], + }, + onlineClassLessonId: { + label: "Online Class Lesson ID", + description: "The ID of the online class lesson.", + optional: false, + propDefinition: [ + app, + "lessonId", + ], + }, + instructorComment: { + type: "string", + label: "Instructor Comment", + description: "The comment from the instructor.", + optional: true, + }, + }, + methods: { + lessonApproval(args = {}) { + return this.app.post({ + path: "/classes/lessons/approve", + ...args, + }); + }, + }, + async run({ $ }) { + const { + lessonApproval, + approvalStatus, + contactEmail, + vaultId, + onlineClassLessonId, + instructorComment, + } = this; + + const response = await lessonApproval({ + $, + data: { + approval_status: approvalStatus, + contact_email: contactEmail, + vault_id: vaultId, + online_class_lesson_id: onlineClassLessonId, + instructor_comment: instructorComment, + }, + }); + + $.export("$summary", "Successfully updated lesson approval status."); + return response; + }, +}; diff --git a/components/attractwell/attractwell.app.mjs b/components/attractwell/attractwell.app.mjs index 4b793833e3ef3..4fb53f72678d4 100644 --- a/components/attractwell/attractwell.app.mjs +++ b/components/attractwell/attractwell.app.mjs @@ -1,11 +1,193 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "attractwell", - propDefinitions: {}, + propDefinitions: { + tag: { + type: "string", + label: "Tag", + description: "The name of the tag.", + optional: true, + async options() { + const tags = await this.listContactTags(); + return tags.map(({ tag: value }) => value); + }, + }, + campaignId: { + type: "string", + label: "Campaign ID", + description: "The id of the campaign.", + optional: true, + async options() { + const campaigns = await this.listCampaigns(); + return campaigns.map(({ + name: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + vaultId: { + type: "string", + label: "Vault ID", + description: "The id of the vault.", + optional: true, + async options() { + const vaults = await this.listVaults(); + return vaults.map(({ + name: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + followUpPlanId: { + type: "string", + label: "Follow-Up Plan ID", + description: "The id of the follow-up plan.", + optional: true, + async options() { + const followUpPlans = await this.listFollowUpPlans(); + return followUpPlans.map(({ + name: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + automationId: { + type: "string", + label: "Automation ID", + description: "The id of the automation.", + optional: true, + async options() { + const automations = await this.listAutomations(); + return automations.map(({ + name: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + classId: { + type: "string", + label: "Class ID", + description: "The id of the class.", + optional: true, + async options({ page }) { + const classes = await this.listClasses({ + params: { + page, + }, + }); + return classes.map(({ + name: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + lessonId: { + type: "string", + label: "Lesson ID", + description: "The id of the lesson.", + optional: true, + async options({ page }) { + const classLessons = await this.listClassLessons({ + params: { + page, + }, + }); + return classLessons.map(({ + name: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + "Content-Type": "application/json", + "Accept": "application/json", + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + delete(args = {}) { + return this._makeRequest({ + method: "DELETE", + ...args, + }); + }, + listContactTags(args = {}) { + return this._makeRequest({ + path: "/contacts/tags", + ...args, + }); + }, + listCampaigns(args = {}) { + return this._makeRequest({ + path: "/campaigns", + ...args, + }); + }, + listVaults(args = {}) { + return this._makeRequest({ + path: "/vaults", + ...args, + }); + }, + listFollowUpPlans(args = {}) { + return this._makeRequest({ + path: "/follow-up-plans", + ...args, + }); + }, + listAutomations(args = {}) { + return this._makeRequest({ + path: "/automations", + ...args, + }); + }, + listClasses(args = {}) { + return this._makeRequest({ + path: "/classes", + ...args, + }); + }, + listClassLessons(args = {}) { + return this._makeRequest({ + path: "/classes/lessons", + ...args, + }); }, }, }; diff --git a/components/attractwell/common/constants.mjs b/components/attractwell/common/constants.mjs new file mode 100644 index 0000000000000..447f450389fc9 --- /dev/null +++ b/components/attractwell/common/constants.mjs @@ -0,0 +1,9 @@ +const BASE_URL = "https://api.attractwell.com"; +const VERSION_PATH = "/api/v1"; +const TRIGGER_CONTEXT_ID = "triggerContextId"; + +export default { + BASE_URL, + VERSION_PATH, + TRIGGER_CONTEXT_ID, +}; diff --git a/components/attractwell/common/utils.mjs b/components/attractwell/common/utils.mjs new file mode 100644 index 0000000000000..2e10f7e6444f5 --- /dev/null +++ b/components/attractwell/common/utils.mjs @@ -0,0 +1,28 @@ +import { ConfigurationError } from "@pipedream/platform"; + +function parseArray(value) { + try { + if (!value) { + return; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid array object"); + } +} + +export default { + parseArray, +}; diff --git a/components/attractwell/package.json b/components/attractwell/package.json index 39f0ed5de2719..5a7eb02944c7d 100644 --- a/components/attractwell/package.json +++ b/components/attractwell/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/attractwell", - "version": "0.0.1", + "version": "0.1.1", "description": "Pipedream AttractWell Components", "main": "attractwell.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.3" } -} \ No newline at end of file +} diff --git a/components/attractwell/sources/common/trigger-contexts.mjs b/components/attractwell/sources/common/trigger-contexts.mjs new file mode 100644 index 0000000000000..d86c6f0dcdf33 --- /dev/null +++ b/components/attractwell/sources/common/trigger-contexts.mjs @@ -0,0 +1,9 @@ +export default { + PAGE: "page", + LANDING_PAGE: "landing_page", + CONTACT_ME: "contact_me", + BLOG_COMMENT: "blog_comment", + EVENT_REGISTRATION: "event_registration", + STORE: "store", + VAULT: "vault", +}; diff --git a/components/attractwell/sources/common/trigger-names.mjs b/components/attractwell/sources/common/trigger-names.mjs new file mode 100644 index 0000000000000..a20c6183434fb --- /dev/null +++ b/components/attractwell/sources/common/trigger-names.mjs @@ -0,0 +1,7 @@ +export default { + NEW_LEAD_OR_SALE: "new_lead_or_sale", + PAYMENT_FAILURE: "payment_failure", + JOIN: "join", + LEAVE: "leave", + PAYMENT_SUCCESS: "payment_success", +}; diff --git a/components/attractwell/sources/common/webhook.mjs b/components/attractwell/sources/common/webhook.mjs new file mode 100644 index 0000000000000..3be9be13c5db2 --- /dev/null +++ b/components/attractwell/sources/common/webhook.mjs @@ -0,0 +1,99 @@ +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../attractwell.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + app, + db: "$.service.db", + http: "$.interface.http", + vaultId: { + optional: false, + propDefinition: [ + app, + "vaultId", + ], + }, + }, + hooks: { + async activate() { + const { + createWebhook, + http: { endpoint: hookUrl }, + vaultId, + getTriggerContext, + getTriggerName, + setTriggerContextId, + } = this; + + const triggerContextId = Date.now(); + + await createWebhook({ + data: { + hookUrl, + vault_id: vaultId, + trigger_name: getTriggerName(), + trigger_context: getTriggerContext(), + trigger_context_id: triggerContextId, + }, + }); + + setTriggerContextId(triggerContextId); + }, + async deactivate() { + const { + deleteWebhook, + vaultId, + getTriggerContextId, + getTriggerName, + getTriggerContext, + } = this; + + await deleteWebhook({ + data: { + vault_id: vaultId, + trigger_name: getTriggerName(), + trigger_context: getTriggerContext(), + trigger_context_id: getTriggerContextId(), + }, + }); + }, + }, + methods: { + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + getTriggerName() { + throw new ConfigurationError("getTriggerName is not implemented"); + }, + getTriggerContext() { + throw new ConfigurationError("getTriggerContext is not implemented"); + }, + setTriggerContextId(value) { + this.db.set(constants.TRIGGER_CONTEXT_ID, value); + }, + getTriggerContextId() { + return this.db.get(constants.TRIGGER_CONTEXT_ID); + }, + processResource(resource) { + this.$emit(resource, this.generateMeta(resource)); + }, + createWebhook(args = {}) { + return this.app.post({ + debug: true, + path: "/zapier/trigger", + ...args, + }); + }, + deleteWebhook(args = {}) { + return this.app.delete({ + debug: true, + path: "/zapier/trigger", + ...args, + }); + }, + }, + async run({ body }) { + this.processResource(body); + }, +}; diff --git a/components/attractwell/sources/contact-joins-vault-instant/contact-joins-vault-instant.mjs b/components/attractwell/sources/contact-joins-vault-instant/contact-joins-vault-instant.mjs new file mode 100644 index 0000000000000..fee6fda784361 --- /dev/null +++ b/components/attractwell/sources/contact-joins-vault-instant/contact-joins-vault-instant.mjs @@ -0,0 +1,29 @@ +import common from "../common/webhook.mjs"; +import triggerNames from "../common/trigger-names.mjs"; +import triggerContexts from "../common/trigger-contexts.mjs"; + +export default { + ...common, + key: "attractwell-contact-joins-vault-instant", + name: "Contact Joins Vault (Instant)", + description: "Emit new event when a contact becomes a new member of a vault.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTriggerName() { + return triggerNames.JOIN; + }, + getTriggerContext() { + return triggerContexts.VAULT; + }, + generateMeta(resource) { + return { + id: resource.contact_id, + summary: `New Contact: ${resource.contact_name}`, + ts: Date.now(), + }; + }, + }, +}; diff --git a/components/attractwell/sources/new-event-registration-instant/new-event-registration-instant.mjs b/components/attractwell/sources/new-event-registration-instant/new-event-registration-instant.mjs new file mode 100644 index 0000000000000..371cc84e7c88b --- /dev/null +++ b/components/attractwell/sources/new-event-registration-instant/new-event-registration-instant.mjs @@ -0,0 +1,30 @@ +import common from "../common/webhook.mjs"; +import triggerNames from "../common/trigger-names.mjs"; +import triggerContexts from "../common/trigger-contexts.mjs"; + +export default { + ...common, + key: "attractwell-new-event-registration-instant", + name: "New Event Registration (Instant)", + description: "Emit new event when a new registration for an event takes place.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTriggerName() { + return triggerNames.NEW_LEAD_OR_SALE; + }, + getTriggerContext() { + return triggerContexts.EVENT_REGISTRATION; + }, + generateMeta(resource) { + const ts = Date.now(); + return { + id: ts, + summary: `New Event: ${resource.name}`, + ts, + }; + }, + }, +}; diff --git a/components/attractwell/sources/new-lead-from-landing-page-instant/new-lead-from-landing-page-instant.mjs b/components/attractwell/sources/new-lead-from-landing-page-instant/new-lead-from-landing-page-instant.mjs new file mode 100644 index 0000000000000..d25edb0abf4f5 --- /dev/null +++ b/components/attractwell/sources/new-lead-from-landing-page-instant/new-lead-from-landing-page-instant.mjs @@ -0,0 +1,30 @@ +import common from "../common/webhook.mjs"; +import triggerNames from "../common/trigger-names.mjs"; +import triggerContexts from "../common/trigger-contexts.mjs"; + +export default { + ...common, + key: "attractwell-new-lead-from-landing-page-instant", + name: "New Lead from Landing Page (Instant)", + description: "Emit new event when a lead is gained from a landing page.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTriggerName() { + return triggerNames.NEW_LEAD_OR_SALE; + }, + getTriggerContext() { + return triggerContexts.LANDING_PAGE; + }, + generateMeta(resource) { + const ts = Date.now(); + return { + id: ts, + summary: `New Lead: ${resource.lead_source_id}`, + ts, + }; + }, + }, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 797a1ce86b743..9e8bec5356663 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -834,7 +834,11 @@ importers: specifier: ^3.0.3 version: 3.0.3 - components/attractwell: {} + components/attractwell: + dependencies: + '@pipedream/platform': + specifier: 3.0.3 + version: 3.0.3 components/autoblogger: dependencies: