@rawdash/connector-hubspot 0.16.0 → 0.17.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 +117 -1
- package/dist/index.js +8 -5
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/dist/index.d.ts
CHANGED
|
@@ -28,6 +28,122 @@ type HubSpotCredentials = typeof hubspotCredentials;
|
|
|
28
28
|
declare const PHASE_ORDER: readonly ["contacts", "companies", "deals", "deal_events", "email_campaigns", "email_stats"];
|
|
29
29
|
type HubSpotPhase = (typeof PHASE_ORDER)[number];
|
|
30
30
|
type HubSpotResource = HubSpotPhase;
|
|
31
|
+
declare const hubspotResources: {
|
|
32
|
+
readonly hubspot_contact: {
|
|
33
|
+
readonly shape: "entity";
|
|
34
|
+
readonly description: "CRM contacts with email, lifecycle stage, lead status, owner, and creation time.";
|
|
35
|
+
readonly endpoint: "POST /crm/v3/objects/contacts/search";
|
|
36
|
+
readonly responses: {
|
|
37
|
+
readonly contacts: z.ZodArray<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
readonly hubspot_company: {
|
|
41
|
+
readonly shape: "entity";
|
|
42
|
+
readonly description: "CRM companies with name, domain, industry, lifecycle stage, and creation time.";
|
|
43
|
+
readonly endpoint: "POST /crm/v3/objects/companies/search";
|
|
44
|
+
readonly responses: {
|
|
45
|
+
readonly companies: z.ZodArray<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
readonly hubspot_deal: {
|
|
49
|
+
readonly shape: "entity";
|
|
50
|
+
readonly description: "CRM deals with name, stage, pipeline, amount, close date, owner, and creation time.";
|
|
51
|
+
readonly endpoint: "POST /crm/v3/objects/deals/search";
|
|
52
|
+
readonly responses: {
|
|
53
|
+
readonly deals: z.ZodArray<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
readonly hubspot_deal_stage_change: {
|
|
57
|
+
readonly shape: "event";
|
|
58
|
+
readonly description: "Deal stage-change events derived from deal property history, one event per stage transition.";
|
|
59
|
+
readonly endpoint: "GET /crm/v3/objects/deals?propertiesWithHistory=dealstage";
|
|
60
|
+
readonly responses: {
|
|
61
|
+
readonly deal_events: z.ZodArray<z.ZodObject<{
|
|
62
|
+
id: z.ZodString;
|
|
63
|
+
propertiesWithHistory: z.ZodOptional<z.ZodObject<{
|
|
64
|
+
dealstage: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
65
|
+
value: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
66
|
+
timestamp: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
67
|
+
sourceType: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
68
|
+
}, z.core.$strip>>>;
|
|
69
|
+
}, z.core.$strip>>;
|
|
70
|
+
}, z.core.$strip>>;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
readonly hubspot_email_campaign: {
|
|
74
|
+
readonly shape: "entity";
|
|
75
|
+
readonly description: "Marketing email campaigns with name, subject, sender, type, send date, and recipient count.";
|
|
76
|
+
readonly endpoint: "GET /email/public/v1/campaigns";
|
|
77
|
+
readonly responses: {
|
|
78
|
+
readonly email_campaigns: z.ZodArray<z.ZodObject<{
|
|
79
|
+
id: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
|
|
80
|
+
name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
81
|
+
subject: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
82
|
+
fromName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
83
|
+
type: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
84
|
+
lastProcessingFinishedAt: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
|
|
85
|
+
numIncluded: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
86
|
+
counters: z.ZodOptional<z.ZodObject<{
|
|
87
|
+
sent: z.ZodOptional<z.ZodNumber>;
|
|
88
|
+
delivered: z.ZodOptional<z.ZodNumber>;
|
|
89
|
+
open: z.ZodOptional<z.ZodNumber>;
|
|
90
|
+
click: z.ZodOptional<z.ZodNumber>;
|
|
91
|
+
bounce: z.ZodOptional<z.ZodNumber>;
|
|
92
|
+
unsubscribed: z.ZodOptional<z.ZodNumber>;
|
|
93
|
+
}, z.core.$strip>>;
|
|
94
|
+
}, z.core.$strip>>;
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
readonly hubspot_email_stats: {
|
|
98
|
+
readonly shape: "metric";
|
|
99
|
+
readonly description: "Per-campaign marketing email engagement stats (sent, delivered, opened, clicked, bounced, unsubscribed) timestamped at the campaign send time.";
|
|
100
|
+
readonly endpoint: "GET /email/public/v1/campaigns/{id}";
|
|
101
|
+
readonly unit: "emails";
|
|
102
|
+
readonly notes: "One sample per campaign; value is the sent count, and every counter (delivered, opened, clicked, bounced, unsubscribed) is also exposed in attributes.";
|
|
103
|
+
readonly dimensions: [{
|
|
104
|
+
readonly name: "campaignId";
|
|
105
|
+
readonly description: "HubSpot campaign id.";
|
|
106
|
+
}, {
|
|
107
|
+
readonly name: "campaignName";
|
|
108
|
+
readonly description: "Campaign name.";
|
|
109
|
+
}, {
|
|
110
|
+
readonly name: "delivered";
|
|
111
|
+
readonly description: "Number of emails delivered.";
|
|
112
|
+
}, {
|
|
113
|
+
readonly name: "opened";
|
|
114
|
+
readonly description: "Number of emails opened.";
|
|
115
|
+
}, {
|
|
116
|
+
readonly name: "clicked";
|
|
117
|
+
readonly description: "Number of emails clicked.";
|
|
118
|
+
}, {
|
|
119
|
+
readonly name: "bounced";
|
|
120
|
+
readonly description: "Number of emails bounced.";
|
|
121
|
+
}, {
|
|
122
|
+
readonly name: "unsubscribed";
|
|
123
|
+
readonly description: "Number of recipients who unsubscribed.";
|
|
124
|
+
}];
|
|
125
|
+
readonly responses: {
|
|
126
|
+
readonly email_stats: z.ZodArray<z.ZodObject<{
|
|
127
|
+
id: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
|
|
128
|
+
name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
129
|
+
subject: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
130
|
+
fromName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
131
|
+
type: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
132
|
+
lastProcessingFinishedAt: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
|
|
133
|
+
numIncluded: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
134
|
+
counters: z.ZodOptional<z.ZodObject<{
|
|
135
|
+
sent: z.ZodOptional<z.ZodNumber>;
|
|
136
|
+
delivered: z.ZodOptional<z.ZodNumber>;
|
|
137
|
+
open: z.ZodOptional<z.ZodNumber>;
|
|
138
|
+
click: z.ZodOptional<z.ZodNumber>;
|
|
139
|
+
bounce: z.ZodOptional<z.ZodNumber>;
|
|
140
|
+
unsubscribed: z.ZodOptional<z.ZodNumber>;
|
|
141
|
+
}, z.core.$strip>>;
|
|
142
|
+
}, z.core.$strip>>;
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
declare const id = "hubspot";
|
|
31
147
|
declare class HubSpotConnector extends BaseConnector<HubSpotSettings, HubSpotCredentials> {
|
|
32
148
|
static readonly id = "hubspot";
|
|
33
149
|
static readonly resources: {
|
|
@@ -224,4 +340,4 @@ declare class HubSpotConnector extends BaseConnector<HubSpotSettings, HubSpotCre
|
|
|
224
340
|
sync(options: SyncOptions, storage: StorageHandle, signal?: AbortSignal): Promise<SyncResult>;
|
|
225
341
|
}
|
|
226
342
|
|
|
227
|
-
export { HubSpotConnector, type HubSpotResource, type HubSpotSettings, configFields, HubSpotConnector as default, doc };
|
|
343
|
+
export { HubSpotConnector, type HubSpotResource, type HubSpotSettings, configFields, HubSpotConnector as default, doc, id, hubspotResources as resources };
|
package/dist/index.js
CHANGED
|
@@ -278,8 +278,9 @@ var hubspotResources = defineResources({
|
|
|
278
278
|
responses: { email_stats: campaignsSchema }
|
|
279
279
|
}
|
|
280
280
|
});
|
|
281
|
+
var id = "hubspot";
|
|
281
282
|
var HubSpotConnector = class _HubSpotConnector extends BaseConnector {
|
|
282
|
-
static id =
|
|
283
|
+
static id = id;
|
|
283
284
|
static resources = hubspotResources;
|
|
284
285
|
static schemas = schemasFromResources(hubspotResources);
|
|
285
286
|
static create(input, ctx) {
|
|
@@ -290,7 +291,7 @@ var HubSpotConnector = class _HubSpotConnector extends BaseConnector {
|
|
|
290
291
|
ctx
|
|
291
292
|
);
|
|
292
293
|
}
|
|
293
|
-
id =
|
|
294
|
+
id = id;
|
|
294
295
|
credentials = hubspotCredentials;
|
|
295
296
|
buildHeaders() {
|
|
296
297
|
return {
|
|
@@ -444,9 +445,9 @@ var HubSpotConnector = class _HubSpotConnector extends BaseConnector {
|
|
|
444
445
|
// -------------------------------------------------------------------------
|
|
445
446
|
// Marketing email campaigns + stats (legacy email campaigns API)
|
|
446
447
|
// -------------------------------------------------------------------------
|
|
447
|
-
async fetchCampaignDetail(
|
|
448
|
+
async fetchCampaignDetail(id2, resource, signal) {
|
|
448
449
|
const res = await this.apiGet(
|
|
449
|
-
`${BASE_URL}/email/public/v1/campaigns/${
|
|
450
|
+
`${BASE_URL}/email/public/v1/campaigns/${id2}`,
|
|
450
451
|
resource,
|
|
451
452
|
signal
|
|
452
453
|
);
|
|
@@ -594,6 +595,8 @@ export {
|
|
|
594
595
|
HubSpotConnector,
|
|
595
596
|
configFields,
|
|
596
597
|
index_default as default,
|
|
597
|
-
doc
|
|
598
|
+
doc,
|
|
599
|
+
id,
|
|
600
|
+
hubspotResources as resources
|
|
598
601
|
};
|
|
599
602
|
//# sourceMappingURL=index.js.map
|
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/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 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\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// ---------------------------------------------------------------------------\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\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\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\nconst 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\n// ---------------------------------------------------------------------------\n// HubSpotConnector\n// ---------------------------------------------------------------------------\n\nexport class HubSpotConnector extends BaseConnector<\n HubSpotSettings,\n HubSpotCredentials\n> {\n static readonly id = 'hubspot';\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 = '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","import { HubSpotConnector } from './hubspot';\n\nexport { configFields, doc, HubSpotConnector } from './hubspot';\nexport type { HubSpotSettings, HubSpotResource } from './hubspot';\nexport default HubSpotConnector;\n"],"mappings":"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","names":[]}
|
|
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 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\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// ---------------------------------------------------------------------------\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\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\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\n// ---------------------------------------------------------------------------\n// HubSpotConnector\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 // -------------------------------------------------------------------------\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","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,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rawdash/connector-hubspot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.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",
|
|
7
|
+
"sideEffects": false,
|
|
7
8
|
"repository": {
|
|
8
9
|
"type": "git",
|
|
9
10
|
"url": "https://github.com/rawdash/rawdash.git",
|
|
@@ -23,7 +24,7 @@
|
|
|
23
24
|
},
|
|
24
25
|
"dependencies": {
|
|
25
26
|
"zod": "^4.4.3",
|
|
26
|
-
"@rawdash/core": "0.
|
|
27
|
+
"@rawdash/core": "0.17.0"
|
|
27
28
|
},
|
|
28
29
|
"devDependencies": {
|
|
29
30
|
"fast-check": "^4.8.0",
|
|
@@ -31,7 +32,7 @@
|
|
|
31
32
|
"typescript": "^5.7.2",
|
|
32
33
|
"vitest": "^4.1.4",
|
|
33
34
|
"@rawdash/connector-shared": "0.3.0",
|
|
34
|
-
"@rawdash/connector-test-utils": "0.0.
|
|
35
|
+
"@rawdash/connector-test-utils": "0.0.4"
|
|
35
36
|
},
|
|
36
37
|
"scripts": {
|
|
37
38
|
"build": "tsup",
|