@rawdash/connector-hubspot 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,180 @@
1
+ # @rawdash/connector-hubspot
2
+
3
+ Rawdash connector for HubSpot — syncs CRM contacts, companies, and deals, deal stage-change events, and marketing email campaign performance into the six-shape storage model. Covers both the sales (pipeline, win rate) and marketing (email engagement) verticals from a single connector.
4
+
5
+ ## Auth setup
6
+
7
+ The connector authenticates with a **private app access token**.
8
+
9
+ 1. In HubSpot, go to **Settings → Integrations → Private Apps**.
10
+ 2. Click **Create a private app**.
11
+ 3. On the **Basic Info** tab, give it a name (e.g. `rawdash`).
12
+ 4. On the **Scopes** tab, enable read access for the resources you want to sync:
13
+ - `crm.objects.contacts.read`
14
+ - `crm.objects.companies.read`
15
+ - `crm.objects.deals.read`
16
+ - `marketing-email` (only needed for `email_campaigns` / `email_stats`)
17
+ 5. Click **Create app**, then **Continue creating** to confirm.
18
+ 6. Open the app's **Auth** tab and copy the **Access token** (starts with `pat-`).
19
+
20
+ > **Note:** The token only needs read scopes for the resources you actually sync. Pick the narrowest set that covers your dashboards.
21
+
22
+ ## Configuration
23
+
24
+ ```ts
25
+ import { secret } from '@rawdash/core';
26
+
27
+ const hubspot = {
28
+ name: 'hubspot',
29
+ connectorId: 'hubspot',
30
+ config: {
31
+ accessToken: secret('HUBSPOT_ACCESS_TOKEN'),
32
+ // resources: ['contacts', 'deals', 'deal_events'], // optional, defaults to all
33
+ },
34
+ };
35
+ ```
36
+
37
+ Register the connector class when mounting the engine:
38
+
39
+ ```ts
40
+ import { HubSpotConnector } from '@rawdash/connector-hubspot';
41
+ import { mountEngine } from '@rawdash/hono';
42
+
43
+ mountEngine(config, { connectorRegistry: { hubspot: HubSpotConnector } });
44
+ ```
45
+
46
+ ### Choosing resources
47
+
48
+ By default the connector syncs every supported resource. To sync only a subset, pass `resources` with any combination of:
49
+
50
+ `contacts`, `companies`, `deals`, `deal_events`, `email_campaigns`, `email_stats`
51
+
52
+ The access token only needs read scopes for the resources you list, and picking only what you need reduces API calls during full syncs.
53
+
54
+ ### Example dashboard
55
+
56
+ ```ts
57
+ import { defineConfig, defineDashboard, defineMetric } from '@rawdash/core';
58
+
59
+ export default defineConfig({
60
+ connectors: [hubspot],
61
+ dashboards: {
62
+ sales: defineDashboard({
63
+ widgets: {
64
+ open_deal_value: {
65
+ kind: 'stat',
66
+ title: 'Open deal value',
67
+ metric: defineMetric({
68
+ connector: hubspot,
69
+ shape: 'entity',
70
+ entityType: 'hubspot_deal',
71
+ field: 'amount',
72
+ fn: 'sum',
73
+ filter: [{ field: 'dealStage', op: 'neq', value: 'closedlost' }],
74
+ }),
75
+ },
76
+ open_deals: {
77
+ kind: 'stat',
78
+ title: 'Open deals',
79
+ metric: defineMetric({
80
+ connector: hubspot,
81
+ shape: 'entity',
82
+ entityType: 'hubspot_deal',
83
+ fn: 'count',
84
+ filter: [
85
+ { field: 'dealstage', op: 'eq', value: 'appointmentscheduled' },
86
+ ],
87
+ }),
88
+ },
89
+ contacts_by_lifecycle: {
90
+ kind: 'distribution',
91
+ title: 'Contacts by lifecycle stage',
92
+ metric: defineMetric({
93
+ connector: hubspot,
94
+ shape: 'entity',
95
+ entityType: 'hubspot_contact',
96
+ fn: 'count',
97
+ groupBy: { field: 'lifecycleStage' },
98
+ }),
99
+ },
100
+ email_opens: {
101
+ kind: 'timeseries',
102
+ title: 'Email opens per campaign',
103
+ window: '90d',
104
+ metric: defineMetric({
105
+ connector: hubspot,
106
+ shape: 'metric',
107
+ name: 'hubspot_email_stats',
108
+ field: 'opened',
109
+ fn: 'sum',
110
+ window: '90d',
111
+ groupBy: { field: 'ts', granularity: 'day' },
112
+ }),
113
+ },
114
+ },
115
+ }),
116
+ },
117
+ });
118
+ ```
119
+
120
+ ## Data model
121
+
122
+ Monetary amounts (deal `amount`) are in the account's currency, as returned by HubSpot. Timestamps stored in attributes are Unix milliseconds.
123
+
124
+ | Storage shape | Entity/event/metric type | Key attributes |
125
+ | ------------- | --------------------------- | --------------------------------------------------------------------------------- |
126
+ | entity | `hubspot_contact` | email, lifecycleStage, leadStatus, ownerId, createdAt |
127
+ | entity | `hubspot_company` | name, domain, industry, lifecycleStage, createdAt |
128
+ | entity | `hubspot_deal` | dealName, dealStage, pipeline, amount, closeDate, ownerId, createdAt |
129
+ | event | `hubspot_deal_stage_change` | dealId, stage, sourceType |
130
+ | entity | `hubspot_email_campaign` | name, subject, fromName, type, sentDate, numIncluded |
131
+ | metric | `hubspot_email_stats` | campaignId, campaignName, sent, delivered, opened, clicked, bounced, unsubscribed |
132
+
133
+ - **`hubspot_deal_stage_change`** events come from the `dealstage` property history on each deal (`propertiesWithHistory=dealstage`). One event per recorded stage value, timestamped at the moment the stage was set.
134
+ - **`hubspot_email_stats`** metrics carry one sample per campaign; `value` is the `sent` count and every counter is also exposed in `attributes` so timeseries / distribution widgets can chart any of them.
135
+
136
+ ## Schemas
137
+
138
+ `HubSpotConnector.schemas` declares the Zod schema for each resource's raw API response (the array of records for CRM resources, the deal-history record array, and the campaign-detail object array). Used by the cloud shape-drift pipeline to populate `connector_baselines`, and by the package's property tests.
139
+
140
+ ## Sync behaviour
141
+
142
+ - **Backfill** (`mode: 'full'`): CRM objects are fetched via the Search API (`POST /crm/v3/objects/{object}/search`) sorted by last-modified ascending, paginated with the `after` cursor; deal events and email data are enumerated and rewritten in full.
143
+ - **Incremental** (`mode: 'latest'`): CRM searches add a `filterGroups` `GTE` filter on the object's last-modified property so only changed records are fetched. Entity phases upsert (no clear); the event and metric phases rewrite their requested window each run.
144
+ - **Resumable**: each phase yields an `after`/offset cursor on abort, so an interrupted sync resumes from the same page.
145
+ - **Rate limits**: HubSpot returns `429` when the per-app limit (100 requests / 10s) is exceeded. The shared HTTP client retries automatically with exponential back-off and honors `Retry-After`.
146
+
147
+ > **Search API ceiling:** HubSpot's Search API caps results at 10,000 per query. Very large CRM portfolios may not backfill in full in a single window; incremental syncs (which filter on `hs_lastmodifieddate`) stay well under the ceiling.
148
+
149
+ ## Aggregates
150
+
151
+ | Function | Resource | Upstream call |
152
+ | -------- | ----------------- | ------------------------------------------------------------ |
153
+ | `count` | `hubspot_contact` | `POST /crm/v3/objects/contacts/search` (`limit=1`, `total`) |
154
+ | `count` | `hubspot_company` | `POST /crm/v3/objects/companies/search` (`limit=1`, `total`) |
155
+ | `count` | `hubspot_deal` | `POST /crm/v3/objects/deals/search` (`limit=1`, `total`) |
156
+
157
+ `count` widgets are served directly from the Search API `total`, so the runner can skip backfilling the underlying entities for that tick. Filter conditions translate to HubSpot search operators (`eq→EQ`, `neq→NEQ`, `gt→GT`, `gte→GTE`, `lt→LT`, `lte→LTE`, `contains→CONTAINS_TOKEN`). `OR` clauses and `latest` aggregates aren't supported and fall back to evaluating against synced storage rows.
158
+
159
+ ## Registering in the MCP server
160
+
161
+ To make the connector available via the `add_connector` MCP tool, include it in `connectorFactories`:
162
+
163
+ ```ts
164
+ import { HubSpotConnector, configFields } from '@rawdash/connector-hubspot';
165
+
166
+ createMcpServer({
167
+ // ...
168
+ connectorFactories: [
169
+ {
170
+ id: 'hubspot',
171
+ configFields,
172
+ create: HubSpotConnector.create,
173
+ },
174
+ ],
175
+ });
176
+ ```
177
+
178
+ ## Property tests
179
+
180
+ The CRM entity resources (`contacts`, `companies`, `deals`) have fast-check property tests under `src/property.test.ts` that generate synthetic API payloads from each resource's Zod schema, run them through `connector.sync()` against an `InMemoryStorage`, and assert universal invariants (non-empty ids, finite timestamps, no `undefined` in storage, no thrown errors) plus per-resource entity counts. Deal events, email campaigns, and email stats are covered by example-driven unit tests in `src/hubspot.test.ts`.
@@ -0,0 +1,109 @@
1
+ import { BaseConnector, ConnectorContext, SyncOptions, StorageHandle, SyncResult, AggregateRequest, AggregateValue, FilterClause } from '@rawdash/core';
2
+ import { z } from 'zod';
3
+
4
+ declare const configFields: z.ZodObject<{
5
+ accessToken: z.ZodObject<{
6
+ $secret: z.ZodString;
7
+ }, z.core.$strip>;
8
+ resources: z.ZodOptional<z.ZodArray<z.ZodEnum<{
9
+ contacts: "contacts";
10
+ companies: "companies";
11
+ deals: "deals";
12
+ deal_events: "deal_events";
13
+ email_campaigns: "email_campaigns";
14
+ email_stats: "email_stats";
15
+ }>>>;
16
+ }, z.core.$strip>;
17
+ interface HubSpotSettings {
18
+ resources?: readonly HubSpotResource[];
19
+ }
20
+ declare const hubspotCredentials: {
21
+ accessToken: {
22
+ description: string;
23
+ auth: "required";
24
+ };
25
+ };
26
+ type HubSpotCredentials = typeof hubspotCredentials;
27
+ declare const PHASE_ORDER: readonly ["contacts", "companies", "deals", "deal_events", "email_campaigns", "email_stats"];
28
+ type HubSpotPhase = (typeof PHASE_ORDER)[number];
29
+ type HubSpotResource = HubSpotPhase;
30
+ declare class HubSpotConnector extends BaseConnector<HubSpotSettings, HubSpotCredentials> {
31
+ static readonly id = "hubspot";
32
+ static readonly schemas: {
33
+ readonly contacts: z.ZodArray<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
34
+ readonly companies: z.ZodArray<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
35
+ readonly deals: z.ZodArray<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
36
+ readonly deal_events: z.ZodArray<z.ZodObject<{
37
+ id: z.ZodString;
38
+ propertiesWithHistory: z.ZodOptional<z.ZodObject<{
39
+ dealstage: z.ZodOptional<z.ZodArray<z.ZodObject<{
40
+ value: z.ZodOptional<z.ZodNullable<z.ZodString>>;
41
+ timestamp: z.ZodOptional<z.ZodNullable<z.ZodString>>;
42
+ sourceType: z.ZodOptional<z.ZodNullable<z.ZodString>>;
43
+ }, z.core.$strip>>>;
44
+ }, z.core.$strip>>;
45
+ }, z.core.$strip>>;
46
+ readonly email_campaigns: z.ZodArray<z.ZodObject<{
47
+ id: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
48
+ name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
49
+ subject: z.ZodOptional<z.ZodNullable<z.ZodString>>;
50
+ fromName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
51
+ type: z.ZodOptional<z.ZodNullable<z.ZodString>>;
52
+ lastProcessingFinishedAt: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
53
+ numIncluded: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
54
+ counters: z.ZodOptional<z.ZodObject<{
55
+ sent: z.ZodOptional<z.ZodNumber>;
56
+ delivered: z.ZodOptional<z.ZodNumber>;
57
+ open: z.ZodOptional<z.ZodNumber>;
58
+ click: z.ZodOptional<z.ZodNumber>;
59
+ bounce: z.ZodOptional<z.ZodNumber>;
60
+ unsubscribed: z.ZodOptional<z.ZodNumber>;
61
+ }, z.core.$strip>>;
62
+ }, z.core.$strip>>;
63
+ readonly email_stats: z.ZodArray<z.ZodObject<{
64
+ id: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
65
+ name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
66
+ subject: z.ZodOptional<z.ZodNullable<z.ZodString>>;
67
+ fromName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
68
+ type: z.ZodOptional<z.ZodNullable<z.ZodString>>;
69
+ lastProcessingFinishedAt: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
70
+ numIncluded: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
71
+ counters: z.ZodOptional<z.ZodObject<{
72
+ sent: z.ZodOptional<z.ZodNumber>;
73
+ delivered: z.ZodOptional<z.ZodNumber>;
74
+ open: z.ZodOptional<z.ZodNumber>;
75
+ click: z.ZodOptional<z.ZodNumber>;
76
+ bounce: z.ZodOptional<z.ZodNumber>;
77
+ unsubscribed: z.ZodOptional<z.ZodNumber>;
78
+ }, z.core.$strip>>;
79
+ }, z.core.$strip>>;
80
+ };
81
+ static create(input: unknown, ctx?: ConnectorContext): HubSpotConnector;
82
+ readonly id = "hubspot";
83
+ readonly credentials: {
84
+ accessToken: {
85
+ description: string;
86
+ auth: "required";
87
+ };
88
+ };
89
+ private buildHeaders;
90
+ private apiGet;
91
+ private apiPost;
92
+ private buildSearchBody;
93
+ private fetchSearchPage;
94
+ private writeSearchPhase;
95
+ private fetchDealHistoryPage;
96
+ private writeDealEvents;
97
+ private fetchCampaignDetail;
98
+ private fetchCampaignsPage;
99
+ private writeEmailCampaigns;
100
+ private writeEmailStats;
101
+ private clearScopeOnFirstPage;
102
+ private writePhase;
103
+ sync(options: SyncOptions, storage: StorageHandle, signal?: AbortSignal): Promise<SyncResult>;
104
+ aggregate(req: AggregateRequest, signal?: AbortSignal): Promise<AggregateValue>;
105
+ validateCountFilter(resource: string, filter: FilterClause[]): void;
106
+ private translateCountFilter;
107
+ }
108
+
109
+ export { HubSpotConnector, type HubSpotResource, type HubSpotSettings, configFields, HubSpotConnector as default };
package/dist/index.js ADDED
@@ -0,0 +1,600 @@
1
+ // ../../connector-shared/dist/index.js
2
+ var HTTP_CLIENT_VERSION = "0.0.0";
3
+ var DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
4
+ function connectorUserAgent(connectorId) {
5
+ return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
6
+ }
7
+ function parseEpoch(value, unit) {
8
+ if (value === null || value === void 0) {
9
+ return null;
10
+ }
11
+ if (unit === "iso") {
12
+ if (typeof value !== "string") {
13
+ return null;
14
+ }
15
+ const ms = new Date(value).getTime();
16
+ return Number.isFinite(ms) ? ms : null;
17
+ }
18
+ if (typeof value === "string" && value.trim() === "") {
19
+ return null;
20
+ }
21
+ const n = typeof value === "number" ? value : Number(value);
22
+ if (!Number.isFinite(n)) {
23
+ return null;
24
+ }
25
+ const result = unit === "s" ? n * 1e3 : n;
26
+ return Number.isFinite(result) ? result : null;
27
+ }
28
+
29
+ // src/hubspot.ts
30
+ import {
31
+ BaseConnector,
32
+ defineConfigFields,
33
+ makeChunkedCursorGuard,
34
+ paginateChunked,
35
+ selectActivePhases
36
+ } from "@rawdash/core";
37
+ import { z } from "zod";
38
+ var configFields = defineConfigFields(
39
+ z.object({
40
+ accessToken: z.object({ $secret: z.string() }).meta({
41
+ label: "Private App access token",
42
+ description: "HubSpot private app access token with read scopes for contacts, companies, deals, and marketing email. Create one at Settings \u2192 Integrations \u2192 Private Apps.",
43
+ placeholder: "pat-na1-...",
44
+ secret: true
45
+ }),
46
+ resources: z.array(
47
+ z.enum([
48
+ "contacts",
49
+ "companies",
50
+ "deals",
51
+ "deal_events",
52
+ "email_campaigns",
53
+ "email_stats"
54
+ ])
55
+ ).nonempty().optional().meta({
56
+ label: "Resources",
57
+ description: "Which HubSpot resources to sync. Omit to sync all resources. The access token only needs read scopes for the resources listed here."
58
+ })
59
+ })
60
+ );
61
+ var hubspotCredentials = {
62
+ accessToken: {
63
+ description: "HubSpot private app access token",
64
+ auth: "required"
65
+ }
66
+ };
67
+ var PHASE_ORDER = [
68
+ "contacts",
69
+ "companies",
70
+ "deals",
71
+ "deal_events",
72
+ "email_campaigns",
73
+ "email_stats"
74
+ ];
75
+ var isHubSpotSyncCursor = makeChunkedCursorGuard(PHASE_ORDER);
76
+ var BASE_URL = "https://api.hubapi.com";
77
+ var SEARCH_LIMIT = 100;
78
+ var LIST_LIMIT = 100;
79
+ var SEARCH_PROPERTIES = {
80
+ contacts: [
81
+ "email",
82
+ "lifecyclestage",
83
+ "hs_lead_status",
84
+ "createdate",
85
+ "lastmodifieddate",
86
+ "hubspot_owner_id"
87
+ ],
88
+ companies: [
89
+ "name",
90
+ "domain",
91
+ "industry",
92
+ "createdate",
93
+ "lifecyclestage",
94
+ "hs_lastmodifieddate"
95
+ ],
96
+ deals: [
97
+ "dealname",
98
+ "dealstage",
99
+ "pipeline",
100
+ "amount",
101
+ "closedate",
102
+ "hubspot_owner_id",
103
+ "createdate",
104
+ "hs_lastmodifieddate"
105
+ ]
106
+ };
107
+ var MODIFIED_PROPERTY = {
108
+ contacts: "lastmodifieddate",
109
+ companies: "hs_lastmodifieddate",
110
+ deals: "hs_lastmodifieddate"
111
+ };
112
+ var ENTITY_TYPE_BY_PHASE = {
113
+ contacts: "hubspot_contact",
114
+ companies: "hubspot_company",
115
+ deals: "hubspot_deal",
116
+ email_campaigns: "hubspot_email_campaign"
117
+ };
118
+ var DEAL_STAGE_EVENT = "hubspot_deal_stage_change";
119
+ var EMAIL_STATS_METRIC = "hubspot_email_stats";
120
+ var COUNT_RESOURCE_TO_OBJECT = {
121
+ hubspot_contact: "contacts",
122
+ hubspot_company: "companies",
123
+ hubspot_deal: "deals"
124
+ };
125
+ var FILTER_OP_TO_HUBSPOT = {
126
+ eq: "EQ",
127
+ neq: "NEQ",
128
+ gt: "GT",
129
+ gte: "GTE",
130
+ lt: "LT",
131
+ lte: "LTE",
132
+ contains: "CONTAINS_TOKEN"
133
+ };
134
+ function finiteNumberOrNull(value) {
135
+ if (value === null || value === void 0 || value.trim() === "") {
136
+ return null;
137
+ }
138
+ const n = Number(value);
139
+ return Number.isFinite(n) ? n : null;
140
+ }
141
+ function counterValue(value) {
142
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
143
+ }
144
+ function unsupportedAggregate(req) {
145
+ return new Error(
146
+ `HubSpot aggregate: unsupported ${req.fn} for resource=${req.resource}`
147
+ );
148
+ }
149
+ var idString = z.string().min(1);
150
+ var contactProperties = z.object({
151
+ email: z.string().nullish(),
152
+ lifecyclestage: z.string().nullish(),
153
+ hs_lead_status: z.string().nullish(),
154
+ createdate: z.string().nullish(),
155
+ lastmodifieddate: z.string().nullish(),
156
+ hubspot_owner_id: z.string().nullish()
157
+ });
158
+ var companyProperties = z.object({
159
+ name: z.string().nullish(),
160
+ domain: z.string().nullish(),
161
+ industry: z.string().nullish(),
162
+ createdate: z.string().nullish(),
163
+ lifecyclestage: z.string().nullish(),
164
+ hs_lastmodifieddate: z.string().nullish()
165
+ });
166
+ var dealProperties = z.object({
167
+ dealname: z.string().nullish(),
168
+ dealstage: z.string().nullish(),
169
+ pipeline: z.string().nullish(),
170
+ amount: z.string().nullish(),
171
+ closedate: z.string().nullish(),
172
+ hubspot_owner_id: z.string().nullish(),
173
+ createdate: z.string().nullish(),
174
+ hs_lastmodifieddate: z.string().nullish()
175
+ });
176
+ function crmRecordSchema(props) {
177
+ return z.object({
178
+ id: idString,
179
+ properties: props,
180
+ createdAt: z.string(),
181
+ updatedAt: z.string(),
182
+ archived: z.boolean().optional()
183
+ });
184
+ }
185
+ var dealHistoryRecordSchema = z.object({
186
+ id: idString,
187
+ propertiesWithHistory: z.object({
188
+ dealstage: z.array(
189
+ z.object({
190
+ value: z.string().nullish(),
191
+ timestamp: z.string().nullish(),
192
+ sourceType: z.string().nullish()
193
+ })
194
+ ).optional()
195
+ }).optional()
196
+ });
197
+ var campaignDetailSchema = z.object({
198
+ id: z.union([z.string(), z.number()]),
199
+ name: z.string().nullish(),
200
+ subject: z.string().nullish(),
201
+ fromName: z.string().nullish(),
202
+ type: z.string().nullish(),
203
+ lastProcessingFinishedAt: z.union([z.string(), z.number()]).nullish(),
204
+ numIncluded: z.number().nullish(),
205
+ counters: z.object({
206
+ sent: z.number().optional(),
207
+ delivered: z.number().optional(),
208
+ open: z.number().optional(),
209
+ click: z.number().optional(),
210
+ bounce: z.number().optional(),
211
+ unsubscribed: z.number().optional()
212
+ }).optional()
213
+ });
214
+ var HubSpotConnector = class _HubSpotConnector extends BaseConnector {
215
+ static id = "hubspot";
216
+ static schemas = {
217
+ contacts: z.array(crmRecordSchema(contactProperties)),
218
+ companies: z.array(crmRecordSchema(companyProperties)),
219
+ deals: z.array(crmRecordSchema(dealProperties)),
220
+ deal_events: z.array(dealHistoryRecordSchema),
221
+ email_campaigns: z.array(campaignDetailSchema),
222
+ email_stats: z.array(campaignDetailSchema)
223
+ };
224
+ static create(input, ctx) {
225
+ const parsed = configFields.parse(input);
226
+ return new _HubSpotConnector(
227
+ { resources: parsed.resources },
228
+ { accessToken: parsed.accessToken },
229
+ ctx
230
+ );
231
+ }
232
+ id = "hubspot";
233
+ credentials = hubspotCredentials;
234
+ buildHeaders() {
235
+ return {
236
+ Authorization: `Bearer ${this.creds.accessToken}`,
237
+ "Content-Type": "application/json",
238
+ "User-Agent": connectorUserAgent("hubspot")
239
+ };
240
+ }
241
+ apiGet(url, resource, signal) {
242
+ return this.get(url, {
243
+ resource,
244
+ headers: this.buildHeaders(),
245
+ signal
246
+ });
247
+ }
248
+ apiPost(url, resource, body, signal) {
249
+ return this.post(url, {
250
+ resource,
251
+ headers: this.buildHeaders(),
252
+ body: JSON.stringify(body),
253
+ signal
254
+ });
255
+ }
256
+ // -------------------------------------------------------------------------
257
+ // CRM search phases (contacts / companies / deals)
258
+ // -------------------------------------------------------------------------
259
+ buildSearchBody(phase, after, options) {
260
+ const modifiedProperty = MODIFIED_PROPERTY[phase];
261
+ const filterGroups = [];
262
+ if (options.since) {
263
+ const sinceMs = new Date(options.since).getTime();
264
+ if (Number.isFinite(sinceMs)) {
265
+ filterGroups.push({
266
+ filters: [
267
+ {
268
+ propertyName: modifiedProperty,
269
+ operator: "GTE",
270
+ value: String(sinceMs)
271
+ }
272
+ ]
273
+ });
274
+ }
275
+ }
276
+ return {
277
+ filterGroups,
278
+ // Ascending modified-time keeps pagination stable while the `since`
279
+ // filter trims the set upstream, so an incremental sync never scans
280
+ // the whole object.
281
+ sorts: [{ propertyName: modifiedProperty, direction: "ASCENDING" }],
282
+ properties: SEARCH_PROPERTIES[phase],
283
+ limit: SEARCH_LIMIT,
284
+ ...after ? { after } : {}
285
+ };
286
+ }
287
+ async fetchSearchPage(phase, after, options, signal) {
288
+ const body = this.buildSearchBody(phase, after, options);
289
+ const res = await this.apiPost(
290
+ `${BASE_URL}/crm/v3/objects/${phase}/search`,
291
+ phase,
292
+ body,
293
+ signal
294
+ );
295
+ return {
296
+ items: res.body.results,
297
+ next: res.body.paging?.next?.after ?? null
298
+ };
299
+ }
300
+ async writeSearchPhase(storage, phase, items) {
301
+ for (const record of items) {
302
+ const props = record.properties;
303
+ const modifiedProperty = MODIFIED_PROPERTY[phase];
304
+ const updatedAt = parseEpoch(record.updatedAt, "iso") ?? parseEpoch(props[modifiedProperty], "ms") ?? 0;
305
+ let attributes;
306
+ if (phase === "contacts") {
307
+ attributes = {
308
+ email: props.email ?? null,
309
+ lifecycleStage: props.lifecyclestage ?? null,
310
+ leadStatus: props.hs_lead_status ?? null,
311
+ ownerId: props.hubspot_owner_id ?? null,
312
+ createdAt: parseEpoch(props.createdate, "ms")
313
+ };
314
+ } else if (phase === "companies") {
315
+ attributes = {
316
+ name: props.name ?? null,
317
+ domain: props.domain ?? null,
318
+ industry: props.industry ?? null,
319
+ lifecycleStage: props.lifecyclestage ?? null,
320
+ createdAt: parseEpoch(props.createdate, "ms")
321
+ };
322
+ } else {
323
+ attributes = {
324
+ dealName: props.dealname ?? null,
325
+ dealStage: props.dealstage ?? null,
326
+ pipeline: props.pipeline ?? null,
327
+ amount: finiteNumberOrNull(props.amount),
328
+ closeDate: parseEpoch(props.closedate, "ms"),
329
+ ownerId: props.hubspot_owner_id ?? null,
330
+ createdAt: parseEpoch(props.createdate, "ms")
331
+ };
332
+ }
333
+ await storage.entity({
334
+ type: ENTITY_TYPE_BY_PHASE[phase],
335
+ id: record.id,
336
+ attributes,
337
+ updated_at: updatedAt
338
+ });
339
+ }
340
+ }
341
+ // -------------------------------------------------------------------------
342
+ // Deal stage-change events (deal property history)
343
+ // -------------------------------------------------------------------------
344
+ async fetchDealHistoryPage(after, signal) {
345
+ const url = new URL(`${BASE_URL}/crm/v3/objects/deals`);
346
+ url.searchParams.set("limit", String(LIST_LIMIT));
347
+ url.searchParams.set("properties", "dealstage");
348
+ url.searchParams.set("propertiesWithHistory", "dealstage");
349
+ if (after) {
350
+ url.searchParams.set("after", after);
351
+ }
352
+ const res = await this.apiGet(
353
+ url.toString(),
354
+ "deal_events",
355
+ signal
356
+ );
357
+ return {
358
+ items: res.body.results,
359
+ next: res.body.paging?.next?.after ?? null
360
+ };
361
+ }
362
+ async writeDealEvents(storage, items, _options) {
363
+ for (const record of items) {
364
+ const history = record.propertiesWithHistory?.dealstage ?? [];
365
+ for (const entry of history) {
366
+ const ts = parseEpoch(entry.timestamp, "iso");
367
+ if (ts === null) {
368
+ continue;
369
+ }
370
+ await storage.event({
371
+ name: DEAL_STAGE_EVENT,
372
+ start_ts: ts,
373
+ end_ts: null,
374
+ attributes: {
375
+ dealId: record.id,
376
+ stage: entry.value ?? null,
377
+ sourceType: entry.sourceType ?? null
378
+ }
379
+ });
380
+ }
381
+ }
382
+ }
383
+ // -------------------------------------------------------------------------
384
+ // Marketing email campaigns + stats (legacy email campaigns API)
385
+ // -------------------------------------------------------------------------
386
+ async fetchCampaignDetail(id, resource, signal) {
387
+ const res = await this.apiGet(
388
+ `${BASE_URL}/email/public/v1/campaigns/${id}`,
389
+ resource,
390
+ signal
391
+ );
392
+ return res.body;
393
+ }
394
+ async fetchCampaignsPage(phase, after, signal) {
395
+ const url = new URL(`${BASE_URL}/email/public/v1/campaigns`);
396
+ url.searchParams.set("limit", String(LIST_LIMIT));
397
+ if (after) {
398
+ url.searchParams.set("offset", after);
399
+ }
400
+ const listRes = await this.apiGet(
401
+ url.toString(),
402
+ `${phase}_list`,
403
+ signal
404
+ );
405
+ const { campaigns, hasMore, offset } = listRes.body;
406
+ const details = [];
407
+ for (const campaign of campaigns) {
408
+ details.push(await this.fetchCampaignDetail(campaign.id, phase, signal));
409
+ }
410
+ const next = hasMore && offset !== void 0 && offset !== null ? String(offset) : null;
411
+ return { items: details, next };
412
+ }
413
+ async writeEmailCampaigns(storage, items) {
414
+ for (const detail of items) {
415
+ const sentDate = parseEpoch(detail.lastProcessingFinishedAt, "ms");
416
+ await storage.entity({
417
+ type: ENTITY_TYPE_BY_PHASE.email_campaigns,
418
+ id: String(detail.id),
419
+ attributes: {
420
+ name: detail.name ?? null,
421
+ subject: detail.subject ?? null,
422
+ fromName: detail.fromName ?? null,
423
+ type: detail.type ?? null,
424
+ sentDate,
425
+ numIncluded: detail.numIncluded ?? null
426
+ },
427
+ updated_at: sentDate ?? 0
428
+ });
429
+ }
430
+ }
431
+ async writeEmailStats(storage, items) {
432
+ for (const detail of items) {
433
+ const ts = parseEpoch(detail.lastProcessingFinishedAt, "ms");
434
+ if (ts === null) {
435
+ continue;
436
+ }
437
+ const counters = detail.counters ?? {};
438
+ const sent = counterValue(counters.sent);
439
+ await storage.metric({
440
+ name: EMAIL_STATS_METRIC,
441
+ ts,
442
+ value: sent,
443
+ attributes: {
444
+ campaignId: String(detail.id),
445
+ campaignName: detail.name ?? null,
446
+ sent,
447
+ delivered: counterValue(counters.delivered),
448
+ opened: counterValue(counters.open),
449
+ clicked: counterValue(counters.click),
450
+ bounced: counterValue(counters.bounce),
451
+ unsubscribed: counterValue(counters.unsubscribed)
452
+ }
453
+ });
454
+ }
455
+ }
456
+ // -------------------------------------------------------------------------
457
+ // Scope clearing (idempotency)
458
+ // -------------------------------------------------------------------------
459
+ async clearScopeOnFirstPage(storage, phase, isFull) {
460
+ if (phase === "deal_events") {
461
+ await storage.events([], { names: [DEAL_STAGE_EVENT] });
462
+ return;
463
+ }
464
+ if (phase === "email_stats") {
465
+ await storage.metrics([], { names: [EMAIL_STATS_METRIC] });
466
+ return;
467
+ }
468
+ if (!isFull) {
469
+ return;
470
+ }
471
+ const entityType = ENTITY_TYPE_BY_PHASE[phase];
472
+ if (entityType) {
473
+ await storage.entities([], { types: [entityType] });
474
+ }
475
+ }
476
+ async writePhase(storage, phase, items, options) {
477
+ switch (phase) {
478
+ case "contacts":
479
+ case "companies":
480
+ case "deals":
481
+ await this.writeSearchPhase(storage, phase, items);
482
+ return;
483
+ case "deal_events":
484
+ await this.writeDealEvents(
485
+ storage,
486
+ items,
487
+ options
488
+ );
489
+ return;
490
+ case "email_campaigns":
491
+ await this.writeEmailCampaigns(storage, items);
492
+ return;
493
+ case "email_stats":
494
+ await this.writeEmailStats(storage, items);
495
+ return;
496
+ }
497
+ }
498
+ async sync(options, storage, signal) {
499
+ const cursor = isHubSpotSyncCursor(options.cursor) ? options.cursor : void 0;
500
+ const isFull = options.mode === "full";
501
+ const phases = selectActivePhases(
502
+ (r) => r,
503
+ PHASE_ORDER,
504
+ this.settings.resources
505
+ );
506
+ return paginateChunked({
507
+ phases,
508
+ cursor,
509
+ signal,
510
+ logger: this.logger,
511
+ fetchPage: async (phase, page, sig) => {
512
+ if (phase === "contacts" || phase === "companies" || phase === "deals") {
513
+ return this.fetchSearchPage(phase, page, options, sig);
514
+ }
515
+ if (phase === "deal_events") {
516
+ return this.fetchDealHistoryPage(page, sig);
517
+ }
518
+ return this.fetchCampaignsPage(phase, page, sig);
519
+ },
520
+ writeBatch: async (phase, items, page) => {
521
+ if (page === null) {
522
+ await this.clearScopeOnFirstPage(storage, phase, isFull);
523
+ }
524
+ await this.writePhase(storage, phase, items, options);
525
+ }
526
+ });
527
+ }
528
+ // -------------------------------------------------------------------------
529
+ // Aggregates — count via the CRM Search API `total` (one request)
530
+ // -------------------------------------------------------------------------
531
+ async aggregate(req, signal) {
532
+ if (req.fn !== "count") {
533
+ throw unsupportedAggregate(req);
534
+ }
535
+ const object = COUNT_RESOURCE_TO_OBJECT[req.resource];
536
+ if (!object) {
537
+ throw unsupportedAggregate(req);
538
+ }
539
+ const filterGroups = this.translateCountFilter(req.filter);
540
+ const res = await this.apiPost(
541
+ `${BASE_URL}/crm/v3/objects/${object}/search`,
542
+ object,
543
+ { filterGroups, properties: [], limit: 1 },
544
+ signal
545
+ );
546
+ const value = res.body.total ?? 0;
547
+ this.logger.info("aggregate", {
548
+ fn: "count",
549
+ resource: req.resource,
550
+ filter: req.filter,
551
+ value,
552
+ via: "CRM search API"
553
+ });
554
+ return value;
555
+ }
556
+ validateCountFilter(resource, filter) {
557
+ if (!COUNT_RESOURCE_TO_OBJECT[resource]) {
558
+ throw new Error(
559
+ `HubSpot aggregate count: unsupported resource=${resource}`
560
+ );
561
+ }
562
+ this.translateCountFilter(filter);
563
+ }
564
+ // Translates flat AND filter conditions into HubSpot search `filterGroups`.
565
+ // OR clauses aren't expressible alongside the rest of the group model, so
566
+ // they throw "unsupported" and the runner falls back to storage rows.
567
+ translateCountFilter(filter) {
568
+ if (!filter || filter.length === 0) {
569
+ return [];
570
+ }
571
+ const filters = filter.map((clause) => {
572
+ if ("or" in clause) {
573
+ throw new Error(
574
+ "HubSpot aggregate count: OR filter clauses are not supported"
575
+ );
576
+ }
577
+ const operator = FILTER_OP_TO_HUBSPOT[clause.op];
578
+ if (!operator) {
579
+ throw new Error(
580
+ `HubSpot aggregate count: unsupported filter operator ${clause.op}`
581
+ );
582
+ }
583
+ return {
584
+ propertyName: clause.field,
585
+ operator,
586
+ value: String(clause.value)
587
+ };
588
+ });
589
+ return [{ filters }];
590
+ }
591
+ };
592
+
593
+ // src/index.ts
594
+ var index_default = HubSpotConnector;
595
+ export {
596
+ HubSpotConnector,
597
+ configFields,
598
+ index_default as default
599
+ };
600
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../connector-shared/src/errors.ts","../../../connector-shared/src/retry.ts","../../../connector-shared/src/version.ts","../../../connector-shared/src/request.ts","../../../connector-shared/src/rate-limit.ts","../../../connector-shared/src/sanitize.ts","../../../connector-shared/src/epoch.ts","../../../connector-shared/src/pagination.ts","../../../connector-shared/src/logger.ts","../src/hubspot.ts","../src/index.ts"],"sourcesContent":["import type { HttpResponse } from './types';\n\nexport type HttpErrorKind =\n | 'transient'\n | 'rate_limit'\n | 'auth'\n | 'upstream_bug'\n | 'client_bug';\n\nexport abstract class HttpClientError extends Error {\n abstract readonly kind: HttpErrorKind;\n readonly response?: HttpResponse;\n\n constructor(message: string, response?: HttpResponse) {\n super(message);\n this.name = new.target.name;\n this.response = response;\n }\n}\n\nexport class TransientError extends HttpClientError {\n readonly kind = 'transient' as const;\n}\n\nexport class RateLimitError extends HttpClientError {\n readonly kind = 'rate_limit' as const;\n readonly retryAfter?: Date;\n\n constructor(message: string, response?: HttpResponse, retryAfter?: Date) {\n super(message, response);\n this.retryAfter = retryAfter;\n }\n}\n\nexport class AuthError extends HttpClientError {\n readonly kind = 'auth' as const;\n}\n\nexport class UpstreamBugError extends HttpClientError {\n readonly kind = 'upstream_bug' as const;\n}\n\nexport class ClientBugError extends HttpClientError {\n readonly kind = 'client_bug' as const;\n}\n\nexport function classifyStatus(status: number): HttpErrorKind {\n if (status === 429) {\n return 'rate_limit';\n }\n if (status === 401 || status === 403) {\n return 'auth';\n }\n if (status === 408) {\n return 'transient';\n }\n if (status >= 500) {\n return 'upstream_bug';\n }\n if (status >= 400) {\n return 'client_bug';\n }\n return 'client_bug';\n}\n\nexport function errorForStatus(\n message: string,\n response: HttpResponse,\n retryAfter?: Date,\n): HttpClientError {\n const kind = classifyStatus(response.status);\n switch (kind) {\n case 'rate_limit':\n return new RateLimitError(message, response, retryAfter);\n case 'auth':\n return new AuthError(message, response);\n case 'transient':\n return new TransientError(message, response);\n case 'upstream_bug':\n return new UpstreamBugError(message, response);\n case 'client_bug':\n return new ClientBugError(message, response);\n }\n}\n","import { HttpClientError, RateLimitError, TransientError } from './errors';\n\nexport interface RetryPolicy {\n maxAttempts?: number;\n initialDelayMs?: number;\n maxDelayMs?: number;\n retryOn?: (status: number | null, err?: Error) => boolean;\n}\n\nexport const defaultRetryOn = (status: number | null, err?: Error): boolean => {\n if (err instanceof RateLimitError) {\n return true;\n }\n if (err instanceof TransientError) {\n return true;\n }\n if (status === null) {\n return err instanceof Error && !(err instanceof HttpClientError);\n }\n if (status === 408 || status === 429) {\n return true;\n }\n if (status >= 500) {\n return true;\n }\n return false;\n};\n\nexport function backoffDelayMs(\n attempt: number,\n policy: Required<Pick<RetryPolicy, 'initialDelayMs' | 'maxDelayMs'>>,\n): number {\n const base = policy.initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, policy.maxDelayMs);\n}\n\nexport function parseRetryAfter(\n headerValue: string | null,\n now: Date = new Date(),\n): Date | undefined {\n if (!headerValue) {\n return undefined;\n }\n const trimmed = headerValue.trim();\n if (/^\\d+$/.test(trimmed)) {\n return new Date(now.getTime() + Number(trimmed) * 1000);\n }\n const parsed = Date.parse(trimmed);\n if (Number.isNaN(parsed)) {\n return undefined;\n }\n return new Date(parsed);\n}\n\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) {\n return Promise.reject(signal.reason ?? new Error('Aborted'));\n }\n return new Promise<void>((resolve, reject) => {\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal!.reason ?? new Error('Aborted'));\n };\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n","export const HTTP_CLIENT_VERSION = '0.0.0';\n\nexport const DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n\nexport function connectorUserAgent(connectorId: string): string {\n return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n}\n","import {\n AuthError,\n ClientBugError,\n HttpClientError,\n RateLimitError,\n TransientError,\n UpstreamBugError,\n errorForStatus,\n} from './errors';\nimport { defaultRetryOn, parseRetryAfter, sleep } from './retry';\nimport type { FetchLike, HttpMethod, HttpRequest, HttpResponse } from './types';\nimport { DEFAULT_USER_AGENT } from './version';\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_MAX_ATTEMPTS = 3;\nconst DEFAULT_INITIAL_DELAY_MS = 1000;\nconst DEFAULT_MAX_DELAY_MS = 60_000;\nconst OBSERVER_TIMEOUT_MS = 250;\n\nexport interface RequestObservation {\n url: string;\n method: HttpMethod;\n status: number;\n resource: string;\n requestId: string;\n body: unknown;\n}\n\nexport type RequestObserver = (\n event: RequestObservation,\n) => void | Promise<void>;\n\nexport interface RequestOptions {\n fetch?: FetchLike;\n observer?: RequestObserver;\n resource: string;\n requestId?: string;\n}\n\nasync function notifyObserver(\n observer: RequestObserver,\n event: RequestObservation,\n): Promise<void> {\n let result: void | Promise<void>;\n try {\n result = observer(event);\n } catch (err) {\n console.warn('[connector-shared] request observer threw:', err);\n return;\n }\n if (!(result instanceof Promise)) {\n return;\n }\n const guarded = result.catch((err) => {\n console.warn('[connector-shared] request observer rejected:', err);\n });\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, OBSERVER_TIMEOUT_MS);\n });\n try {\n await Promise.race([guarded, timeout]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n}\n\nfunction newRequestId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) {\n return c.randomUUID();\n }\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction mergeHeaders(\n defaults: Record<string, string>,\n overrides: Record<string, string> | undefined,\n): Record<string, string> {\n const merged: Record<string, string> = {};\n for (const [k, v] of Object.entries(defaults)) {\n merged[k.toLowerCase()] = v;\n }\n if (overrides) {\n for (const [k, v] of Object.entries(overrides)) {\n merged[k.toLowerCase()] = v;\n }\n }\n return merged;\n}\n\nfunction linkTimeoutSignal(\n parent: AbortSignal | undefined,\n timeoutMs: number,\n): { signal: AbortSignal; cancel: () => void } {\n const controller = new AbortController();\n const onParentAbort = () => {\n controller.abort(parent?.reason);\n };\n if (parent) {\n if (parent.aborted) {\n controller.abort(parent.reason);\n } else {\n parent.addEventListener('abort', onParentAbort, { once: true });\n }\n }\n const timer = setTimeout(() => {\n controller.abort(new Error(`Request timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n return {\n signal: controller.signal,\n cancel: () => {\n clearTimeout(timer);\n if (parent) {\n parent.removeEventListener('abort', onParentAbort);\n }\n },\n };\n}\n\nasync function readBody(res: Response, parseJson: boolean): Promise<unknown> {\n if (res.status === 204 || res.status === 205) {\n return null;\n }\n const contentType = res.headers.get('content-type') ?? '';\n if (parseJson && contentType.includes('application/json')) {\n const text = await res.text();\n if (text.length === 0) {\n return null;\n }\n return JSON.parse(text);\n }\n return res.text();\n}\n\nexport async function request<T = unknown>(\n req: HttpRequest,\n options: RequestOptions,\n): Promise<HttpResponse<T>> {\n const fetchImpl: FetchLike = options.fetch ?? (globalThis.fetch as FetchLike);\n const retry = req.retry ?? {};\n const maxAttempts = retry.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n const initialDelayMs = retry.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;\n const maxDelayMs = retry.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;\n const retryOn = retry.retryOn ?? defaultRetryOn;\n const timeoutMs = req.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const parseJson = req.parseJson ?? true;\n\n const headers = mergeHeaders(\n {\n 'User-Agent': DEFAULT_USER_AGENT,\n Accept: 'application/json',\n },\n req.headers,\n );\n\n let lastErr: Error | undefined;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n req.signal?.throwIfAborted();\n\n const { signal, cancel } = linkTimeoutSignal(req.signal, timeoutMs);\n let res: Response;\n try {\n res = await fetchImpl(req.url, {\n method: req.method ?? 'GET',\n headers,\n body: req.body as RequestInit['body'],\n signal,\n });\n } catch (err) {\n cancel();\n if (req.signal?.aborted) {\n throw req.signal.reason ?? err;\n }\n const error = err instanceof Error ? err : new Error(String(err));\n lastErr = error;\n if (attempt < maxAttempts - 1 && retryOn(null, error)) {\n const delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n await sleep(delay, req.signal);\n continue;\n }\n throw new TransientError(error.message);\n }\n cancel();\n\n const body = await readBody(res, parseJson);\n const httpResponse: HttpResponse<T> = {\n status: res.status,\n headers: res.headers,\n body: body as T,\n };\n if (req.rateLimit) {\n const state = req.rateLimit.parse(res.headers);\n if (state) {\n httpResponse.rateLimitState = state;\n }\n }\n\n if (options.observer) {\n await notifyObserver(options.observer, {\n url: req.url,\n method: req.method ?? 'GET',\n status: res.status,\n resource: options.resource,\n requestId: options.requestId ?? newRequestId(),\n body,\n });\n }\n\n if (res.ok) {\n return httpResponse;\n }\n\n const retryAfter = parseRetryAfter(res.headers.get('retry-after'));\n const message = `HTTP ${res.status} ${res.statusText} for ${req.method ?? 'GET'} ${req.url}`;\n const err = errorForStatus(message, httpResponse, retryAfter);\n\n if (\n attempt < maxAttempts - 1 &&\n retryOn(res.status, err) &&\n !(err instanceof AuthError) &&\n !(err instanceof ClientBugError)\n ) {\n lastErr = err;\n let delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n if (err instanceof RateLimitError && retryAfter) {\n const wait = retryAfter.getTime() - Date.now();\n if (wait > 0) {\n delay = Math.min(wait, maxDelayMs);\n }\n }\n await sleep(delay, req.signal);\n continue;\n }\n\n throw err;\n }\n\n throw lastErr ?? new UpstreamBugError('Exhausted retry attempts');\n}\n\nfunction computeDelay(\n attempt: number,\n initialDelayMs: number,\n maxDelayMs: number,\n): number {\n const base = initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, maxDelayMs);\n}\n\nexport { HttpClientError };\n","export interface RateLimitState {\n remaining: number;\n resetAt: Date;\n}\n\nexport interface RateLimitPolicy {\n parse(headers: Headers): RateLimitState | null;\n}\n\nexport interface StandardRateLimitPolicyConfig {\n remainingHeader: string;\n resetHeader: string;\n resetUnit: 's' | 'ms';\n resetFallbackMs?: number;\n}\n\nexport function standardRateLimitPolicy(\n config: StandardRateLimitPolicyConfig,\n): RateLimitPolicy {\n const { remainingHeader, resetHeader, resetUnit, resetFallbackMs } = config;\n const multiplier = resetUnit === 's' ? 1000 : 1;\n return {\n parse(h) {\n const remainingRaw = h.get(remainingHeader);\n if (remainingRaw === null || remainingRaw.trim() === '') {\n return null;\n }\n const remaining = Number(remainingRaw);\n if (!Number.isFinite(remaining)) {\n return null;\n }\n const resetRaw = h.get(resetHeader);\n if (resetRaw === null) {\n if (resetFallbackMs === undefined) {\n return null;\n }\n return {\n remaining,\n resetAt: new Date(Date.now() + resetFallbackMs),\n };\n }\n if (resetRaw.trim() === '') {\n return null;\n }\n const reset = Number(resetRaw);\n if (!Number.isFinite(reset) || reset < 0) {\n return null;\n }\n const resetMs = reset * multiplier;\n if (!Number.isFinite(resetMs)) {\n return null;\n }\n return { remaining, resetAt: new Date(resetMs) };\n },\n };\n}\n","export interface SanitizeAllowedUrlOptions {\n url: string | null;\n host: string;\n pathname: string;\n protocol?: 'https:' | 'http:';\n}\n\nexport function sanitizeAllowedUrl(\n options: SanitizeAllowedUrlOptions,\n): string | null {\n const { url, host, pathname, protocol = 'https:' } = options;\n if (url === null) {\n return null;\n }\n try {\n const u = new URL(url);\n if (u.protocol !== protocol || u.host !== host || u.pathname !== pathname) {\n return null;\n }\n return u.toString();\n } catch {\n return null;\n }\n}\n","export type EpochUnit = 'ms' | 's' | 'iso';\n\nexport function parseEpoch(\n value: number | string | null | undefined,\n unit: EpochUnit,\n): number | null {\n if (value === null || value === undefined) {\n return null;\n }\n if (unit === 'iso') {\n if (typeof value !== 'string') {\n return null;\n }\n const ms = new Date(value).getTime();\n return Number.isFinite(ms) ? ms : null;\n }\n if (typeof value === 'string' && value.trim() === '') {\n return null;\n }\n const n = typeof value === 'number' ? value : Number(value);\n if (!Number.isFinite(n)) {\n return null;\n }\n const result = unit === 's' ? n * 1000 : n;\n return Number.isFinite(result) ? result : null;\n}\n","import { request } from './request';\nimport type { HttpRequest } from './types';\n\nexport function parseLinkHeader(header: string | null): Record<string, string> {\n if (!header) {\n return {};\n }\n const result: Record<string, string> = {};\n for (const part of header.split(',')) {\n const match = part.match(/<([^>]+)>\\s*;\\s*rel=\"([^\"]+)\"/);\n if (match) {\n result[match[2]!] = match[1]!;\n }\n }\n return result;\n}\n\nexport async function* paginateLink<T>(\n initial: HttpRequest,\n parse: (body: unknown) => T[],\n options: { resource: string },\n): AsyncIterable<T> {\n let next: string | null = initial.url;\n while (next) {\n const res: Awaited<ReturnType<typeof request>> = await request(\n {\n ...initial,\n url: next,\n },\n { resource: options.resource },\n );\n for (const item of parse(res.body)) {\n yield item;\n }\n const links = parseLinkHeader(res.headers.get('link'));\n next = links['next'] ?? null;\n }\n}\n\nexport async function* paginateCursor<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; nextCursor: string | null },\n buildNext: (req: HttpRequest, cursor: string) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let req: HttpRequest = initial;\n while (true) {\n const res = await request(req, { resource: options.resource });\n const { items, nextCursor } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!nextCursor) {\n return;\n }\n req = buildNext(req, nextCursor);\n }\n}\n\nexport async function* paginatePage<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; hasMore: boolean },\n buildPage: (req: HttpRequest, page: number) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let page = 1;\n while (true) {\n const req = page === 1 ? initial : buildPage(initial, page);\n const res = await request(req, { resource: options.resource });\n const { items, hasMore } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!hasMore || items.length === 0) {\n return;\n }\n page++;\n }\n}\n","export type LogFields = Record<string, unknown>;\n\nexport interface ConnectorLogger {\n info(event: string, fields?: LogFields): void;\n warn(event: string, fields?: LogFields): void;\n}\n\nexport interface ConnectorLoggerOptions {\n scope: string;\n}\n\nconst MAX_VALUE_LEN = 120;\n\nfunction truncate(s: string, max = MAX_VALUE_LEN): string {\n if (s.length <= max) {\n return s;\n }\n return `${s.slice(0, max - 1)}…`;\n}\n\nfunction formatValue(value: unknown): string {\n if (value === null) {\n return 'null';\n }\n if (value === undefined) {\n return '';\n }\n if (typeof value === 'number' || typeof value === 'boolean') {\n return String(value);\n }\n if (typeof value === 'string') {\n const t = truncate(value);\n if (/[\\s\"=]/.test(t)) {\n return JSON.stringify(t);\n }\n return t;\n }\n if (typeof value === 'bigint') {\n return value.toString();\n }\n let json: string | undefined;\n try {\n json = JSON.stringify(value);\n } catch {\n json = undefined;\n }\n return truncate(json ?? String(value));\n}\n\nexport function formatLogFields(fields?: LogFields): string {\n if (!fields) {\n return '';\n }\n const parts: string[] = [];\n for (const [k, v] of Object.entries(fields)) {\n if (v === undefined) {\n continue;\n }\n parts.push(`${k}=${formatValue(v)}`);\n }\n return parts.length > 0 ? ` ${parts.join(' ')}` : '';\n}\n\nexport function formatLogLine(\n scope: string,\n event: string,\n fields?: LogFields,\n): string {\n return `[${scope}] ${event}${formatLogFields(fields)}`;\n}\n\nexport function createDefaultConnectorLogger(\n opts: ConnectorLoggerOptions,\n): ConnectorLogger {\n return {\n info(event, fields) {\n console.info(formatLogLine(opts.scope, event, fields));\n },\n warn(event, fields) {\n console.warn(formatLogLine(opts.scope, event, fields));\n },\n };\n}\n\nconst NOOP_LOGGER: ConnectorLogger = {\n info() {},\n warn() {},\n};\n\nexport function noopConnectorLogger(): ConnectorLogger {\n return NOOP_LOGGER;\n}\n","import {\n type HttpResponse,\n connectorUserAgent,\n parseEpoch,\n} from '@rawdash/connector-shared';\nimport {\n type AggregateRequest,\n type AggregateValue,\n BaseConnector,\n type ConnectorContext,\n type CredentialsSchema,\n type FilterClause,\n type StorageHandle,\n type SyncOptions,\n type SyncResult,\n defineConfigFields,\n makeChunkedCursorGuard,\n paginateChunked,\n selectActivePhases,\n} from '@rawdash/core';\nimport { z } from 'zod';\n\nexport const configFields = defineConfigFields(\n z.object({\n accessToken: z.object({ $secret: z.string() }).meta({\n label: 'Private App access token',\n description:\n 'HubSpot private app access token with read scopes for contacts, companies, deals, and marketing email. Create one at Settings → Integrations → Private Apps.',\n placeholder: 'pat-na1-...',\n secret: true,\n }),\n resources: z\n .array(\n z.enum([\n 'contacts',\n 'companies',\n 'deals',\n 'deal_events',\n 'email_campaigns',\n 'email_stats',\n ]),\n )\n .nonempty()\n .optional()\n .meta({\n label: 'Resources',\n description:\n 'Which HubSpot resources to sync. Omit to sync all resources. The access token only needs read scopes for the resources listed here.',\n }),\n }),\n);\n\nexport interface HubSpotSettings {\n resources?: readonly HubSpotResource[];\n}\n\n// ---------------------------------------------------------------------------\n// HubSpot API types\n// ---------------------------------------------------------------------------\n\ntype HubSpotProperties = Record<string, string | null | undefined>;\n\ninterface CrmRecord {\n id: string;\n properties: HubSpotProperties;\n createdAt: string;\n updatedAt: string;\n archived?: boolean;\n}\n\ninterface CrmSearchResponse {\n total?: number;\n results: CrmRecord[];\n paging?: { next?: { after?: string } };\n}\n\ninterface CrmListResponse {\n results: DealHistoryRecord[];\n paging?: { next?: { after?: string } };\n}\n\ninterface DealHistoryEntry {\n value?: string | null;\n timestamp?: string | null;\n sourceType?: string | null;\n}\n\ninterface DealHistoryRecord {\n id: string;\n propertiesWithHistory?: {\n dealstage?: DealHistoryEntry[];\n };\n}\n\ninterface CampaignListResponse {\n campaigns: Array<{ id: number | string }>;\n hasMore?: boolean;\n offset?: number | string;\n}\n\ninterface CampaignCounters {\n sent?: number;\n delivered?: number;\n open?: number;\n click?: number;\n bounce?: number;\n unsubscribed?: number;\n}\n\ninterface CampaignDetail {\n id: number | string;\n name?: string | null;\n subject?: string | null;\n fromName?: string | null;\n type?: string | null;\n lastProcessingFinishedAt?: number | string | null;\n numIncluded?: number | null;\n counters?: CampaignCounters;\n}\n\n// ---------------------------------------------------------------------------\n// Credentials\n// ---------------------------------------------------------------------------\n\nconst hubspotCredentials = {\n accessToken: {\n description: 'HubSpot private app access token',\n auth: 'required' as const,\n },\n} satisfies CredentialsSchema;\n\ntype HubSpotCredentials = typeof hubspotCredentials;\n\n// ---------------------------------------------------------------------------\n// Sync phases + cursor\n// ---------------------------------------------------------------------------\n\nconst PHASE_ORDER = [\n 'contacts',\n 'companies',\n 'deals',\n 'deal_events',\n 'email_campaigns',\n 'email_stats',\n] as const;\n\ntype HubSpotPhase = (typeof PHASE_ORDER)[number];\n\nexport type HubSpotResource = HubSpotPhase;\n\nconst isHubSpotSyncCursor = makeChunkedCursorGuard(PHASE_ORDER);\n\nconst BASE_URL = 'https://api.hubapi.com';\nconst SEARCH_LIMIT = 100;\nconst LIST_LIMIT = 100;\n\ntype CrmObjectPhase = 'contacts' | 'companies' | 'deals';\n\n// CRM object name + property names requested per search phase.\nconst SEARCH_PROPERTIES: Record<CrmObjectPhase, readonly string[]> = {\n contacts: [\n 'email',\n 'lifecyclestage',\n 'hs_lead_status',\n 'createdate',\n 'lastmodifieddate',\n 'hubspot_owner_id',\n ],\n companies: [\n 'name',\n 'domain',\n 'industry',\n 'createdate',\n 'lifecyclestage',\n 'hs_lastmodifieddate',\n ],\n deals: [\n 'dealname',\n 'dealstage',\n 'pipeline',\n 'amount',\n 'closedate',\n 'hubspot_owner_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ],\n};\n\n// Property each CRM object stamps with its last-modified time, used both for\n// the incremental `since` filter and the entity `updated_at` fallback.\nconst MODIFIED_PROPERTY: Record<CrmObjectPhase, string> = {\n contacts: 'lastmodifieddate',\n companies: 'hs_lastmodifieddate',\n deals: 'hs_lastmodifieddate',\n};\n\nconst ENTITY_TYPE_BY_PHASE: Partial<Record<HubSpotPhase, string>> = {\n contacts: 'hubspot_contact',\n companies: 'hubspot_company',\n deals: 'hubspot_deal',\n email_campaigns: 'hubspot_email_campaign',\n};\n\nconst DEAL_STAGE_EVENT = 'hubspot_deal_stage_change';\nconst EMAIL_STATS_METRIC = 'hubspot_email_stats';\n\n// Aggregate `resource` (the widget's entity type) → CRM search object.\nconst COUNT_RESOURCE_TO_OBJECT: Record<string, CrmObjectPhase> = {\n hubspot_contact: 'contacts',\n hubspot_company: 'companies',\n hubspot_deal: 'deals',\n};\n\nconst FILTER_OP_TO_HUBSPOT: Record<string, string> = {\n eq: 'EQ',\n neq: 'NEQ',\n gt: 'GT',\n gte: 'GTE',\n lt: 'LT',\n lte: 'LTE',\n contains: 'CONTAINS_TOKEN',\n};\n\n// ---------------------------------------------------------------------------\n// Value helpers\n// ---------------------------------------------------------------------------\n\nfunction finiteNumberOrNull(value: string | null | undefined): number | null {\n if (value === null || value === undefined || value.trim() === '') {\n return null;\n }\n const n = Number(value);\n return Number.isFinite(n) ? n : null;\n}\n\nfunction counterValue(value: number | undefined): number {\n return typeof value === 'number' && Number.isFinite(value) ? value : 0;\n}\n\nfunction unsupportedAggregate(req: AggregateRequest): Error {\n return new Error(\n `HubSpot aggregate: unsupported ${req.fn} for resource=${req.resource}`,\n );\n}\n\n// ---------------------------------------------------------------------------\n// Schemas — describe the per-resource API response shape consumed by request()\n// ---------------------------------------------------------------------------\n\nconst idString = z.string().min(1);\n\nconst contactProperties = z.object({\n email: z.string().nullish(),\n lifecyclestage: z.string().nullish(),\n hs_lead_status: z.string().nullish(),\n createdate: z.string().nullish(),\n lastmodifieddate: z.string().nullish(),\n hubspot_owner_id: z.string().nullish(),\n});\n\nconst companyProperties = z.object({\n name: z.string().nullish(),\n domain: z.string().nullish(),\n industry: z.string().nullish(),\n createdate: z.string().nullish(),\n lifecyclestage: z.string().nullish(),\n hs_lastmodifieddate: z.string().nullish(),\n});\n\nconst dealProperties = z.object({\n dealname: z.string().nullish(),\n dealstage: z.string().nullish(),\n pipeline: z.string().nullish(),\n amount: z.string().nullish(),\n closedate: z.string().nullish(),\n hubspot_owner_id: z.string().nullish(),\n createdate: z.string().nullish(),\n hs_lastmodifieddate: z.string().nullish(),\n});\n\nfunction crmRecordSchema(props: z.ZodType): z.ZodType {\n return z.object({\n id: idString,\n properties: props,\n createdAt: z.string(),\n updatedAt: z.string(),\n archived: z.boolean().optional(),\n });\n}\n\nconst dealHistoryRecordSchema = z.object({\n id: idString,\n propertiesWithHistory: z\n .object({\n dealstage: z\n .array(\n z.object({\n value: z.string().nullish(),\n timestamp: z.string().nullish(),\n sourceType: z.string().nullish(),\n }),\n )\n .optional(),\n })\n .optional(),\n});\n\nconst campaignDetailSchema = z.object({\n id: z.union([z.string(), z.number()]),\n name: z.string().nullish(),\n subject: z.string().nullish(),\n fromName: z.string().nullish(),\n type: z.string().nullish(),\n lastProcessingFinishedAt: z.union([z.string(), z.number()]).nullish(),\n numIncluded: z.number().nullish(),\n counters: z\n .object({\n sent: z.number().optional(),\n delivered: z.number().optional(),\n open: z.number().optional(),\n click: z.number().optional(),\n bounce: z.number().optional(),\n unsubscribed: z.number().optional(),\n })\n .optional(),\n});\n\n// ---------------------------------------------------------------------------\n// HubSpotConnector\n// ---------------------------------------------------------------------------\n\nexport class HubSpotConnector extends BaseConnector<\n HubSpotSettings,\n HubSpotCredentials\n> {\n static readonly id = 'hubspot';\n\n static readonly schemas = {\n contacts: z.array(crmRecordSchema(contactProperties)),\n companies: z.array(crmRecordSchema(companyProperties)),\n deals: z.array(crmRecordSchema(dealProperties)),\n deal_events: z.array(dealHistoryRecordSchema),\n email_campaigns: z.array(campaignDetailSchema),\n email_stats: z.array(campaignDetailSchema),\n } as const;\n\n static create(input: unknown, ctx?: ConnectorContext): HubSpotConnector {\n const parsed = configFields.parse(input);\n return new HubSpotConnector(\n { resources: parsed.resources },\n { accessToken: parsed.accessToken },\n ctx,\n );\n }\n\n readonly id = 'hubspot';\n override readonly credentials = hubspotCredentials;\n\n private buildHeaders(): Record<string, string> {\n return {\n Authorization: `Bearer ${this.creds.accessToken}`,\n 'Content-Type': 'application/json',\n 'User-Agent': connectorUserAgent('hubspot'),\n };\n }\n\n private apiGet<T>(\n url: string,\n resource: string,\n signal?: AbortSignal,\n ): Promise<HttpResponse<T>> {\n return this.get<T>(url, {\n resource,\n headers: this.buildHeaders(),\n signal,\n });\n }\n\n private apiPost<T>(\n url: string,\n resource: string,\n body: unknown,\n signal?: AbortSignal,\n ): Promise<HttpResponse<T>> {\n return this.post<T>(url, {\n resource,\n headers: this.buildHeaders(),\n body: JSON.stringify(body),\n signal,\n });\n }\n\n // -------------------------------------------------------------------------\n // CRM search phases (contacts / companies / deals)\n // -------------------------------------------------------------------------\n\n private buildSearchBody(\n phase: CrmObjectPhase,\n after: string | null,\n options: SyncOptions,\n ): Record<string, unknown> {\n const modifiedProperty = MODIFIED_PROPERTY[phase];\n const filterGroups: unknown[] = [];\n if (options.since) {\n const sinceMs = new Date(options.since).getTime();\n if (Number.isFinite(sinceMs)) {\n filterGroups.push({\n filters: [\n {\n propertyName: modifiedProperty,\n operator: 'GTE',\n value: String(sinceMs),\n },\n ],\n });\n }\n }\n return {\n filterGroups,\n // Ascending modified-time keeps pagination stable while the `since`\n // filter trims the set upstream, so an incremental sync never scans\n // the whole object.\n sorts: [{ propertyName: modifiedProperty, direction: 'ASCENDING' }],\n properties: SEARCH_PROPERTIES[phase],\n limit: SEARCH_LIMIT,\n ...(after ? { after } : {}),\n };\n }\n\n private async fetchSearchPage(\n phase: CrmObjectPhase,\n after: string | null,\n options: SyncOptions,\n signal?: AbortSignal,\n ): Promise<{ items: unknown[]; next: string | null }> {\n const body = this.buildSearchBody(phase, after, options);\n const res = await this.apiPost<CrmSearchResponse>(\n `${BASE_URL}/crm/v3/objects/${phase}/search`,\n phase,\n body,\n signal,\n );\n return {\n items: res.body.results,\n next: res.body.paging?.next?.after ?? null,\n };\n }\n\n private async writeSearchPhase(\n storage: StorageHandle,\n phase: CrmObjectPhase,\n items: CrmRecord[],\n ): Promise<void> {\n for (const record of items) {\n const props = record.properties;\n const modifiedProperty = MODIFIED_PROPERTY[phase];\n const updatedAt =\n parseEpoch(record.updatedAt, 'iso') ??\n parseEpoch(props[modifiedProperty], 'ms') ??\n 0;\n\n let attributes: Record<string, string | number | null>;\n if (phase === 'contacts') {\n attributes = {\n email: props.email ?? null,\n lifecycleStage: props.lifecyclestage ?? null,\n leadStatus: props.hs_lead_status ?? null,\n ownerId: props.hubspot_owner_id ?? null,\n createdAt: parseEpoch(props.createdate, 'ms'),\n };\n } else if (phase === 'companies') {\n attributes = {\n name: props.name ?? null,\n domain: props.domain ?? null,\n industry: props.industry ?? null,\n lifecycleStage: props.lifecyclestage ?? null,\n createdAt: parseEpoch(props.createdate, 'ms'),\n };\n } else {\n attributes = {\n dealName: props.dealname ?? null,\n dealStage: props.dealstage ?? null,\n pipeline: props.pipeline ?? null,\n amount: finiteNumberOrNull(props.amount),\n closeDate: parseEpoch(props.closedate, 'ms'),\n ownerId: props.hubspot_owner_id ?? null,\n createdAt: parseEpoch(props.createdate, 'ms'),\n };\n }\n\n await storage.entity({\n type: ENTITY_TYPE_BY_PHASE[phase]!,\n id: record.id,\n attributes,\n updated_at: updatedAt,\n });\n }\n }\n\n // -------------------------------------------------------------------------\n // Deal stage-change events (deal property history)\n // -------------------------------------------------------------------------\n\n private async fetchDealHistoryPage(\n after: string | null,\n signal?: AbortSignal,\n ): Promise<{ items: unknown[]; next: string | null }> {\n const url = new URL(`${BASE_URL}/crm/v3/objects/deals`);\n url.searchParams.set('limit', String(LIST_LIMIT));\n url.searchParams.set('properties', 'dealstage');\n url.searchParams.set('propertiesWithHistory', 'dealstage');\n if (after) {\n url.searchParams.set('after', after);\n }\n const res = await this.apiGet<CrmListResponse>(\n url.toString(),\n 'deal_events',\n signal,\n );\n return {\n items: res.body.results,\n next: res.body.paging?.next?.after ?? null,\n };\n }\n\n private async writeDealEvents(\n storage: StorageHandle,\n items: DealHistoryRecord[],\n _options: SyncOptions,\n ): Promise<void> {\n for (const record of items) {\n const history = record.propertiesWithHistory?.dealstage ?? [];\n for (const entry of history) {\n const ts = parseEpoch(entry.timestamp, 'iso');\n if (ts === null) {\n continue;\n }\n await storage.event({\n name: DEAL_STAGE_EVENT,\n start_ts: ts,\n end_ts: null,\n attributes: {\n dealId: record.id,\n stage: entry.value ?? null,\n sourceType: entry.sourceType ?? null,\n },\n });\n }\n }\n }\n\n // -------------------------------------------------------------------------\n // Marketing email campaigns + stats (legacy email campaigns API)\n // -------------------------------------------------------------------------\n\n private async fetchCampaignDetail(\n id: number | string,\n resource: string,\n signal?: AbortSignal,\n ): Promise<CampaignDetail> {\n const res = await this.apiGet<CampaignDetail>(\n `${BASE_URL}/email/public/v1/campaigns/${id}`,\n resource,\n signal,\n );\n return res.body;\n }\n\n private async fetchCampaignsPage(\n phase: 'email_campaigns' | 'email_stats',\n after: string | null,\n signal?: AbortSignal,\n ): Promise<{ items: unknown[]; next: string | null }> {\n const url = new URL(`${BASE_URL}/email/public/v1/campaigns`);\n url.searchParams.set('limit', String(LIST_LIMIT));\n if (after) {\n url.searchParams.set('offset', after);\n }\n const listRes = await this.apiGet<CampaignListResponse>(\n url.toString(),\n `${phase}_list`,\n signal,\n );\n const { campaigns, hasMore, offset } = listRes.body;\n\n const details: CampaignDetail[] = [];\n for (const campaign of campaigns) {\n details.push(await this.fetchCampaignDetail(campaign.id, phase, signal));\n }\n\n const next =\n hasMore && offset !== undefined && offset !== null\n ? String(offset)\n : null;\n return { items: details, next };\n }\n\n private async writeEmailCampaigns(\n storage: StorageHandle,\n items: CampaignDetail[],\n ): Promise<void> {\n for (const detail of items) {\n const sentDate = parseEpoch(detail.lastProcessingFinishedAt, 'ms');\n await storage.entity({\n type: ENTITY_TYPE_BY_PHASE.email_campaigns!,\n id: String(detail.id),\n attributes: {\n name: detail.name ?? null,\n subject: detail.subject ?? null,\n fromName: detail.fromName ?? null,\n type: detail.type ?? null,\n sentDate,\n numIncluded: detail.numIncluded ?? null,\n },\n updated_at: sentDate ?? 0,\n });\n }\n }\n\n private async writeEmailStats(\n storage: StorageHandle,\n items: CampaignDetail[],\n ): Promise<void> {\n for (const detail of items) {\n const ts = parseEpoch(detail.lastProcessingFinishedAt, 'ms');\n if (ts === null) {\n continue;\n }\n const counters = detail.counters ?? {};\n const sent = counterValue(counters.sent);\n await storage.metric({\n name: EMAIL_STATS_METRIC,\n ts,\n value: sent,\n attributes: {\n campaignId: String(detail.id),\n campaignName: detail.name ?? null,\n sent,\n delivered: counterValue(counters.delivered),\n opened: counterValue(counters.open),\n clicked: counterValue(counters.click),\n bounced: counterValue(counters.bounce),\n unsubscribed: counterValue(counters.unsubscribed),\n },\n });\n }\n }\n\n // -------------------------------------------------------------------------\n // Scope clearing (idempotency)\n // -------------------------------------------------------------------------\n\n private async clearScopeOnFirstPage(\n storage: StorageHandle,\n phase: HubSpotPhase,\n isFull: boolean,\n ): Promise<void> {\n if (phase === 'deal_events') {\n // Events never upsert and the list endpoint has no `since` filter, so\n // every sync rewrites the requested window — clear in both modes.\n await storage.events([], { names: [DEAL_STAGE_EVENT] });\n return;\n }\n if (phase === 'email_stats') {\n await storage.metrics([], { names: [EMAIL_STATS_METRIC] });\n return;\n }\n // Entity phases upsert by id, so only a full backfill needs to drop stale\n // rows; incremental ticks just overwrite the records they touch.\n if (!isFull) {\n return;\n }\n const entityType = ENTITY_TYPE_BY_PHASE[phase];\n if (entityType) {\n await storage.entities([], { types: [entityType] });\n }\n }\n\n private async writePhase(\n storage: StorageHandle,\n phase: HubSpotPhase,\n items: unknown[],\n options: SyncOptions,\n ): Promise<void> {\n switch (phase) {\n case 'contacts':\n case 'companies':\n case 'deals':\n await this.writeSearchPhase(storage, phase, items as CrmRecord[]);\n return;\n case 'deal_events':\n await this.writeDealEvents(\n storage,\n items as DealHistoryRecord[],\n options,\n );\n return;\n case 'email_campaigns':\n await this.writeEmailCampaigns(storage, items as CampaignDetail[]);\n return;\n case 'email_stats':\n await this.writeEmailStats(storage, items as CampaignDetail[]);\n return;\n }\n }\n\n async sync(\n options: SyncOptions,\n storage: StorageHandle,\n signal?: AbortSignal,\n ): Promise<SyncResult> {\n const cursor = isHubSpotSyncCursor(options.cursor)\n ? options.cursor\n : undefined;\n const isFull = options.mode === 'full';\n\n const phases = selectActivePhases<HubSpotResource, HubSpotPhase>(\n (r) => r,\n PHASE_ORDER,\n this.settings.resources,\n );\n\n return paginateChunked<HubSpotPhase, string>({\n phases,\n cursor,\n signal,\n logger: this.logger,\n fetchPage: async (phase, page, sig) => {\n if (\n phase === 'contacts' ||\n phase === 'companies' ||\n phase === 'deals'\n ) {\n return this.fetchSearchPage(phase, page, options, sig);\n }\n if (phase === 'deal_events') {\n return this.fetchDealHistoryPage(page, sig);\n }\n return this.fetchCampaignsPage(phase, page, sig);\n },\n writeBatch: async (phase, items, page) => {\n if (page === null) {\n await this.clearScopeOnFirstPage(storage, phase, isFull);\n }\n await this.writePhase(storage, phase, items, options);\n },\n });\n }\n\n // -------------------------------------------------------------------------\n // Aggregates — count via the CRM Search API `total` (one request)\n // -------------------------------------------------------------------------\n\n override async aggregate(\n req: AggregateRequest,\n signal?: AbortSignal,\n ): Promise<AggregateValue> {\n if (req.fn !== 'count') {\n throw unsupportedAggregate(req);\n }\n const object = COUNT_RESOURCE_TO_OBJECT[req.resource];\n if (!object) {\n throw unsupportedAggregate(req);\n }\n const filterGroups = this.translateCountFilter(req.filter);\n const res = await this.apiPost<CrmSearchResponse>(\n `${BASE_URL}/crm/v3/objects/${object}/search`,\n object,\n { filterGroups, properties: [], limit: 1 },\n signal,\n );\n const value = res.body.total ?? 0;\n this.logger.info('aggregate', {\n fn: 'count',\n resource: req.resource,\n filter: req.filter,\n value,\n via: 'CRM search API',\n });\n return value;\n }\n\n validateCountFilter(resource: string, filter: FilterClause[]): void {\n if (!COUNT_RESOURCE_TO_OBJECT[resource]) {\n throw new Error(\n `HubSpot aggregate count: unsupported resource=${resource}`,\n );\n }\n this.translateCountFilter(filter);\n }\n\n // Translates flat AND filter conditions into HubSpot search `filterGroups`.\n // OR clauses aren't expressible alongside the rest of the group model, so\n // they throw \"unsupported\" and the runner falls back to storage rows.\n private translateCountFilter(\n filter: FilterClause[] | undefined,\n ): Array<{ filters: unknown[] }> {\n if (!filter || filter.length === 0) {\n return [];\n }\n const filters = filter.map((clause) => {\n if ('or' in clause) {\n throw new Error(\n 'HubSpot aggregate count: OR filter clauses are not supported',\n );\n }\n const operator = FILTER_OP_TO_HUBSPOT[clause.op];\n if (!operator) {\n throw new Error(\n `HubSpot aggregate count: unsupported filter operator ${clause.op}`,\n );\n }\n return {\n propertyName: clause.field,\n operator,\n value: String(clause.value),\n };\n });\n return [{ filters }];\n }\n}\n","import { HubSpotConnector } from './hubspot';\n\nexport { configFields, HubSpotConnector } from './hubspot';\nexport type { HubSpotSettings, HubSpotResource } from './hubspot';\nexport default HubSpotConnector;\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@rawdash/connector-hubspot",
3
+ "version": "0.15.0",
4
+ "description": "Rawdash connector for HubSpot — CRM contacts, companies, deals, and marketing email performance",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/rawdash/rawdash.git",
10
+ "directory": "packages/connectors/hubspot"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "exports": {
18
+ ".": {
19
+ "@rawdash/source": "./src/index.ts",
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js"
22
+ }
23
+ },
24
+ "scripts": {
25
+ "build": "tsup",
26
+ "typecheck": "tsc --noEmit",
27
+ "lint": "eslint src",
28
+ "test": "vitest run"
29
+ },
30
+ "dependencies": {
31
+ "@rawdash/core": "workspace:*",
32
+ "zod": "^4.4.3"
33
+ },
34
+ "devDependencies": {
35
+ "@rawdash/connector-shared": "workspace:*",
36
+ "@rawdash/connector-test-utils": "workspace:*",
37
+ "fast-check": "^4.8.0",
38
+ "tsup": "^8.0.0",
39
+ "typescript": "^5.7.2",
40
+ "vitest": "^4.1.4"
41
+ }
42
+ }