-
Notifications
You must be signed in to change notification settings - Fork 5.5k
Hubspot - bug fix to sources w/ property changes #18345
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
Conversation
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
WalkthroughThe PR prevents undefined last-modified filters by initializing polling timestamps (maxTs = after || 0) and only adding GTE last-modified filters when an Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Timer
participant Source as HubSpot Source
participant DB as State DB (_after)
participant HubSpot as HubSpot API
Timer->>Source: run trigger
Source->>DB: get("after")
alt no after (first run)
Note right of Source #f0f4c3: Build params WITHOUT GTE filter
else after exists
Note right of Source #d0f0ff: Append lastmodifieddate GTE = after
end
Source->>HubSpot: searchObjects(params)
HubSpot-->>Source: results (items)
Source->>Source: processEvents(items)\nmaxTs = after || 0\nupdate maxTs per item
Source->>DB: _setAfter(maxTs)
Source-->>Timer: done
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Pre-merge checks (3 passed, 2 warnings)❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (25)
✅ Files skipped from review due to trivial changes (21)
🚧 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). (4)
✨ 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. Comment |
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: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
components/hubspot/sources/new-company-property-change/new-company-property-change.mjs (1)
62-74
: Fix: Unconditional GTE filter sends undefined value (repro symptom).This recreates the “operator GTE requires a value” error when
after
is not set on first run. Make the GTE filter conditional.- 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, - }, - ], - }, - ], - }, - }; - }, + getParams(after) { + const params = { + object: "companies", + data: { + limit: DEFAULT_LIMIT, + properties: [ this.property ], + sorts: [ + { propertyName: "hs_lastmodifieddate", direction: "DESCENDING" }, + ], + filterGroups: [ + { + filters: [ + { propertyName: this.property, operator: "HAS_PROPERTY" }, + ], + }, + ], + }, + }; + if (after) { + params.data.filterGroups[0].filters.push({ + propertyName: "hs_lastmodifieddate", + operator: "GTE", + value: after, + }); + } + return params; + },components/hubspot/sources/common/common.mjs (3)
55-66
: Advance the cursor even when nothing is emitted (seed on first run).Currently, maxTs is only updated inside the isRelevant branch, so on first run (after undefined) sources that implement isRelevant(ts > after) won’t emit nor advance the cursor, and you may persist 0. Seed maxTs using every item’s ts; emit only when relevant.
Apply:
- async processEvents(resources, after) { - let maxTs = after || 0; - for (const result of resources) { - if (await this.isRelevant(result, after)) { - this.emitEvent(result); - const ts = this.getTs(result); - if (ts > maxTs) { - maxTs = ts; - } - } - } - this._setAfter(maxTs); - }, + async processEvents(resources, after) { + let maxTs = after || 0; + for (const result of resources) { + const ts = this.getTs(result); + if (await this.isRelevant(result, after, ts)) { + this.emitEvent(result); + } + if (ts > maxTs) { + maxTs = ts; + } + } + this._setAfter(maxTs); + },
110-141
: Mirror seeding logic in paginateUsingHasMore.Same first-run issue: cursor only advances when isRelevant, risking repeated first-page fetches on fresh deployments.
Apply:
- for (const item of items) { - if (await this.isRelevant(item, after)) { - this.emitEvent(item); - const ts = this.getTs(item); - if (ts > maxTs) { - maxTs = ts; - this._setAfter(ts); - } - } - } + for (const item of items) { + const ts = this.getTs(item); + if (await this.isRelevant(item, after, ts)) { + this.emitEvent(item); + } + if (ts > maxTs) { + maxTs = ts; + this._setAfter(ts); + } + }
1-189
: Guard GTE/updated__gte filters behind anafter
check.Most property-change sources already push the GTE filter only when
after
is set; however components/hubspot/sources/new-or-updated-blog-article/new-or-updated-blog-article.mjs setsupdated__gte: after
unconditionally (around line 50) — wrap that assignment inif (after)
or only include the param whenafter
is defined to avoid HubSpot "requires a value" errors.components/hubspot/sources/new-contact-property-change/new-contact-property-change.mjs (1)
49-81
: Guard GTE filter — prefer 24h fallback or a nullish check (apply across HubSpot sources)
- Good: avoiding an unconditional GTE prevents "operator GTE requires a value".
- Findings: the truthy check pattern
if (after) { ... operator: "GTE", value: after }
appears in multiple HubSpot sources; components/hubspot/sources/new-company-property-change/new-company-property-change.mjs contains an unconditional GTE (usesafter
without a guard) — this must be fixed.- Action: Preferred (Option A) — default to now - 24h on first run to avoid fetching large historical datasets. Minimal (Option B) — change truthy check to a nullish check to avoid skipping valid 0-values.
- Note: use the correct last-modified property per object (contacts: "lastmodifieddate", others: "hs_lastmodifieddate") when applying the change.
Option A (preferred: 24h fallback):
- if (after) { - params.data.filterGroups[0].filters.push({ - propertyName: "lastmodifieddate", - operator: "GTE", - value: after, - }); - } + const from = (after ?? (Date.now() - 24 * 60 * 60 * 1000)); + params.data.filterGroups[0].filters.push({ + propertyName: "lastmodifieddate", + operator: "GTE", + value: from, + });Option B (minimal: robust null-check):
- if (after) { + if (after != null) { params.data.filterGroups[0].filters.push({ propertyName: "lastmodifieddate", operator: "GTE", value: after, }); - } + }Apply the chosen change to these files (at the getParams / filter construction sites):
- components/hubspot/sources/new-contact-property-change/new-contact-property-change.mjs
- components/hubspot/sources/new-ticket-property-change/new-ticket-property-change.mjs
- components/hubspot/sources/new-custom-object-property-change/new-custom-object-property-change.mjs
- components/hubspot/sources/new-deal-property-change/new-deal-property-change.mjs
- components/hubspot/sources/new-or-updated-contact/new-or-updated-contact.mjs
- components/hubspot/sources/new-company-property-change/new-company-property-change.mjs (critical — currently uses GTE with
after
unguarded)
🧹 Nitpick comments (12)
components/hubspot/sources/new-email-event/new-email-event.mjs (1)
40-51
: Guard startTimestamp to avoid passing undefined.Some HTTP clients drop undefined, others serialize it. Safer to include only when
after
is truthy.- getParams(after) { - const params = { - limit: DEFAULT_LIMIT, - startTimestamp: after, - }; + getParams(after) { + const params = { limit: DEFAULT_LIMIT }; + if (after) params.startTimestamp = after; if (this.type) { params.eventType = this.type; } return { params, }; },components/hubspot/sources/new-social-media-message/new-social-media-message.mjs (1)
41-47
: Conditionally include “since” to avoid empty param.Prevents sending
since: undefined
on first run.- getParams(after) { - return { - params: { - withChannelKeys: this.channel, - since: after, - }, - }; - }, + getParams(after) { + const params = { withChannelKeys: this.channel }; + if (after) params.since = after; + return { params }; + },components/hubspot/sources/new-engagement/new-engagement.mjs (1)
19-19
: Typo in user-facing text (“engagment”).Correct spelling improves UX.
- description: "Filter results by the type of engagment", + description: "Filter results by the type of engagement",components/hubspot/sources/common/common.mjs (1)
183-187
: Optional: default after for first run to reduce load.If you prefer bounded first-run scans, consider seeding after to Date.now() - 24h when absent, and keep the conditional “only add GTE filter when after is set” in sources.
I can open a follow-up PR if you want this policy applied repo-wide.
components/hubspot/sources/new-deal-in-stage/new-deal-in-stage.mjs (1)
90-116
: Advance cursor when no events emitted (first-run).processDeals only updates after inside isRelevant, so fresh runs with undefined after won’t advance the cursor and may re-scan. Update maxTs for all items; emit only when relevant.
Suggested change:
- for (const deal of results.results) { - const ts = await this.getTs(deal); - if (this.isRelevant(ts, after)) { - if (deal.properties.hubspot_owner_id) { - deal.properties.owner = await this.getOwner( - deal.properties.hubspot_owner_id, - ); - } - this.emitEvent(deal, ts); - if (ts > maxTs) { - maxTs = ts; - this._setAfter(ts); - } - } - } + for (const deal of results.results) { + const ts = await this.getTs(deal); + if (this.isRelevant(ts, after)) { + if (deal.properties.hubspot_owner_id) { + deal.properties.owner = await this.getOwner(deal.properties.hubspot_owner_id); + } + this.emitEvent(deal, ts); + } + if (ts > maxTs) { + maxTs = ts; + this._setAfter(ts); + } + }components/hubspot/sources/new-email-subscriptions-timeline/new-email-subscriptions-timeline.mjs (1)
29-35
: Guard startTimestamp when after is unset.To avoid sending an explicit 0 or undefined, only set startTimestamp when after is defined, or choose a bounded default (e.g., now - 24h).
- getParams(after) { - return { - params: { - startTimestamp: after, - }, - }; - }, + getParams(after) { + const params = {}; + if (after) params.startTimestamp = after; + return { params }; + },components/hubspot/sources/new-or-updated-blog-article/new-or-updated-blog-article.mjs (1)
46-54
: Avoid passing undefined query params.Only include updated__gte when after is provided to prevent accidental “undefined” serialization by clients.
- getParams(after) { - return { - params: { - limit: DEFAULT_LIMIT, - updated__gte: after, - sort: "-updatedAt", - }, - }; - }, + getParams(after) { + const params = { + limit: DEFAULT_LIMIT, + sort: "-updatedAt", + }; + if (after) params.updated__gte = after; + return { params }; + },components/hubspot/sources/delete-blog-article/delete-blog-article.mjs (1)
29-35
: Avoid sendingdeletedAt__gte
whenafter
is undefined.To mirror the property-change fixes and prevent malformed requests on first run, add the filter only when
after
is truthy.- getParams(after) { - return { - params: { - limit: 100, - deletedAt__gte: after, - sort: "-updatedAt", - }, - }; - }, + getParams(after) { + const params = { + params: { + limit: 100, + sort: "-updatedAt", + }, + }; + if (after) { + params.params.deletedAt__gte = after; + } + return params; + },components/hubspot/sources/new-ticket-property-change/new-ticket-property-change.mjs (2)
46-48
: Guard against undefined timestamps to avoid emitting events without history.On first run (
updatedAfter
falsy), tickets lacking history returnundefined
fromgetTs
, andisRelevant
would evaluate totrue
, producing events withts: undefined
. Guard onts
.- isRelevant(ticket, updatedAfter) { - return !updatedAfter || this.getTs(ticket) > updatedAfter; - }, + isRelevant(ticket, updatedAfter) { + const ts = this.getTs(ticket); + return Boolean(ts) && (!updatedAfter || ts > updatedAfter); + },
50-83
: Optional: apply a 24h baseline on first run to avoid heavy backfill and match the linked issue’s expectation.If you want first-run to start from “now - 1 day”, set a fallback when
after
is falsy and always include the filter.- if (after) { - params.data.filterGroups[0].filters.push({ - propertyName: "hs_lastmodifieddate", - operator: "GTE", - value: after, - }); - } + const baselineAfter = Date.now() - 24 * 60 * 60 * 1000; // 24h + const filterAfter = (after ?? baselineAfter); + params.data.filterGroups[0].filters.push({ + propertyName: "hs_lastmodifieddate", + operator: "GTE", + value: filterAfter, + });If backfill is desired, keep current behavior; otherwise this caps first-run load.
components/hubspot/sources/new-custom-object-property-change/new-custom-object-property-change.mjs (1)
55-87
: Conditional GTE avoids HubSpot error; mirror 24h fallback/null-check pattern.Match contact/deal sources for consistent behavior and first-run load control.
Option A (preferred: 24h fallback):
- if (after) { - params.data.filterGroups[0].filters.push({ - propertyName: "hs_lastmodifieddate", - operator: "GTE", - value: after, - }); - } + const from = (after ?? (Date.now() - 24 * 60 * 60 * 1000)); + params.data.filterGroups[0].filters.push({ + propertyName: "hs_lastmodifieddate", + operator: "GTE", + value: from, + });Option B (minimal: robust null-check):
- if (after) { + if (after != null) { params.data.filterGroups[0].filters.push({ propertyName: "hs_lastmodifieddate", operator: "GTE", value: after, }); - } + }components/hubspot/sources/new-deal-property-change/new-deal-property-change.mjs (1)
47-79
: Good guard against empty GTE; suggest consistent 24h default and null-check.Same rationale as other sources: prevent broad first-run pulls and handle
0
safely.Option A (preferred: 24h fallback):
- if (after) { - params.data.filterGroups[0].filters.push({ - propertyName: "hs_lastmodifieddate", - operator: "GTE", - value: after, - }); - } + const from = (after ?? (Date.now() - 24 * 60 * 60 * 1000)); + params.data.filterGroups[0].filters.push({ + propertyName: "hs_lastmodifieddate", + operator: "GTE", + value: from, + });Option B (minimal: robust null-check):
- if (after) { + if (after != null) { params.data.filterGroups[0].filters.push({ propertyName: "hs_lastmodifieddate", operator: "GTE", value: after, }); - } + }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (27)
components/hubspot/package.json
(1 hunks)components/hubspot/sources/common/common.mjs
(1 hunks)components/hubspot/sources/delete-blog-article/delete-blog-article.mjs
(1 hunks)components/hubspot/sources/new-company-property-change/new-company-property-change.mjs
(1 hunks)components/hubspot/sources/new-contact-added-to-list/new-contact-added-to-list.mjs
(1 hunks)components/hubspot/sources/new-contact-property-change/new-contact-property-change.mjs
(3 hunks)components/hubspot/sources/new-custom-object-property-change/new-custom-object-property-change.mjs
(3 hunks)components/hubspot/sources/new-deal-in-stage/new-deal-in-stage.mjs
(1 hunks)components/hubspot/sources/new-deal-property-change/new-deal-property-change.mjs
(3 hunks)components/hubspot/sources/new-email-event/new-email-event.mjs
(1 hunks)components/hubspot/sources/new-email-subscriptions-timeline/new-email-subscriptions-timeline.mjs
(1 hunks)components/hubspot/sources/new-engagement/new-engagement.mjs
(1 hunks)components/hubspot/sources/new-event/new-event.mjs
(1 hunks)components/hubspot/sources/new-form-submission/new-form-submission.mjs
(1 hunks)components/hubspot/sources/new-note/new-note.mjs
(1 hunks)components/hubspot/sources/new-or-updated-blog-article/new-or-updated-blog-article.mjs
(1 hunks)components/hubspot/sources/new-or-updated-company/new-or-updated-company.mjs
(1 hunks)components/hubspot/sources/new-or-updated-contact/new-or-updated-contact.mjs
(1 hunks)components/hubspot/sources/new-or-updated-crm-object/new-or-updated-crm-object.mjs
(1 hunks)components/hubspot/sources/new-or-updated-custom-object/new-or-updated-custom-object.mjs
(1 hunks)components/hubspot/sources/new-or-updated-deal/new-or-updated-deal.mjs
(1 hunks)components/hubspot/sources/new-or-updated-line-item/new-or-updated-line-item.mjs
(1 hunks)components/hubspot/sources/new-or-updated-product/new-or-updated-product.mjs
(1 hunks)components/hubspot/sources/new-social-media-message/new-social-media-message.mjs
(1 hunks)components/hubspot/sources/new-task/new-task.mjs
(1 hunks)components/hubspot/sources/new-ticket-property-change/new-ticket-property-change.mjs
(3 hunks)components/hubspot/sources/new-ticket/new-ticket.mjs
(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2024-10-10T19:18:27.998Z
Learnt from: GTFalcao
PR: PipedreamHQ/pipedream#14265
File: components/the_magic_drip/sources/common.mjs:35-43
Timestamp: 2024-10-10T19:18:27.998Z
Learning: In `components/the_magic_drip/sources/common.mjs`, when processing items in `getAndProcessData`, `savedIds` is intentionally updated with IDs of both emitted and non-emitted items to avoid emitting retroactive events upon first deployment and ensure only new events are emitted as they occur.
Applied to files:
components/hubspot/sources/common/common.mjs
🧬 Code graph analysis (5)
components/hubspot/sources/common/common.mjs (2)
components/hubspot/sources/new-deal-in-stage/new-deal-in-stage.mjs (1)
maxTs
(91-91)components/hubspot/sources/new-form-submission/new-form-submission.mjs (1)
maxTs
(26-26)
components/hubspot/sources/new-ticket-property-change/new-ticket-property-change.mjs (4)
components/hubspot/sources/common/common.mjs (2)
params
(185-185)after
(184-184)components/hubspot/sources/new-contact-property-change/new-contact-property-change.mjs (1)
params
(49-73)components/hubspot/sources/new-custom-object-property-change/new-custom-object-property-change.mjs (1)
params
(55-79)components/hubspot/sources/new-deal-property-change/new-deal-property-change.mjs (1)
params
(47-71)
components/hubspot/sources/new-contact-property-change/new-contact-property-change.mjs (5)
components/hubspot/sources/new-or-updated-contact/new-or-updated-contact.mjs (1)
params
(105-120)components/hubspot/sources/common/common.mjs (2)
params
(185-185)after
(184-184)components/hubspot/sources/new-custom-object-property-change/new-custom-object-property-change.mjs (1)
params
(55-79)components/hubspot/sources/new-deal-property-change/new-deal-property-change.mjs (1)
params
(47-71)components/hubspot/sources/new-ticket-property-change/new-ticket-property-change.mjs (1)
params
(50-74)
components/hubspot/sources/new-deal-property-change/new-deal-property-change.mjs (5)
components/hubspot/sources/new-or-updated-deal/new-or-updated-deal.mjs (1)
params
(87-102)components/hubspot/sources/common/common.mjs (2)
params
(185-185)after
(184-184)components/hubspot/sources/new-contact-property-change/new-contact-property-change.mjs (1)
params
(49-73)components/hubspot/sources/new-custom-object-property-change/new-custom-object-property-change.mjs (1)
params
(55-79)components/hubspot/sources/new-ticket-property-change/new-ticket-property-change.mjs (1)
params
(50-74)
components/hubspot/sources/new-custom-object-property-change/new-custom-object-property-change.mjs (4)
components/hubspot/sources/common/common.mjs (2)
params
(185-185)after
(184-184)components/hubspot/sources/new-contact-property-change/new-contact-property-change.mjs (1)
params
(49-73)components/hubspot/sources/new-deal-property-change/new-deal-property-change.mjs (1)
params
(47-71)components/hubspot/sources/new-ticket-property-change/new-ticket-property-change.mjs (1)
params
(50-74)
⏰ 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)
- GitHub Check: Verify TypeScript components
- GitHub Check: Publish TypeScript components
- GitHub Check: Lint Code Base
🔇 Additional comments (26)
components/hubspot/package.json (1)
3-3
: Version bump only — LGTM.components/hubspot/sources/new-task/new-task.mjs (1)
12-12
: Version bump only — LGTM.components/hubspot/sources/new-or-updated-deal/new-or-updated-deal.mjs (1)
13-13
: Version bump only — LGTM.components/hubspot/sources/new-email-event/new-email-event.mjs (1)
11-11
: Version bump only — LGTM.components/hubspot/sources/new-social-media-message/new-social-media-message.mjs (1)
10-10
: Version bump only — LGTM.components/hubspot/sources/new-company-property-change/new-company-property-change.mjs (1)
10-10
: Version bump noted.components/hubspot/sources/new-engagement/new-engagement.mjs (1)
11-11
: Version bump only — LGTM.components/hubspot/sources/new-or-updated-crm-object/new-or-updated-crm-object.mjs (1)
10-10
: Version bump only — LGTM.components/hubspot/sources/new-or-updated-custom-object/new-or-updated-custom-object.mjs (1)
10-10
: LGTM on version bump.No functional changes here.
components/hubspot/sources/new-event/new-event.mjs (1)
11-11
: LGTM on version bump.No logic changes.
components/hubspot/sources/new-deal-in-stage/new-deal-in-stage.mjs (1)
14-14
: LGTM on version bump.No functional changes in this file.
components/hubspot/sources/new-email-subscriptions-timeline/new-email-subscriptions-timeline.mjs (1)
9-9
: LGTM on version bump.No behavior changes.
components/hubspot/sources/new-or-updated-product/new-or-updated-product.mjs (1)
13-13
: LGTM on version bump.No functional changes detected.
components/hubspot/sources/new-or-updated-blog-article/new-or-updated-blog-article.mjs (1)
10-10
: LGTM on version bump.No logic changes.
components/hubspot/sources/new-ticket/new-ticket.mjs (1)
13-13
: LGTM on version bump.No behavior changes.
components/hubspot/sources/new-note/new-note.mjs (1)
11-11
: Version bump only — OK.components/hubspot/sources/new-form-submission/new-form-submission.mjs (1)
9-9
: Version bump only — OK.components/hubspot/sources/new-or-updated-company/new-or-updated-company.mjs (1)
13-13
: Version bump only — OK.components/hubspot/sources/new-contact-added-to-list/new-contact-added-to-list.mjs (1)
15-15
: Version bump only — OK.components/hubspot/sources/new-or-updated-line-item/new-or-updated-line-item.mjs (1)
13-13
: Version bump only — OK.components/hubspot/sources/new-or-updated-contact/new-or-updated-contact.mjs (1)
13-13
: Version bump only — OK.components/hubspot/sources/delete-blog-article/delete-blog-article.mjs (1)
9-9
: Version bump only — OK.components/hubspot/sources/new-ticket-property-change/new-ticket-property-change.mjs (1)
50-83
: Fix looks good: conditionalhs_lastmodifieddate
filter resolves the “GTE requires a value” bug.Building the base params and pushing the GTE filter only when
after
exists prevents malformed HubSpot requests on first run.components/hubspot/sources/new-contact-property-change/new-contact-property-change.mjs (1)
10-10
: Patch version bump looks good.components/hubspot/sources/new-custom-object-property-change/new-custom-object-property-change.mjs (1)
10-10
: Patch version bump looks good.components/hubspot/sources/new-deal-property-change/new-deal-property-change.mjs (1)
10-10
: Patch version bump looks good.
components/hubspot/sources/new-ticket-property-change/new-ticket-property-change.mjs
Show resolved
Hide resolved
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 @michelle0927, I hope you’re doing well!
Just wanted to kindly check in to see if there are any updates on this PR. My team and I were hoping to release this trigger to production earlier this week, and we’re really looking forward to seeing it merged into the master branch 🙏
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 @michelle0927 lgtm! Ready for QA!
Resolves #18339
Summary by CodeRabbit
Bug Fixes
Chores