@rawdash/connector-salesforce 0.22.0 → 0.24.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 +64 -0
- package/dist/index.js +73 -7
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -46,6 +46,11 @@ type SalesforceResource = SalesforcePhase;
|
|
|
46
46
|
declare const salesforceResources: {
|
|
47
47
|
readonly salesforce_user: {
|
|
48
48
|
readonly shape: "entity";
|
|
49
|
+
readonly filterable: [{
|
|
50
|
+
readonly field: "isActive";
|
|
51
|
+
readonly ops: ["eq"];
|
|
52
|
+
readonly values: ["true", "false"];
|
|
53
|
+
}];
|
|
49
54
|
readonly description: "Salesforce users, keyed by user id, with name, email, and active state. Used to attribute opportunities, accounts, and stage changes to owners.";
|
|
50
55
|
readonly endpoint: "GET /services/data/v{version}/query (SOQL: FROM User)";
|
|
51
56
|
readonly notes: "Users are backfilled in full on every run; the table is small.";
|
|
@@ -74,6 +79,10 @@ declare const salesforceResources: {
|
|
|
74
79
|
};
|
|
75
80
|
readonly salesforce_account: {
|
|
76
81
|
readonly shape: "entity";
|
|
82
|
+
readonly filterable: [{
|
|
83
|
+
readonly field: "industry";
|
|
84
|
+
readonly ops: ["eq"];
|
|
85
|
+
}];
|
|
77
86
|
readonly description: "Accounts (companies), keyed by account id, with industry, annual revenue, owner, and creation time.";
|
|
78
87
|
readonly endpoint: "GET /services/data/v{version}/query (SOQL: FROM Account)";
|
|
79
88
|
readonly notes: "Upserts by id; incremental syncs filter on LastModifiedDate.";
|
|
@@ -107,6 +116,13 @@ declare const salesforceResources: {
|
|
|
107
116
|
};
|
|
108
117
|
readonly salesforce_lead: {
|
|
109
118
|
readonly shape: "entity";
|
|
119
|
+
readonly filterable: [{
|
|
120
|
+
readonly field: "status";
|
|
121
|
+
readonly ops: ["eq"];
|
|
122
|
+
}, {
|
|
123
|
+
readonly field: "source";
|
|
124
|
+
readonly ops: ["eq"];
|
|
125
|
+
}];
|
|
110
126
|
readonly description: "Leads, keyed by lead id, with email, status, source, and conversion time.";
|
|
111
127
|
readonly endpoint: "GET /services/data/v{version}/query (SOQL: FROM Lead)";
|
|
112
128
|
readonly notes: "Upserts by id; incremental syncs filter on LastModifiedDate.";
|
|
@@ -140,6 +156,21 @@ declare const salesforceResources: {
|
|
|
140
156
|
};
|
|
141
157
|
readonly salesforce_opportunity: {
|
|
142
158
|
readonly shape: "entity";
|
|
159
|
+
readonly filterable: [{
|
|
160
|
+
readonly field: "stage";
|
|
161
|
+
readonly ops: ["eq"];
|
|
162
|
+
}, {
|
|
163
|
+
readonly field: "isClosed";
|
|
164
|
+
readonly ops: ["eq"];
|
|
165
|
+
readonly values: ["true", "false"];
|
|
166
|
+
}, {
|
|
167
|
+
readonly field: "isWon";
|
|
168
|
+
readonly ops: ["eq"];
|
|
169
|
+
readonly values: ["true", "false"];
|
|
170
|
+
}, {
|
|
171
|
+
readonly field: "forecastCategory";
|
|
172
|
+
readonly ops: ["eq"];
|
|
173
|
+
}];
|
|
143
174
|
readonly description: "Opportunities, keyed by opportunity id, with stage, amount, close date, owner, probability, forecast category, and closed/won flags.";
|
|
144
175
|
readonly endpoint: "GET /services/data/v{version}/query (SOQL: FROM Opportunity)";
|
|
145
176
|
readonly notes: "Upserts by id; incremental syncs filter on LastModifiedDate.";
|
|
@@ -193,6 +224,7 @@ declare const salesforceResources: {
|
|
|
193
224
|
};
|
|
194
225
|
readonly salesforce_opportunity_stage_change: {
|
|
195
226
|
readonly shape: "event";
|
|
227
|
+
readonly filterable: [];
|
|
196
228
|
readonly description: "Opportunity stage transitions derived from OpportunityFieldHistory rows where Field = StageName. One event per transition, timestamped at the change CreatedDate.";
|
|
197
229
|
readonly endpoint: "GET /services/data/v{version}/query (SOQL: FROM OpportunityFieldHistory WHERE Field = 'StageName')";
|
|
198
230
|
readonly notes: "Stage-change events are immutable; their scope is only cleared on a full sync so an incremental window does not drop history outside its range.";
|
|
@@ -231,6 +263,11 @@ declare class SalesforceConnector extends BaseConnector<SalesforceSettings, Sale
|
|
|
231
263
|
static readonly resources: {
|
|
232
264
|
readonly salesforce_user: {
|
|
233
265
|
readonly shape: "entity";
|
|
266
|
+
readonly filterable: [{
|
|
267
|
+
readonly field: "isActive";
|
|
268
|
+
readonly ops: ["eq"];
|
|
269
|
+
readonly values: ["true", "false"];
|
|
270
|
+
}];
|
|
234
271
|
readonly description: "Salesforce users, keyed by user id, with name, email, and active state. Used to attribute opportunities, accounts, and stage changes to owners.";
|
|
235
272
|
readonly endpoint: "GET /services/data/v{version}/query (SOQL: FROM User)";
|
|
236
273
|
readonly notes: "Users are backfilled in full on every run; the table is small.";
|
|
@@ -259,6 +296,10 @@ declare class SalesforceConnector extends BaseConnector<SalesforceSettings, Sale
|
|
|
259
296
|
};
|
|
260
297
|
readonly salesforce_account: {
|
|
261
298
|
readonly shape: "entity";
|
|
299
|
+
readonly filterable: [{
|
|
300
|
+
readonly field: "industry";
|
|
301
|
+
readonly ops: ["eq"];
|
|
302
|
+
}];
|
|
262
303
|
readonly description: "Accounts (companies), keyed by account id, with industry, annual revenue, owner, and creation time.";
|
|
263
304
|
readonly endpoint: "GET /services/data/v{version}/query (SOQL: FROM Account)";
|
|
264
305
|
readonly notes: "Upserts by id; incremental syncs filter on LastModifiedDate.";
|
|
@@ -292,6 +333,13 @@ declare class SalesforceConnector extends BaseConnector<SalesforceSettings, Sale
|
|
|
292
333
|
};
|
|
293
334
|
readonly salesforce_lead: {
|
|
294
335
|
readonly shape: "entity";
|
|
336
|
+
readonly filterable: [{
|
|
337
|
+
readonly field: "status";
|
|
338
|
+
readonly ops: ["eq"];
|
|
339
|
+
}, {
|
|
340
|
+
readonly field: "source";
|
|
341
|
+
readonly ops: ["eq"];
|
|
342
|
+
}];
|
|
295
343
|
readonly description: "Leads, keyed by lead id, with email, status, source, and conversion time.";
|
|
296
344
|
readonly endpoint: "GET /services/data/v{version}/query (SOQL: FROM Lead)";
|
|
297
345
|
readonly notes: "Upserts by id; incremental syncs filter on LastModifiedDate.";
|
|
@@ -325,6 +373,21 @@ declare class SalesforceConnector extends BaseConnector<SalesforceSettings, Sale
|
|
|
325
373
|
};
|
|
326
374
|
readonly salesforce_opportunity: {
|
|
327
375
|
readonly shape: "entity";
|
|
376
|
+
readonly filterable: [{
|
|
377
|
+
readonly field: "stage";
|
|
378
|
+
readonly ops: ["eq"];
|
|
379
|
+
}, {
|
|
380
|
+
readonly field: "isClosed";
|
|
381
|
+
readonly ops: ["eq"];
|
|
382
|
+
readonly values: ["true", "false"];
|
|
383
|
+
}, {
|
|
384
|
+
readonly field: "isWon";
|
|
385
|
+
readonly ops: ["eq"];
|
|
386
|
+
readonly values: ["true", "false"];
|
|
387
|
+
}, {
|
|
388
|
+
readonly field: "forecastCategory";
|
|
389
|
+
readonly ops: ["eq"];
|
|
390
|
+
}];
|
|
328
391
|
readonly description: "Opportunities, keyed by opportunity id, with stage, amount, close date, owner, probability, forecast category, and closed/won flags.";
|
|
329
392
|
readonly endpoint: "GET /services/data/v{version}/query (SOQL: FROM Opportunity)";
|
|
330
393
|
readonly notes: "Upserts by id; incremental syncs filter on LastModifiedDate.";
|
|
@@ -378,6 +441,7 @@ declare class SalesforceConnector extends BaseConnector<SalesforceSettings, Sale
|
|
|
378
441
|
};
|
|
379
442
|
readonly salesforce_opportunity_stage_change: {
|
|
380
443
|
readonly shape: "event";
|
|
444
|
+
readonly filterable: [];
|
|
381
445
|
readonly description: "Opportunity stage transitions derived from OpportunityFieldHistory rows where Field = StageName. One event per transition, timestamped at the change CreatedDate.";
|
|
382
446
|
readonly endpoint: "GET /services/data/v{version}/query (SOQL: FROM OpportunityFieldHistory WHERE Field = 'StageName')";
|
|
383
447
|
readonly notes: "Stage-change events are immutable; their scope is only cleared on a full sync so an incremental window does not drop history outside its range.";
|
package/dist/index.js
CHANGED
|
@@ -175,6 +175,7 @@ var fieldHistorySchema = z.object({
|
|
|
175
175
|
var salesforceResources = defineResources({
|
|
176
176
|
salesforce_user: {
|
|
177
177
|
shape: "entity",
|
|
178
|
+
filterable: [{ field: "isActive", ops: ["eq"], values: ["true", "false"] }],
|
|
178
179
|
description: "Salesforce users, keyed by user id, with name, email, and active state. Used to attribute opportunities, accounts, and stage changes to owners.",
|
|
179
180
|
endpoint: "GET /services/data/v{version}/query (SOQL: FROM User)",
|
|
180
181
|
notes: "Users are backfilled in full on every run; the table is small.",
|
|
@@ -190,6 +191,7 @@ var salesforceResources = defineResources({
|
|
|
190
191
|
},
|
|
191
192
|
salesforce_account: {
|
|
192
193
|
shape: "entity",
|
|
194
|
+
filterable: [{ field: "industry", ops: ["eq"] }],
|
|
193
195
|
description: "Accounts (companies), keyed by account id, with industry, annual revenue, owner, and creation time.",
|
|
194
196
|
endpoint: "GET /services/data/v{version}/query (SOQL: FROM Account)",
|
|
195
197
|
notes: "Upserts by id; incremental syncs filter on LastModifiedDate.",
|
|
@@ -210,6 +212,10 @@ var salesforceResources = defineResources({
|
|
|
210
212
|
},
|
|
211
213
|
salesforce_lead: {
|
|
212
214
|
shape: "entity",
|
|
215
|
+
filterable: [
|
|
216
|
+
{ field: "status", ops: ["eq"] },
|
|
217
|
+
{ field: "source", ops: ["eq"] }
|
|
218
|
+
],
|
|
213
219
|
description: "Leads, keyed by lead id, with email, status, source, and conversion time.",
|
|
214
220
|
endpoint: "GET /services/data/v{version}/query (SOQL: FROM Lead)",
|
|
215
221
|
notes: "Upserts by id; incremental syncs filter on LastModifiedDate.",
|
|
@@ -227,6 +233,12 @@ var salesforceResources = defineResources({
|
|
|
227
233
|
},
|
|
228
234
|
salesforce_opportunity: {
|
|
229
235
|
shape: "entity",
|
|
236
|
+
filterable: [
|
|
237
|
+
{ field: "stage", ops: ["eq"] },
|
|
238
|
+
{ field: "isClosed", ops: ["eq"], values: ["true", "false"] },
|
|
239
|
+
{ field: "isWon", ops: ["eq"], values: ["true", "false"] },
|
|
240
|
+
{ field: "forecastCategory", ops: ["eq"] }
|
|
241
|
+
],
|
|
230
242
|
description: "Opportunities, keyed by opportunity id, with stage, amount, close date, owner, probability, forecast category, and closed/won flags.",
|
|
231
243
|
endpoint: "GET /services/data/v{version}/query (SOQL: FROM Opportunity)",
|
|
232
244
|
notes: "Upserts by id; incremental syncs filter on LastModifiedDate.",
|
|
@@ -252,6 +264,7 @@ var salesforceResources = defineResources({
|
|
|
252
264
|
},
|
|
253
265
|
salesforce_opportunity_stage_change: {
|
|
254
266
|
shape: "event",
|
|
267
|
+
filterable: [],
|
|
255
268
|
description: "Opportunity stage transitions derived from OpportunityFieldHistory rows where Field = StageName. One event per transition, timestamped at the change CreatedDate.",
|
|
256
269
|
endpoint: "GET /services/data/v{version}/query (SOQL: FROM OpportunityFieldHistory WHERE Field = 'StageName')",
|
|
257
270
|
notes: "Stage-change events are immutable; their scope is only cleared on a full sync so an incremental window does not drop history outside its range.",
|
|
@@ -295,15 +308,61 @@ function soqlDateLiteral(iso) {
|
|
|
295
308
|
}
|
|
296
309
|
return `${d.toISOString().replace(/\.\d{3}Z$/, "Z")}`;
|
|
297
310
|
}
|
|
298
|
-
|
|
311
|
+
var SOQL_PUSH_FIELDS = {
|
|
312
|
+
users: { isActive: { soqlField: "IsActive", type: "bool" } },
|
|
313
|
+
accounts: { industry: { soqlField: "Industry", type: "string" } },
|
|
314
|
+
leads: {
|
|
315
|
+
status: { soqlField: "Status", type: "string" },
|
|
316
|
+
source: { soqlField: "LeadSource", type: "string" }
|
|
317
|
+
},
|
|
318
|
+
opportunities: {
|
|
319
|
+
stage: { soqlField: "StageName", type: "string" },
|
|
320
|
+
isClosed: { soqlField: "IsClosed", type: "bool" },
|
|
321
|
+
isWon: { soqlField: "IsWon", type: "bool" },
|
|
322
|
+
forecastCategory: { soqlField: "ForecastCategoryName", type: "string" }
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
function soqlStringLiteral(value) {
|
|
326
|
+
return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
327
|
+
}
|
|
328
|
+
function soqlPushConditions(phase, filter) {
|
|
329
|
+
const fields = SOQL_PUSH_FIELDS[phase];
|
|
330
|
+
if (!fields || !filter) {
|
|
331
|
+
return [];
|
|
332
|
+
}
|
|
333
|
+
const conditions = [];
|
|
334
|
+
for (const clause of filter) {
|
|
335
|
+
if (!("field" in clause) || clause.op !== "eq") {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
const mapping = fields[clause.field];
|
|
339
|
+
if (!mapping) {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (mapping.type === "bool") {
|
|
343
|
+
const truthy = clause.value === true || clause.value === "true";
|
|
344
|
+
conditions.push(`${mapping.soqlField} = ${truthy ? "true" : "false"}`);
|
|
345
|
+
} else if (typeof clause.value === "string") {
|
|
346
|
+
conditions.push(
|
|
347
|
+
`${mapping.soqlField} = ${soqlStringLiteral(clause.value)}`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return conditions;
|
|
352
|
+
}
|
|
353
|
+
function buildPhaseSoql(phase, since, pushConditions = []) {
|
|
299
354
|
const base = PHASE_SOQL[phase];
|
|
300
355
|
const timestampField = PHASE_TIMESTAMP[phase];
|
|
301
|
-
|
|
302
|
-
|
|
356
|
+
const conditions = [...pushConditions];
|
|
357
|
+
if (timestampField && since) {
|
|
358
|
+
conditions.push(`${timestampField} >= ${soqlDateLiteral(since)}`);
|
|
359
|
+
}
|
|
360
|
+
let soql = base;
|
|
361
|
+
if (conditions.length > 0) {
|
|
362
|
+
const keyword = base.includes("WHERE") ? "AND" : "WHERE";
|
|
363
|
+
soql += ` ${keyword} ${conditions.join(" AND ")}`;
|
|
303
364
|
}
|
|
304
|
-
|
|
305
|
-
const connector = base.includes("WHERE") ? "AND" : "WHERE";
|
|
306
|
-
return `${base} ${connector} ${timestampField} >= ${literal} ORDER BY ${timestampField} ASC`;
|
|
365
|
+
return `${soql} ORDER BY ${timestampField || "Id"} ASC`;
|
|
307
366
|
}
|
|
308
367
|
function parseDateMs(value) {
|
|
309
368
|
if (value === null || value === void 0 || value === "") {
|
|
@@ -407,7 +466,14 @@ var SalesforceConnector = class _SalesforceConnector extends BaseConnector {
|
|
|
407
466
|
}
|
|
408
467
|
path = safePage;
|
|
409
468
|
} else {
|
|
410
|
-
const
|
|
469
|
+
const resource = ENTITY_TYPE_BY_PHASE[phase];
|
|
470
|
+
const specs = resource ? options.fetchSpecs?.[resource] : void 0;
|
|
471
|
+
const spec = specs && specs.length === 1 ? specs[0] : void 0;
|
|
472
|
+
const soql = buildPhaseSoql(
|
|
473
|
+
phase,
|
|
474
|
+
options.since,
|
|
475
|
+
soqlPushConditions(phase, spec?.filter)
|
|
476
|
+
);
|
|
411
477
|
const url = new URL(
|
|
412
478
|
`${this.baseUrl()}/services/data/v${this.apiVersion()}/query`
|
|
413
479
|
);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../connector-shared/src/errors.ts","../../../connector-shared/src/retry.ts","../../../connector-shared/src/version.ts","../../../connector-shared/src/request.ts","../../../connector-shared/src/rate-limit.ts","../../../connector-shared/src/map-concurrent.ts","../../../connector-shared/src/sanitize.ts","../../../connector-shared/src/epoch.ts","../../../connector-shared/src/pagination.ts","../../../connector-shared/src/logger.ts","../src/salesforce.ts","../src/index.ts"],"sourcesContent":["import type { HttpResponse } from './types';\n\nexport type HttpErrorKind =\n | 'transient'\n | 'rate_limit'\n | 'auth'\n | 'upstream_bug'\n | 'client_bug';\n\nexport abstract class HttpClientError extends Error {\n abstract readonly kind: HttpErrorKind;\n readonly response?: HttpResponse;\n\n constructor(message: string, response?: HttpResponse) {\n super(message);\n this.name = new.target.name;\n this.response = response;\n }\n}\n\nexport class TransientError extends HttpClientError {\n readonly kind = 'transient' as const;\n}\n\nexport class RateLimitError extends HttpClientError {\n readonly kind = 'rate_limit' as const;\n readonly retryAfter?: Date;\n\n constructor(message: string, response?: HttpResponse, retryAfter?: Date) {\n super(message, response);\n this.retryAfter = retryAfter;\n }\n}\n\nexport class AuthError extends HttpClientError {\n readonly kind = 'auth' as const;\n}\n\nexport class UpstreamBugError extends HttpClientError {\n readonly kind = 'upstream_bug' as const;\n}\n\nexport class ClientBugError extends HttpClientError {\n readonly kind = 'client_bug' as const;\n}\n\nexport function classifyStatus(status: number): HttpErrorKind {\n if (status === 429) {\n return 'rate_limit';\n }\n if (status === 401 || status === 403) {\n return 'auth';\n }\n if (status === 408) {\n return 'transient';\n }\n if (status >= 500) {\n return 'upstream_bug';\n }\n if (status >= 400) {\n return 'client_bug';\n }\n return 'client_bug';\n}\n\nexport function errorForStatus(\n message: string,\n response: HttpResponse,\n retryAfter?: Date,\n): HttpClientError {\n const kind = classifyStatus(response.status);\n switch (kind) {\n case 'rate_limit':\n return new RateLimitError(message, response, retryAfter);\n case 'auth':\n return new AuthError(message, response);\n case 'transient':\n return new TransientError(message, response);\n case 'upstream_bug':\n return new UpstreamBugError(message, response);\n case 'client_bug':\n return new ClientBugError(message, response);\n }\n}\n","import { HttpClientError, RateLimitError, TransientError } from './errors';\n\nexport interface RetryPolicy {\n maxAttempts?: number;\n initialDelayMs?: number;\n maxDelayMs?: number;\n retryOn?: (status: number | null, err?: Error) => boolean;\n}\n\nexport const defaultRetryOn = (status: number | null, err?: Error): boolean => {\n if (err instanceof RateLimitError) {\n return true;\n }\n if (err instanceof TransientError) {\n return true;\n }\n if (status === null) {\n return err instanceof Error && !(err instanceof HttpClientError);\n }\n if (status === 408 || status === 429) {\n return true;\n }\n if (status >= 500) {\n return true;\n }\n return false;\n};\n\nexport function backoffDelayMs(\n attempt: number,\n policy: Required<Pick<RetryPolicy, 'initialDelayMs' | 'maxDelayMs'>>,\n): number {\n const base = policy.initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, policy.maxDelayMs);\n}\n\nexport function parseRetryAfter(\n headerValue: string | null,\n now: Date = new Date(),\n): Date | undefined {\n if (!headerValue) {\n return undefined;\n }\n const trimmed = headerValue.trim();\n if (/^\\d+$/.test(trimmed)) {\n return new Date(now.getTime() + Number(trimmed) * 1000);\n }\n const parsed = Date.parse(trimmed);\n if (Number.isNaN(parsed)) {\n return undefined;\n }\n return new Date(parsed);\n}\n\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) {\n return Promise.reject(signal.reason ?? new Error('Aborted'));\n }\n return new Promise<void>((resolve, reject) => {\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal!.reason ?? new Error('Aborted'));\n };\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n","export const HTTP_CLIENT_VERSION = '0.0.0';\n\nexport const DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n\nexport function connectorUserAgent(connectorId: string): string {\n return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n}\n","import {\n AuthError,\n ClientBugError,\n HttpClientError,\n RateLimitError,\n TransientError,\n UpstreamBugError,\n errorForStatus,\n} from './errors';\nimport { defaultRetryOn, parseRetryAfter, sleep } from './retry';\nimport type { FetchLike, HttpMethod, HttpRequest, HttpResponse } from './types';\nimport { DEFAULT_USER_AGENT } from './version';\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_MAX_ATTEMPTS = 3;\nconst DEFAULT_INITIAL_DELAY_MS = 1000;\nconst DEFAULT_MAX_DELAY_MS = 60_000;\nconst OBSERVER_TIMEOUT_MS = 250;\n\nexport interface RequestObservation {\n url: string;\n method: HttpMethod;\n status: number;\n resource: string;\n requestId: string;\n body: unknown;\n}\n\nexport type RequestObserver = (\n event: RequestObservation,\n) => void | Promise<void>;\n\nexport interface RequestOptions {\n fetch?: FetchLike;\n observer?: RequestObserver;\n resource: string;\n requestId?: string;\n}\n\nasync function notifyObserver(\n observer: RequestObserver,\n event: RequestObservation,\n): Promise<void> {\n let result: void | Promise<void>;\n try {\n result = observer(event);\n } catch (err) {\n console.warn('[connector-shared] request observer threw:', err);\n return;\n }\n if (!(result instanceof Promise)) {\n return;\n }\n const guarded = result.catch((err) => {\n console.warn('[connector-shared] request observer rejected:', err);\n });\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, OBSERVER_TIMEOUT_MS);\n });\n try {\n await Promise.race([guarded, timeout]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n}\n\nfunction newRequestId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) {\n return c.randomUUID();\n }\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction mergeHeaders(\n defaults: Record<string, string>,\n overrides: Record<string, string> | undefined,\n): Record<string, string> {\n const merged: Record<string, string> = {};\n for (const [k, v] of Object.entries(defaults)) {\n merged[k.toLowerCase()] = v;\n }\n if (overrides) {\n for (const [k, v] of Object.entries(overrides)) {\n merged[k.toLowerCase()] = v;\n }\n }\n return merged;\n}\n\nfunction linkTimeoutSignal(\n parent: AbortSignal | undefined,\n timeoutMs: number,\n): { signal: AbortSignal; cancel: () => void } {\n const controller = new AbortController();\n const onParentAbort = () => {\n controller.abort(parent?.reason);\n };\n if (parent) {\n if (parent.aborted) {\n controller.abort(parent.reason);\n } else {\n parent.addEventListener('abort', onParentAbort, { once: true });\n }\n }\n const timer = setTimeout(() => {\n controller.abort(new Error(`Request timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n return {\n signal: controller.signal,\n cancel: () => {\n clearTimeout(timer);\n if (parent) {\n parent.removeEventListener('abort', onParentAbort);\n }\n },\n };\n}\n\nasync function readBody(res: Response, parseJson: boolean): Promise<unknown> {\n if (res.status === 204 || res.status === 205) {\n return null;\n }\n const contentType = res.headers.get('content-type') ?? '';\n if (parseJson && contentType.includes('application/json')) {\n const text = await res.text();\n if (text.length === 0) {\n return null;\n }\n return JSON.parse(text);\n }\n return res.text();\n}\n\nexport async function request<T = unknown>(\n req: HttpRequest,\n options: RequestOptions,\n): Promise<HttpResponse<T>> {\n const fetchImpl: FetchLike = options.fetch ?? (globalThis.fetch as FetchLike);\n const retry = req.retry ?? {};\n const maxAttempts = retry.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n const initialDelayMs = retry.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;\n const maxDelayMs = retry.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;\n const retryOn = retry.retryOn ?? defaultRetryOn;\n const timeoutMs = req.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const parseJson = req.parseJson ?? true;\n\n const headers = mergeHeaders(\n {\n 'User-Agent': DEFAULT_USER_AGENT,\n Accept: 'application/json',\n },\n req.headers,\n );\n\n let lastErr: Error | undefined;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n req.signal?.throwIfAborted();\n\n const { signal, cancel } = linkTimeoutSignal(req.signal, timeoutMs);\n let res: Response;\n try {\n res = await fetchImpl(req.url, {\n method: req.method ?? 'GET',\n headers,\n body: req.body as RequestInit['body'],\n signal,\n });\n } catch (err) {\n cancel();\n if (req.signal?.aborted) {\n throw req.signal.reason ?? err;\n }\n const error = err instanceof Error ? err : new Error(String(err));\n lastErr = error;\n if (attempt < maxAttempts - 1 && retryOn(null, error)) {\n const delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n await sleep(delay, req.signal);\n continue;\n }\n throw new TransientError(error.message);\n }\n cancel();\n\n const body = await readBody(res, parseJson);\n const httpResponse: HttpResponse<T> = {\n status: res.status,\n headers: res.headers,\n body: body as T,\n };\n if (req.rateLimit) {\n const state = req.rateLimit.parse(res.headers);\n if (state) {\n httpResponse.rateLimitState = state;\n }\n }\n\n if (options.observer) {\n await notifyObserver(options.observer, {\n url: req.url,\n method: req.method ?? 'GET',\n status: res.status,\n resource: options.resource,\n requestId: options.requestId ?? newRequestId(),\n body,\n });\n }\n\n if (res.ok) {\n return httpResponse;\n }\n\n const retryAfter = parseRetryAfter(res.headers.get('retry-after'));\n const message = `HTTP ${res.status} ${res.statusText} for ${req.method ?? 'GET'} ${req.url}`;\n const err = errorForStatus(message, httpResponse, retryAfter);\n\n if (\n attempt < maxAttempts - 1 &&\n retryOn(res.status, err) &&\n !(err instanceof AuthError) &&\n !(err instanceof ClientBugError)\n ) {\n lastErr = err;\n let delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n if (err instanceof RateLimitError && retryAfter) {\n const wait = retryAfter.getTime() - Date.now();\n if (wait > 0) {\n delay = Math.min(wait, maxDelayMs);\n }\n }\n await sleep(delay, req.signal);\n continue;\n }\n\n throw err;\n }\n\n throw lastErr ?? new UpstreamBugError('Exhausted retry attempts');\n}\n\nfunction computeDelay(\n attempt: number,\n initialDelayMs: number,\n maxDelayMs: number,\n): number {\n const base = initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, maxDelayMs);\n}\n\nexport { HttpClientError };\n","export interface RateLimitState {\n remaining: number;\n resetAt: Date;\n}\n\nexport interface RateLimitPolicy {\n parse(headers: Headers): RateLimitState | null;\n}\n\nexport interface StandardRateLimitPolicyConfig {\n remainingHeader: string;\n resetHeader: string;\n resetUnit: 's' | 'ms';\n resetFallbackMs?: number;\n}\n\nexport function standardRateLimitPolicy(\n config: StandardRateLimitPolicyConfig,\n): RateLimitPolicy {\n const { remainingHeader, resetHeader, resetUnit, resetFallbackMs } = config;\n const multiplier = resetUnit === 's' ? 1000 : 1;\n return {\n parse(h) {\n const remainingRaw = h.get(remainingHeader);\n if (remainingRaw === null || remainingRaw.trim() === '') {\n return null;\n }\n const remaining = Number(remainingRaw);\n if (!Number.isFinite(remaining)) {\n return null;\n }\n const resetRaw = h.get(resetHeader);\n if (resetRaw === null) {\n if (resetFallbackMs === undefined) {\n return null;\n }\n return {\n remaining,\n resetAt: new Date(Date.now() + resetFallbackMs),\n };\n }\n if (resetRaw.trim() === '') {\n return null;\n }\n const reset = Number(resetRaw);\n if (!Number.isFinite(reset) || reset < 0) {\n return null;\n }\n const resetMs = reset * multiplier;\n if (!Number.isFinite(resetMs)) {\n return null;\n }\n return { remaining, resetAt: new Date(resetMs) };\n },\n };\n}\n","export async function mapWithConcurrency<T, R>(\n items: readonly T[],\n concurrency: number,\n fn: (item: T, index: number) => Promise<R>,\n): Promise<R[]> {\n const results = new Array<R>(items.length);\n if (items.length === 0) {\n return results;\n }\n const normalized = Number.isFinite(concurrency) ? Math.floor(concurrency) : 1;\n const limit = Math.max(1, Math.min(normalized, items.length));\n let next = 0;\n let failed = false;\n\n async function worker(): Promise<void> {\n while (!failed) {\n const i = next++;\n if (i >= items.length) {\n return;\n }\n try {\n results[i] = await fn(items[i]!, i);\n } catch (err) {\n failed = true;\n throw err;\n }\n }\n }\n\n const workers: Promise<void>[] = [];\n for (let w = 0; w < limit; w++) {\n workers.push(worker());\n }\n await Promise.all(workers);\n return results;\n}\n","export interface SanitizeAllowedUrlOptions {\n url: string | null;\n host: string;\n pathname: string;\n protocol?: 'https:' | 'http:';\n}\n\nexport function sanitizeAllowedUrl(\n options: SanitizeAllowedUrlOptions,\n): string | null {\n const { url, host, pathname, protocol = 'https:' } = options;\n if (url === null) {\n return null;\n }\n try {\n const u = new URL(url);\n if (u.protocol !== protocol || u.host !== host || u.pathname !== pathname) {\n return null;\n }\n return u.toString();\n } catch {\n return null;\n }\n}\n","export type EpochUnit = 'ms' | 's' | 'iso';\n\nexport function parseEpoch(\n value: number | string | null | undefined,\n unit: EpochUnit,\n): number | null {\n if (value === null || value === undefined) {\n return null;\n }\n if (unit === 'iso') {\n if (typeof value !== 'string') {\n return null;\n }\n const ms = new Date(value).getTime();\n return Number.isFinite(ms) ? ms : null;\n }\n if (typeof value === 'string' && value.trim() === '') {\n return null;\n }\n const n = typeof value === 'number' ? value : Number(value);\n if (!Number.isFinite(n)) {\n return null;\n }\n const result = unit === 's' ? n * 1000 : n;\n return Number.isFinite(result) ? result : null;\n}\n","import { request } from './request';\nimport type { HttpRequest } from './types';\n\nexport function parseLinkHeader(header: string | null): Record<string, string> {\n if (!header) {\n return {};\n }\n const result: Record<string, string> = {};\n for (const part of header.split(',')) {\n const match = part.match(/<([^>]+)>\\s*;\\s*rel=\"([^\"]+)\"/);\n if (match) {\n result[match[2]!] = match[1]!;\n }\n }\n return result;\n}\n\nexport async function* paginateLink<T>(\n initial: HttpRequest,\n parse: (body: unknown) => T[],\n options: { resource: string },\n): AsyncIterable<T> {\n let next: string | null = initial.url;\n while (next) {\n const res: Awaited<ReturnType<typeof request>> = await request(\n {\n ...initial,\n url: next,\n },\n { resource: options.resource },\n );\n for (const item of parse(res.body)) {\n yield item;\n }\n const links = parseLinkHeader(res.headers.get('link'));\n next = links['next'] ?? null;\n }\n}\n\nexport async function* paginateCursor<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; nextCursor: string | null },\n buildNext: (req: HttpRequest, cursor: string) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let req: HttpRequest = initial;\n while (true) {\n const res = await request(req, { resource: options.resource });\n const { items, nextCursor } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!nextCursor) {\n return;\n }\n req = buildNext(req, nextCursor);\n }\n}\n\nexport async function* paginatePage<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; hasMore: boolean },\n buildPage: (req: HttpRequest, page: number) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let page = 1;\n while (true) {\n const req = page === 1 ? initial : buildPage(initial, page);\n const res = await request(req, { resource: options.resource });\n const { items, hasMore } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!hasMore || items.length === 0) {\n return;\n }\n page++;\n }\n}\n","export type LogFields = Record<string, unknown>;\n\nexport interface ConnectorLogger {\n info(event: string, fields?: LogFields): void;\n warn(event: string, fields?: LogFields): void;\n}\n\nexport interface ConnectorLoggerOptions {\n scope: string;\n}\n\nconst MAX_VALUE_LEN = 120;\n\nfunction truncate(s: string, max = MAX_VALUE_LEN): string {\n if (s.length <= max) {\n return s;\n }\n return `${s.slice(0, max - 1)}…`;\n}\n\nfunction formatValue(value: unknown): string {\n if (value === null) {\n return 'null';\n }\n if (value === undefined) {\n return '';\n }\n if (typeof value === 'number' || typeof value === 'boolean') {\n return String(value);\n }\n if (typeof value === 'string') {\n const t = truncate(value);\n if (/[\\s\"=]/.test(t)) {\n return JSON.stringify(t);\n }\n return t;\n }\n if (typeof value === 'bigint') {\n return value.toString();\n }\n let json: string | undefined;\n try {\n json = JSON.stringify(value);\n } catch {\n json = undefined;\n }\n return truncate(json ?? String(value));\n}\n\nexport function formatLogFields(fields?: LogFields): string {\n if (!fields) {\n return '';\n }\n const parts: string[] = [];\n for (const [k, v] of Object.entries(fields)) {\n if (v === undefined) {\n continue;\n }\n parts.push(`${k}=${formatValue(v)}`);\n }\n return parts.length > 0 ? ` ${parts.join(' ')}` : '';\n}\n\nexport function formatLogLine(\n scope: string,\n event: string,\n fields?: LogFields,\n): string {\n return `[${scope}] ${event}${formatLogFields(fields)}`;\n}\n\nexport function createDefaultConnectorLogger(\n opts: ConnectorLoggerOptions,\n): ConnectorLogger {\n return {\n info(event, fields) {\n console.info(formatLogLine(opts.scope, event, fields));\n },\n warn(event, fields) {\n console.warn(formatLogLine(opts.scope, event, fields));\n },\n };\n}\n\nconst NOOP_LOGGER: ConnectorLogger = {\n info() {},\n warn() {},\n};\n\nexport function noopConnectorLogger(): ConnectorLogger {\n return NOOP_LOGGER;\n}\n","import {\n type HttpResponse,\n connectorUserAgent,\n} 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 clientId: z.string().min(1).meta({\n label: 'Connected app consumer key',\n description:\n 'Consumer key (client ID) of the Salesforce Connected App used for OAuth 2.0 refresh-token exchange.',\n placeholder: '3MVG9...',\n }),\n clientSecret: z.object({ $secret: z.string().min(1) }).meta({\n label: 'Connected app consumer secret',\n description: 'Consumer secret of the Salesforce Connected App.',\n placeholder: 'SF_CLIENT_SECRET',\n secret: true,\n }),\n refreshToken: z.object({ $secret: z.string().min(1) }).meta({\n label: 'OAuth refresh token',\n description:\n 'OAuth 2.0 refresh token obtained from the Connected App authorization code flow. Stored as a secret.',\n placeholder: 'SF_REFRESH_TOKEN',\n secret: true,\n }),\n instanceUrl: z.string().url().meta({\n label: 'Instance URL',\n description:\n 'Salesforce instance URL, e.g. https://mycompany.my.salesforce.com. Returned alongside the refresh token from the OAuth flow; never use the generic login.salesforce.com URL here.',\n placeholder: 'https://mycompany.my.salesforce.com',\n }),\n apiVersion: z\n .string()\n .regex(/^\\d+\\.\\d+$/)\n .optional()\n .meta({\n label: 'REST API version',\n description:\n 'Salesforce REST API version, e.g. \"59.0\". Defaults to 59.0; bump to pick up newer SOQL semantics.',\n placeholder: '59.0',\n }),\n resources: z\n .array(\n z.enum([\n 'users',\n 'accounts',\n 'leads',\n 'opportunities',\n 'opportunity_events',\n ]),\n )\n .nonempty()\n .optional()\n .meta({\n label: 'Resources',\n description:\n 'Which Salesforce resources to sync. Omit to sync all resources. The Connected App only needs read access for the resources listed here.',\n }),\n }),\n);\n\nexport const doc: ConnectorDoc = defineConnectorDoc({\n displayName: 'Salesforce',\n category: 'sales',\n brandColor: '#00A1E0',\n tagline:\n 'Sync opportunities, opportunity stage-change events, accounts, leads, and users from a Salesforce org for pipeline, forecast, and quota-attainment dashboards.',\n vendor: {\n name: 'Salesforce',\n domain: 'salesforce.com',\n apiDocs:\n 'https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/',\n website: 'https://www.salesforce.com',\n },\n auth: {\n summary:\n 'OAuth 2.0 with a refresh token issued by a Salesforce Connected App. Requires the consumer key/secret, a refresh token, and the org instance URL.',\n setup: [\n 'In Salesforce, go to Setup → App Manager → New Connected App and check \"Enable OAuth Settings\".',\n 'Set the callback URL to a URL you control (e.g. https://localhost:8080/callback); it only has to be reachable when minting the initial refresh token.',\n 'Under Selected OAuth Scopes add \"Access and manage your data (api)\" and \"Perform requests on your behalf at any time (refresh_token, offline_access)\".',\n 'Save, then copy the Consumer Key (client ID) and Consumer Secret from the connected app detail page.',\n 'Authorize via https://login.salesforce.com/services/oauth2/authorize?response_type=code&client_id=<KEY>&redirect_uri=<URL> and exchange the resulting code at /services/oauth2/token to obtain a refresh token and the org instance_url.',\n 'Use the org instance URL from the token response (e.g. https://mycompany.my.salesforce.com), not login.salesforce.com.',\n 'Store the consumer secret and refresh token as rawdash secrets and reference them as secret(\"SF_CLIENT_SECRET\") and secret(\"SF_REFRESH_TOKEN\").',\n ],\n },\n rateLimit:\n 'Salesforce caps total API calls per org per 24 hours. Responses include a Sforce-Limit-Info header (api-usage=NN/MM); size sync intervals so the daily budget is not exhausted. The shared HTTP client retries on 429 with Retry-After.',\n limitations: [\n 'Custom objects are out of scope for v1; only the standard objects listed above are synced.',\n 'Salesforce Marketing Cloud is tracked under a separate connector.',\n ],\n});\n\nexport interface SalesforceSettings {\n instanceUrl: string;\n apiVersion?: string;\n resources?: readonly SalesforceResource[];\n}\n\nconst salesforceCredentials = {\n clientId: {\n description: 'Salesforce Connected App consumer key (client ID)',\n auth: 'required' as const,\n },\n clientSecret: {\n description: 'Salesforce Connected App consumer secret',\n auth: 'required' as const,\n },\n refreshToken: {\n description: 'Salesforce OAuth 2.0 refresh token',\n auth: 'required' as const,\n },\n} satisfies CredentialsSchema;\n\ntype SalesforceCredentials = typeof salesforceCredentials;\n\nconst PHASE_ORDER = [\n 'users',\n 'accounts',\n 'leads',\n 'opportunities',\n 'opportunity_events',\n] as const;\n\ntype SalesforcePhase = (typeof PHASE_ORDER)[number];\n\nexport type SalesforceResource = SalesforcePhase;\n\nconst isSalesforceSyncCursor = makeChunkedCursorGuard(PHASE_ORDER);\n\nconst ENTITY_TYPE_BY_PHASE: Partial<Record<SalesforcePhase, string>> = {\n users: 'salesforce_user',\n accounts: 'salesforce_account',\n leads: 'salesforce_lead',\n opportunities: 'salesforce_opportunity',\n};\n\nconst STAGE_CHANGE_EVENT = 'salesforce_opportunity_stage_change';\n\nconst DEFAULT_API_VERSION = '59.0';\n\ninterface QueryResponse<T> {\n totalSize: number;\n done: boolean;\n nextRecordsUrl?: string;\n records: T[];\n}\n\ninterface OauthTokenResponse {\n access_token: string;\n instance_url?: string;\n}\n\ninterface SalesforceUser {\n Id: string;\n Name: string | null;\n Email: string | null;\n IsActive: boolean;\n}\n\ninterface SalesforceAccount {\n Id: string;\n Name: string | null;\n Industry: string | null;\n AnnualRevenue: number | null;\n OwnerId: string | null;\n CreatedDate: string;\n LastModifiedDate: string;\n}\n\ninterface SalesforceLead {\n Id: string;\n Email: string | null;\n Status: string | null;\n LeadSource: string | null;\n ConvertedDate: string | null;\n CreatedDate: string;\n LastModifiedDate: string;\n}\n\ninterface SalesforceOpportunity {\n Id: string;\n Name: string | null;\n StageName: string | null;\n Amount: number | null;\n CloseDate: string | null;\n OwnerId: string | null;\n Probability: number | null;\n ForecastCategoryName: string | null;\n IsClosed: boolean;\n IsWon: boolean;\n CreatedDate: string;\n LastModifiedDate: string;\n}\n\ninterface SalesforceFieldHistory {\n Id: string;\n OpportunityId: string;\n Field: string;\n OldValue: string | null;\n NewValue: string | null;\n CreatedDate: string;\n CreatedById: string | null;\n}\n\nconst idString = z.string().min(1);\nconst isoDate = z.string().min(1);\n\nconst oauthTokenSchema = z.object({\n access_token: z.string().min(1),\n instance_url: z.string().optional(),\n});\n\nconst userSchema = z.object({\n Id: idString,\n Name: z.string().nullable(),\n Email: z.string().nullable(),\n IsActive: z.boolean(),\n});\n\nconst accountSchema = z.object({\n Id: idString,\n Name: z.string().nullable(),\n Industry: z.string().nullable(),\n AnnualRevenue: z.number().nullable(),\n OwnerId: z.string().nullable(),\n CreatedDate: isoDate,\n LastModifiedDate: isoDate,\n});\n\nconst leadSchema = z.object({\n Id: idString,\n Email: z.string().nullable(),\n Status: z.string().nullable(),\n LeadSource: z.string().nullable(),\n ConvertedDate: z.string().nullable(),\n CreatedDate: isoDate,\n LastModifiedDate: isoDate,\n});\n\nconst opportunitySchema = z.object({\n Id: idString,\n Name: z.string().nullable(),\n StageName: z.string().nullable(),\n Amount: z.number().nullable(),\n CloseDate: z.string().nullable(),\n OwnerId: z.string().nullable(),\n Probability: z.number().nullable(),\n ForecastCategoryName: z.string().nullable(),\n IsClosed: z.boolean(),\n IsWon: z.boolean(),\n CreatedDate: isoDate,\n LastModifiedDate: isoDate,\n});\n\nconst fieldHistorySchema = z.object({\n Id: idString,\n OpportunityId: idString,\n Field: z.string(),\n OldValue: z.string().nullable(),\n NewValue: z.string().nullable(),\n CreatedDate: isoDate,\n CreatedById: z.string().nullable(),\n});\n\nexport const salesforceResources = defineResources({\n salesforce_user: {\n shape: 'entity',\n description:\n 'Salesforce users, keyed by user id, with name, email, and active state. Used to attribute opportunities, accounts, and stage changes to owners.',\n endpoint: 'GET /services/data/v{version}/query (SOQL: FROM User)',\n notes: 'Users are backfilled in full on every run; the table is small.',\n fields: [\n { name: 'name', description: 'Full name of the user.' },\n { name: 'email', description: 'User email address.' },\n { name: 'isActive', description: 'Whether the user is active.' },\n ],\n responses: {\n oauth_token: oauthTokenSchema,\n users: z.array(userSchema),\n },\n },\n salesforce_account: {\n shape: 'entity',\n description:\n 'Accounts (companies), keyed by account id, with industry, annual revenue, owner, and creation time.',\n endpoint: 'GET /services/data/v{version}/query (SOQL: FROM Account)',\n notes: 'Upserts by id; incremental syncs filter on LastModifiedDate.',\n fields: [\n { name: 'name', description: 'Account name.' },\n { name: 'industry', description: 'Industry classification.' },\n {\n name: 'annualRevenue',\n description: 'Annual revenue in the org currency.',\n },\n { name: 'ownerId', description: 'User id of the account owner.' },\n {\n name: 'createdAt',\n description: 'Account creation time (Unix ms).',\n },\n ],\n responses: { accounts: z.array(accountSchema) },\n },\n salesforce_lead: {\n shape: 'entity',\n description:\n 'Leads, keyed by lead id, with email, status, source, and conversion time.',\n endpoint: 'GET /services/data/v{version}/query (SOQL: FROM Lead)',\n notes: 'Upserts by id; incremental syncs filter on LastModifiedDate.',\n fields: [\n { name: 'email', description: 'Lead email address.' },\n { name: 'status', description: 'Lead status.' },\n { name: 'source', description: 'Lead source (LeadSource).' },\n {\n name: 'convertedAt',\n description: 'When the lead was converted (Unix ms), if any.',\n },\n { name: 'createdAt', description: 'Lead creation time (Unix ms).' },\n ],\n responses: { leads: z.array(leadSchema) },\n },\n salesforce_opportunity: {\n shape: 'entity',\n description:\n 'Opportunities, keyed by opportunity id, with stage, amount, close date, owner, probability, forecast category, and closed/won flags.',\n endpoint: 'GET /services/data/v{version}/query (SOQL: FROM Opportunity)',\n notes: 'Upserts by id; incremental syncs filter on LastModifiedDate.',\n fields: [\n { name: 'name', description: 'Opportunity name.' },\n { name: 'stage', description: 'Current StageName.' },\n { name: 'amount', description: 'Opportunity amount in org currency.' },\n { name: 'closeDate', description: 'Expected close date (Unix ms).' },\n { name: 'ownerId', description: 'User id of the opportunity owner.' },\n { name: 'probability', description: 'Win probability percentage.' },\n {\n name: 'forecastCategory',\n description: 'Forecast category name.',\n },\n { name: 'isClosed', description: 'Whether the opportunity is closed.' },\n { name: 'isWon', description: 'Whether the opportunity is won.' },\n {\n name: 'createdAt',\n description: 'Opportunity creation time (Unix ms).',\n },\n ],\n responses: { opportunities: z.array(opportunitySchema) },\n },\n salesforce_opportunity_stage_change: {\n shape: 'event',\n description:\n 'Opportunity stage transitions derived from OpportunityFieldHistory rows where Field = StageName. One event per transition, timestamped at the change CreatedDate.',\n endpoint:\n \"GET /services/data/v{version}/query (SOQL: FROM OpportunityFieldHistory WHERE Field = 'StageName')\",\n notes:\n 'Stage-change events are immutable; their scope is only cleared on a full sync so an incremental window does not drop history outside its range.',\n fields: [\n {\n name: 'historyId',\n description: 'OpportunityFieldHistory row id.',\n },\n {\n name: 'opportunityId',\n description: 'Id of the opportunity that changed stage.',\n },\n { name: 'fromStage', description: 'Previous StageName (OldValue).' },\n { name: 'toStage', description: 'New StageName (NewValue).' },\n {\n name: 'actorId',\n description: 'User id who made the change (CreatedById).',\n },\n ],\n responses: { opportunity_events: z.array(fieldHistorySchema) },\n },\n});\n\nconst PHASE_SOQL: Record<SalesforcePhase, string> = {\n users: 'SELECT Id, Name, Email, IsActive FROM User',\n accounts:\n 'SELECT Id, Name, Industry, AnnualRevenue, OwnerId, CreatedDate, LastModifiedDate FROM Account',\n leads:\n 'SELECT Id, Email, Status, LeadSource, ConvertedDate, CreatedDate, LastModifiedDate FROM Lead',\n opportunities:\n 'SELECT Id, Name, StageName, Amount, CloseDate, OwnerId, Probability, ForecastCategoryName, IsClosed, IsWon, CreatedDate, LastModifiedDate FROM Opportunity',\n opportunity_events:\n \"SELECT Id, OpportunityId, Field, OldValue, NewValue, CreatedDate, CreatedById FROM OpportunityFieldHistory WHERE Field = 'StageName'\",\n};\n\nconst PHASE_TIMESTAMP: Record<SalesforcePhase, string> = {\n users: '',\n accounts: 'LastModifiedDate',\n leads: 'LastModifiedDate',\n opportunities: 'LastModifiedDate',\n opportunity_events: 'CreatedDate',\n};\n\nfunction soqlDateLiteral(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) {\n return iso;\n }\n return `${d.toISOString().replace(/\\.\\d{3}Z$/, 'Z')}`;\n}\n\nfunction buildPhaseSoql(\n phase: SalesforcePhase,\n since: string | undefined,\n): string {\n const base = PHASE_SOQL[phase];\n const timestampField = PHASE_TIMESTAMP[phase];\n if (!timestampField || !since) {\n return timestampField\n ? `${base} ORDER BY ${timestampField} ASC`\n : `${base} ORDER BY Id ASC`;\n }\n const literal = soqlDateLiteral(since);\n const connector = base.includes('WHERE') ? 'AND' : 'WHERE';\n return `${base} ${connector} ${timestampField} >= ${literal} ORDER BY ${timestampField} ASC`;\n}\n\nfunction parseDateMs(value: string | null | undefined): number | null {\n if (value === null || value === undefined || value === '') {\n return null;\n }\n const ms = Date.parse(value);\n return Number.isFinite(ms) ? ms : null;\n}\n\nexport const id = 'salesforce';\n\nexport class SalesforceConnector extends BaseConnector<\n SalesforceSettings,\n SalesforceCredentials\n> {\n static readonly id = id;\n\n static readonly resources = salesforceResources;\n\n static readonly schemas = schemasFromResources(salesforceResources);\n\n static create(input: unknown, ctx?: ConnectorContext): SalesforceConnector {\n const parsed = configFields.parse(input);\n return new SalesforceConnector(\n {\n instanceUrl: parsed.instanceUrl,\n apiVersion: parsed.apiVersion,\n resources: parsed.resources,\n },\n {\n clientId: parsed.clientId,\n clientSecret: parsed.clientSecret,\n refreshToken: parsed.refreshToken,\n },\n ctx,\n );\n }\n\n readonly id = id;\n override readonly credentials = salesforceCredentials;\n\n private accessToken: string | null = null;\n\n private apiVersion(): string {\n return this.settings.apiVersion ?? DEFAULT_API_VERSION;\n }\n\n private baseUrl(): string {\n return this.settings.instanceUrl.replace(/\\/+$/, '');\n }\n\n private async refreshAccessToken(signal?: AbortSignal): Promise<string> {\n const body = new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: this.creds.clientId,\n client_secret: this.creds.clientSecret,\n refresh_token: this.creds.refreshToken,\n }).toString();\n const res = await this.post<OauthTokenResponse>(\n `${this.baseUrl()}/services/oauth2/token`,\n {\n resource: 'oauth_token',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n 'User-Agent': connectorUserAgent('salesforce'),\n },\n body,\n signal,\n },\n );\n return res.body.access_token;\n }\n\n private async getAccessToken(signal?: AbortSignal): Promise<string> {\n if (!this.accessToken) {\n this.accessToken = await this.refreshAccessToken(signal);\n }\n return this.accessToken;\n }\n\n private async apiGet<T>(\n pathOrUrl: string,\n resource: string,\n signal?: AbortSignal,\n ): Promise<HttpResponse<T>> {\n const url = pathOrUrl.startsWith('http')\n ? pathOrUrl\n : `${this.baseUrl()}${pathOrUrl}`;\n const token = await this.getAccessToken(signal);\n return this.get<T>(url, {\n resource,\n headers: {\n Authorization: `Bearer ${token}`,\n Accept: 'application/json',\n 'User-Agent': connectorUserAgent('salesforce'),\n },\n signal,\n });\n }\n\n private sanitizeNextRecordsUrl(value: string | undefined): string | null {\n if (!value) {\n return null;\n }\n if (value.startsWith('/services/data/')) {\n return value;\n }\n try {\n const parsed = new URL(value);\n if (parsed.pathname.startsWith('/services/data/')) {\n return parsed.pathname + parsed.search;\n }\n } catch {\n // ignore — fall through\n }\n return null;\n }\n\n private async fetchPage(\n phase: SalesforcePhase,\n page: string | null,\n options: SyncOptions,\n signal: AbortSignal | undefined,\n ): Promise<{ items: unknown[]; next: string | null }> {\n let path: string;\n if (page) {\n const safePage = this.sanitizeNextRecordsUrl(page);\n if (!safePage) {\n throw new Error(`Invalid Salesforce cursor page: ${page}`);\n }\n path = safePage;\n } else {\n const soql = buildPhaseSoql(phase, options.since);\n const url = new URL(\n `${this.baseUrl()}/services/data/v${this.apiVersion()}/query`,\n );\n url.searchParams.set('q', soql);\n path = `${url.pathname}?${url.searchParams.toString()}`;\n }\n const res = await this.apiGet<QueryResponse<unknown>>(path, phase, signal);\n return {\n items: res.body.records,\n next: res.body.done\n ? null\n : this.sanitizeNextRecordsUrl(res.body.nextRecordsUrl),\n };\n }\n\n private async writeUsers(\n storage: StorageHandle,\n items: SalesforceUser[],\n ): Promise<void> {\n for (const u of items) {\n await storage.entity({\n type: 'salesforce_user',\n id: u.Id,\n attributes: {\n name: u.Name,\n email: u.Email,\n isActive: u.IsActive,\n },\n updated_at: 0,\n });\n }\n }\n\n private async writeAccounts(\n storage: StorageHandle,\n items: SalesforceAccount[],\n ): Promise<void> {\n for (const a of items) {\n await storage.entity({\n type: 'salesforce_account',\n id: a.Id,\n attributes: {\n name: a.Name,\n industry: a.Industry,\n annualRevenue: a.AnnualRevenue,\n ownerId: a.OwnerId,\n createdAt: parseDateMs(a.CreatedDate),\n },\n updated_at: parseDateMs(a.LastModifiedDate) ?? 0,\n });\n }\n }\n\n private async writeLeads(\n storage: StorageHandle,\n items: SalesforceLead[],\n ): Promise<void> {\n for (const l of items) {\n await storage.entity({\n type: 'salesforce_lead',\n id: l.Id,\n attributes: {\n email: l.Email,\n status: l.Status,\n source: l.LeadSource,\n convertedAt: parseDateMs(l.ConvertedDate),\n createdAt: parseDateMs(l.CreatedDate),\n },\n updated_at: parseDateMs(l.LastModifiedDate) ?? 0,\n });\n }\n }\n\n private async writeOpportunities(\n storage: StorageHandle,\n items: SalesforceOpportunity[],\n ): Promise<void> {\n for (const o of items) {\n await storage.entity({\n type: 'salesforce_opportunity',\n id: o.Id,\n attributes: {\n name: o.Name,\n stage: o.StageName,\n amount: o.Amount,\n closeDate: parseDateMs(o.CloseDate),\n ownerId: o.OwnerId,\n probability: o.Probability,\n forecastCategory: o.ForecastCategoryName,\n isClosed: o.IsClosed,\n isWon: o.IsWon,\n createdAt: parseDateMs(o.CreatedDate),\n },\n updated_at: parseDateMs(o.LastModifiedDate) ?? 0,\n });\n }\n }\n\n private async writeOpportunityEvents(\n storage: StorageHandle,\n items: SalesforceFieldHistory[],\n ): Promise<void> {\n for (const h of items) {\n const ts = parseDateMs(h.CreatedDate);\n if (ts === null) {\n continue;\n }\n await storage.event({\n name: STAGE_CHANGE_EVENT,\n start_ts: ts,\n end_ts: null,\n attributes: {\n historyId: h.Id,\n opportunityId: h.OpportunityId,\n fromStage: h.OldValue,\n toStage: h.NewValue,\n actorId: h.CreatedById,\n },\n });\n }\n }\n\n private async writePhase(\n storage: StorageHandle,\n phase: SalesforcePhase,\n items: unknown[],\n ): Promise<void> {\n switch (phase) {\n case 'users':\n return this.writeUsers(storage, items as SalesforceUser[]);\n case 'accounts':\n return this.writeAccounts(storage, items as SalesforceAccount[]);\n case 'leads':\n return this.writeLeads(storage, items as SalesforceLead[]);\n case 'opportunities':\n return this.writeOpportunities(\n storage,\n items as SalesforceOpportunity[],\n );\n case 'opportunity_events':\n return this.writeOpportunityEvents(\n storage,\n items as SalesforceFieldHistory[],\n );\n }\n }\n\n private async clearScopeOnFirstPage(\n storage: StorageHandle,\n phase: SalesforcePhase,\n isFull: boolean,\n ): Promise<void> {\n if (phase === 'opportunity_events') {\n if (isFull) {\n await storage.events([], { names: [STAGE_CHANGE_EVENT] });\n }\n return;\n }\n if (!isFull) {\n return;\n }\n const entityType = ENTITY_TYPE_BY_PHASE[phase];\n if (entityType) {\n await storage.entities([], { types: [entityType] });\n }\n }\n\n async sync(\n options: SyncOptions,\n storage: StorageHandle,\n signal?: AbortSignal,\n ): Promise<SyncResult> {\n const cursor = isSalesforceSyncCursor(options.cursor)\n ? options.cursor\n : undefined;\n const isFull = options.mode === 'full';\n\n const phases = selectActivePhases<SalesforceResource, SalesforcePhase>(\n (r) => r,\n PHASE_ORDER,\n this.settings.resources,\n );\n\n return paginateChunked<SalesforcePhase, string>({\n phases,\n cursor,\n signal,\n logger: this.logger,\n fetchPage: (phase, page, sig) =>\n this.fetchPage(phase, page, options, sig),\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);\n },\n });\n }\n}\n","import { SalesforceConnector } from './salesforce';\n\nexport {\n configFields,\n doc,\n SalesforceConnector,\n salesforceResources as resources,\n id,\n} from './salesforce';\nexport type { SalesforceSettings, SalesforceResource } from './salesforce';\nexport default SalesforceConnector;\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/map-concurrent.ts","../../../connector-shared/src/sanitize.ts","../../../connector-shared/src/epoch.ts","../../../connector-shared/src/pagination.ts","../../../connector-shared/src/logger.ts","../src/salesforce.ts","../src/index.ts"],"sourcesContent":["import type { HttpResponse } from './types';\n\nexport type HttpErrorKind =\n | 'transient'\n | 'rate_limit'\n | 'auth'\n | 'upstream_bug'\n | 'client_bug';\n\nexport abstract class HttpClientError extends Error {\n abstract readonly kind: HttpErrorKind;\n readonly response?: HttpResponse;\n\n constructor(message: string, response?: HttpResponse) {\n super(message);\n this.name = new.target.name;\n this.response = response;\n }\n}\n\nexport class TransientError extends HttpClientError {\n readonly kind = 'transient' as const;\n}\n\nexport class RateLimitError extends HttpClientError {\n readonly kind = 'rate_limit' as const;\n readonly retryAfter?: Date;\n\n constructor(message: string, response?: HttpResponse, retryAfter?: Date) {\n super(message, response);\n this.retryAfter = retryAfter;\n }\n}\n\nexport class AuthError extends HttpClientError {\n readonly kind = 'auth' as const;\n}\n\nexport class UpstreamBugError extends HttpClientError {\n readonly kind = 'upstream_bug' as const;\n}\n\nexport class ClientBugError extends HttpClientError {\n readonly kind = 'client_bug' as const;\n}\n\nexport function classifyStatus(status: number): HttpErrorKind {\n if (status === 429) {\n return 'rate_limit';\n }\n if (status === 401 || status === 403) {\n return 'auth';\n }\n if (status === 408) {\n return 'transient';\n }\n if (status >= 500) {\n return 'upstream_bug';\n }\n if (status >= 400) {\n return 'client_bug';\n }\n return 'client_bug';\n}\n\nexport function errorForStatus(\n message: string,\n response: HttpResponse,\n retryAfter?: Date,\n): HttpClientError {\n const kind = classifyStatus(response.status);\n switch (kind) {\n case 'rate_limit':\n return new RateLimitError(message, response, retryAfter);\n case 'auth':\n return new AuthError(message, response);\n case 'transient':\n return new TransientError(message, response);\n case 'upstream_bug':\n return new UpstreamBugError(message, response);\n case 'client_bug':\n return new ClientBugError(message, response);\n }\n}\n","import { HttpClientError, RateLimitError, TransientError } from './errors';\n\nexport interface RetryPolicy {\n maxAttempts?: number;\n initialDelayMs?: number;\n maxDelayMs?: number;\n retryOn?: (status: number | null, err?: Error) => boolean;\n}\n\nexport const defaultRetryOn = (status: number | null, err?: Error): boolean => {\n if (err instanceof RateLimitError) {\n return true;\n }\n if (err instanceof TransientError) {\n return true;\n }\n if (status === null) {\n return err instanceof Error && !(err instanceof HttpClientError);\n }\n if (status === 408 || status === 429) {\n return true;\n }\n if (status >= 500) {\n return true;\n }\n return false;\n};\n\nexport function backoffDelayMs(\n attempt: number,\n policy: Required<Pick<RetryPolicy, 'initialDelayMs' | 'maxDelayMs'>>,\n): number {\n const base = policy.initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, policy.maxDelayMs);\n}\n\nexport function parseRetryAfter(\n headerValue: string | null,\n now: Date = new Date(),\n): Date | undefined {\n if (!headerValue) {\n return undefined;\n }\n const trimmed = headerValue.trim();\n if (/^\\d+$/.test(trimmed)) {\n return new Date(now.getTime() + Number(trimmed) * 1000);\n }\n const parsed = Date.parse(trimmed);\n if (Number.isNaN(parsed)) {\n return undefined;\n }\n return new Date(parsed);\n}\n\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) {\n return Promise.reject(signal.reason ?? new Error('Aborted'));\n }\n return new Promise<void>((resolve, reject) => {\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal!.reason ?? new Error('Aborted'));\n };\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n","export const HTTP_CLIENT_VERSION = '0.0.0';\n\nexport const DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n\nexport function connectorUserAgent(connectorId: string): string {\n return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n}\n","import {\n AuthError,\n ClientBugError,\n HttpClientError,\n RateLimitError,\n TransientError,\n UpstreamBugError,\n errorForStatus,\n} from './errors';\nimport { defaultRetryOn, parseRetryAfter, sleep } from './retry';\nimport type { FetchLike, HttpMethod, HttpRequest, HttpResponse } from './types';\nimport { DEFAULT_USER_AGENT } from './version';\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_MAX_ATTEMPTS = 3;\nconst DEFAULT_INITIAL_DELAY_MS = 1000;\nconst DEFAULT_MAX_DELAY_MS = 60_000;\nconst OBSERVER_TIMEOUT_MS = 250;\n\nexport interface RequestObservation {\n url: string;\n method: HttpMethod;\n status: number;\n resource: string;\n requestId: string;\n body: unknown;\n}\n\nexport type RequestObserver = (\n event: RequestObservation,\n) => void | Promise<void>;\n\nexport interface RequestOptions {\n fetch?: FetchLike;\n observer?: RequestObserver;\n resource: string;\n requestId?: string;\n}\n\nasync function notifyObserver(\n observer: RequestObserver,\n event: RequestObservation,\n): Promise<void> {\n let result: void | Promise<void>;\n try {\n result = observer(event);\n } catch (err) {\n console.warn('[connector-shared] request observer threw:', err);\n return;\n }\n if (!(result instanceof Promise)) {\n return;\n }\n const guarded = result.catch((err) => {\n console.warn('[connector-shared] request observer rejected:', err);\n });\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, OBSERVER_TIMEOUT_MS);\n });\n try {\n await Promise.race([guarded, timeout]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n}\n\nfunction newRequestId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) {\n return c.randomUUID();\n }\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction mergeHeaders(\n defaults: Record<string, string>,\n overrides: Record<string, string> | undefined,\n): Record<string, string> {\n const merged: Record<string, string> = {};\n for (const [k, v] of Object.entries(defaults)) {\n merged[k.toLowerCase()] = v;\n }\n if (overrides) {\n for (const [k, v] of Object.entries(overrides)) {\n merged[k.toLowerCase()] = v;\n }\n }\n return merged;\n}\n\nfunction linkTimeoutSignal(\n parent: AbortSignal | undefined,\n timeoutMs: number,\n): { signal: AbortSignal; cancel: () => void } {\n const controller = new AbortController();\n const onParentAbort = () => {\n controller.abort(parent?.reason);\n };\n if (parent) {\n if (parent.aborted) {\n controller.abort(parent.reason);\n } else {\n parent.addEventListener('abort', onParentAbort, { once: true });\n }\n }\n const timer = setTimeout(() => {\n controller.abort(new Error(`Request timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n return {\n signal: controller.signal,\n cancel: () => {\n clearTimeout(timer);\n if (parent) {\n parent.removeEventListener('abort', onParentAbort);\n }\n },\n };\n}\n\nasync function readBody(res: Response, parseJson: boolean): Promise<unknown> {\n if (res.status === 204 || res.status === 205) {\n return null;\n }\n const contentType = res.headers.get('content-type') ?? '';\n if (parseJson && contentType.includes('application/json')) {\n const text = await res.text();\n if (text.length === 0) {\n return null;\n }\n return JSON.parse(text);\n }\n return res.text();\n}\n\nexport async function request<T = unknown>(\n req: HttpRequest,\n options: RequestOptions,\n): Promise<HttpResponse<T>> {\n const fetchImpl: FetchLike = options.fetch ?? (globalThis.fetch as FetchLike);\n const retry = req.retry ?? {};\n const maxAttempts = retry.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n const initialDelayMs = retry.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;\n const maxDelayMs = retry.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;\n const retryOn = retry.retryOn ?? defaultRetryOn;\n const timeoutMs = req.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const parseJson = req.parseJson ?? true;\n\n const headers = mergeHeaders(\n {\n 'User-Agent': DEFAULT_USER_AGENT,\n Accept: 'application/json',\n },\n req.headers,\n );\n\n let lastErr: Error | undefined;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n req.signal?.throwIfAborted();\n\n const { signal, cancel } = linkTimeoutSignal(req.signal, timeoutMs);\n let res: Response;\n try {\n res = await fetchImpl(req.url, {\n method: req.method ?? 'GET',\n headers,\n body: req.body as RequestInit['body'],\n signal,\n });\n } catch (err) {\n cancel();\n if (req.signal?.aborted) {\n throw req.signal.reason ?? err;\n }\n const error = err instanceof Error ? err : new Error(String(err));\n lastErr = error;\n if (attempt < maxAttempts - 1 && retryOn(null, error)) {\n const delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n await sleep(delay, req.signal);\n continue;\n }\n throw new TransientError(error.message);\n }\n cancel();\n\n const body = await readBody(res, parseJson);\n const httpResponse: HttpResponse<T> = {\n status: res.status,\n headers: res.headers,\n body: body as T,\n };\n if (req.rateLimit) {\n const state = req.rateLimit.parse(res.headers);\n if (state) {\n httpResponse.rateLimitState = state;\n }\n }\n\n if (options.observer) {\n await notifyObserver(options.observer, {\n url: req.url,\n method: req.method ?? 'GET',\n status: res.status,\n resource: options.resource,\n requestId: options.requestId ?? newRequestId(),\n body,\n });\n }\n\n if (res.ok) {\n return httpResponse;\n }\n\n const retryAfter = parseRetryAfter(res.headers.get('retry-after'));\n const message = `HTTP ${res.status} ${res.statusText} for ${req.method ?? 'GET'} ${req.url}`;\n const err = errorForStatus(message, httpResponse, retryAfter);\n\n if (\n attempt < maxAttempts - 1 &&\n retryOn(res.status, err) &&\n !(err instanceof AuthError) &&\n !(err instanceof ClientBugError)\n ) {\n lastErr = err;\n let delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n if (err instanceof RateLimitError && retryAfter) {\n const wait = retryAfter.getTime() - Date.now();\n if (wait > 0) {\n delay = Math.min(wait, maxDelayMs);\n }\n }\n await sleep(delay, req.signal);\n continue;\n }\n\n throw err;\n }\n\n throw lastErr ?? new UpstreamBugError('Exhausted retry attempts');\n}\n\nfunction computeDelay(\n attempt: number,\n initialDelayMs: number,\n maxDelayMs: number,\n): number {\n const base = initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, maxDelayMs);\n}\n\nexport { HttpClientError };\n","export interface RateLimitState {\n remaining: number;\n resetAt: Date;\n}\n\nexport interface RateLimitPolicy {\n parse(headers: Headers): RateLimitState | null;\n}\n\nexport interface StandardRateLimitPolicyConfig {\n remainingHeader: string;\n resetHeader: string;\n resetUnit: 's' | 'ms';\n resetFallbackMs?: number;\n}\n\nexport function standardRateLimitPolicy(\n config: StandardRateLimitPolicyConfig,\n): RateLimitPolicy {\n const { remainingHeader, resetHeader, resetUnit, resetFallbackMs } = config;\n const multiplier = resetUnit === 's' ? 1000 : 1;\n return {\n parse(h) {\n const remainingRaw = h.get(remainingHeader);\n if (remainingRaw === null || remainingRaw.trim() === '') {\n return null;\n }\n const remaining = Number(remainingRaw);\n if (!Number.isFinite(remaining)) {\n return null;\n }\n const resetRaw = h.get(resetHeader);\n if (resetRaw === null) {\n if (resetFallbackMs === undefined) {\n return null;\n }\n return {\n remaining,\n resetAt: new Date(Date.now() + resetFallbackMs),\n };\n }\n if (resetRaw.trim() === '') {\n return null;\n }\n const reset = Number(resetRaw);\n if (!Number.isFinite(reset) || reset < 0) {\n return null;\n }\n const resetMs = reset * multiplier;\n if (!Number.isFinite(resetMs)) {\n return null;\n }\n return { remaining, resetAt: new Date(resetMs) };\n },\n };\n}\n","export async function mapWithConcurrency<T, R>(\n items: readonly T[],\n concurrency: number,\n fn: (item: T, index: number) => Promise<R>,\n): Promise<R[]> {\n const results = new Array<R>(items.length);\n if (items.length === 0) {\n return results;\n }\n const normalized = Number.isFinite(concurrency) ? Math.floor(concurrency) : 1;\n const limit = Math.max(1, Math.min(normalized, items.length));\n let next = 0;\n let failed = false;\n\n async function worker(): Promise<void> {\n while (!failed) {\n const i = next++;\n if (i >= items.length) {\n return;\n }\n try {\n results[i] = await fn(items[i]!, i);\n } catch (err) {\n failed = true;\n throw err;\n }\n }\n }\n\n const workers: Promise<void>[] = [];\n for (let w = 0; w < limit; w++) {\n workers.push(worker());\n }\n await Promise.all(workers);\n return results;\n}\n","export interface SanitizeAllowedUrlOptions {\n url: string | null;\n host: string;\n pathname: string;\n protocol?: 'https:' | 'http:';\n}\n\nexport function sanitizeAllowedUrl(\n options: SanitizeAllowedUrlOptions,\n): string | null {\n const { url, host, pathname, protocol = 'https:' } = options;\n if (url === null) {\n return null;\n }\n try {\n const u = new URL(url);\n if (u.protocol !== protocol || u.host !== host || u.pathname !== pathname) {\n return null;\n }\n return u.toString();\n } catch {\n return null;\n }\n}\n","export type EpochUnit = 'ms' | 's' | 'iso';\n\nexport function parseEpoch(\n value: number | string | null | undefined,\n unit: EpochUnit,\n): number | null {\n if (value === null || value === undefined) {\n return null;\n }\n if (unit === 'iso') {\n if (typeof value !== 'string') {\n return null;\n }\n const ms = new Date(value).getTime();\n return Number.isFinite(ms) ? ms : null;\n }\n if (typeof value === 'string' && value.trim() === '') {\n return null;\n }\n const n = typeof value === 'number' ? value : Number(value);\n if (!Number.isFinite(n)) {\n return null;\n }\n const result = unit === 's' ? n * 1000 : n;\n return Number.isFinite(result) ? result : null;\n}\n","import { request } from './request';\nimport type { HttpRequest } from './types';\n\nexport function parseLinkHeader(header: string | null): Record<string, string> {\n if (!header) {\n return {};\n }\n const result: Record<string, string> = {};\n for (const part of header.split(',')) {\n const match = part.match(/<([^>]+)>\\s*;\\s*rel=\"([^\"]+)\"/);\n if (match) {\n result[match[2]!] = match[1]!;\n }\n }\n return result;\n}\n\nexport async function* paginateLink<T>(\n initial: HttpRequest,\n parse: (body: unknown) => T[],\n options: { resource: string },\n): AsyncIterable<T> {\n let next: string | null = initial.url;\n while (next) {\n const res: Awaited<ReturnType<typeof request>> = await request(\n {\n ...initial,\n url: next,\n },\n { resource: options.resource },\n );\n for (const item of parse(res.body)) {\n yield item;\n }\n const links = parseLinkHeader(res.headers.get('link'));\n next = links['next'] ?? null;\n }\n}\n\nexport async function* paginateCursor<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; nextCursor: string | null },\n buildNext: (req: HttpRequest, cursor: string) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let req: HttpRequest = initial;\n while (true) {\n const res = await request(req, { resource: options.resource });\n const { items, nextCursor } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!nextCursor) {\n return;\n }\n req = buildNext(req, nextCursor);\n }\n}\n\nexport async function* paginatePage<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; hasMore: boolean },\n buildPage: (req: HttpRequest, page: number) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let page = 1;\n while (true) {\n const req = page === 1 ? initial : buildPage(initial, page);\n const res = await request(req, { resource: options.resource });\n const { items, hasMore } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!hasMore || items.length === 0) {\n return;\n }\n page++;\n }\n}\n","export type LogFields = Record<string, unknown>;\n\nexport interface ConnectorLogger {\n info(event: string, fields?: LogFields): void;\n warn(event: string, fields?: LogFields): void;\n}\n\nexport interface ConnectorLoggerOptions {\n scope: string;\n}\n\nconst MAX_VALUE_LEN = 120;\n\nfunction truncate(s: string, max = MAX_VALUE_LEN): string {\n if (s.length <= max) {\n return s;\n }\n return `${s.slice(0, max - 1)}…`;\n}\n\nfunction formatValue(value: unknown): string {\n if (value === null) {\n return 'null';\n }\n if (value === undefined) {\n return '';\n }\n if (typeof value === 'number' || typeof value === 'boolean') {\n return String(value);\n }\n if (typeof value === 'string') {\n const t = truncate(value);\n if (/[\\s\"=]/.test(t)) {\n return JSON.stringify(t);\n }\n return t;\n }\n if (typeof value === 'bigint') {\n return value.toString();\n }\n let json: string | undefined;\n try {\n json = JSON.stringify(value);\n } catch {\n json = undefined;\n }\n return truncate(json ?? String(value));\n}\n\nexport function formatLogFields(fields?: LogFields): string {\n if (!fields) {\n return '';\n }\n const parts: string[] = [];\n for (const [k, v] of Object.entries(fields)) {\n if (v === undefined) {\n continue;\n }\n parts.push(`${k}=${formatValue(v)}`);\n }\n return parts.length > 0 ? ` ${parts.join(' ')}` : '';\n}\n\nexport function formatLogLine(\n scope: string,\n event: string,\n fields?: LogFields,\n): string {\n return `[${scope}] ${event}${formatLogFields(fields)}`;\n}\n\nexport function createDefaultConnectorLogger(\n opts: ConnectorLoggerOptions,\n): ConnectorLogger {\n return {\n info(event, fields) {\n console.info(formatLogLine(opts.scope, event, fields));\n },\n warn(event, fields) {\n console.warn(formatLogLine(opts.scope, event, fields));\n },\n };\n}\n\nconst NOOP_LOGGER: ConnectorLogger = {\n info() {},\n warn() {},\n};\n\nexport function noopConnectorLogger(): ConnectorLogger {\n return NOOP_LOGGER;\n}\n","import {\n type HttpResponse,\n connectorUserAgent,\n} from '@rawdash/connector-shared';\nimport {\n BaseConnector,\n type ConnectorContext,\n type ConnectorDoc,\n type CredentialsSchema,\n type FetchSpec,\n type FilterClause,\n type StorageHandle,\n type SyncOptions,\n type SyncResult,\n defineConfigFields,\n defineConnectorDoc,\n defineResources,\n makeChunkedCursorGuard,\n paginateChunked,\n schemasFromResources,\n selectActivePhases,\n} from '@rawdash/core';\nimport { z } from 'zod';\n\nexport const configFields = defineConfigFields(\n z.object({\n clientId: z.string().min(1).meta({\n label: 'Connected app consumer key',\n description:\n 'Consumer key (client ID) of the Salesforce Connected App used for OAuth 2.0 refresh-token exchange.',\n placeholder: '3MVG9...',\n }),\n clientSecret: z.object({ $secret: z.string().min(1) }).meta({\n label: 'Connected app consumer secret',\n description: 'Consumer secret of the Salesforce Connected App.',\n placeholder: 'SF_CLIENT_SECRET',\n secret: true,\n }),\n refreshToken: z.object({ $secret: z.string().min(1) }).meta({\n label: 'OAuth refresh token',\n description:\n 'OAuth 2.0 refresh token obtained from the Connected App authorization code flow. Stored as a secret.',\n placeholder: 'SF_REFRESH_TOKEN',\n secret: true,\n }),\n instanceUrl: z.string().url().meta({\n label: 'Instance URL',\n description:\n 'Salesforce instance URL, e.g. https://mycompany.my.salesforce.com. Returned alongside the refresh token from the OAuth flow; never use the generic login.salesforce.com URL here.',\n placeholder: 'https://mycompany.my.salesforce.com',\n }),\n apiVersion: z\n .string()\n .regex(/^\\d+\\.\\d+$/)\n .optional()\n .meta({\n label: 'REST API version',\n description:\n 'Salesforce REST API version, e.g. \"59.0\". Defaults to 59.0; bump to pick up newer SOQL semantics.',\n placeholder: '59.0',\n }),\n resources: z\n .array(\n z.enum([\n 'users',\n 'accounts',\n 'leads',\n 'opportunities',\n 'opportunity_events',\n ]),\n )\n .nonempty()\n .optional()\n .meta({\n label: 'Resources',\n description:\n 'Which Salesforce resources to sync. Omit to sync all resources. The Connected App only needs read access for the resources listed here.',\n }),\n }),\n);\n\nexport const doc: ConnectorDoc = defineConnectorDoc({\n displayName: 'Salesforce',\n category: 'sales',\n brandColor: '#00A1E0',\n tagline:\n 'Sync opportunities, opportunity stage-change events, accounts, leads, and users from a Salesforce org for pipeline, forecast, and quota-attainment dashboards.',\n vendor: {\n name: 'Salesforce',\n domain: 'salesforce.com',\n apiDocs:\n 'https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/',\n website: 'https://www.salesforce.com',\n },\n auth: {\n summary:\n 'OAuth 2.0 with a refresh token issued by a Salesforce Connected App. Requires the consumer key/secret, a refresh token, and the org instance URL.',\n setup: [\n 'In Salesforce, go to Setup → App Manager → New Connected App and check \"Enable OAuth Settings\".',\n 'Set the callback URL to a URL you control (e.g. https://localhost:8080/callback); it only has to be reachable when minting the initial refresh token.',\n 'Under Selected OAuth Scopes add \"Access and manage your data (api)\" and \"Perform requests on your behalf at any time (refresh_token, offline_access)\".',\n 'Save, then copy the Consumer Key (client ID) and Consumer Secret from the connected app detail page.',\n 'Authorize via https://login.salesforce.com/services/oauth2/authorize?response_type=code&client_id=<KEY>&redirect_uri=<URL> and exchange the resulting code at /services/oauth2/token to obtain a refresh token and the org instance_url.',\n 'Use the org instance URL from the token response (e.g. https://mycompany.my.salesforce.com), not login.salesforce.com.',\n 'Store the consumer secret and refresh token as rawdash secrets and reference them as secret(\"SF_CLIENT_SECRET\") and secret(\"SF_REFRESH_TOKEN\").',\n ],\n },\n rateLimit:\n 'Salesforce caps total API calls per org per 24 hours. Responses include a Sforce-Limit-Info header (api-usage=NN/MM); size sync intervals so the daily budget is not exhausted. The shared HTTP client retries on 429 with Retry-After.',\n limitations: [\n 'Custom objects are out of scope for v1; only the standard objects listed above are synced.',\n 'Salesforce Marketing Cloud is tracked under a separate connector.',\n ],\n});\n\nexport interface SalesforceSettings {\n instanceUrl: string;\n apiVersion?: string;\n resources?: readonly SalesforceResource[];\n}\n\nconst salesforceCredentials = {\n clientId: {\n description: 'Salesforce Connected App consumer key (client ID)',\n auth: 'required' as const,\n },\n clientSecret: {\n description: 'Salesforce Connected App consumer secret',\n auth: 'required' as const,\n },\n refreshToken: {\n description: 'Salesforce OAuth 2.0 refresh token',\n auth: 'required' as const,\n },\n} satisfies CredentialsSchema;\n\ntype SalesforceCredentials = typeof salesforceCredentials;\n\nconst PHASE_ORDER = [\n 'users',\n 'accounts',\n 'leads',\n 'opportunities',\n 'opportunity_events',\n] as const;\n\ntype SalesforcePhase = (typeof PHASE_ORDER)[number];\n\nexport type SalesforceResource = SalesforcePhase;\n\nconst isSalesforceSyncCursor = makeChunkedCursorGuard(PHASE_ORDER);\n\nconst ENTITY_TYPE_BY_PHASE: Partial<Record<SalesforcePhase, string>> = {\n users: 'salesforce_user',\n accounts: 'salesforce_account',\n leads: 'salesforce_lead',\n opportunities: 'salesforce_opportunity',\n};\n\nconst STAGE_CHANGE_EVENT = 'salesforce_opportunity_stage_change';\n\nconst DEFAULT_API_VERSION = '59.0';\n\ninterface QueryResponse<T> {\n totalSize: number;\n done: boolean;\n nextRecordsUrl?: string;\n records: T[];\n}\n\ninterface OauthTokenResponse {\n access_token: string;\n instance_url?: string;\n}\n\ninterface SalesforceUser {\n Id: string;\n Name: string | null;\n Email: string | null;\n IsActive: boolean;\n}\n\ninterface SalesforceAccount {\n Id: string;\n Name: string | null;\n Industry: string | null;\n AnnualRevenue: number | null;\n OwnerId: string | null;\n CreatedDate: string;\n LastModifiedDate: string;\n}\n\ninterface SalesforceLead {\n Id: string;\n Email: string | null;\n Status: string | null;\n LeadSource: string | null;\n ConvertedDate: string | null;\n CreatedDate: string;\n LastModifiedDate: string;\n}\n\ninterface SalesforceOpportunity {\n Id: string;\n Name: string | null;\n StageName: string | null;\n Amount: number | null;\n CloseDate: string | null;\n OwnerId: string | null;\n Probability: number | null;\n ForecastCategoryName: string | null;\n IsClosed: boolean;\n IsWon: boolean;\n CreatedDate: string;\n LastModifiedDate: string;\n}\n\ninterface SalesforceFieldHistory {\n Id: string;\n OpportunityId: string;\n Field: string;\n OldValue: string | null;\n NewValue: string | null;\n CreatedDate: string;\n CreatedById: string | null;\n}\n\nconst idString = z.string().min(1);\nconst isoDate = z.string().min(1);\n\nconst oauthTokenSchema = z.object({\n access_token: z.string().min(1),\n instance_url: z.string().optional(),\n});\n\nconst userSchema = z.object({\n Id: idString,\n Name: z.string().nullable(),\n Email: z.string().nullable(),\n IsActive: z.boolean(),\n});\n\nconst accountSchema = z.object({\n Id: idString,\n Name: z.string().nullable(),\n Industry: z.string().nullable(),\n AnnualRevenue: z.number().nullable(),\n OwnerId: z.string().nullable(),\n CreatedDate: isoDate,\n LastModifiedDate: isoDate,\n});\n\nconst leadSchema = z.object({\n Id: idString,\n Email: z.string().nullable(),\n Status: z.string().nullable(),\n LeadSource: z.string().nullable(),\n ConvertedDate: z.string().nullable(),\n CreatedDate: isoDate,\n LastModifiedDate: isoDate,\n});\n\nconst opportunitySchema = z.object({\n Id: idString,\n Name: z.string().nullable(),\n StageName: z.string().nullable(),\n Amount: z.number().nullable(),\n CloseDate: z.string().nullable(),\n OwnerId: z.string().nullable(),\n Probability: z.number().nullable(),\n ForecastCategoryName: z.string().nullable(),\n IsClosed: z.boolean(),\n IsWon: z.boolean(),\n CreatedDate: isoDate,\n LastModifiedDate: isoDate,\n});\n\nconst fieldHistorySchema = z.object({\n Id: idString,\n OpportunityId: idString,\n Field: z.string(),\n OldValue: z.string().nullable(),\n NewValue: z.string().nullable(),\n CreatedDate: isoDate,\n CreatedById: z.string().nullable(),\n});\n\nexport const salesforceResources = defineResources({\n salesforce_user: {\n shape: 'entity',\n filterable: [{ field: 'isActive', ops: ['eq'], values: ['true', 'false'] }],\n description:\n 'Salesforce users, keyed by user id, with name, email, and active state. Used to attribute opportunities, accounts, and stage changes to owners.',\n endpoint: 'GET /services/data/v{version}/query (SOQL: FROM User)',\n notes: 'Users are backfilled in full on every run; the table is small.',\n fields: [\n { name: 'name', description: 'Full name of the user.' },\n { name: 'email', description: 'User email address.' },\n { name: 'isActive', description: 'Whether the user is active.' },\n ],\n responses: {\n oauth_token: oauthTokenSchema,\n users: z.array(userSchema),\n },\n },\n salesforce_account: {\n shape: 'entity',\n filterable: [{ field: 'industry', ops: ['eq'] }],\n description:\n 'Accounts (companies), keyed by account id, with industry, annual revenue, owner, and creation time.',\n endpoint: 'GET /services/data/v{version}/query (SOQL: FROM Account)',\n notes: 'Upserts by id; incremental syncs filter on LastModifiedDate.',\n fields: [\n { name: 'name', description: 'Account name.' },\n { name: 'industry', description: 'Industry classification.' },\n {\n name: 'annualRevenue',\n description: 'Annual revenue in the org currency.',\n },\n { name: 'ownerId', description: 'User id of the account owner.' },\n {\n name: 'createdAt',\n description: 'Account creation time (Unix ms).',\n },\n ],\n responses: { accounts: z.array(accountSchema) },\n },\n salesforce_lead: {\n shape: 'entity',\n filterable: [\n { field: 'status', ops: ['eq'] },\n { field: 'source', ops: ['eq'] },\n ],\n description:\n 'Leads, keyed by lead id, with email, status, source, and conversion time.',\n endpoint: 'GET /services/data/v{version}/query (SOQL: FROM Lead)',\n notes: 'Upserts by id; incremental syncs filter on LastModifiedDate.',\n fields: [\n { name: 'email', description: 'Lead email address.' },\n { name: 'status', description: 'Lead status.' },\n { name: 'source', description: 'Lead source (LeadSource).' },\n {\n name: 'convertedAt',\n description: 'When the lead was converted (Unix ms), if any.',\n },\n { name: 'createdAt', description: 'Lead creation time (Unix ms).' },\n ],\n responses: { leads: z.array(leadSchema) },\n },\n salesforce_opportunity: {\n shape: 'entity',\n filterable: [\n { field: 'stage', ops: ['eq'] },\n { field: 'isClosed', ops: ['eq'], values: ['true', 'false'] },\n { field: 'isWon', ops: ['eq'], values: ['true', 'false'] },\n { field: 'forecastCategory', ops: ['eq'] },\n ],\n description:\n 'Opportunities, keyed by opportunity id, with stage, amount, close date, owner, probability, forecast category, and closed/won flags.',\n endpoint: 'GET /services/data/v{version}/query (SOQL: FROM Opportunity)',\n notes: 'Upserts by id; incremental syncs filter on LastModifiedDate.',\n fields: [\n { name: 'name', description: 'Opportunity name.' },\n { name: 'stage', description: 'Current StageName.' },\n { name: 'amount', description: 'Opportunity amount in org currency.' },\n { name: 'closeDate', description: 'Expected close date (Unix ms).' },\n { name: 'ownerId', description: 'User id of the opportunity owner.' },\n { name: 'probability', description: 'Win probability percentage.' },\n {\n name: 'forecastCategory',\n description: 'Forecast category name.',\n },\n { name: 'isClosed', description: 'Whether the opportunity is closed.' },\n { name: 'isWon', description: 'Whether the opportunity is won.' },\n {\n name: 'createdAt',\n description: 'Opportunity creation time (Unix ms).',\n },\n ],\n responses: { opportunities: z.array(opportunitySchema) },\n },\n salesforce_opportunity_stage_change: {\n shape: 'event',\n filterable: [],\n description:\n 'Opportunity stage transitions derived from OpportunityFieldHistory rows where Field = StageName. One event per transition, timestamped at the change CreatedDate.',\n endpoint:\n \"GET /services/data/v{version}/query (SOQL: FROM OpportunityFieldHistory WHERE Field = 'StageName')\",\n notes:\n 'Stage-change events are immutable; their scope is only cleared on a full sync so an incremental window does not drop history outside its range.',\n fields: [\n {\n name: 'historyId',\n description: 'OpportunityFieldHistory row id.',\n },\n {\n name: 'opportunityId',\n description: 'Id of the opportunity that changed stage.',\n },\n { name: 'fromStage', description: 'Previous StageName (OldValue).' },\n { name: 'toStage', description: 'New StageName (NewValue).' },\n {\n name: 'actorId',\n description: 'User id who made the change (CreatedById).',\n },\n ],\n responses: { opportunity_events: z.array(fieldHistorySchema) },\n },\n});\n\nconst PHASE_SOQL: Record<SalesforcePhase, string> = {\n users: 'SELECT Id, Name, Email, IsActive FROM User',\n accounts:\n 'SELECT Id, Name, Industry, AnnualRevenue, OwnerId, CreatedDate, LastModifiedDate FROM Account',\n leads:\n 'SELECT Id, Email, Status, LeadSource, ConvertedDate, CreatedDate, LastModifiedDate FROM Lead',\n opportunities:\n 'SELECT Id, Name, StageName, Amount, CloseDate, OwnerId, Probability, ForecastCategoryName, IsClosed, IsWon, CreatedDate, LastModifiedDate FROM Opportunity',\n opportunity_events:\n \"SELECT Id, OpportunityId, Field, OldValue, NewValue, CreatedDate, CreatedById FROM OpportunityFieldHistory WHERE Field = 'StageName'\",\n};\n\nconst PHASE_TIMESTAMP: Record<SalesforcePhase, string> = {\n users: '',\n accounts: 'LastModifiedDate',\n leads: 'LastModifiedDate',\n opportunities: 'LastModifiedDate',\n opportunity_events: 'CreatedDate',\n};\n\nfunction soqlDateLiteral(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) {\n return iso;\n }\n return `${d.toISOString().replace(/\\.\\d{3}Z$/, 'Z')}`;\n}\n\ninterface SoqlPushField {\n soqlField: string;\n type: 'string' | 'bool';\n}\n\nconst SOQL_PUSH_FIELDS: Partial<\n Record<SalesforcePhase, Record<string, SoqlPushField>>\n> = {\n users: { isActive: { soqlField: 'IsActive', type: 'bool' } },\n accounts: { industry: { soqlField: 'Industry', type: 'string' } },\n leads: {\n status: { soqlField: 'Status', type: 'string' },\n source: { soqlField: 'LeadSource', type: 'string' },\n },\n opportunities: {\n stage: { soqlField: 'StageName', type: 'string' },\n isClosed: { soqlField: 'IsClosed', type: 'bool' },\n isWon: { soqlField: 'IsWon', type: 'bool' },\n forecastCategory: { soqlField: 'ForecastCategoryName', type: 'string' },\n },\n};\n\nfunction soqlStringLiteral(value: string): string {\n return `'${value.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\")}'`;\n}\n\nfunction soqlPushConditions(\n phase: SalesforcePhase,\n filter: FilterClause[] | undefined,\n): string[] {\n const fields = SOQL_PUSH_FIELDS[phase];\n if (!fields || !filter) {\n return [];\n }\n const conditions: string[] = [];\n for (const clause of filter) {\n if (!('field' in clause) || clause.op !== 'eq') {\n continue;\n }\n const mapping = fields[clause.field];\n if (!mapping) {\n continue;\n }\n if (mapping.type === 'bool') {\n const truthy = clause.value === true || clause.value === 'true';\n conditions.push(`${mapping.soqlField} = ${truthy ? 'true' : 'false'}`);\n } else if (typeof clause.value === 'string') {\n conditions.push(\n `${mapping.soqlField} = ${soqlStringLiteral(clause.value)}`,\n );\n }\n }\n return conditions;\n}\n\nfunction buildPhaseSoql(\n phase: SalesforcePhase,\n since: string | undefined,\n pushConditions: string[] = [],\n): string {\n const base = PHASE_SOQL[phase];\n const timestampField = PHASE_TIMESTAMP[phase];\n const conditions = [...pushConditions];\n if (timestampField && since) {\n conditions.push(`${timestampField} >= ${soqlDateLiteral(since)}`);\n }\n let soql = base;\n if (conditions.length > 0) {\n const keyword = base.includes('WHERE') ? 'AND' : 'WHERE';\n soql += ` ${keyword} ${conditions.join(' AND ')}`;\n }\n return `${soql} ORDER BY ${timestampField || 'Id'} ASC`;\n}\n\nfunction parseDateMs(value: string | null | undefined): number | null {\n if (value === null || value === undefined || value === '') {\n return null;\n }\n const ms = Date.parse(value);\n return Number.isFinite(ms) ? ms : null;\n}\n\nexport const id = 'salesforce';\n\nexport class SalesforceConnector extends BaseConnector<\n SalesforceSettings,\n SalesforceCredentials\n> {\n static readonly id = id;\n\n static readonly resources = salesforceResources;\n\n static readonly schemas = schemasFromResources(salesforceResources);\n\n static create(input: unknown, ctx?: ConnectorContext): SalesforceConnector {\n const parsed = configFields.parse(input);\n return new SalesforceConnector(\n {\n instanceUrl: parsed.instanceUrl,\n apiVersion: parsed.apiVersion,\n resources: parsed.resources,\n },\n {\n clientId: parsed.clientId,\n clientSecret: parsed.clientSecret,\n refreshToken: parsed.refreshToken,\n },\n ctx,\n );\n }\n\n readonly id = id;\n override readonly credentials = salesforceCredentials;\n\n private accessToken: string | null = null;\n\n private apiVersion(): string {\n return this.settings.apiVersion ?? DEFAULT_API_VERSION;\n }\n\n private baseUrl(): string {\n return this.settings.instanceUrl.replace(/\\/+$/, '');\n }\n\n private async refreshAccessToken(signal?: AbortSignal): Promise<string> {\n const body = new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: this.creds.clientId,\n client_secret: this.creds.clientSecret,\n refresh_token: this.creds.refreshToken,\n }).toString();\n const res = await this.post<OauthTokenResponse>(\n `${this.baseUrl()}/services/oauth2/token`,\n {\n resource: 'oauth_token',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n 'User-Agent': connectorUserAgent('salesforce'),\n },\n body,\n signal,\n },\n );\n return res.body.access_token;\n }\n\n private async getAccessToken(signal?: AbortSignal): Promise<string> {\n if (!this.accessToken) {\n this.accessToken = await this.refreshAccessToken(signal);\n }\n return this.accessToken;\n }\n\n private async apiGet<T>(\n pathOrUrl: string,\n resource: string,\n signal?: AbortSignal,\n ): Promise<HttpResponse<T>> {\n const url = pathOrUrl.startsWith('http')\n ? pathOrUrl\n : `${this.baseUrl()}${pathOrUrl}`;\n const token = await this.getAccessToken(signal);\n return this.get<T>(url, {\n resource,\n headers: {\n Authorization: `Bearer ${token}`,\n Accept: 'application/json',\n 'User-Agent': connectorUserAgent('salesforce'),\n },\n signal,\n });\n }\n\n private sanitizeNextRecordsUrl(value: string | undefined): string | null {\n if (!value) {\n return null;\n }\n if (value.startsWith('/services/data/')) {\n return value;\n }\n try {\n const parsed = new URL(value);\n if (parsed.pathname.startsWith('/services/data/')) {\n return parsed.pathname + parsed.search;\n }\n } catch {\n // ignore — fall through\n }\n return null;\n }\n\n private async fetchPage(\n phase: SalesforcePhase,\n page: string | null,\n options: SyncOptions,\n signal: AbortSignal | undefined,\n ): Promise<{ items: unknown[]; next: string | null }> {\n let path: string;\n if (page) {\n const safePage = this.sanitizeNextRecordsUrl(page);\n if (!safePage) {\n throw new Error(`Invalid Salesforce cursor page: ${page}`);\n }\n path = safePage;\n } else {\n const resource = ENTITY_TYPE_BY_PHASE[phase];\n const specs = resource ? options.fetchSpecs?.[resource] : undefined;\n const spec: FetchSpec | undefined =\n specs && specs.length === 1 ? specs[0] : undefined;\n const soql = buildPhaseSoql(\n phase,\n options.since,\n soqlPushConditions(phase, spec?.filter),\n );\n const url = new URL(\n `${this.baseUrl()}/services/data/v${this.apiVersion()}/query`,\n );\n url.searchParams.set('q', soql);\n path = `${url.pathname}?${url.searchParams.toString()}`;\n }\n const res = await this.apiGet<QueryResponse<unknown>>(path, phase, signal);\n return {\n items: res.body.records,\n next: res.body.done\n ? null\n : this.sanitizeNextRecordsUrl(res.body.nextRecordsUrl),\n };\n }\n\n private async writeUsers(\n storage: StorageHandle,\n items: SalesforceUser[],\n ): Promise<void> {\n for (const u of items) {\n await storage.entity({\n type: 'salesforce_user',\n id: u.Id,\n attributes: {\n name: u.Name,\n email: u.Email,\n isActive: u.IsActive,\n },\n updated_at: 0,\n });\n }\n }\n\n private async writeAccounts(\n storage: StorageHandle,\n items: SalesforceAccount[],\n ): Promise<void> {\n for (const a of items) {\n await storage.entity({\n type: 'salesforce_account',\n id: a.Id,\n attributes: {\n name: a.Name,\n industry: a.Industry,\n annualRevenue: a.AnnualRevenue,\n ownerId: a.OwnerId,\n createdAt: parseDateMs(a.CreatedDate),\n },\n updated_at: parseDateMs(a.LastModifiedDate) ?? 0,\n });\n }\n }\n\n private async writeLeads(\n storage: StorageHandle,\n items: SalesforceLead[],\n ): Promise<void> {\n for (const l of items) {\n await storage.entity({\n type: 'salesforce_lead',\n id: l.Id,\n attributes: {\n email: l.Email,\n status: l.Status,\n source: l.LeadSource,\n convertedAt: parseDateMs(l.ConvertedDate),\n createdAt: parseDateMs(l.CreatedDate),\n },\n updated_at: parseDateMs(l.LastModifiedDate) ?? 0,\n });\n }\n }\n\n private async writeOpportunities(\n storage: StorageHandle,\n items: SalesforceOpportunity[],\n ): Promise<void> {\n for (const o of items) {\n await storage.entity({\n type: 'salesforce_opportunity',\n id: o.Id,\n attributes: {\n name: o.Name,\n stage: o.StageName,\n amount: o.Amount,\n closeDate: parseDateMs(o.CloseDate),\n ownerId: o.OwnerId,\n probability: o.Probability,\n forecastCategory: o.ForecastCategoryName,\n isClosed: o.IsClosed,\n isWon: o.IsWon,\n createdAt: parseDateMs(o.CreatedDate),\n },\n updated_at: parseDateMs(o.LastModifiedDate) ?? 0,\n });\n }\n }\n\n private async writeOpportunityEvents(\n storage: StorageHandle,\n items: SalesforceFieldHistory[],\n ): Promise<void> {\n for (const h of items) {\n const ts = parseDateMs(h.CreatedDate);\n if (ts === null) {\n continue;\n }\n await storage.event({\n name: STAGE_CHANGE_EVENT,\n start_ts: ts,\n end_ts: null,\n attributes: {\n historyId: h.Id,\n opportunityId: h.OpportunityId,\n fromStage: h.OldValue,\n toStage: h.NewValue,\n actorId: h.CreatedById,\n },\n });\n }\n }\n\n private async writePhase(\n storage: StorageHandle,\n phase: SalesforcePhase,\n items: unknown[],\n ): Promise<void> {\n switch (phase) {\n case 'users':\n return this.writeUsers(storage, items as SalesforceUser[]);\n case 'accounts':\n return this.writeAccounts(storage, items as SalesforceAccount[]);\n case 'leads':\n return this.writeLeads(storage, items as SalesforceLead[]);\n case 'opportunities':\n return this.writeOpportunities(\n storage,\n items as SalesforceOpportunity[],\n );\n case 'opportunity_events':\n return this.writeOpportunityEvents(\n storage,\n items as SalesforceFieldHistory[],\n );\n }\n }\n\n private async clearScopeOnFirstPage(\n storage: StorageHandle,\n phase: SalesforcePhase,\n isFull: boolean,\n ): Promise<void> {\n if (phase === 'opportunity_events') {\n if (isFull) {\n await storage.events([], { names: [STAGE_CHANGE_EVENT] });\n }\n return;\n }\n if (!isFull) {\n return;\n }\n const entityType = ENTITY_TYPE_BY_PHASE[phase];\n if (entityType) {\n await storage.entities([], { types: [entityType] });\n }\n }\n\n async sync(\n options: SyncOptions,\n storage: StorageHandle,\n signal?: AbortSignal,\n ): Promise<SyncResult> {\n const cursor = isSalesforceSyncCursor(options.cursor)\n ? options.cursor\n : undefined;\n const isFull = options.mode === 'full';\n\n const phases = selectActivePhases<SalesforceResource, SalesforcePhase>(\n (r) => r,\n PHASE_ORDER,\n this.settings.resources,\n );\n\n return paginateChunked<SalesforcePhase, string>({\n phases,\n cursor,\n signal,\n logger: this.logger,\n fetchPage: (phase, page, sig) =>\n this.fetchPage(phase, page, options, sig),\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);\n },\n });\n }\n}\n","import { SalesforceConnector } from './salesforce';\n\nexport {\n configFields,\n doc,\n SalesforceConnector,\n salesforceResources as resources,\n id,\n} from './salesforce';\nexport type { SalesforceSettings, SalesforceResource } from './salesforce';\nexport default SalesforceConnector;\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rawdash/connector-salesforce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Rawdash connector for Salesforce — opportunities, stage-change events, accounts, leads, and users",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"zod": "^4.4.3",
|
|
27
|
-
"@rawdash/core": "0.
|
|
27
|
+
"@rawdash/core": "0.24.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"fast-check": "^4.8.0",
|