-
Notifications
You must be signed in to change notification settings - Fork 5.5k
18029 actionhubspot cms and marketing api #18155
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
- Added new actions: Create Contact Workflow, Clone Email, Clone Site Page, Create Form, Update Landing Page, and Update Page. - Enhanced existing actions with version increments for better compatibility. - Introduced utility functions for improved data handling and object parsing. - Updated dependencies in package.json to the latest versions. - Refactored code for consistency and clarity across various actions and sources.
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
WalkthroughAdds many HubSpot CMS and Marketing actions (forms, pages, emails, workflows), shared page props and utilities (cleanObject, LANGUAGE_OPTIONS), extensive hubspot.app extensions (new methods and propDefinitions) with Bottleneck rate limiting, plus many version bumps and minor import reorders; one functional tweak links created leads to contacts via associations. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant Action as CMS/Marketing Action
participant App as HubSpot App
participant RL as Rate Limiter
participant API as HubSpot API
User->>Action: invoke action with props
Action->>App: call create/update/clone method (payload, $)
App->>RL: schedule/request via Bottleneck
RL->>API: HTTP request
API-->>RL: response
RL-->>App: response
App-->>Action: response data
Action-->>User: $.export("$summary") + return
note right of Action: parseObject/cleanObject applied where used
sequenceDiagram
autonumber
actor User
participant Action as Create Contact Workflow
participant App as HubSpot App
participant API as HubSpot Automation v4
User->>Action: configure workflow props
Action->>App: createContactWorkflow(data)
App->>API: POST /automation/v4/flows
API-->>App: { id, ... }
App-->>Action: response
Action-->>User: "$summary: created workflow {id}"
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Suggested reviewers
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. 📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (4)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
…e-companies to 0.0.3 and new-email-event to 0.0.29 for improved compatibility.
- Added dependencies for components/prisma_management_api. - Cleaned up specifier entries for components/mindee and components/zapr_link. - Included transitive peer dependencies for better compatibility.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 23
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (31)
components/hubspot/sources/new-company-property-change/new-company-property-change.mjs (1)
49-79
: Guard the date filter whenafter
is undefined and coerce it to a stringOn first runs,
after
will beundefined
, and sending{ propertyName: "hs_lastmodifieddate", operator: "GTE", value: undefined }can lead to CRM Search errors. The Search API also expects filter values as strings. In components/hubspot/sources/new-company-property-change/new-company-property-change.mjs (lines 49–79), change
getParams
to:getParams(after) { - return { - object: "companies", - data: { - limit: DEFAULT_LIMIT, - properties: [ this.property ], - sorts: [{ propertyName: "hs_lastmodifieddate", direction: "DESCENDING" }], - filterGroups: [ - { - filters: [ - { propertyName: this.property, operator: "HAS_PROPERTY" }, - { - propertyName: "hs_lastmodifieddate", - operator: "GTE", - value: after, - }, - ], - }, - ], - }, - }; - }, + // Always require the property, but only filter by date if `after` is defined + const filters = [ + { propertyName: this.property, operator: "HAS_PROPERTY" }, + ]; + if (after != null) { + filters.push({ + propertyName: "hs_lastmodifieddate", + operator: "GTE", + value: String(after), // ensure the API gets a string + }); + } + + return { + object: "companies", + data: { + limit: DEFAULT_LIMIT, + properties: [ this.property ], + sorts: [{ propertyName: "hs_lastmodifieddate", direction: "DESCENDING" }], + filterGroups: [{ filters }], + }, + }; + },
- Moves the date filter behind an
if (after != null)
guard to avoidvalue: undefined
- Uses
String(after)
to satisfy the API’s requirement for string‐typed filter valuescomponents/hubspot/actions/list-marketing-emails/list-marketing-emails.mjs (1)
81-116
: Critical bug:results
shadowing and self-appending within iteration corrupts pagination and output.
- You declare
const results = []
(outer accumulator), then destructure{ paging, results }
from the API response, shadowing the accumulator.- Inside
for (const item of results)
, you push into the sameresults
you’re iterating, which can extend iteration unexpectedly and never populates the outer accumulator. The returned array will likely be empty and the loop may over-iterate.Fix by using distinct names and pushing into the accumulator; also guard
paging?.next?.after
and only setparams.after
when present.Apply this diff:
async run({ $ }) { - - const results = []; - let hasMore, count = 0; + const allResults = []; + let hasMore; + let count = 0; @@ - do { - const { - paging, results, - } = await this.hubspot.listMarketingEmails({ + do { + const { paging, results: pageResults } = await this.hubspot.listMarketingEmails({ $, params, }); - if (!results?.length) { + if (!pageResults?.length) { break; } - for (const item of results) { - results.push(item); + for (const item of pageResults) { + allResults.push(item); count++; if (count >= this.maxResults) { break; } } - hasMore = paging?.next.after; - params.after = paging?.next.after; + hasMore = Boolean(paging?.next?.after); + if (hasMore) { + params.after = paging.next.after; + } } while (hasMore && count < this.maxResults); - $.export("$summary", `Found ${results.length} email${results.length === 1 + $.export("$summary", `Found ${allResults.length} email${allResults.length === 1 ? "" : "s"}`); - return results; + return allResults; },components/hubspot/actions/list-blog-posts/list-blog-posts.mjs (2)
75-105
: Fix results shadowing and mutation bug — function always returns [] and may duplicate items.
- Outer accumulator
results
(Line 75) is shadowed by the destructuredresults
from the API (Line 91).- Inside the loop you push into the API’s
results
while iterating it, not the accumulator (Lines 99-101). This both duplicates items and leaves the outerresults
empty, so the action returns[]
.Apply this minimal fix:
- const results = []; + const items = []; let hasMore, count = 0; @@ - const { - paging, results, - } = await this.hubspot.getBlogPosts({ + const { + paging, results: apiResults, + } = await this.hubspot.getBlogPosts({ $, params, }); - if (!results?.length) { + if (!apiResults?.length) { break; } - for (const item of results) { - results.push(item); + for (const item of apiResults) { + items.push(item); count++; if (count >= this.maxResults) { break; } } - hasMore = paging?.next.after; - params.after = paging?.next.after; + hasMore = Boolean(paging?.next?.after); + params.after = paging?.next?.after; } while (hasMore && count < this.maxResults); @@ - $.export("$summary", `Found ${results.length} page${results.length === 1 + $.export("$summary", `Found ${items.length} blog post${items.length === 1 ? "" : "s"}`); - return results; + return items;
53-65
: Action Required: Alignsort.options
with HubSpot’s CMS Blog Posts API property namesThe
sort
parameter itself is correct—HubSpot expects asort
query parameter with the property name (optionally prefixed by-
for descending) (developers.hubspot.com). However, the code’s list of allowed fields must exactly match the property keys exposed by the CMS Blog Posts API:
- Replace
createdBy
→createdById
- Replace
updatedBy
→updatedById
Additionally, if you wish to support sorting by publish date (a common use case), consider adding
publishDate
to the options.Please update the snippet in
components/hubspot/actions/list-blog-posts/list-blog-posts.mjs
accordingly:sort: { type: "string", label: "Sort", description: "Sort the results by the specified field", - options: [ - "name", - "createdAt", - "updatedAt", - "createdBy", - "updatedBy", - ], + options: [ + "name", + "createdAt", + "updatedAt", + "createdById", + "updatedById", + // Optional: sort by publish date + "publishDate", + ], optional: true, },components/hubspot/actions/list-campaigns/list-campaigns.mjs (1)
34-66
: Fix variable shadowing and self-appending loop that corrupts results
- The outer
results
array is shadowed by the destructured{ results }
from the API response.- Inside the loop, you push into the same array you’re iterating (
results.push(item)
), duplicating entries and risking pathological iteration. The outerresults
stays empty, causing an empty return and summary.Refactor to separate page results from the accumulator and handle paging defensively.
Apply this diff:
- const results = []; - let hasMore, count = 0; + const campaigns = []; + let after; + let count = 0; @@ - const params = { - sort: this.sort, - }; + const params = { sort: this.sort }; @@ - const { - paging, results, - } = await this.hubspot.listCampaigns({ + const { + paging, results: page, + } = await this.hubspot.listCampaigns({ $, params, }); - if (!results?.length) { + if (!page?.length) { break; } - for (const item of results) { - results.push(item); + for (const item of page) { + campaigns.push(item); count++; if (count >= this.maxResults) { break; } } - hasMore = paging?.next.after; - params.after = paging?.next.after; - } while (hasMore && count < this.maxResults); + after = paging?.next?.after; + if (after) params.after = after; + else delete params.after; + } while (after && count < this.maxResults); @@ - $.export("$summary", `Found ${results.length} campaign${results.length === 1 + $.export("$summary", `Found ${campaigns.length} campaign${campaigns.length === 1 ? "" : "s"}`); - return results; + return campaigns;components/hubspot/actions/get-file-public-url/get-file-public-url.mjs (1)
31-35
: Guard against undefined file before dereferencing idIf
find
returnsundefined
, accessingfile.id
throws before your explicit not-found check. Validatefile
first.Apply this diff:
- const file = files.find(({ url }) => url === fileUrl ); - const fileId = file.id; - if (!fileId) { - throw new Error(`File not found at ${fileUrl}`); - } + const file = files.find(({ url }) => url === fileUrl); + if (!file) { + throw new Error(`File not found at ${fileUrl}`); + } + const fileId = file.id;components/hubspot/actions/batch-create-or-update-contact/batch-create-or-update-contact.mjs (3)
28-44
: Potential crash when no results are returned and undefined emails are includedTwo issues:
- Line 28 collects
undefined
can be sent to the IN filter.- Line 42 sets
updateEmails = results?.map(...)
, which becomesundefined
whenresults
is falsy, leading toupdateEmails.includes(...)
throwing at Line 43.Patch below makes this path safe with minimal change.
- const emails = contacts.map(({ email }) => email); + const emails = contacts.map(({ email }) => email).filter(Boolean); @@ - const updateEmails = results?.map(({ properties }) => properties.email); - const insertProperties = contacts.filter(({ email }) => !updateEmails.includes(email)) + const updateEmails = (results ?? []).map(({ properties }) => properties.email).filter(Boolean); + const insertProperties = contacts.filter(({ email }) => !updateEmails.includes(email)) .map((properties) => ({ properties, }));
27-58
: Partition create/update correctly, dedupe updates, and validate inputsCurrent flow risks:
- Contacts with an
id
can be pushed into both create and update arrays (due to only filtering by email for creates) causing duplicate/conflicting requests.- Update set may contain duplicates when mixing “found by email” and “provided id” paths.
- Records without
id
and withoutRefactor:
- Validate each contact has either
id
(update) or- Partition by presence of
id
.- Search only the “no id but has email” cohort, compute inserts (non-existing emails) and updates (existing emails).
- Merge updates from “by email” and “by id”, deduping by
id
(id-driven updates take precedence).Apply this diff:
@@ - async searchExistingContactProperties(contacts, $) { - const emails = contacts.map(({ email }) => email).filter(Boolean); - const { results } = await this.hubspot.searchCRM({ - $, - object: "contact", - data: { - filters: [ - { - propertyName: "email", - operator: "IN", - values: emails, - }, - ], - }, - }); - const updateEmails = (results ?? []).map(({ properties }) => properties.email).filter(Boolean); - const insertProperties = contacts.filter(({ email }) => !updateEmails.includes(email)) - .map((properties) => ({ - properties, - })); - const updateProperties = []; - for (const contact of results) { - updateProperties.push({ - id: contact.id, - properties: contacts.find(({ email }) => contact.properties.email === email), - }); - } - return { - insertProperties, - updateProperties, - }; - }, + async searchExistingContactProperties(contacts, $) { + // Consider only contacts that do NOT have an id and DO have an email + const candidates = contacts.filter((c) => !Object.prototype.hasOwnProperty.call(c, "id") && c.email); + const emails = [...new Set(candidates.map(({ email }) => email).filter(Boolean))]; + if (!emails.length) { + return { insertProperties: [], updateProperties: [] }; + } + const { results = [] } = await this.hubspot.searchCRM({ + $, + object: "contact", + data: { + filters: [ + { + propertyName: "email", + operator: "IN", + values: emails, + }, + ], + }, + }); + const existingEmails = new Set(results.map(({ properties }) => properties.email).filter(Boolean)); + const insertProperties = candidates + .filter(({ email }) => !existingEmails.has(email)) + .map((properties) => ({ properties })); + const byEmail = new Map(candidates.map((c) => [c.email, c])); + const updateProperties = results.map(({ id, properties }) => ({ + id, + properties: byEmail.get(properties.email), + })); + return { insertProperties, updateProperties }; + }, @@ - const { - insertProperties, updateProperties, - } = await this.searchExistingContactProperties(contacts, $); + // Validate minimal required identifiers + const invalid = contacts.filter((c) => + !Object.prototype.hasOwnProperty.call(c, "id") && !c.email); + if (invalid.length) { + throw new Error("Each contact must include either an 'id' (for update) or an 'email' (for create/update)."); + } + const { insertProperties, updateProperties } = + await this.searchExistingContactProperties(contacts, $); @@ - const updatePropertiesWithId = contacts.filter((contact) => (Object.prototype.hasOwnProperty.call(contact, "id"))) - .map(({ - id, ...properties - }) => ({ - id: id, - properties, - })); - - if (updatePropertiesWithId?.length) { - updateProperties.push(...updatePropertiesWithId); - } + const updatesById = contacts + .filter((c) => Object.prototype.hasOwnProperty.call(c, "id")) + .map(({ id, ...properties }) => ({ id, properties })); + // Merge and dedupe updates by id (id-driven updates take precedence) + const mergedUpdateMap = new Map(updateProperties.map((u) => [String(u.id), u])); + for (const u of updatesById) mergedUpdateMap.set(String(u.id), u); + const mergedUpdates = Array.from(mergedUpdateMap.values()); @@ - response.created = await this.hubspot.batchCreateContacts({ - $, - data: { - inputs: insertProperties, - }, - }); - response.updated = await this.hubspot.batchUpdateContacts({ - $, - data: { - inputs: updateProperties, - }, - }); + response.created = await this.hubspot.batchCreateContacts({ + $, + data: { inputs: insertProperties }, + }); + response.updated = await this.hubspot.batchUpdateContacts({ + $, + data: { inputs: mergedUpdates }, + }); @@ - $.export("$summary", `Successfully created ${insertProperties.length} and updated ${updateProperties.length} contacts`); + $.export("$summary", `Successfully created ${insertProperties.length} and updated ${mergedUpdates.length} contacts`);Also applies to: 61-77, 79-96
79-91
: Implement chunking for HubSpot batch endpointsThe
batchCreateContacts
andbatchUpdateContacts
methods inhubspot.app.mjs
do not enforce the 100-item limit; they simply pass through youropts
as-is. To avoid request failures wheninsertProperties
orupdateProperties
exceeds HubSpot’s 100-item cap, add client-side chunking in this action:• In
components/hubspot/actions/batch-create-or-update-contact/batch-create-or-update-contact.mjs
(around lines 79–91), wrap each call in a loop that:
- Splits the full
inputs
array into subarrays of at most 100 entries.- Calls
this.hubspot.batchCreateContacts
(orbatchUpdateContacts
) for each chunk.- Aggregates the individual responses into a combined
response.created
/response.updated
.• You can implement a small helper in this module, for example:
const CHUNK_SIZE = 100; function chunkArray(arr, size) { const chunks = []; for (let i = 0; i < arr.length; i += size) { chunks.push(arr.slice(i, i + size)); } return chunks; } // then in your action: response.created = []; for (const chunk of chunkArray(insertProperties, CHUNK_SIZE)) { const res = await this.hubspot.batchCreateContacts({ $, data: { inputs: chunk } }); response.created.push(...res.results); }This will ensure you never send more than 100 contacts per request and handle arbitrary-sized payloads gracefully.
components/hubspot/sources/new-engagement/new-engagement.mjs (1)
19-19
: Typo in user-facing description (“engagment”)Fix the typo to avoid user confusion in the UI.
- description: "Filter results by the type of engagment", + description: "Filter results by the type of engagement",components/hubspot/actions/list-marketing-events/list-marketing-events.mjs (1)
26-45
: Critical: variable shadowing and self-appending during iteration corrupt resultsInside the loop:
- You destructure
results
from the API response, which shadows the outerresults
accumulator.- You then
for ... of results
and push toresults
inside the same loop. This mutates the array being iterated and, combined with shadowing, results in incorrect accumulation (and can lead to unexpected iteration growth). The outer accumulator remains empty, so the function returns[]
and the summary reports0
.Fix by separating page results from the accumulator and pushing into the accumulator only.
- const results = []; + const events = []; @@ - const { - paging, results, - } = await this.hubspot.listMarketingEvents({ + const { + paging, results: pageResults, + } = await this.hubspot.listMarketingEvents({ $, params, }); - if (!results?.length) { + if (!pageResults?.length) { break; } - for (const item of results) { - results.push(item); + for (const item of pageResults) { + events.push(item); count++; if (count >= this.maxResults) { break; } } hasMore = paging?.next.after; params.after = paging?.next.after; } while (hasMore && count < this.maxResults); @@ - $.export("$summary", `Found ${results.length} event${results.length === 1 + $.export("$summary", `Found ${events.length} event${events.length === 1 ? "" : "s"}`); - return results; + return events;Also applies to: 20-24, 47-51
components/hubspot/actions/get-associated-meetings/get-associated-meetings.mjs (1)
192-202
: Custom timeframe filter operators are invertedFor a custom range, start should be GTE
startDate
and end should be LTEendDate
. Current logic uses the opposite, which will return incorrect/empty results.case "custom": return { hs_meeting_start_time: { - operator: "LTE", - value: startDate, + operator: "GTE", + value: startDate, }, hs_meeting_end_time: { - operator: "GTE", - value: endDate, + operator: "LTE", + value: endDate, }, };components/hubspot/sources/new-task/new-task.mjs (2)
36-41
: Fix: spreading an undefined value throws at runtime.If listSchemas() returns an object without a results array, custom becomes undefined and
...customObjects
will throw. Default to an empty array.- const { results: custom } = await this.hubspot.listSchemas(); - const customObjects = custom?.map(({ fullyQualifiedName }) => fullyQualifiedName); + const { results: custom = [] } = await this.hubspot.listSchemas(); + const customObjects = custom.map(({ fullyQualifiedName }) => fullyQualifiedName);
52-53
: Use correct binding context forhubspot.listTasks
Verified that
listTasks
inhubspot.app.mjs
invokesthis.makeRequest
(line 1265), so binding it to the integration component (this
) will break its context. It must be bound to the HubSpot client instance instead.• File:
components/hubspot/sources/new-task/new-task.mjs
(around lines 52–53)
• Replace with:- const tasks = await this.getPaginatedItems(this.hubspot.listTasks.bind(this), params); + const tasks = await this.getPaginatedItems(this.hubspot.listTasks.bind(this.hubspot), params);components/hubspot/sources/new-deal-property-change/new-deal-property-change.mjs (1)
27-33
: Fix: robust timestamp handling for property history.HubSpot property history timestamps are often epoch milliseconds (number).
Date.parse(number)
returns NaN. Handle both number and string to prevent NaN timestamps propagating to dedupe logic and meta.- getTs(deal) { - const history = deal.propertiesWithHistory[this.property]; - if (!history || !(history.length > 0)) { - return; - } - return Date.parse(history[0].timestamp); - }, + getTs(deal) { + const history = deal.propertiesWithHistory?.[this.property]; + if (!history?.length) return; + const t = history[0]?.timestamp; + const ts = typeof t === "number" ? t : Date.parse(t); + return Number.isFinite(ts) ? ts : undefined; + },components/hubspot/sources/new-note/new-note.mjs (2)
36-41
: Fix: spreading an undefined value throws at runtime.Mirror the task source fix—default
results
to [] to keep...customObjects
safe.- const { results: custom } = await this.hubspot.listSchemas(); - const customObjects = custom?.map(({ fullyQualifiedName }) => fullyQualifiedName); + const { results: custom = [] } = await this.hubspot.listSchemas(); + const customObjects = custom.map(({ fullyQualifiedName }) => fullyQualifiedName);
52-53
: BindlistNotes
to the HubSpot client contextThe
listNotes
implementation invokesthis.makeRequest
, so binding it to the component’sthis
will break its internalthis
reference. Update the call incomponents/hubspot/sources/new-note/new-note.mjs
accordingly:• File: components/hubspot/sources/new-note/new-note.mjs
• Lines: 52–53- const notes = await this.getPaginatedItems(this.hubspot.listNotes.bind(this), params); + const notes = await this.getPaginatedItems(this.hubspot.listNotes.bind(this.hubspot), params); await this.processEvents(notes, after);components/hubspot/actions/create-meeting/create-meeting.mjs (2)
83-85
: Tighten validation when association fields are partially filledIf either association field is provided, require both and a target type. This prevents confusing 400s from HubSpot.
Apply this diff:
- if ((toObjectId && !associationType) || (!toObjectId && associationType)) { - throw new ConfigurationError("Both `toObjectId` and `associationType` must be entered"); - } + if ((toObjectId && !associationType) || (!toObjectId && associationType)) { + throw new ConfigurationError("Both `toObjectId` and `associationType` must be entered"); + } + if ((toObjectId || associationType) && !toObjectType) { + throw new ConfigurationError("`toObjectType` is required when associating a meeting."); + }
100-105
: DeriveassociationCategory
dynamically instead of hard-codingHUBSPOT_DEFINED
The current implementation always forces
associationCategory: ASSOCIATION_CATEGORY.HUBSPOT_DEFINED
, which will break for USER_DEFINED or INTEGRATOR_DEFINED types.Key locations to update:
- components/hubspot/hubspot.app.mjs (around line 369)
TheassociationType
propDefinition returns only an integer (typeId
) as the value—and nocategory
field—so downstream code can’t know the correct category.- components/hubspot/actions/create-meeting/create-meeting.mjs (lines 100–105)
Thetypes
array is still hard-coded toHUBSPOT_DEFINED
.You should refactor one of two ways:
Extend the prop to return both
id
andcategory
:
Incommon.props.hubspot.associationType.options
, map each type toreturn associationTypes.map((t) => ({ label: t.label, - value: t.typeId, + value: { id: t.typeId, category: t.category }, }));Then update your
create-meeting
action to unpack that object.Fetch the category in the action itself:
const { results } = await this.hubspot.getAssociationTypes({ fromObjectType, toObjectType }); const assoc = results.find(({ typeId }) => typeId === associationType); // … types: [ { associationTypeId: assoc.typeId, associationCategory: assoc.category, }, ],Finally, apply this diff in
create-meeting.mjs
to replace the hard-coded snippet:- types: [ - { - associationTypeId: associationType, - associationCategory: ASSOCIATION_CATEGORY.HUBSPOT_DEFINED, - }, - ], + types: [ + (() => { + // derive id/category from the prop or by calling getAssociationTypes + const assoc = /* … */; + return { + associationTypeId: assoc.id, + associationCategory: assoc.category, + }; + })(), + ],This change is critical: forcing
HUBSPOT_DEFINED
for user- or integrator-defined types will cause API errors. Please address before merging.components/hubspot/actions/create-lead/create-lead.mjs (1)
35-55
: Hardcoded associationTypeId and unconditional association can cause failures or brittleness.
- The magic number 578 can change or vary; hardcoding is brittle.
- If contactId is absent/invalid, the API call will fail.
- Overwrites any associations provided in opts.data instead of merging.
Refactor to (a) merge with any provided associations, (b) guard on contactId, (c) cast id to string, and (d) use a named constant.
Apply this diff:
createObject(opts) { - return this.hubspot.createObject({ - ...opts, - data: { - ...opts?.data, - associations: [ - { - types: [ - { - associationCategory: ASSOCIATION_CATEGORY.HUBSPOT_DEFINED, - associationTypeId: 578, // ID for "Lead with Primary Contact" - }, - ], - to: { - id: this.contactId, - }, - }, - ], - }, - }); + const incomingAssociations = opts?.data?.associations ?? []; + const associations = [...incomingAssociations]; + + if (this.contactId) { + associations.push({ + types: [ + { + associationCategory: ASSOCIATION_CATEGORY.HUBSPOT_DEFINED, + associationTypeId: ASSOCIATION_TYPE_ID.LEAD_WITH_PRIMARY_CONTACT, + }, + ], + to: { + id: String(this.contactId), + }, + }); + } + + return this.hubspot.createObject({ + ...opts, + data: { + ...opts?.data, + associations, + }, + }); },Add this constant to your HubSpot constants (see next snippet), or alternatively resolve the association type dynamically at runtime via the Associations API.
components/hubspot/actions/list-pages/list-pages.mjs (1)
75-105
: Critical: variable shadowing corrupts results and can trigger an unbounded loop.
- Outer results array is shadowed by the destructured results from the API response.
- You then push into the array you’re iterating (inner results), potentially extending iteration indefinitely until maxResults, and you never populate the outer results, so you return [].
Fix by renaming one side and pushing into the outer accumulator. Also ensure hasMore is boolean.
Apply this diff:
- const results = []; + const pages = []; @@ - const { - paging, results, - } = await this.hubspot.listPages({ + const { + paging, results: pageResults, + } = await this.hubspot.listPages({ $, params, }); - if (!results?.length) { + if (!pageResults?.length) { break; } - for (const item of results) { - results.push(item); + for (const item of pageResults) { + pages.push(item); count++; if (count >= this.maxResults) { break; } } - hasMore = paging?.next.after; + hasMore = Boolean(paging?.next?.after); params.after = paging?.next.after; } while (hasMore && count < this.maxResults); @@ - $.export("$summary", `Found ${results.length} page${results.length === 1 + $.export("$summary", `Found ${pages.length} page${pages.length === 1 ? "" : "s"}`); - return results; + return pages;components/hubspot/actions/list-forms/list-forms.mjs (2)
26-52
: Fix variable shadowing and self-appending bug; results are never accumulated and loop can degenerate.
const results = []
(accumulator) is shadowed by the destructured{ results }
from the API response.- Inside the
for...of
, you push to the response array you’re iterating instead of the accumulator, which can cause growth during iteration and incorrect behavior. The outer accumulator remains empty.Apply this diff to use distinct names and push into the correct accumulator:
- const results = []; - let hasMore, count = 0; + const items = []; + let after; + let count = 0; @@ - const params = { - archived: this.archived, - }; + const params = { + archived: this.archived, + }; @@ - const { - paging, results, - } = await this.hubspot.listMarketingForms({ + const { + paging, results: pageResults, + } = await this.hubspot.listMarketingForms({ $, params, }); - if (!results?.length) { + if (!pageResults?.length) { break; } - for (const item of results) { - results.push(item); + for (const item of pageResults) { + items.push(item); count++; if (count >= this.maxResults) { break; } } - hasMore = paging?.next.after; - params.after = paging?.next.after; - } while (hasMore && count < this.maxResults); + after = paging?.next?.after; + params.after = after; + } while (after && count < this.maxResults);
54-58
: Return and summary currently reference the wrong variable (always 0).Update to use the accumulator from the fix above:
- $.export("$summary", `Found ${results.length} form${results.length === 1 + $.export("$summary", `Found ${items.length} form${items.length === 1 ? "" : "s"}`); - return results; + return items;components/hubspot/actions/create-associations/create-associations.mjs (1)
69-80
: Guard against missing association and use the argument value (notthis.*
).
results.find(...)
may returnundefined
, causingassociation.category
to throw. Also, prefer the passedassociationType
arg overthis.associationType
inside the method.async getAssociationCategory({ $, fromObjectType, toObjectType, associationType, }) { const { results } = await this.hubspot.getAssociationTypes({ $, fromObjectType, toObjectType, associationType, }); - const association = results.find(({ typeId }) => typeId === this.associationType); - return association.category; + const association = results?.find(({ typeId }) => typeId === associationType); + if (!association) { + throw new ConfigurationError("Association type not found for the selected object types."); + } + return association.category; },components/hubspot/actions/batch-create-companies/batch-create-companies.mjs (1)
42-45
: Make error parsing robust; current logic can throw and hide the real error.
JSON.parse(JSON.parse(error.message).message)
is brittle. Prefer safe parsing and fallbacks toerror.response.data
.- } catch (error) { - const message = JSON.parse((JSON.parse(error.message).message).split(/:(.+)/)[1])[0].message; - throw new ConfigurationError(message.split(/:(.+)/)[0]); - } + } catch (error) { + const fallback = error?.message || "Failed to create companies."; + let details = fallback; + try { + // Try message -> nested message -> array of errors + const parsed1 = typeof error?.message === "string" ? JSON.parse(error.message) : null; + const parsed2 = parsed1 && typeof parsed1.message === "string" ? JSON.parse(parsed1.message) : null; + details = Array.isArray(parsed2) + ? parsed2.map((e) => e?.message).filter(Boolean).join("; ") + : parsed1?.message || fallback; + } catch { + const data = error?.response?.data; + details = data?.errors?.map((e) => e?.message).filter(Boolean).join("; ") + || data?.message + || fallback; + } + throw new ConfigurationError(details); + }components/hubspot/actions/batch-upsert-companies/batch-upsert-companies.mjs (2)
46-48
: Brittle error parsing can throw secondary JSON.parse errors; prefer robust extraction from error.response.data.Current approach double-parses strings and splits on colons, which breaks on non-JSON messages and hides original errors. Extract directly from Axios-style response where available, with safe fallbacks.
Apply this diff:
- const message = JSON.parse((JSON.parse(error.message).message).split(/:(.+)/)[1])[0].message; - throw new ConfigurationError(message.split(/:(.+)/)[0]); + const data = error?.response?.data; + const details = Array.isArray(data?.errors) + ? data.errors.map((e) => e?.message).filter(Boolean) + : []; + const base = [data?.message, ...details].filter(Boolean).join(" | "); + const msg = base || error?.message || "Unknown HubSpot error"; + throw new ConfigurationError(msg);
1-50
: Extract and Centralize HubSpot Error Parsing LogicI ran the provided search and found the same brittle error-parsing pattern duplicated in three action files. To ensure consistency and simplify future updates, extract this logic into a shared helper (e.g., in
common/utils.mjs
or on the HubSpot app) and replace each inline parsing with a call to that helper.Files and locations to refactor:
components/hubspot/actions/batch-upsert-companies/batch-upsert-companies.mjs
(lines 46–47)components/hubspot/actions/batch-update-companies/batch-update-companies.mjs
(lines 43–44)components/hubspot/actions/batch-create-companies/batch-create-companies.mjs
(lines 43–44)Suggested steps:
- Create a shared function, e.g.:
// common/utils.mjs export function parseHubspotError(error) { // Extract and return the first error message const raw = JSON.parse(error.message); const detail = JSON.parse(raw.message).split(/:(.+)/)[1]; return JSON.parse(detail)[0].message.split(/:(.+)/)[0]; }- In each action’s
catch
block, replace the inline logic:- const message = JSON.parse((JSON.parse(error.message).message).split(/:(.+)/)[1])[0].message; - throw new ConfigurationError(message.split(/:(.+)/)[0]); + throw new ConfigurationError(parseHubspotError(error));This refactor will reduce duplication and improve maintainability.
components/hubspot/actions/batch-update-companies/batch-update-companies.mjs (1)
43-45
: Harden error extraction — avoid double JSON.parse and regex splitting.Mirror the safer approach suggested in batch-upsert to prevent masking the original error.
Apply this diff:
- const message = JSON.parse((JSON.parse(error.message).message).split(/:(.+)/)[1])[0].message; - throw new ConfigurationError(message.split(/:(.+)/)[0]); + const data = error?.response?.data; + const details = Array.isArray(data?.errors) + ? data.errors.map((e) => e?.message).filter(Boolean) + : []; + const base = [data?.message, ...details].filter(Boolean).join(" | "); + const msg = base || error?.message || "Unknown HubSpot error"; + throw new ConfigurationError(msg);components/hubspot/actions/search-crm/search-crm.mjs (3)
83-89
: Guard against missing property metadata when building Search Property optionsIf a name listed in
schema.searchableProperties
isn’t present inschema.properties
,propData
will beundefined
, causing a crash when accessing.label
. Add a fallback and filter out unresolved props.- const searchableProperties = schema.searchableProperties?.map((prop) => { - const propData = properties.find(({ name }) => name === prop); - return { - label: propData.label, - value: propData.name, - }; - }); + const searchableProperties = schema.searchableProperties + ?.map((prop) => { + const propData = properties.find(({ name }) => name === prop); + return propData + ? { label: propData.label, value: propData.name } + : { label: prop, value: prop }; + }) + .filter(Boolean);
252-257
: Validate presence and validity ofsearchProperty
before calling the APIIf
searchProperty
is undefined, the current message is misleading. Also, guard against schemas that don’t exposesearchableProperties
.- if (!schema.searchableProperties.includes(searchProperty)) { + if (!searchProperty) { + throw new ConfigurationError("A Search Property is required for this action."); + } + if (!Array.isArray(schema.searchableProperties) || !schema.searchableProperties.includes(searchProperty)) { throw new ConfigurationError( `Property \`${searchProperty}\` is not a searchable property of object type \`${objectType}\`. ` + `\n\nAvailable searchable properties are: \`${schema.searchableProperties.join("`, `")}\``, ); }
289-293
: Make partial-match filtering resilient to non-string property values
toLowerCase()
will throw for non-strings (numbers, booleans). Normalize both sides to string before comparison.- if (!exactMatch) { - results = results.filter((result) => - result.properties[searchProperty] - && result.properties[searchProperty].toLowerCase().includes(searchValue.toLowerCase())); - } + if (!exactMatch) { + const needle = String(searchValue ?? "").toLowerCase(); + results = results.filter((result) => { + const raw = result?.properties?.[searchProperty]; + if (raw === undefined || raw === null) return false; + const hay = String(raw).toLowerCase(); + return hay.includes(needle); + }); + }
components/hubspot/actions/create-contact-workflow/create-contact-workflow.mjs
Show resolved
Hide resolved
components/hubspot/actions/create-contact-workflow/create-contact-workflow.mjs
Show resolved
Hide resolved
components/hubspot/actions/create-contact-workflow/create-contact-workflow.mjs
Show resolved
Hide resolved
components/hubspot/actions/create-contact-workflow/create-contact-workflow.mjs
Show resolved
Hide resolved
components/hubspot/sources/new-or-updated-crm-object/new-or-updated-crm-object.mjs
Show resolved
Hide resolved
components/hubspot/sources/new-or-updated-custom-object/new-or-updated-custom-object.mjs
Show resolved
Hide resolved
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…page.mjs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @luancazarine lgtm! Ready for QA!
Resolves #18029
Summary by CodeRabbit
New Features
Improvements
Chores