@rawdash/connector-hubspot 0.21.1 → 0.23.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/dist/index.d.ts CHANGED
@@ -31,6 +31,13 @@ type HubSpotResource = HubSpotPhase;
31
31
  declare const hubspotResources: {
32
32
  readonly hubspot_contact: {
33
33
  readonly shape: "entity";
34
+ readonly filterable: [{
35
+ readonly field: "lifecycleStage";
36
+ readonly ops: ["eq"];
37
+ }, {
38
+ readonly field: "leadStatus";
39
+ readonly ops: ["eq"];
40
+ }];
34
41
  readonly description: "CRM contacts with email, lifecycle stage, lead status, owner, and creation time.";
35
42
  readonly endpoint: "POST /crm/v3/objects/contacts/search";
36
43
  readonly responses: {
@@ -39,6 +46,13 @@ declare const hubspotResources: {
39
46
  };
40
47
  readonly hubspot_company: {
41
48
  readonly shape: "entity";
49
+ readonly filterable: [{
50
+ readonly field: "lifecycleStage";
51
+ readonly ops: ["eq"];
52
+ }, {
53
+ readonly field: "industry";
54
+ readonly ops: ["eq"];
55
+ }];
42
56
  readonly description: "CRM companies with name, domain, industry, lifecycle stage, and creation time.";
43
57
  readonly endpoint: "POST /crm/v3/objects/companies/search";
44
58
  readonly responses: {
@@ -47,6 +61,13 @@ declare const hubspotResources: {
47
61
  };
48
62
  readonly hubspot_deal: {
49
63
  readonly shape: "entity";
64
+ readonly filterable: [{
65
+ readonly field: "dealStage";
66
+ readonly ops: ["eq"];
67
+ }, {
68
+ readonly field: "pipeline";
69
+ readonly ops: ["eq"];
70
+ }];
50
71
  readonly description: "CRM deals with name, stage, pipeline, amount, close date, owner, and creation time.";
51
72
  readonly endpoint: "POST /crm/v3/objects/deals/search";
52
73
  readonly responses: {
@@ -55,6 +76,7 @@ declare const hubspotResources: {
55
76
  };
56
77
  readonly hubspot_deal_stage_change: {
57
78
  readonly shape: "event";
79
+ readonly filterable: [];
58
80
  readonly description: "Deal stage-change events derived from deal property history, one event per stage transition.";
59
81
  readonly endpoint: "GET /crm/v3/objects/deals?propertiesWithHistory=dealstage";
60
82
  readonly responses: {
@@ -72,6 +94,7 @@ declare const hubspotResources: {
72
94
  };
73
95
  readonly hubspot_email_campaign: {
74
96
  readonly shape: "entity";
97
+ readonly filterable: [];
75
98
  readonly description: "Marketing email campaigns with name, subject, sender, type, send date, and recipient count.";
76
99
  readonly endpoint: "GET /email/public/v1/campaigns";
77
100
  readonly responses: {
@@ -149,6 +172,13 @@ declare class HubSpotConnector extends BaseConnector<HubSpotSettings, HubSpotCre
149
172
  static readonly resources: {
150
173
  readonly hubspot_contact: {
151
174
  readonly shape: "entity";
175
+ readonly filterable: [{
176
+ readonly field: "lifecycleStage";
177
+ readonly ops: ["eq"];
178
+ }, {
179
+ readonly field: "leadStatus";
180
+ readonly ops: ["eq"];
181
+ }];
152
182
  readonly description: "CRM contacts with email, lifecycle stage, lead status, owner, and creation time.";
153
183
  readonly endpoint: "POST /crm/v3/objects/contacts/search";
154
184
  readonly responses: {
@@ -157,6 +187,13 @@ declare class HubSpotConnector extends BaseConnector<HubSpotSettings, HubSpotCre
157
187
  };
158
188
  readonly hubspot_company: {
159
189
  readonly shape: "entity";
190
+ readonly filterable: [{
191
+ readonly field: "lifecycleStage";
192
+ readonly ops: ["eq"];
193
+ }, {
194
+ readonly field: "industry";
195
+ readonly ops: ["eq"];
196
+ }];
160
197
  readonly description: "CRM companies with name, domain, industry, lifecycle stage, and creation time.";
161
198
  readonly endpoint: "POST /crm/v3/objects/companies/search";
162
199
  readonly responses: {
@@ -165,6 +202,13 @@ declare class HubSpotConnector extends BaseConnector<HubSpotSettings, HubSpotCre
165
202
  };
166
203
  readonly hubspot_deal: {
167
204
  readonly shape: "entity";
205
+ readonly filterable: [{
206
+ readonly field: "dealStage";
207
+ readonly ops: ["eq"];
208
+ }, {
209
+ readonly field: "pipeline";
210
+ readonly ops: ["eq"];
211
+ }];
168
212
  readonly description: "CRM deals with name, stage, pipeline, amount, close date, owner, and creation time.";
169
213
  readonly endpoint: "POST /crm/v3/objects/deals/search";
170
214
  readonly responses: {
@@ -173,6 +217,7 @@ declare class HubSpotConnector extends BaseConnector<HubSpotSettings, HubSpotCre
173
217
  };
174
218
  readonly hubspot_deal_stage_change: {
175
219
  readonly shape: "event";
220
+ readonly filterable: [];
176
221
  readonly description: "Deal stage-change events derived from deal property history, one event per stage transition.";
177
222
  readonly endpoint: "GET /crm/v3/objects/deals?propertiesWithHistory=dealstage";
178
223
  readonly responses: {
@@ -190,6 +235,7 @@ declare class HubSpotConnector extends BaseConnector<HubSpotSettings, HubSpotCre
190
235
  };
191
236
  readonly hubspot_email_campaign: {
192
237
  readonly shape: "entity";
238
+ readonly filterable: [];
193
239
  readonly description: "Marketing email campaigns with name, subject, sender, type, send date, and recipient count.";
194
240
  readonly endpoint: "GET /email/public/v1/campaigns";
195
241
  readonly responses: {
@@ -326,6 +372,7 @@ declare class HubSpotConnector extends BaseConnector<HubSpotSettings, HubSpotCre
326
372
  private buildHeaders;
327
373
  private apiGet;
328
374
  private apiPost;
375
+ private singleSpec;
329
376
  private buildSearchBody;
330
377
  private fetchSearchPage;
331
378
  private writeSearchPhase;
package/dist/index.js CHANGED
@@ -68,6 +68,7 @@ var doc = defineConnectorDoc({
68
68
  tagline: "Sync CRM contacts, companies, and deals plus deal stage-change events and marketing email campaign stats from HubSpot.",
69
69
  vendor: {
70
70
  name: "HubSpot",
71
+ domain: "hubspot.com",
71
72
  apiDocs: "https://developers.hubspot.com/docs/api/overview",
72
73
  website: "https://www.hubspot.com"
73
74
  },
@@ -144,6 +145,11 @@ var ENTITY_TYPE_BY_PHASE = {
144
145
  deals: "hubspot_deal",
145
146
  email_campaigns: "hubspot_email_campaign"
146
147
  };
148
+ var SEARCH_PUSH_FIELDS = {
149
+ contacts: { lifecycleStage: "lifecyclestage", leadStatus: "hs_lead_status" },
150
+ companies: { lifecycleStage: "lifecyclestage", industry: "industry" },
151
+ deals: { dealStage: "dealstage", pipeline: "pipeline" }
152
+ };
147
153
  var DEAL_STAGE_EVENT = "hubspot_deal_stage_change";
148
154
  var EMAIL_STATS_METRIC = "hubspot_email_stats";
149
155
  function finiteNumberOrNull(value) {
@@ -229,30 +235,44 @@ var campaignsSchema = z.array(campaignDetailSchema);
229
235
  var hubspotResources = defineResources({
230
236
  hubspot_contact: {
231
237
  shape: "entity",
238
+ filterable: [
239
+ { field: "lifecycleStage", ops: ["eq"] },
240
+ { field: "leadStatus", ops: ["eq"] }
241
+ ],
232
242
  description: "CRM contacts with email, lifecycle stage, lead status, owner, and creation time.",
233
243
  endpoint: "POST /crm/v3/objects/contacts/search",
234
244
  responses: { contacts: contactsSchema }
235
245
  },
236
246
  hubspot_company: {
237
247
  shape: "entity",
248
+ filterable: [
249
+ { field: "lifecycleStage", ops: ["eq"] },
250
+ { field: "industry", ops: ["eq"] }
251
+ ],
238
252
  description: "CRM companies with name, domain, industry, lifecycle stage, and creation time.",
239
253
  endpoint: "POST /crm/v3/objects/companies/search",
240
254
  responses: { companies: companiesSchema }
241
255
  },
242
256
  hubspot_deal: {
243
257
  shape: "entity",
258
+ filterable: [
259
+ { field: "dealStage", ops: ["eq"] },
260
+ { field: "pipeline", ops: ["eq"] }
261
+ ],
244
262
  description: "CRM deals with name, stage, pipeline, amount, close date, owner, and creation time.",
245
263
  endpoint: "POST /crm/v3/objects/deals/search",
246
264
  responses: { deals: dealsSchema }
247
265
  },
248
266
  hubspot_deal_stage_change: {
249
267
  shape: "event",
268
+ filterable: [],
250
269
  description: "Deal stage-change events derived from deal property history, one event per stage transition.",
251
270
  endpoint: "GET /crm/v3/objects/deals?propertiesWithHistory=dealstage",
252
271
  responses: { deal_events: dealEventsSchema }
253
272
  },
254
273
  hubspot_email_campaign: {
255
274
  shape: "entity",
275
+ filterable: [],
256
276
  description: "Marketing email campaigns with name, subject, sender, type, send date, and recipient count.",
257
277
  endpoint: "GET /email/public/v1/campaigns",
258
278
  responses: { email_campaigns: campaignsSchema }
@@ -279,6 +299,17 @@ var hubspotResources = defineResources({
279
299
  }
280
300
  });
281
301
  var id = "hubspot";
302
+ function pushableEq(filter, field) {
303
+ if (!filter) {
304
+ return null;
305
+ }
306
+ for (const clause of filter) {
307
+ if ("field" in clause && clause.field === field && clause.op === "eq" && typeof clause.value === "string") {
308
+ return clause.value;
309
+ }
310
+ }
311
+ return null;
312
+ }
282
313
  var HubSpotConnector = class _HubSpotConnector extends BaseConnector {
283
314
  static id = id;
284
315
  static resources = hubspotResources;
@@ -315,23 +346,37 @@ var HubSpotConnector = class _HubSpotConnector extends BaseConnector {
315
346
  signal
316
347
  });
317
348
  }
349
+ singleSpec(options, resource) {
350
+ const specs = options.fetchSpecs?.[resource];
351
+ return specs && specs.length === 1 ? specs[0] : void 0;
352
+ }
318
353
  buildSearchBody(phase, after, options) {
319
354
  const modifiedProperty = MODIFIED_PROPERTY[phase];
320
- const filterGroups = [];
355
+ const filters = [];
321
356
  if (options.since) {
322
357
  const sinceMs = new Date(options.since).getTime();
323
358
  if (Number.isFinite(sinceMs)) {
324
- filterGroups.push({
325
- filters: [
326
- {
327
- propertyName: modifiedProperty,
328
- operator: "GTE",
329
- value: String(sinceMs)
330
- }
331
- ]
359
+ filters.push({
360
+ propertyName: modifiedProperty,
361
+ operator: "GTE",
362
+ value: String(sinceMs)
332
363
  });
333
364
  }
334
365
  }
366
+ const pushMap = SEARCH_PUSH_FIELDS[phase];
367
+ if (pushMap) {
368
+ const filter = this.singleSpec(
369
+ options,
370
+ ENTITY_TYPE_BY_PHASE[phase]
371
+ )?.filter;
372
+ for (const [ourField, propertyName] of Object.entries(pushMap)) {
373
+ const value = pushableEq(filter, ourField);
374
+ if (value !== null) {
375
+ filters.push({ propertyName, operator: "EQ", value });
376
+ }
377
+ }
378
+ }
379
+ const filterGroups = filters.length > 0 ? [{ filters }] : [];
335
380
  return {
336
381
  filterGroups,
337
382
  sorts: [{ propertyName: modifiedProperty, direction: "ASCENDING" }],
package/dist/index.js.map CHANGED
@@ -1 +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/map-concurrent.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 async function mapWithConcurrency<T, R>(\n items: readonly T[],\n concurrency: number,\n fn: (item: T, index: number) => Promise<R>,\n): Promise<R[]> {\n const results = new Array<R>(items.length);\n if (items.length === 0) {\n return results;\n }\n const normalized = Number.isFinite(concurrency) ? Math.floor(concurrency) : 1;\n const limit = Math.max(1, Math.min(normalized, items.length));\n let next = 0;\n let failed = false;\n\n async function worker(): Promise<void> {\n while (!failed) {\n const i = next++;\n if (i >= items.length) {\n return;\n }\n try {\n results[i] = await fn(items[i]!, i);\n } catch (err) {\n failed = true;\n throw err;\n }\n }\n }\n\n const workers: Promise<void>[] = [];\n for (let w = 0; w < limit; w++) {\n workers.push(worker());\n }\n await Promise.all(workers);\n return results;\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 BaseConnector,\n type ConnectorContext,\n type ConnectorDoc,\n type CredentialsSchema,\n type StorageHandle,\n type SyncOptions,\n type SyncResult,\n defineConfigFields,\n defineConnectorDoc,\n defineResources,\n makeChunkedCursorGuard,\n paginateChunked,\n schemasFromResources,\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 const doc: ConnectorDoc = defineConnectorDoc({\n displayName: 'HubSpot',\n category: 'sales',\n brandColor: '#FF7A59',\n tagline:\n 'Sync CRM contacts, companies, and deals plus deal stage-change events and marketing email campaign stats from HubSpot.',\n vendor: {\n name: 'HubSpot',\n apiDocs: 'https://developers.hubspot.com/docs/api/overview',\n website: 'https://www.hubspot.com',\n },\n auth: {\n summary:\n 'A HubSpot private app access token with read scopes for the resources you sync (contacts, companies, deals, and marketing email).',\n setup: [\n 'In HubSpot, go to Settings → Integrations → Private Apps and create a private app.',\n 'Grant read scopes for the resources you intend to sync (CRM contacts, companies, deals, and marketing email).',\n 'Copy the generated access token (starts with `pat-`).',\n 'Store it as a secret and reference it from the connector config as `accessToken: secret(\"HUBSPOT_ACCESS_TOKEN\")`.',\n ],\n },\n rateLimit:\n 'HubSpot allows 100 requests / 10s; the Search API caps results at 10,000 per query.',\n limitations: [\n 'Deal stage-change events are rewritten on every sync because the deal list endpoint has no incremental `since` filter.',\n 'Marketing email campaign data comes from the legacy email campaigns API and is only available for marketing emails.',\n 'Very large CRM portfolios may not backfill in full because the Search API caps at 10,000 results per query.',\n ],\n});\n\nexport interface HubSpotSettings {\n resources?: readonly HubSpotResource[];\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\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\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\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\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\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\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\nconst contactsSchema = z.array(crmRecordSchema(contactProperties));\nconst companiesSchema = z.array(crmRecordSchema(companyProperties));\nconst dealsSchema = z.array(crmRecordSchema(dealProperties));\nconst dealEventsSchema = z.array(dealHistoryRecordSchema);\nconst campaignsSchema = z.array(campaignDetailSchema);\n\nexport const hubspotResources = defineResources({\n hubspot_contact: {\n shape: 'entity',\n description:\n 'CRM contacts with email, lifecycle stage, lead status, owner, and creation time.',\n endpoint: 'POST /crm/v3/objects/contacts/search',\n responses: { contacts: contactsSchema },\n },\n hubspot_company: {\n shape: 'entity',\n description:\n 'CRM companies with name, domain, industry, lifecycle stage, and creation time.',\n endpoint: 'POST /crm/v3/objects/companies/search',\n responses: { companies: companiesSchema },\n },\n hubspot_deal: {\n shape: 'entity',\n description:\n 'CRM deals with name, stage, pipeline, amount, close date, owner, and creation time.',\n endpoint: 'POST /crm/v3/objects/deals/search',\n responses: { deals: dealsSchema },\n },\n hubspot_deal_stage_change: {\n shape: 'event',\n description:\n 'Deal stage-change events derived from deal property history, one event per stage transition.',\n endpoint: 'GET /crm/v3/objects/deals?propertiesWithHistory=dealstage',\n responses: { deal_events: dealEventsSchema },\n },\n hubspot_email_campaign: {\n shape: 'entity',\n description:\n 'Marketing email campaigns with name, subject, sender, type, send date, and recipient count.',\n endpoint: 'GET /email/public/v1/campaigns',\n responses: { email_campaigns: campaignsSchema },\n },\n hubspot_email_stats: {\n shape: 'metric',\n description:\n 'Per-campaign marketing email engagement stats (sent, delivered, opened, clicked, bounced, unsubscribed) timestamped at the campaign send time.',\n endpoint: 'GET /email/public/v1/campaigns/{id}',\n unit: 'emails',\n notes:\n 'One sample per campaign; value is the sent count, and every counter (delivered, opened, clicked, bounced, unsubscribed) is also exposed in attributes.',\n dimensions: [\n { name: 'campaignId', description: 'HubSpot campaign id.' },\n { name: 'campaignName', description: 'Campaign name.' },\n { name: 'delivered', description: 'Number of emails delivered.' },\n { name: 'opened', description: 'Number of emails opened.' },\n { name: 'clicked', description: 'Number of emails clicked.' },\n { name: 'bounced', description: 'Number of emails bounced.' },\n {\n name: 'unsubscribed',\n description: 'Number of recipients who unsubscribed.',\n },\n ],\n responses: { email_stats: campaignsSchema },\n },\n});\n\nexport const id = 'hubspot';\n\nexport class HubSpotConnector extends BaseConnector<\n HubSpotSettings,\n HubSpotCredentials\n> {\n static readonly id = id;\n\n static readonly resources = hubspotResources;\n\n static readonly schemas = schemasFromResources(hubspotResources);\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 = id;\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 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 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 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 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 private async clearScopeOnFirstPage(\n storage: StorageHandle,\n phase: HubSpotPhase,\n isFull: boolean,\n ): Promise<void> {\n if (phase === 'deal_events') {\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 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","import { HubSpotConnector } from './hubspot';\n\nexport {\n configFields,\n doc,\n HubSpotConnector,\n hubspotResources as resources,\n id,\n} from './hubspot';\nexport type { HubSpotSettings, HubSpotResource } from './hubspot';\nexport default HubSpotConnector;\n"],"mappings":"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","names":["id"]}
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/map-concurrent.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 async function mapWithConcurrency<T, R>(\n items: readonly T[],\n concurrency: number,\n fn: (item: T, index: number) => Promise<R>,\n): Promise<R[]> {\n const results = new Array<R>(items.length);\n if (items.length === 0) {\n return results;\n }\n const normalized = Number.isFinite(concurrency) ? Math.floor(concurrency) : 1;\n const limit = Math.max(1, Math.min(normalized, items.length));\n let next = 0;\n let failed = false;\n\n async function worker(): Promise<void> {\n while (!failed) {\n const i = next++;\n if (i >= items.length) {\n return;\n }\n try {\n results[i] = await fn(items[i]!, i);\n } catch (err) {\n failed = true;\n throw err;\n }\n }\n }\n\n const workers: Promise<void>[] = [];\n for (let w = 0; w < limit; w++) {\n workers.push(worker());\n }\n await Promise.all(workers);\n return results;\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 BaseConnector,\n type ConnectorContext,\n type ConnectorDoc,\n type CredentialsSchema,\n type FetchSpec,\n type FilterClause,\n type StorageHandle,\n type SyncOptions,\n type SyncResult,\n defineConfigFields,\n defineConnectorDoc,\n defineResources,\n makeChunkedCursorGuard,\n paginateChunked,\n schemasFromResources,\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 const doc: ConnectorDoc = defineConnectorDoc({\n displayName: 'HubSpot',\n category: 'sales',\n brandColor: '#FF7A59',\n tagline:\n 'Sync CRM contacts, companies, and deals plus deal stage-change events and marketing email campaign stats from HubSpot.',\n vendor: {\n name: 'HubSpot',\n domain: 'hubspot.com',\n apiDocs: 'https://developers.hubspot.com/docs/api/overview',\n website: 'https://www.hubspot.com',\n },\n auth: {\n summary:\n 'A HubSpot private app access token with read scopes for the resources you sync (contacts, companies, deals, and marketing email).',\n setup: [\n 'In HubSpot, go to Settings → Integrations → Private Apps and create a private app.',\n 'Grant read scopes for the resources you intend to sync (CRM contacts, companies, deals, and marketing email).',\n 'Copy the generated access token (starts with `pat-`).',\n 'Store it as a secret and reference it from the connector config as `accessToken: secret(\"HUBSPOT_ACCESS_TOKEN\")`.',\n ],\n },\n rateLimit:\n 'HubSpot allows 100 requests / 10s; the Search API caps results at 10,000 per query.',\n limitations: [\n 'Deal stage-change events are rewritten on every sync because the deal list endpoint has no incremental `since` filter.',\n 'Marketing email campaign data comes from the legacy email campaigns API and is only available for marketing emails.',\n 'Very large CRM portfolios may not backfill in full because the Search API caps at 10,000 results per query.',\n ],\n});\n\nexport interface HubSpotSettings {\n resources?: readonly HubSpotResource[];\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\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\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\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\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 SEARCH_PUSH_FIELDS: Partial<\n Record<CrmObjectPhase, Record<string, string>>\n> = {\n contacts: { lifecycleStage: 'lifecyclestage', leadStatus: 'hs_lead_status' },\n companies: { lifecycleStage: 'lifecyclestage', industry: 'industry' },\n deals: { dealStage: 'dealstage', pipeline: 'pipeline' },\n};\n\nconst DEAL_STAGE_EVENT = 'hubspot_deal_stage_change';\nconst EMAIL_STATS_METRIC = 'hubspot_email_stats';\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\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\nconst contactsSchema = z.array(crmRecordSchema(contactProperties));\nconst companiesSchema = z.array(crmRecordSchema(companyProperties));\nconst dealsSchema = z.array(crmRecordSchema(dealProperties));\nconst dealEventsSchema = z.array(dealHistoryRecordSchema);\nconst campaignsSchema = z.array(campaignDetailSchema);\n\nexport const hubspotResources = defineResources({\n hubspot_contact: {\n shape: 'entity',\n filterable: [\n { field: 'lifecycleStage', ops: ['eq'] },\n { field: 'leadStatus', ops: ['eq'] },\n ],\n description:\n 'CRM contacts with email, lifecycle stage, lead status, owner, and creation time.',\n endpoint: 'POST /crm/v3/objects/contacts/search',\n responses: { contacts: contactsSchema },\n },\n hubspot_company: {\n shape: 'entity',\n filterable: [\n { field: 'lifecycleStage', ops: ['eq'] },\n { field: 'industry', ops: ['eq'] },\n ],\n description:\n 'CRM companies with name, domain, industry, lifecycle stage, and creation time.',\n endpoint: 'POST /crm/v3/objects/companies/search',\n responses: { companies: companiesSchema },\n },\n hubspot_deal: {\n shape: 'entity',\n filterable: [\n { field: 'dealStage', ops: ['eq'] },\n { field: 'pipeline', ops: ['eq'] },\n ],\n description:\n 'CRM deals with name, stage, pipeline, amount, close date, owner, and creation time.',\n endpoint: 'POST /crm/v3/objects/deals/search',\n responses: { deals: dealsSchema },\n },\n hubspot_deal_stage_change: {\n shape: 'event',\n filterable: [],\n description:\n 'Deal stage-change events derived from deal property history, one event per stage transition.',\n endpoint: 'GET /crm/v3/objects/deals?propertiesWithHistory=dealstage',\n responses: { deal_events: dealEventsSchema },\n },\n hubspot_email_campaign: {\n shape: 'entity',\n filterable: [],\n description:\n 'Marketing email campaigns with name, subject, sender, type, send date, and recipient count.',\n endpoint: 'GET /email/public/v1/campaigns',\n responses: { email_campaigns: campaignsSchema },\n },\n hubspot_email_stats: {\n shape: 'metric',\n description:\n 'Per-campaign marketing email engagement stats (sent, delivered, opened, clicked, bounced, unsubscribed) timestamped at the campaign send time.',\n endpoint: 'GET /email/public/v1/campaigns/{id}',\n unit: 'emails',\n notes:\n 'One sample per campaign; value is the sent count, and every counter (delivered, opened, clicked, bounced, unsubscribed) is also exposed in attributes.',\n dimensions: [\n { name: 'campaignId', description: 'HubSpot campaign id.' },\n { name: 'campaignName', description: 'Campaign name.' },\n { name: 'delivered', description: 'Number of emails delivered.' },\n { name: 'opened', description: 'Number of emails opened.' },\n { name: 'clicked', description: 'Number of emails clicked.' },\n { name: 'bounced', description: 'Number of emails bounced.' },\n {\n name: 'unsubscribed',\n description: 'Number of recipients who unsubscribed.',\n },\n ],\n responses: { email_stats: campaignsSchema },\n },\n});\n\nexport const id = 'hubspot';\n\nfunction pushableEq(\n filter: FilterClause[] | undefined,\n field: string,\n): string | null {\n if (!filter) {\n return null;\n }\n for (const clause of filter) {\n if (\n 'field' in clause &&\n clause.field === field &&\n clause.op === 'eq' &&\n typeof clause.value === 'string'\n ) {\n return clause.value;\n }\n }\n return null;\n}\n\nexport class HubSpotConnector extends BaseConnector<\n HubSpotSettings,\n HubSpotCredentials\n> {\n static readonly id = id;\n\n static readonly resources = hubspotResources;\n\n static readonly schemas = schemasFromResources(hubspotResources);\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 = id;\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 private singleSpec(\n options: SyncOptions,\n resource: string,\n ): FetchSpec | undefined {\n const specs = options.fetchSpecs?.[resource];\n return specs && specs.length === 1 ? specs[0] : undefined;\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 filters: unknown[] = [];\n if (options.since) {\n const sinceMs = new Date(options.since).getTime();\n if (Number.isFinite(sinceMs)) {\n filters.push({\n propertyName: modifiedProperty,\n operator: 'GTE',\n value: String(sinceMs),\n });\n }\n }\n const pushMap = SEARCH_PUSH_FIELDS[phase];\n if (pushMap) {\n const filter = this.singleSpec(\n options,\n ENTITY_TYPE_BY_PHASE[phase]!,\n )?.filter;\n for (const [ourField, propertyName] of Object.entries(pushMap)) {\n const value = pushableEq(filter, ourField);\n if (value !== null) {\n filters.push({ propertyName, operator: 'EQ', value });\n }\n }\n }\n const filterGroups = filters.length > 0 ? [{ filters }] : [];\n return {\n filterGroups,\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 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 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 private async clearScopeOnFirstPage(\n storage: StorageHandle,\n phase: HubSpotPhase,\n isFull: boolean,\n ): Promise<void> {\n if (phase === 'deal_events') {\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 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","import { HubSpotConnector } from './hubspot';\n\nexport {\n configFields,\n doc,\n HubSpotConnector,\n hubspotResources as resources,\n id,\n} from './hubspot';\nexport type { HubSpotSettings, HubSpotResource } from './hubspot';\nexport default HubSpotConnector;\n"],"mappings":"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","names":["id"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rawdash/connector-hubspot",
3
- "version": "0.21.1",
3
+ "version": "0.23.0",
4
4
  "description": "Rawdash connector for HubSpot — CRM contacts, companies, deals, and marketing email performance",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -24,15 +24,15 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "zod": "^4.4.3",
27
- "@rawdash/core": "0.21.1"
27
+ "@rawdash/core": "0.23.0"
28
28
  },
29
29
  "devDependencies": {
30
30
  "fast-check": "^4.8.0",
31
31
  "tsup": "^8.0.0",
32
32
  "typescript": "^5.7.2",
33
33
  "vitest": "^4.1.4",
34
- "@rawdash/connector-test-utils": "0.0.9",
35
- "@rawdash/connector-shared": "0.3.1"
34
+ "@rawdash/connector-shared": "0.3.1",
35
+ "@rawdash/connector-test-utils": "0.0.10"
36
36
  },
37
37
  "scripts": {
38
38
  "build": "tsup",