@cerios/salesforce-sobjectly 0.0.0-stage → 1.0.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/LICENSE +8 -0
- package/README.md +231 -2
- package/dist/cli.cjs +188 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +196 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/codegen-BgDgU2RR.cjs +1209 -0
- package/dist/codegen-BgDgU2RR.cjs.map +1 -0
- package/dist/codegen-CSx0WAtK.mjs +1110 -0
- package/dist/codegen-CSx0WAtK.mjs.map +1 -0
- package/dist/codegen.cjs +23 -0
- package/dist/codegen.d.cts +312 -0
- package/dist/codegen.d.mts +312 -0
- package/dist/codegen.mjs +10 -0
- package/dist/describe-Dj3J8o-x.d.cts +284 -0
- package/dist/describe-Dj3J8o-x.d.mts +284 -0
- package/dist/index.cjs +65 -0
- package/dist/index.d.cts +2451 -0
- package/dist/index.d.mts +2451 -0
- package/dist/index.mjs +33 -0
- package/dist/index.mjs.map +1 -0
- package/dist/sf-cli-CtWtdkJ0.mjs +3986 -0
- package/dist/sf-cli-CtWtdkJ0.mjs.map +1 -0
- package/dist/sf-cli-DEVZQTR7.cjs +4344 -0
- package/dist/sf-cli-DEVZQTR7.cjs.map +1 -0
- package/package.json +112 -5
- package/sobjectly.config.schema.json +147 -0
|
@@ -0,0 +1,3986 @@
|
|
|
1
|
+
import { createSign } from "node:crypto";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
//#region \0rolldown/runtime.js
|
|
4
|
+
var __esmMin = (fn, res, err) => () => {
|
|
5
|
+
if (err) throw err[0];
|
|
6
|
+
try {
|
|
7
|
+
return fn && (res = fn(fn = 0)), res;
|
|
8
|
+
} catch (e) {
|
|
9
|
+
throw err = [e], e;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/shared.ts
|
|
15
|
+
/** Starts `load` with a signal of its own and returns a handle callers can `join`. */
|
|
16
|
+
function share(load) {
|
|
17
|
+
const controller = new AbortController();
|
|
18
|
+
let waiters = 0;
|
|
19
|
+
const promise = load(controller.signal);
|
|
20
|
+
promise.catch(() => void 0);
|
|
21
|
+
return {
|
|
22
|
+
promise,
|
|
23
|
+
get abandoned() {
|
|
24
|
+
return controller.signal.aborted;
|
|
25
|
+
},
|
|
26
|
+
join(signal) {
|
|
27
|
+
waiters++;
|
|
28
|
+
if (!signal) return promise;
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const leave = () => {
|
|
31
|
+
signal.removeEventListener("abort", onAbort);
|
|
32
|
+
};
|
|
33
|
+
const onAbort = () => {
|
|
34
|
+
leave();
|
|
35
|
+
waiters--;
|
|
36
|
+
if (waiters === 0) controller.abort(signal.reason);
|
|
37
|
+
reject(signal.reason);
|
|
38
|
+
};
|
|
39
|
+
if (signal.aborted) {
|
|
40
|
+
onAbort();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
44
|
+
promise.then((value) => {
|
|
45
|
+
leave();
|
|
46
|
+
resolve(value);
|
|
47
|
+
}, (error) => {
|
|
48
|
+
leave();
|
|
49
|
+
reject(error);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
var init_shared = __esmMin((() => {}));
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/cache.ts
|
|
58
|
+
var MetadataCache;
|
|
59
|
+
var init_cache = __esmMin((() => {
|
|
60
|
+
init_shared();
|
|
61
|
+
MetadataCache = class {
|
|
62
|
+
_enabled;
|
|
63
|
+
_entries = /* @__PURE__ */ new Map();
|
|
64
|
+
constructor(_enabled = true) {
|
|
65
|
+
this._enabled = _enabled;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Returns the cached value for `key`, or loads (and caches) it. `load` receives a signal of
|
|
69
|
+
* its own: aborting `signal` only stops this caller from waiting.
|
|
70
|
+
*/
|
|
71
|
+
getOrLoad(key, load, signal) {
|
|
72
|
+
if (!this._enabled) return load(signal);
|
|
73
|
+
let entry = this._entries.get(key);
|
|
74
|
+
if (!entry || entry.abandoned) {
|
|
75
|
+
const created = share((ownSignal) => load(ownSignal));
|
|
76
|
+
this._entries.set(key, created);
|
|
77
|
+
created.promise.catch(() => {
|
|
78
|
+
if (this._entries.get(key) === created) this._entries.delete(key);
|
|
79
|
+
});
|
|
80
|
+
entry = created;
|
|
81
|
+
}
|
|
82
|
+
return entry.join(signal);
|
|
83
|
+
}
|
|
84
|
+
/** Removes one entry, or every entry when `key` is omitted. */
|
|
85
|
+
clear(key) {
|
|
86
|
+
if (key === void 0) this._entries.clear();
|
|
87
|
+
else this._entries.delete(key);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}));
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/api-version.ts
|
|
93
|
+
/** `true` for a version such as `"v66.0"`. */
|
|
94
|
+
function isApiVersion(value) {
|
|
95
|
+
return typeof value === "string" && API_VERSION_PATTERN.test(value);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Throws a `TypeError` that explains how to set the version when `value` is missing or not a
|
|
99
|
+
* version such as `"v66.0"`. `hint` is appended to the message.
|
|
100
|
+
*/
|
|
101
|
+
function assertApiVersion(value, context, hint) {
|
|
102
|
+
if (value === void 0 || value === null || value === "") throw new TypeError(`${context}: apiVersion is required, e.g. "v66.0". ${hint}`);
|
|
103
|
+
if (!isApiVersion(value)) throw new TypeError(`${context}: invalid apiVersion ${JSON.stringify(value)}. Expected a value like "v66.0". ${hint}`);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Accepts the ways people write an API version (`"66"`, `"66.0"`, `"v66"`, `"v66.0"`) and
|
|
107
|
+
* returns the canonical form (`"v66.0"`), or `undefined` when the input is not a version.
|
|
108
|
+
*/
|
|
109
|
+
function normalizeApiVersion(input) {
|
|
110
|
+
const match = /^v?(\d+)(?:\.(\d+))?$/i.exec(input.trim());
|
|
111
|
+
if (!match) return;
|
|
112
|
+
return `v${Number(match[1])}.${Number(match[2] ?? 0)}`;
|
|
113
|
+
}
|
|
114
|
+
var API_VERSION_PATTERN;
|
|
115
|
+
var init_api_version = __esmMin((() => {
|
|
116
|
+
API_VERSION_PATTERN = /^v\d+\.\d+$/;
|
|
117
|
+
}));
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/errors.ts
|
|
120
|
+
function toRestErrors(body) {
|
|
121
|
+
return (Array.isArray(body) ? body : body !== null && typeof body === "object" ? [body] : []).filter((item) => typeof item === "object" && item !== null && "errorCode" in item && "message" in item);
|
|
122
|
+
}
|
|
123
|
+
function describeBody(body) {
|
|
124
|
+
if (body === void 0 || body === null) return "";
|
|
125
|
+
if (typeof body === "string") return body.slice(0, 500);
|
|
126
|
+
if (body instanceof Uint8Array) return `${body.byteLength} bytes`;
|
|
127
|
+
try {
|
|
128
|
+
return JSON.stringify(body).slice(0, 500);
|
|
129
|
+
} catch {
|
|
130
|
+
return "";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* `true` when `error` is a `SalesforceError` (non-2xx response), optionally with `code` as one of
|
|
135
|
+
* its error codes.
|
|
136
|
+
*/
|
|
137
|
+
function isSalesforceError(error, code) {
|
|
138
|
+
if (!(error instanceof SalesforceError)) return false;
|
|
139
|
+
return code === void 0 || error.errors.some((item) => item.errorCode === code);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* `true` when `error` carries error code `code`: a `SalesforceError` from a failed request, or a
|
|
143
|
+
* `SalesforceSaveError` with a failed item (save results, collections, actions). Handy for
|
|
144
|
+
* `catch` blocks and retry decisions, e.g. `hasErrorCode(error, "UNABLE_TO_LOCK_ROW")`.
|
|
145
|
+
*/
|
|
146
|
+
function hasErrorCode(error, code) {
|
|
147
|
+
if (error instanceof SalesforceError) return error.errors.some((item) => item.errorCode === code);
|
|
148
|
+
if (error instanceof SalesforceSaveError) return error.errors.some((item) => item.statusCode === code);
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
/** Runs chunked work; when a later chunk fails, the error keeps the results of the earlier ones. */
|
|
152
|
+
async function runChunks(chunks, run, message) {
|
|
153
|
+
const results = [];
|
|
154
|
+
for (const chunk of chunks) try {
|
|
155
|
+
results.push(...await run(chunk));
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (results.length === 0) throw error;
|
|
158
|
+
throw new SalesforcePartialFailureError(message, results, error);
|
|
159
|
+
}
|
|
160
|
+
return results;
|
|
161
|
+
}
|
|
162
|
+
var SalesforceError, SalesforceAuthError, SalesforceSaveError, SalesforceBulkJobError, SalesforcePartialFailureError;
|
|
163
|
+
var init_errors = __esmMin((() => {
|
|
164
|
+
SalesforceError = class extends Error {
|
|
165
|
+
name = "SalesforceError";
|
|
166
|
+
status;
|
|
167
|
+
method;
|
|
168
|
+
path;
|
|
169
|
+
/** The `errorCode` of the first error, e.g. `"MALFORMED_QUERY"` or `"INVALID_FIELD"`. */
|
|
170
|
+
errorCode;
|
|
171
|
+
/** All errors Salesforce returned, when the body was the standard error array. */
|
|
172
|
+
errors;
|
|
173
|
+
/** The parsed response body. */
|
|
174
|
+
body;
|
|
175
|
+
/** Response headers, without `set-cookie`. */
|
|
176
|
+
headers;
|
|
177
|
+
/** The value of the `Sforce-Limit-Info` header, e.g. `"api-usage=18/15000"`. */
|
|
178
|
+
limitInfo;
|
|
179
|
+
constructor(details) {
|
|
180
|
+
const errors = toRestErrors(details.body);
|
|
181
|
+
const first = errors[0];
|
|
182
|
+
const reason = first ? `${first.errorCode}: ${first.message}` : describeBody(details.body);
|
|
183
|
+
super(`Salesforce ${details.method} ${details.path} failed with status ${details.status}${reason ? ` - ${reason}` : ""}`);
|
|
184
|
+
this.status = details.status;
|
|
185
|
+
this.method = details.method;
|
|
186
|
+
this.path = details.path;
|
|
187
|
+
this.errors = errors;
|
|
188
|
+
this.errorCode = first?.errorCode;
|
|
189
|
+
this.body = details.body;
|
|
190
|
+
this.headers = details.headers;
|
|
191
|
+
this.limitInfo = details.headers["sforce-limit-info"];
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
SalesforceAuthError = class extends Error {
|
|
195
|
+
name = "SalesforceAuthError";
|
|
196
|
+
status;
|
|
197
|
+
/** The OAuth `error` code, e.g. `"invalid_client"` or `"invalid_grant"`. */
|
|
198
|
+
error;
|
|
199
|
+
errorDescription;
|
|
200
|
+
constructor(message, details = {}) {
|
|
201
|
+
super(message);
|
|
202
|
+
this.status = details.status;
|
|
203
|
+
this.error = details.error;
|
|
204
|
+
this.errorDescription = details.errorDescription;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
SalesforceSaveError = class extends Error {
|
|
208
|
+
name = "SalesforceSaveError";
|
|
209
|
+
/** Every item result, including the successful ones. */
|
|
210
|
+
results;
|
|
211
|
+
/** The errors of all failed items, flattened. */
|
|
212
|
+
errors;
|
|
213
|
+
constructor(message, results, errors) {
|
|
214
|
+
const first = errors[0];
|
|
215
|
+
super(first ? `${message} - ${first.statusCode}: ${first.message}` : message);
|
|
216
|
+
this.results = results;
|
|
217
|
+
this.errors = errors;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
SalesforceBulkJobError = class extends Error {
|
|
221
|
+
name = "SalesforceBulkJobError";
|
|
222
|
+
jobId;
|
|
223
|
+
state;
|
|
224
|
+
jobInfo;
|
|
225
|
+
constructor(message, jobId, state, jobInfo) {
|
|
226
|
+
super(message);
|
|
227
|
+
this.jobId = jobId;
|
|
228
|
+
this.state = state;
|
|
229
|
+
this.jobInfo = jobInfo;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
SalesforcePartialFailureError = class extends Error {
|
|
233
|
+
name = "SalesforcePartialFailureError";
|
|
234
|
+
completedResults;
|
|
235
|
+
cause;
|
|
236
|
+
constructor(message, completedResults, cause) {
|
|
237
|
+
super(`${message} (${completedResults.length} item(s) were already processed): ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
238
|
+
this.completedResults = completedResults;
|
|
239
|
+
this.cause = cause;
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
}));
|
|
243
|
+
//#endregion
|
|
244
|
+
//#region src/soql/escape.ts
|
|
245
|
+
/**
|
|
246
|
+
* Marks `sql` as a raw SOQL literal, e.g. a date literal such as `soqlLiteral("LAST_N_DAYS:30")`
|
|
247
|
+
* or `soqlLiteral("TODAY")`. Never pass untrusted input.
|
|
248
|
+
*/
|
|
249
|
+
function soqlLiteral(sql) {
|
|
250
|
+
if (typeof sql !== "string" || sql.trim().length === 0) throw new Error("soqlLiteral() requires a non-blank SOQL fragment.");
|
|
251
|
+
const literal = Object.freeze({ sql });
|
|
252
|
+
literals.add(literal);
|
|
253
|
+
return literal;
|
|
254
|
+
}
|
|
255
|
+
/** `true` for values created with `soqlLiteral` (and the helpers built on it). */
|
|
256
|
+
function isSoqlLiteral(value) {
|
|
257
|
+
return typeof value === "object" && value !== null && literals.has(value);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Formats a value as a SOQL literal. Strings are quoted with backslashes, quotes and control
|
|
261
|
+
* characters escaped, which prevents SOQL injection for values from user or external input.
|
|
262
|
+
* In `LIKE` patterns `%` and `_` stay wildcards; use {@link soqlLike} to match them literally.
|
|
263
|
+
*
|
|
264
|
+
* A `Date` becomes a DateTime literal (`yyyy-MM-ddTHH:mm:ssZ`, UTC), which only works against
|
|
265
|
+
* DateTime fields. Use {@link soqlDate} for Date-only fields (e.g. `Birthdate`).
|
|
266
|
+
*/
|
|
267
|
+
function soqlEscape(value) {
|
|
268
|
+
if (value === null) return "null";
|
|
269
|
+
if (value instanceof Date) return formatDateTime$1(value);
|
|
270
|
+
if (typeof value === "object") {
|
|
271
|
+
if (isSoqlLiteral(value)) return value.sql;
|
|
272
|
+
throw new TypeError("Objects can't be used as SOQL values; use soqlLiteral() for raw SOQL.");
|
|
273
|
+
}
|
|
274
|
+
if (typeof value === "number") return formatNumber(value);
|
|
275
|
+
if (typeof value === "boolean") return String(value);
|
|
276
|
+
if (typeof value !== "string") throw new TypeError(`Unsupported SOQL value of type ${typeof value}.`);
|
|
277
|
+
return `'${escapeText(value)}'`;
|
|
278
|
+
}
|
|
279
|
+
/** Escapes quotes, backslashes and control characters for use inside a quoted SOQL string. */
|
|
280
|
+
function escapeText(value) {
|
|
281
|
+
return value.replace(/[\\'"\n\r\t\b\f\u0000-\u001f]/g, (char) => {
|
|
282
|
+
switch (char) {
|
|
283
|
+
case "\\": return "\\\\";
|
|
284
|
+
case "'": return "\\'";
|
|
285
|
+
case "\"": return "\\\"";
|
|
286
|
+
case "\n": return "\\n";
|
|
287
|
+
case "\r": return "\\r";
|
|
288
|
+
case " ": return "\\t";
|
|
289
|
+
case "\b": return "\\b";
|
|
290
|
+
case "\f": return "\\f";
|
|
291
|
+
default: throw new Error(`SOQL strings can't contain the control character U+${char.charCodeAt(0).toString(16).padStart(4, "0")}.`);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
/** Formats a number without exponent notation, which SOQL doesn't accept (`1e21`, `1e-7`). */
|
|
296
|
+
function formatNumber(value) {
|
|
297
|
+
if (!Number.isFinite(value)) throw new Error(`Cannot use ${value} in a SOQL query.`);
|
|
298
|
+
const plain = String(value);
|
|
299
|
+
if (!/e/i.test(plain)) return Object.is(value, -0) ? "0" : plain;
|
|
300
|
+
const [, sign = "", whole = "", fraction = "", exponent = "0"] = /^(-?)(\d+)(?:\.(\d+))?e([+-]\d+)$/i.exec(plain) ?? [];
|
|
301
|
+
const digits = whole + fraction;
|
|
302
|
+
const point = whole.length + Number(exponent);
|
|
303
|
+
if (point <= 0) return `${sign}0.${"0".repeat(-point)}${digits}`;
|
|
304
|
+
if (point >= digits.length) return sign + digits + "0".repeat(point - digits.length);
|
|
305
|
+
return `${sign}${digits.slice(0, point)}.${digits.slice(point)}`;
|
|
306
|
+
}
|
|
307
|
+
function assertYear(year, value) {
|
|
308
|
+
if (Number.isNaN(year)) throw new Error("Invalid Date can't be used in a SOQL query.");
|
|
309
|
+
if (year < MIN_YEAR || year > MAX_YEAR) throw new Error(`Salesforce only accepts dates from ${MIN_YEAR} to ${MAX_YEAR}, got ${value.toISOString()}.`);
|
|
310
|
+
}
|
|
311
|
+
/** `yyyy-MM-ddTHH:mm:ssZ` in UTC, one of the DateTime formats SOQL documents. */
|
|
312
|
+
function formatDateTime$1(value) {
|
|
313
|
+
assertYear(value.getUTCFullYear(), value);
|
|
314
|
+
return `${value.toISOString().slice(0, 19)}Z`;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Formats a `Date` as a Date-only literal (`yyyy-MM-dd`), for use against Date fields. Uses the
|
|
318
|
+
* UTC date unless `local` is set, in which case the date in the local time zone is used.
|
|
319
|
+
*/
|
|
320
|
+
function soqlEscapeDateOnly(value, options = {}) {
|
|
321
|
+
if (!options.local) {
|
|
322
|
+
assertYear(value.getUTCFullYear(), value);
|
|
323
|
+
return value.toISOString().slice(0, 10);
|
|
324
|
+
}
|
|
325
|
+
assertYear(value.getFullYear(), value);
|
|
326
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
327
|
+
return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}`;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* A Date-only literal for the typed `where` helpers, e.g. `where("Birthdate", ">", soqlDate(d))`.
|
|
331
|
+
* Accepts a `Date` or a `yyyy-MM-dd` string. A `Date` uses its UTC date; pass `{ local: true }`
|
|
332
|
+
* to use the date in the local time zone (e.g. for `new Date(2026, 0, 31)`).
|
|
333
|
+
*/
|
|
334
|
+
function soqlDate(value, options = {}) {
|
|
335
|
+
if (typeof value === "string") {
|
|
336
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) throw new Error(`soqlDate() expects a yyyy-MM-dd string, got "${value}".`);
|
|
337
|
+
return soqlLiteral(value);
|
|
338
|
+
}
|
|
339
|
+
return soqlLiteral(soqlEscapeDateOnly(value, options));
|
|
340
|
+
}
|
|
341
|
+
function soqlDateLiteral(name, n) {
|
|
342
|
+
if (FIXED_DATE_LITERALS.has(name)) return soqlLiteral(name);
|
|
343
|
+
if (!/^(LAST|NEXT)_N_|_AGO$/.test(name) || !/^[A-Z_]+$/.test(name)) throw new Error(`Unknown SOQL date literal "${name}".`);
|
|
344
|
+
if (n === void 0 || !Number.isSafeInteger(n) || n < 0) throw new Error(`soqlDateLiteral("${name}", n) requires a non-negative integer n.`);
|
|
345
|
+
return soqlLiteral(`${name}:${n}`);
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Escapes a term for a SOSL `FIND {...}` clause by backslash-escaping the reserved characters
|
|
349
|
+
* `? & | ! { } [ ] ( ) ^ ~ * : \ " ' + -`.
|
|
350
|
+
*/
|
|
351
|
+
function soslEscape(term) {
|
|
352
|
+
return term.replace(/[?&|!{}[\]()^~*:\\"'+-]/g, (match) => `\\${match}`);
|
|
353
|
+
}
|
|
354
|
+
var literals, MIN_YEAR, MAX_YEAR, FIXED_DATE_LITERALS;
|
|
355
|
+
var init_escape = __esmMin((() => {
|
|
356
|
+
literals = /* @__PURE__ */ new WeakSet();
|
|
357
|
+
MIN_YEAR = 1700;
|
|
358
|
+
MAX_YEAR = 4e3;
|
|
359
|
+
FIXED_DATE_LITERALS = /* @__PURE__ */ new Set([
|
|
360
|
+
"YESTERDAY",
|
|
361
|
+
"TODAY",
|
|
362
|
+
"TOMORROW",
|
|
363
|
+
"LAST_WEEK",
|
|
364
|
+
"THIS_WEEK",
|
|
365
|
+
"NEXT_WEEK",
|
|
366
|
+
"LAST_MONTH",
|
|
367
|
+
"THIS_MONTH",
|
|
368
|
+
"NEXT_MONTH",
|
|
369
|
+
"LAST_90_DAYS",
|
|
370
|
+
"NEXT_90_DAYS",
|
|
371
|
+
"THIS_QUARTER",
|
|
372
|
+
"LAST_QUARTER",
|
|
373
|
+
"NEXT_QUARTER",
|
|
374
|
+
"THIS_YEAR",
|
|
375
|
+
"LAST_YEAR",
|
|
376
|
+
"NEXT_YEAR",
|
|
377
|
+
"THIS_FISCAL_QUARTER",
|
|
378
|
+
"LAST_FISCAL_QUARTER",
|
|
379
|
+
"NEXT_FISCAL_QUARTER",
|
|
380
|
+
"THIS_FISCAL_YEAR",
|
|
381
|
+
"LAST_FISCAL_YEAR",
|
|
382
|
+
"NEXT_FISCAL_YEAR"
|
|
383
|
+
]);
|
|
384
|
+
}));
|
|
385
|
+
//#endregion
|
|
386
|
+
//#region src/http/connection.ts
|
|
387
|
+
function hasErrorCodeIn(response, codes) {
|
|
388
|
+
const body = parseBody(response, "auto");
|
|
389
|
+
return Array.isArray(body) && body.some((error) => codes.includes(error.errorCode));
|
|
390
|
+
}
|
|
391
|
+
async function collect(chunks) {
|
|
392
|
+
const parts = [];
|
|
393
|
+
let length = 0;
|
|
394
|
+
for await (const chunk of chunks) {
|
|
395
|
+
parts.push(chunk);
|
|
396
|
+
length += chunk.byteLength;
|
|
397
|
+
}
|
|
398
|
+
const result = new Uint8Array(length);
|
|
399
|
+
let offset = 0;
|
|
400
|
+
for (const part of parts) {
|
|
401
|
+
result.set(part, offset);
|
|
402
|
+
offset += part.byteLength;
|
|
403
|
+
}
|
|
404
|
+
return result;
|
|
405
|
+
}
|
|
406
|
+
function resolveRetry(retry) {
|
|
407
|
+
if (!retry) return;
|
|
408
|
+
const options = retry === true ? {} : retry;
|
|
409
|
+
return {
|
|
410
|
+
retries: options.retries ?? 2,
|
|
411
|
+
statusCodes: options.statusCodes ?? [
|
|
412
|
+
429,
|
|
413
|
+
502,
|
|
414
|
+
503,
|
|
415
|
+
504
|
|
416
|
+
],
|
|
417
|
+
errorCodes: options.errorCodes ?? [],
|
|
418
|
+
methods: options.methods ?? ["GET", "HEAD"],
|
|
419
|
+
baseDelayMs: options.baseDelayMs ?? 500,
|
|
420
|
+
maxDelayMs: options.maxDelayMs ?? 3e4
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
function appendQuery(url, query) {
|
|
424
|
+
if (!query) return;
|
|
425
|
+
for (const [key, value] of Object.entries(query)) {
|
|
426
|
+
if (value === void 0 || value === null) continue;
|
|
427
|
+
if (value instanceof Date) url.searchParams.append(key, value.toISOString());
|
|
428
|
+
else if (Array.isArray(value)) url.searchParams.append(key, value.join(","));
|
|
429
|
+
else url.searchParams.append(key, String(value));
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function encodeRequest(request, defaultHeaders) {
|
|
433
|
+
const headers = new Headers(defaultHeaders);
|
|
434
|
+
for (const [key, value] of Object.entries(request.headers ?? {})) headers.set(key, value);
|
|
435
|
+
const responseType = request.responseType ?? "auto";
|
|
436
|
+
if (!headers.has("Accept")) headers.set("Accept", responseType === "auto" || responseType === "json" ? "application/json" : "*/*");
|
|
437
|
+
const { body } = request;
|
|
438
|
+
if (body === void 0 || body === null) return {
|
|
439
|
+
headers,
|
|
440
|
+
body: void 0
|
|
441
|
+
};
|
|
442
|
+
if (typeof body === "string") {
|
|
443
|
+
setDefault(headers, "Content-Type", "text/plain; charset=utf-8");
|
|
444
|
+
return {
|
|
445
|
+
headers,
|
|
446
|
+
body
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
const bytes = toBytes(body);
|
|
450
|
+
if (bytes) {
|
|
451
|
+
setDefault(headers, "Content-Type", "application/octet-stream");
|
|
452
|
+
return {
|
|
453
|
+
headers,
|
|
454
|
+
body: bytes
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
if (body instanceof URLSearchParams) {
|
|
458
|
+
setDefault(headers, "Content-Type", "application/x-www-form-urlencoded");
|
|
459
|
+
return {
|
|
460
|
+
headers,
|
|
461
|
+
body: body.toString()
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
assertSerializable(body);
|
|
465
|
+
setDefault(headers, "Content-Type", "application/json");
|
|
466
|
+
return {
|
|
467
|
+
headers,
|
|
468
|
+
body: JSON.stringify(body)
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
/** Binary bodies: `Uint8Array` (incl. `Buffer`), other typed arrays, `DataView` and `ArrayBuffer`. */
|
|
472
|
+
function toBytes(body) {
|
|
473
|
+
if (body instanceof Uint8Array) return body;
|
|
474
|
+
if (body instanceof ArrayBuffer) return new Uint8Array(body);
|
|
475
|
+
if (ArrayBuffer.isView(body)) return new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
|
|
476
|
+
}
|
|
477
|
+
/** Rejects bodies that `JSON.stringify` would silently turn into `{}`. */
|
|
478
|
+
function assertSerializable(body) {
|
|
479
|
+
if (typeof Blob !== "undefined" && body instanceof Blob || typeof FormData !== "undefined" && body instanceof FormData || typeof ReadableStream !== "undefined" && body instanceof ReadableStream) throw new TypeError("Unsupported request body: Blob, FormData and streams can't be sent. Pass a Uint8Array (see buildMultipart for multipart).");
|
|
480
|
+
}
|
|
481
|
+
/** Runs an observability hook; a throwing hook must never change the outcome of a request. */
|
|
482
|
+
function callHook(hook) {
|
|
483
|
+
try {
|
|
484
|
+
hook();
|
|
485
|
+
} catch {}
|
|
486
|
+
}
|
|
487
|
+
function setDefault(headers, name, value) {
|
|
488
|
+
if (!headers.has(name)) headers.set(name, value);
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Parses a response body according to `responseType` and the `Content-Type` header. With
|
|
492
|
+
* `strict`, a body that should be JSON but isn't throws instead of being returned as text.
|
|
493
|
+
*/
|
|
494
|
+
function parseBody(response, responseType, strict = false) {
|
|
495
|
+
if (responseType === "binary") return response.body;
|
|
496
|
+
if (response.body.byteLength === 0) return responseType === "text" ? "" : void 0;
|
|
497
|
+
const contentType = (response.headers.get("content-type") ?? "").toLowerCase();
|
|
498
|
+
if (responseType === "auto" && !isTextContentType(contentType)) return response.body;
|
|
499
|
+
const text = decoder$1.decode(response.body);
|
|
500
|
+
if (responseType === "text") return text;
|
|
501
|
+
if (responseType === "json" || contentType.includes("json")) try {
|
|
502
|
+
return JSON.parse(text);
|
|
503
|
+
} catch {
|
|
504
|
+
if (strict) throw new Error(`Salesforce returned invalid JSON (status ${response.status}, content-type "${contentType}"): ${text.slice(0, 200)}`);
|
|
505
|
+
return text;
|
|
506
|
+
}
|
|
507
|
+
return text;
|
|
508
|
+
}
|
|
509
|
+
function isTextContentType(contentType) {
|
|
510
|
+
return contentType === "" || contentType.includes("json") || contentType.startsWith("text/") || contentType.includes("xml") || contentType.includes("csv");
|
|
511
|
+
}
|
|
512
|
+
function combineSignals(signal, timeoutMs) {
|
|
513
|
+
if (timeoutMs <= 0) return signal;
|
|
514
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
515
|
+
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
516
|
+
}
|
|
517
|
+
function redactHeaders(headers) {
|
|
518
|
+
const result = headersToObject(headers);
|
|
519
|
+
if (result.authorization) result.authorization = "Bearer [REDACTED]";
|
|
520
|
+
return result;
|
|
521
|
+
}
|
|
522
|
+
/** Converts headers to a plain object with lowercase names, dropping `set-cookie`. */
|
|
523
|
+
function headersToObject(headers) {
|
|
524
|
+
const result = {};
|
|
525
|
+
headers.forEach((value, key) => {
|
|
526
|
+
if (key.toLowerCase() !== "set-cookie") result[key.toLowerCase()] = value;
|
|
527
|
+
});
|
|
528
|
+
return result;
|
|
529
|
+
}
|
|
530
|
+
function stripQuery(path) {
|
|
531
|
+
const index = path.indexOf("?");
|
|
532
|
+
return index === -1 ? path : path.slice(0, index);
|
|
533
|
+
}
|
|
534
|
+
function parseRetryAfter(value) {
|
|
535
|
+
if (!value) return;
|
|
536
|
+
const seconds = Number(value);
|
|
537
|
+
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
|
|
538
|
+
const date = Date.parse(value);
|
|
539
|
+
return Number.isNaN(date) ? void 0 : Math.max(0, date - Date.now());
|
|
540
|
+
}
|
|
541
|
+
function sleep(ms, signal) {
|
|
542
|
+
return new Promise((resolve, reject) => {
|
|
543
|
+
if (signal?.aborted) {
|
|
544
|
+
reject(signal.reason);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
const timer = setTimeout(() => {
|
|
548
|
+
signal?.removeEventListener("abort", onAbort);
|
|
549
|
+
resolve();
|
|
550
|
+
}, ms);
|
|
551
|
+
const onAbort = () => {
|
|
552
|
+
clearTimeout(timer);
|
|
553
|
+
reject(signal?.reason);
|
|
554
|
+
};
|
|
555
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
/** An external id value as a path segment; numbers are written without exponent notation. */
|
|
559
|
+
function externalIdText(value) {
|
|
560
|
+
return typeof value === "number" ? formatNumber(value) : value;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Encodes one URL path segment (an sObject name, id or external id value). `.` and `..` are
|
|
564
|
+
* rejected: URL parsing would resolve them and change which resource is called.
|
|
565
|
+
*/
|
|
566
|
+
function segment(value) {
|
|
567
|
+
if (value === "." || value === "..") throw new Error(`"${value}" is not a valid id or name in a URL path.`);
|
|
568
|
+
return encodeURIComponent(value);
|
|
569
|
+
}
|
|
570
|
+
var DEFAULT_TIMEOUT_MS, decoder$1, SalesforceConnection;
|
|
571
|
+
var init_connection = __esmMin((() => {
|
|
572
|
+
init_api_version();
|
|
573
|
+
init_errors();
|
|
574
|
+
init_escape();
|
|
575
|
+
DEFAULT_TIMEOUT_MS = 12e4;
|
|
576
|
+
decoder$1 = new TextDecoder();
|
|
577
|
+
SalesforceConnection = class {
|
|
578
|
+
apiVersion;
|
|
579
|
+
_auth;
|
|
580
|
+
_transport;
|
|
581
|
+
_timeoutMs;
|
|
582
|
+
_retry;
|
|
583
|
+
_allowedOrigins;
|
|
584
|
+
_headers;
|
|
585
|
+
_hooks;
|
|
586
|
+
_apiUsage;
|
|
587
|
+
constructor(options) {
|
|
588
|
+
assertApiVersion(options.apiVersion, "SalesforceClient", "Pass it as { apiVersion }, e.g. the API_VERSION constant exported by your generated sObject types.");
|
|
589
|
+
this.apiVersion = options.apiVersion;
|
|
590
|
+
this._auth = options.auth;
|
|
591
|
+
this._transport = options.transport;
|
|
592
|
+
this._timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
593
|
+
this._retry = resolveRetry(options.retry);
|
|
594
|
+
this._allowedOrigins = new Set((options.allowedOrigins ?? []).map((origin) => new URL(origin).origin));
|
|
595
|
+
this._headers = options.headers ?? {};
|
|
596
|
+
this._hooks = options.hooks ?? {};
|
|
597
|
+
}
|
|
598
|
+
/** API usage as reported by the last response's `Sforce-Limit-Info` header. */
|
|
599
|
+
get apiUsage() {
|
|
600
|
+
return this._apiUsage;
|
|
601
|
+
}
|
|
602
|
+
get transport() {
|
|
603
|
+
return this._transport;
|
|
604
|
+
}
|
|
605
|
+
/** Returns the instance URL of the current token (authenticating if needed). */
|
|
606
|
+
async instanceUrl(signal) {
|
|
607
|
+
const token = await this._auth.getToken({
|
|
608
|
+
transport: this._transport,
|
|
609
|
+
signal
|
|
610
|
+
});
|
|
611
|
+
return new URL(token.instanceUrl).origin;
|
|
612
|
+
}
|
|
613
|
+
/** Sends a request and returns the parsed body. Throws `SalesforceError` for non-2xx responses. */
|
|
614
|
+
async request(request) {
|
|
615
|
+
return (await this.send(request)).data;
|
|
616
|
+
}
|
|
617
|
+
/** Sends a request and returns status, headers and the parsed body. */
|
|
618
|
+
async send(request) {
|
|
619
|
+
const method = request.method ?? "GET";
|
|
620
|
+
const timeoutMs = request.timeoutMs ?? this._timeoutMs;
|
|
621
|
+
let token = await this.getToken(request.signal, timeoutMs);
|
|
622
|
+
let reauthenticated = false;
|
|
623
|
+
let retries = 0;
|
|
624
|
+
for (let attempt = 1;; attempt++) {
|
|
625
|
+
let response;
|
|
626
|
+
try {
|
|
627
|
+
response = await this.sendOnce(request, method, token, attempt, timeoutMs);
|
|
628
|
+
} catch (error) {
|
|
629
|
+
if (this.canRetryFailure(method, request, retries)) {
|
|
630
|
+
retries++;
|
|
631
|
+
await sleep(this.retryDelay(void 0, retries), request.signal);
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
throw error;
|
|
635
|
+
}
|
|
636
|
+
if (response.status === 401 && !reauthenticated && this._auth.invalidate) {
|
|
637
|
+
reauthenticated = true;
|
|
638
|
+
const next = await this.reauthenticate(token, request.signal, timeoutMs);
|
|
639
|
+
if (next.accessToken !== token.accessToken) {
|
|
640
|
+
token = next;
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
if (this.shouldRetry(method, request, response, retries)) {
|
|
645
|
+
retries++;
|
|
646
|
+
await sleep(this.retryDelay(response, retries), request.signal);
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
return this.toResult(request, method, response);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
/** Gets a token, bounded by the caller's signal and the request timeout. */
|
|
653
|
+
getToken(signal, timeoutMs) {
|
|
654
|
+
return this._auth.getToken({
|
|
655
|
+
transport: this._transport,
|
|
656
|
+
signal: combineSignals(signal, timeoutMs)
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
reauthenticate(token, signal, timeoutMs) {
|
|
660
|
+
this._auth.invalidate?.(token);
|
|
661
|
+
return this.getToken(signal, timeoutMs);
|
|
662
|
+
}
|
|
663
|
+
/** Resolves a request path against the instance URL, enforcing the origin allowlist. */
|
|
664
|
+
resolveUrl(path, instanceUrl, query) {
|
|
665
|
+
const base = new URL(instanceUrl);
|
|
666
|
+
let url;
|
|
667
|
+
if (/^https?:\/\//i.test(path)) url = new URL(path);
|
|
668
|
+
else if (path.startsWith("/services/")) url = new URL(path, base.origin);
|
|
669
|
+
else url = new URL(`/services/data/${this.apiVersion}${path.startsWith("/") ? path : `/${path}`}`, base.origin);
|
|
670
|
+
if (url.origin !== base.origin && !this._allowedOrigins.has(url.origin)) throw new Error(`Refusing to send a Salesforce request to ${url.origin}: only the instance origin ${base.origin} and allowedOrigins are permitted.`);
|
|
671
|
+
appendQuery(url, query);
|
|
672
|
+
return url;
|
|
673
|
+
}
|
|
674
|
+
async sendOnce(request, method, token, attempt, timeoutMs) {
|
|
675
|
+
const signal = combineSignals(request.signal, timeoutMs);
|
|
676
|
+
const prepared = this.prepare(request, method, token, attempt, signal, timeoutMs);
|
|
677
|
+
const response = await this._transport.send(prepared.transportRequest);
|
|
678
|
+
this.afterResponse(prepared.event, response.status, response.headers);
|
|
679
|
+
return response;
|
|
680
|
+
}
|
|
681
|
+
prepare(request, method, token, attempt, signal, timeoutMs) {
|
|
682
|
+
const url = this.resolveUrl(request.path, token.instanceUrl, request.query);
|
|
683
|
+
const { headers, body } = encodeRequest(request, this._headers);
|
|
684
|
+
headers.set("Authorization", `Bearer ${token.accessToken}`);
|
|
685
|
+
signal?.throwIfAborted();
|
|
686
|
+
const event = {
|
|
687
|
+
method,
|
|
688
|
+
url: url.toString(),
|
|
689
|
+
headers: redactHeaders(headers),
|
|
690
|
+
attempt
|
|
691
|
+
};
|
|
692
|
+
callHook(() => this._hooks.onRequest?.(event));
|
|
693
|
+
return {
|
|
694
|
+
transportRequest: {
|
|
695
|
+
method,
|
|
696
|
+
url,
|
|
697
|
+
headers,
|
|
698
|
+
body,
|
|
699
|
+
signal,
|
|
700
|
+
timeoutMs: timeoutMs > 0 ? timeoutMs : void 0
|
|
701
|
+
},
|
|
702
|
+
event: {
|
|
703
|
+
...event,
|
|
704
|
+
started: Date.now()
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
afterResponse(event, status, headers) {
|
|
709
|
+
this.trackApiUsage(headers);
|
|
710
|
+
const { started, ...requestEvent } = event;
|
|
711
|
+
callHook(() => this._hooks.onResponse?.({
|
|
712
|
+
...requestEvent,
|
|
713
|
+
status,
|
|
714
|
+
durationMs: Date.now() - started,
|
|
715
|
+
responseHeaders: headersToObject(headers)
|
|
716
|
+
}));
|
|
717
|
+
}
|
|
718
|
+
toResult(request, method, response) {
|
|
719
|
+
if (response.status < 200 || response.status >= 300) throw this.errorFor(request, method, response);
|
|
720
|
+
const data = parseBody(response, request.responseType ?? "auto", true);
|
|
721
|
+
return {
|
|
722
|
+
status: response.status,
|
|
723
|
+
headers: response.headers,
|
|
724
|
+
data
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
errorFor(request, method, response) {
|
|
728
|
+
return new SalesforceError({
|
|
729
|
+
status: response.status,
|
|
730
|
+
method,
|
|
731
|
+
path: stripQuery(request.path),
|
|
732
|
+
body: parseBody(response, "auto"),
|
|
733
|
+
headers: headersToObject(response.headers)
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Sends a GET request and returns the body as a stream of chunks, when the transport supports
|
|
738
|
+
* streaming (`fetchTransport` does). Returns `undefined` otherwise, so callers can fall back to
|
|
739
|
+
* `send`. Non-2xx responses throw `SalesforceError`; the request is re-authenticated once on 401.
|
|
740
|
+
*/
|
|
741
|
+
async stream(request) {
|
|
742
|
+
const stream = this._transport.stream?.bind(this._transport);
|
|
743
|
+
if (!stream) return;
|
|
744
|
+
const method = request.method ?? "GET";
|
|
745
|
+
const timeoutMs = request.timeoutMs ?? this._timeoutMs;
|
|
746
|
+
let token = await this.getToken(request.signal, timeoutMs);
|
|
747
|
+
let reauthenticated = false;
|
|
748
|
+
let retries = 0;
|
|
749
|
+
for (let attempt = 1;; attempt++) {
|
|
750
|
+
const response = await this.openStream(stream, request, method, token, attempt, timeoutMs);
|
|
751
|
+
if (response.status >= 200 && response.status < 300) return response;
|
|
752
|
+
const buffered = {
|
|
753
|
+
status: response.status,
|
|
754
|
+
headers: response.headers,
|
|
755
|
+
body: await collect(response.body)
|
|
756
|
+
};
|
|
757
|
+
if (response.status === 401 && !reauthenticated && this._auth.invalidate) {
|
|
758
|
+
reauthenticated = true;
|
|
759
|
+
const next = await this.reauthenticate(token, request.signal, timeoutMs);
|
|
760
|
+
if (next.accessToken !== token.accessToken) {
|
|
761
|
+
token = next;
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
if (this.shouldRetry(method, request, buffered, retries)) {
|
|
766
|
+
retries++;
|
|
767
|
+
await sleep(this.retryDelay(buffered, retries), request.signal);
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
throw this.errorFor(request, method, buffered);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Opens a streamed response. The timeout only covers waiting for the response headers, so a
|
|
775
|
+
* consumer that reads a large body slowly is not cut off.
|
|
776
|
+
*/
|
|
777
|
+
async openStream(stream, request, method, token, attempt, timeoutMs) {
|
|
778
|
+
const headerTimeout = new AbortController();
|
|
779
|
+
const timer = timeoutMs > 0 ? setTimeout(() => headerTimeout.abort(new DOMException(`The request timed out after ${timeoutMs} ms.`, "TimeoutError")), timeoutMs) : void 0;
|
|
780
|
+
const signal = request.signal ? AbortSignal.any([request.signal, headerTimeout.signal]) : headerTimeout.signal;
|
|
781
|
+
try {
|
|
782
|
+
const prepared = this.prepare(request, method, token, attempt, signal, 0);
|
|
783
|
+
const response = await stream(prepared.transportRequest);
|
|
784
|
+
this.afterResponse(prepared.event, response.status, response.headers);
|
|
785
|
+
return response;
|
|
786
|
+
} finally {
|
|
787
|
+
clearTimeout(timer);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
shouldRetry(method, request, response, retries) {
|
|
791
|
+
const retry = this._retry;
|
|
792
|
+
if (!retry || retries >= retry.retries || request.retry === false) return false;
|
|
793
|
+
if (retry.statusCodes.includes(response.status) && (request.retry === true || retry.methods.includes(method))) return true;
|
|
794
|
+
return retry.errorCodes.length > 0 && response.status >= 400 && hasErrorCodeIn(response, retry.errorCodes);
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Network failures and per-attempt timeouts (the transport threw) are retried for idempotent
|
|
798
|
+
* requests when retries are enabled; a caller abort never is.
|
|
799
|
+
*/
|
|
800
|
+
canRetryFailure(method, request, retries) {
|
|
801
|
+
const retry = this._retry;
|
|
802
|
+
if (!retry || retries >= retry.retries || request.retry === false || request.signal?.aborted) return false;
|
|
803
|
+
return request.retry === true || retry.methods.includes(method);
|
|
804
|
+
}
|
|
805
|
+
retryDelay(response, retries) {
|
|
806
|
+
const retry = this._retry;
|
|
807
|
+
const retryAfter = parseRetryAfter(response?.headers.get("retry-after") ?? null);
|
|
808
|
+
const backoff = retry.baseDelayMs * 2 ** (retries - 1);
|
|
809
|
+
return Math.min(retryAfter ?? backoff, retry.maxDelayMs);
|
|
810
|
+
}
|
|
811
|
+
trackApiUsage(headers) {
|
|
812
|
+
const match = /(?:^|[\s,;])api-usage=(\d+)\/(\d+)/.exec(headers.get("sforce-limit-info") ?? "");
|
|
813
|
+
if (match) this._apiUsage = {
|
|
814
|
+
used: Number(match[1]),
|
|
815
|
+
max: Number(match[2])
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
}));
|
|
820
|
+
//#endregion
|
|
821
|
+
//#region src/http/fetch-transport.ts
|
|
822
|
+
/** The default transport, built on the platform `fetch` (Node.js 20+). Supports streaming. */
|
|
823
|
+
function fetchTransport(options = {}) {
|
|
824
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
825
|
+
if (typeof fetchImpl !== "function") throw new TypeError("No global fetch is available. Use Node.js 20+ or pass a fetch implementation.");
|
|
826
|
+
const call = (request) => fetchImpl(request.url, {
|
|
827
|
+
method: request.method,
|
|
828
|
+
headers: request.headers,
|
|
829
|
+
body: request.body,
|
|
830
|
+
signal: request.signal,
|
|
831
|
+
redirect: "manual"
|
|
832
|
+
});
|
|
833
|
+
return {
|
|
834
|
+
async send(request) {
|
|
835
|
+
const response = await call(request);
|
|
836
|
+
const body = new Uint8Array(await response.arrayBuffer());
|
|
837
|
+
return {
|
|
838
|
+
status: response.status,
|
|
839
|
+
headers: response.headers,
|
|
840
|
+
body
|
|
841
|
+
};
|
|
842
|
+
},
|
|
843
|
+
async stream(request) {
|
|
844
|
+
const response = await call(request);
|
|
845
|
+
return {
|
|
846
|
+
status: response.status,
|
|
847
|
+
headers: response.headers,
|
|
848
|
+
body: chunks$1(response)
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
async function* chunks$1(response) {
|
|
854
|
+
if (!response.body) return;
|
|
855
|
+
const reader = response.body.getReader();
|
|
856
|
+
let finished = false;
|
|
857
|
+
try {
|
|
858
|
+
for (;;) {
|
|
859
|
+
const { done, value } = await reader.read();
|
|
860
|
+
if (done) {
|
|
861
|
+
finished = true;
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
yield value;
|
|
865
|
+
}
|
|
866
|
+
} finally {
|
|
867
|
+
if (!finished) await reader.cancel().catch(() => void 0);
|
|
868
|
+
reader.releaseLock();
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
var init_fetch_transport = __esmMin((() => {}));
|
|
872
|
+
//#endregion
|
|
873
|
+
//#region src/resources/actions.ts
|
|
874
|
+
function actionResults(error) {
|
|
875
|
+
if (!(error instanceof SalesforceError) || error.status !== 400 || !Array.isArray(error.body) || error.body.length === 0) return;
|
|
876
|
+
const items = error.body;
|
|
877
|
+
const isResult = (item) => typeof item === "object" && item !== null && "isSuccess" in item;
|
|
878
|
+
return items.every(isResult) ? items : void 0;
|
|
879
|
+
}
|
|
880
|
+
var ActionsApi;
|
|
881
|
+
var init_actions = __esmMin((() => {
|
|
882
|
+
init_errors();
|
|
883
|
+
init_connection();
|
|
884
|
+
ActionsApi = class {
|
|
885
|
+
_connection;
|
|
886
|
+
constructor(_connection) {
|
|
887
|
+
this._connection = _connection;
|
|
888
|
+
}
|
|
889
|
+
async listStandard(options = {}) {
|
|
890
|
+
return (await this._connection.request({
|
|
891
|
+
path: "/actions/standard",
|
|
892
|
+
signal: options.signal
|
|
893
|
+
})).actions;
|
|
894
|
+
}
|
|
895
|
+
async listCustom(type, options = {}) {
|
|
896
|
+
return (await this._connection.request({
|
|
897
|
+
path: `/actions/custom/${segment(type)}`,
|
|
898
|
+
signal: options.signal
|
|
899
|
+
})).actions;
|
|
900
|
+
}
|
|
901
|
+
describeStandard(name, options = {}) {
|
|
902
|
+
return this._connection.request({
|
|
903
|
+
path: `/actions/standard/${segment(name)}`,
|
|
904
|
+
signal: options.signal
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
describeCustom(type, name, options = {}) {
|
|
908
|
+
return this._connection.request({
|
|
909
|
+
path: `/actions/custom/${segment(type)}/${segment(name)}`,
|
|
910
|
+
signal: options.signal
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
/** Invokes a standard action, e.g. `"emailSimple"` or `"chatterPost"`. One result per input. */
|
|
914
|
+
invokeStandard(name, inputs, options) {
|
|
915
|
+
return this.invoke(`/actions/standard/${segment(name)}`, name, inputs, options);
|
|
916
|
+
}
|
|
917
|
+
/** Invokes a custom action of `type`. One result per input. */
|
|
918
|
+
invokeCustom(type, name, inputs, options) {
|
|
919
|
+
return this.invoke(`/actions/custom/${segment(type)}/${segment(name)}`, name, inputs, options);
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Runs an autolaunched Flow by API name. Input keys are the Flow's input variable names;
|
|
923
|
+
* `outputValues` holds its output variables plus `Flow__InterviewStatus`.
|
|
924
|
+
*/
|
|
925
|
+
invokeFlow(apiName, inputs, options) {
|
|
926
|
+
return this.invokeCustom("flow", apiName, inputs, options);
|
|
927
|
+
}
|
|
928
|
+
/** Invokes an `@InvocableMethod` of an Apex class (`Namespace__ClassName` for managed packages). */
|
|
929
|
+
invokeApex(className, inputs, options) {
|
|
930
|
+
return this.invokeCustom("apex", className, inputs, options);
|
|
931
|
+
}
|
|
932
|
+
async invoke(path, name, inputs, options = {}) {
|
|
933
|
+
if (inputs.length === 0) throw new Error(`Invoking "${name}" requires at least one input (use [{}] for actions without inputs).`);
|
|
934
|
+
let results;
|
|
935
|
+
try {
|
|
936
|
+
results = await this._connection.request({
|
|
937
|
+
method: "POST",
|
|
938
|
+
path,
|
|
939
|
+
body: { inputs },
|
|
940
|
+
signal: options.signal
|
|
941
|
+
});
|
|
942
|
+
} catch (error) {
|
|
943
|
+
const recovered = actionResults(error);
|
|
944
|
+
if (!recovered) throw error;
|
|
945
|
+
results = recovered;
|
|
946
|
+
}
|
|
947
|
+
if (options.throwOnError !== false) {
|
|
948
|
+
const failed = results.filter((result) => !result.isSuccess);
|
|
949
|
+
if (failed.length > 0) throw new SalesforceSaveError(`Action "${name}" failed for ${failed.length} of ${results.length} input(s)`, results, failed.flatMap((result) => result.errors ?? []));
|
|
950
|
+
}
|
|
951
|
+
return results;
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
}));
|
|
955
|
+
//#endregion
|
|
956
|
+
//#region src/resources/approvals.ts
|
|
957
|
+
function requireId$1(id) {
|
|
958
|
+
if (typeof id !== "string" || id.trim().length === 0) throw new Error("A record or work item id is required.");
|
|
959
|
+
return id;
|
|
960
|
+
}
|
|
961
|
+
var WORKITEM_PREFIX, ApprovalsApi;
|
|
962
|
+
var init_approvals = __esmMin((() => {
|
|
963
|
+
init_errors();
|
|
964
|
+
init_escape();
|
|
965
|
+
WORKITEM_PREFIX = "04i";
|
|
966
|
+
ApprovalsApi = class {
|
|
967
|
+
_connection;
|
|
968
|
+
_queries;
|
|
969
|
+
constructor(_connection, _queries) {
|
|
970
|
+
this._connection = _connection;
|
|
971
|
+
this._queries = _queries;
|
|
972
|
+
}
|
|
973
|
+
/** Lists the approval processes per sObject. */
|
|
974
|
+
async list(options = {}) {
|
|
975
|
+
return (await this._connection.request({
|
|
976
|
+
path: "/process/approvals/",
|
|
977
|
+
signal: options.signal
|
|
978
|
+
})).approvals;
|
|
979
|
+
}
|
|
980
|
+
/** Submits a record for approval. */
|
|
981
|
+
submit(recordId, options = {}) {
|
|
982
|
+
return this.process({
|
|
983
|
+
actionType: "Submit",
|
|
984
|
+
contextId: requireId$1(recordId),
|
|
985
|
+
comments: options.comments,
|
|
986
|
+
nextApproverIds: options.nextApproverIds,
|
|
987
|
+
processDefinitionNameOrId: options.processDefinitionNameOrId,
|
|
988
|
+
skipEntryCriteria: options.skipEntryCriteria,
|
|
989
|
+
contextActorId: options.submitterId
|
|
990
|
+
}, options);
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Approves a pending step. Pass the work item id (`04i...`), or the record id to approve its
|
|
994
|
+
* single pending work item.
|
|
995
|
+
*/
|
|
996
|
+
async approve(recordOrWorkitemId, options = {}) {
|
|
997
|
+
const contextId = await this.resolveWorkitem(recordOrWorkitemId, options.signal);
|
|
998
|
+
return this.process({
|
|
999
|
+
actionType: "Approve",
|
|
1000
|
+
contextId,
|
|
1001
|
+
comments: options.comments,
|
|
1002
|
+
nextApproverIds: options.nextApproverIds
|
|
1003
|
+
}, options);
|
|
1004
|
+
}
|
|
1005
|
+
/** Rejects a pending step. Accepts a work item id or a record id, like `approve`. */
|
|
1006
|
+
async reject(recordOrWorkitemId, options = {}) {
|
|
1007
|
+
const contextId = await this.resolveWorkitem(recordOrWorkitemId, options.signal);
|
|
1008
|
+
return this.process({
|
|
1009
|
+
actionType: "Reject",
|
|
1010
|
+
contextId,
|
|
1011
|
+
comments: options.comments
|
|
1012
|
+
}, options);
|
|
1013
|
+
}
|
|
1014
|
+
/** The pending work items of a record (visible to the current user). */
|
|
1015
|
+
async pending(recordId, options = {}) {
|
|
1016
|
+
const soql = `SELECT Id, ActorId, ProcessInstanceId, ProcessInstance.TargetObjectId FROM ProcessInstanceWorkitem WHERE ProcessInstance.TargetObjectId = ${soqlEscape(requireId$1(recordId))} AND ProcessInstance.Status = 'Pending'`;
|
|
1017
|
+
return (await this._queries.collect(soql, { signal: options.signal })).map((row) => ({
|
|
1018
|
+
id: row.Id,
|
|
1019
|
+
actorId: row.ActorId,
|
|
1020
|
+
processInstanceId: row.ProcessInstanceId,
|
|
1021
|
+
targetObjectId: row.ProcessInstance?.TargetObjectId ?? recordId
|
|
1022
|
+
}));
|
|
1023
|
+
}
|
|
1024
|
+
async resolveWorkitem(id, signal) {
|
|
1025
|
+
if (requireId$1(id).startsWith(WORKITEM_PREFIX)) return id;
|
|
1026
|
+
const items = await this.pending(id, { signal });
|
|
1027
|
+
if (items.length === 0) throw new Error(`Record ${id} has no pending approval work item visible to the current user.`);
|
|
1028
|
+
if (items.length > 1) throw new Error(`Record ${id} has ${items.length} pending work items (${items.map((item) => item.id).join(", ")}); pass the work item id instead.`);
|
|
1029
|
+
return items[0].id;
|
|
1030
|
+
}
|
|
1031
|
+
async process(request, options) {
|
|
1032
|
+
const body = Object.fromEntries(Object.entries(request).filter(([, value]) => value !== void 0));
|
|
1033
|
+
const result = await this.send(request, body, options.signal);
|
|
1034
|
+
if (options.throwOnError !== false && !result.success) throw new SalesforceSaveError(`Approval ${request.actionType} failed`, [result], result.errors ?? []);
|
|
1035
|
+
return result;
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Sends one approval request. Failures such as `NO_APPLICABLE_PROCESS` or `ALREADY_IN_PROCESS`
|
|
1039
|
+
* come back as HTTP 400 with an error array; they become an unsuccessful result.
|
|
1040
|
+
*/
|
|
1041
|
+
async send(request, body, signal) {
|
|
1042
|
+
let raw;
|
|
1043
|
+
try {
|
|
1044
|
+
[raw] = await this._connection.request({
|
|
1045
|
+
method: "POST",
|
|
1046
|
+
path: "/process/approvals/",
|
|
1047
|
+
body: { requests: [body] },
|
|
1048
|
+
signal
|
|
1049
|
+
});
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
if (error instanceof SalesforceError && error.status === 400 && error.errors.length > 0) return {
|
|
1052
|
+
actorIds: [],
|
|
1053
|
+
entityId: request.contextId,
|
|
1054
|
+
errors: error.errors.map((item) => ({
|
|
1055
|
+
statusCode: item.errorCode,
|
|
1056
|
+
message: item.message,
|
|
1057
|
+
fields: item.fields ?? []
|
|
1058
|
+
})),
|
|
1059
|
+
instanceId: "",
|
|
1060
|
+
instanceStatus: "",
|
|
1061
|
+
newWorkitemIds: [],
|
|
1062
|
+
success: false
|
|
1063
|
+
};
|
|
1064
|
+
throw error;
|
|
1065
|
+
}
|
|
1066
|
+
if (!raw) throw new Error(`Approval ${request.actionType} returned no result.`);
|
|
1067
|
+
const { newWorkItemIds, ...rest } = raw;
|
|
1068
|
+
return {
|
|
1069
|
+
...rest,
|
|
1070
|
+
newWorkitemIds: rest.newWorkitemIds ?? newWorkItemIds ?? []
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
}));
|
|
1075
|
+
//#endregion
|
|
1076
|
+
//#region src/resources/csv.ts
|
|
1077
|
+
/**
|
|
1078
|
+
* Serializes records to Bulk API CSV. Columns are the union of all keys in first-seen order.
|
|
1079
|
+
* `null` becomes `#N/A` (clears the field), `undefined` stays empty (leaves it unchanged),
|
|
1080
|
+
* `Date` becomes an ISO string (a Date field takes its UTC date), the text `"#N/A"` is written so
|
|
1081
|
+
* that it is stored as text rather than as null, and one level of nesting becomes a relationship column, so
|
|
1082
|
+
* `{ Account: { External_Id__c: "A1" } }` is written as column `Account.External_Id__c`.
|
|
1083
|
+
*/
|
|
1084
|
+
function toCsv(records, options = {}) {
|
|
1085
|
+
const delimiter = options.delimiter ?? ",";
|
|
1086
|
+
const newline = options.lineEnding === "CRLF" ? "\r\n" : "\n";
|
|
1087
|
+
const rows = records.map((record) => flattenRecord(record));
|
|
1088
|
+
const columns = [];
|
|
1089
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1090
|
+
for (const row of rows) for (const key of Object.keys(row)) if (!seen.has(key)) {
|
|
1091
|
+
seen.add(key);
|
|
1092
|
+
columns.push(key);
|
|
1093
|
+
}
|
|
1094
|
+
if (columns.length === 0) throw new Error("Cannot build CSV: the records have no fields.");
|
|
1095
|
+
const lines = [columns.map((column) => quote(column, delimiter)).join(delimiter)];
|
|
1096
|
+
for (const row of rows) lines.push(columns.map((column) => quote(formatValue(row[column]), delimiter)).join(delimiter));
|
|
1097
|
+
return lines.join(newline) + newline;
|
|
1098
|
+
}
|
|
1099
|
+
/** Parses CSV (RFC 4180: quoted fields, escaped quotes, CRLF or LF) into rows of strings. */
|
|
1100
|
+
function parseCsvRows(text, delimiter = ",") {
|
|
1101
|
+
const parser = new CsvRowParser(delimiter);
|
|
1102
|
+
return [...parser.push(text), ...parser.end()];
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Parses streamed CSV (with a header row) into objects keyed by column name, yielding each row
|
|
1106
|
+
* as soon as it is complete.
|
|
1107
|
+
*/
|
|
1108
|
+
async function* parseCsvStream(chunks, delimiter = ",") {
|
|
1109
|
+
const decoder = new TextDecoder();
|
|
1110
|
+
const parser = new CsvRowParser(delimiter);
|
|
1111
|
+
let header;
|
|
1112
|
+
const toRecords = function* (rows) {
|
|
1113
|
+
for (const row of rows) if (!header) header = row;
|
|
1114
|
+
else if (!isBlankLine(header, row)) yield toRecord(header, row);
|
|
1115
|
+
};
|
|
1116
|
+
for await (const chunk of chunks) yield* toRecords(parser.push(decoder.decode(chunk, { stream: true })));
|
|
1117
|
+
yield* toRecords([...parser.push(decoder.decode()), ...parser.end()]);
|
|
1118
|
+
}
|
|
1119
|
+
function toRecord(header, row) {
|
|
1120
|
+
const record = {};
|
|
1121
|
+
header.forEach((column, index) => {
|
|
1122
|
+
record[column] = row[index] ?? "";
|
|
1123
|
+
});
|
|
1124
|
+
return record;
|
|
1125
|
+
}
|
|
1126
|
+
/** Parses CSV with a header row into objects keyed by column name. */
|
|
1127
|
+
function parseCsv(text, delimiter = ",") {
|
|
1128
|
+
const [header, ...rows] = parseCsvRows(text, delimiter);
|
|
1129
|
+
if (!header || header.length === 0) return [];
|
|
1130
|
+
return rows.filter((row) => !isBlankLine(header, row)).map((row) => toRecord(header, row));
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* A blank line separates nothing in multi-column CSV, but in a single-column file it is a row
|
|
1134
|
+
* with an empty (null) value and must be kept.
|
|
1135
|
+
*/
|
|
1136
|
+
function isBlankLine(header, row) {
|
|
1137
|
+
return row.length === 0 && header.length !== 1;
|
|
1138
|
+
}
|
|
1139
|
+
function flattenRecord(record) {
|
|
1140
|
+
const flat = {};
|
|
1141
|
+
for (const [key, value] of Object.entries(record)) {
|
|
1142
|
+
if (key === "attributes") continue;
|
|
1143
|
+
if (isPlainObject(value)) {
|
|
1144
|
+
for (const [nestedKey, nestedValue] of Object.entries(value)) if (nestedKey !== "attributes") flat[`${key}.${nestedKey}`] = nestedValue;
|
|
1145
|
+
} else flat[key] = value;
|
|
1146
|
+
}
|
|
1147
|
+
return flat;
|
|
1148
|
+
}
|
|
1149
|
+
function isPlainObject(value) {
|
|
1150
|
+
return typeof value === "object" && value !== null && !(value instanceof Date) && !Array.isArray(value);
|
|
1151
|
+
}
|
|
1152
|
+
function formatValue(value) {
|
|
1153
|
+
if (value === void 0) return "";
|
|
1154
|
+
if (value === null) return NULL_VALUE;
|
|
1155
|
+
if (value instanceof Date) return value.toISOString();
|
|
1156
|
+
if (Array.isArray(value)) return value.join(";");
|
|
1157
|
+
if (typeof value === "string") return value === NULL_VALUE ? ` ${NULL_VALUE}` : value;
|
|
1158
|
+
if (typeof value === "number") return formatNumber(value);
|
|
1159
|
+
if (typeof value === "boolean" || typeof value === "bigint") return value.toString();
|
|
1160
|
+
return JSON.stringify(value) ?? "";
|
|
1161
|
+
}
|
|
1162
|
+
function quote(value, delimiter) {
|
|
1163
|
+
if (value.includes(delimiter) || value.includes("\"") || value.includes("\n") || value.includes("\r") || value !== value.trim()) return `"${value.replace(/"/g, "\"\"")}"`;
|
|
1164
|
+
return value;
|
|
1165
|
+
}
|
|
1166
|
+
var DELIMITERS, NULL_VALUE, CsvRowParser;
|
|
1167
|
+
var init_csv = __esmMin((() => {
|
|
1168
|
+
init_escape();
|
|
1169
|
+
DELIMITERS = {
|
|
1170
|
+
COMMA: ",",
|
|
1171
|
+
TAB: " ",
|
|
1172
|
+
PIPE: "|",
|
|
1173
|
+
SEMICOLON: ";",
|
|
1174
|
+
CARET: "^",
|
|
1175
|
+
BACKQUOTE: "`"
|
|
1176
|
+
};
|
|
1177
|
+
NULL_VALUE = "#N/A";
|
|
1178
|
+
CsvRowParser = class {
|
|
1179
|
+
_delimiter;
|
|
1180
|
+
_row = [];
|
|
1181
|
+
_field = "";
|
|
1182
|
+
_inQuotes = false;
|
|
1183
|
+
/** The current field was quoted, so it exists even when empty. */
|
|
1184
|
+
_fieldQuoted = false;
|
|
1185
|
+
/** A `"` was seen inside quotes; the next character decides whether it was an escaped quote. */
|
|
1186
|
+
_quotePending = false;
|
|
1187
|
+
/** The last row ended with `\r`; a following `\n` belongs to it. */
|
|
1188
|
+
_skipLineFeed = false;
|
|
1189
|
+
_started = false;
|
|
1190
|
+
constructor(_delimiter = ",") {
|
|
1191
|
+
this._delimiter = _delimiter;
|
|
1192
|
+
}
|
|
1193
|
+
/** Parses a chunk and returns the rows it completed. */
|
|
1194
|
+
push(chunk) {
|
|
1195
|
+
const rows = [];
|
|
1196
|
+
let text = chunk;
|
|
1197
|
+
if (!this._started && text.length > 0) {
|
|
1198
|
+
this._started = true;
|
|
1199
|
+
text = text.charCodeAt(0) === 65279 ? text.slice(1) : text;
|
|
1200
|
+
}
|
|
1201
|
+
for (const char of text) this.consume(char, rows);
|
|
1202
|
+
return rows;
|
|
1203
|
+
}
|
|
1204
|
+
/** Finishes parsing and returns the last row, if any. */
|
|
1205
|
+
end() {
|
|
1206
|
+
this._quotePending = false;
|
|
1207
|
+
this._inQuotes = false;
|
|
1208
|
+
if (this._field.length > 0 || this._row.length > 0 || this._fieldQuoted) {
|
|
1209
|
+
this._row.push(this._field);
|
|
1210
|
+
const row = this._row;
|
|
1211
|
+
this._row = [];
|
|
1212
|
+
this._field = "";
|
|
1213
|
+
this._fieldQuoted = false;
|
|
1214
|
+
return [row];
|
|
1215
|
+
}
|
|
1216
|
+
return [];
|
|
1217
|
+
}
|
|
1218
|
+
consume(char, rows) {
|
|
1219
|
+
if (this._skipLineFeed) {
|
|
1220
|
+
this._skipLineFeed = false;
|
|
1221
|
+
if (char === "\n") return;
|
|
1222
|
+
}
|
|
1223
|
+
if (this._quotePending) {
|
|
1224
|
+
this._quotePending = false;
|
|
1225
|
+
if (char === "\"") {
|
|
1226
|
+
this._field += "\"";
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
this._inQuotes = false;
|
|
1230
|
+
}
|
|
1231
|
+
if (this._inQuotes) {
|
|
1232
|
+
if (char === "\"") this._quotePending = true;
|
|
1233
|
+
else this._field += char;
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
this.consumeUnquoted(char, rows);
|
|
1237
|
+
}
|
|
1238
|
+
consumeUnquoted(char, rows) {
|
|
1239
|
+
if (char === "\"") {
|
|
1240
|
+
this._inQuotes = true;
|
|
1241
|
+
this._fieldQuoted = true;
|
|
1242
|
+
} else if (char === this._delimiter) {
|
|
1243
|
+
this._row.push(this._field);
|
|
1244
|
+
this._field = "";
|
|
1245
|
+
this._fieldQuoted = false;
|
|
1246
|
+
} else if (char === "\n" || char === "\r") {
|
|
1247
|
+
this._skipLineFeed = char === "\r";
|
|
1248
|
+
const blank = this._row.length === 0 && this._field === "" && !this._fieldQuoted;
|
|
1249
|
+
rows.push(blank ? [] : [...this._row, this._field]);
|
|
1250
|
+
this._row = [];
|
|
1251
|
+
this._field = "";
|
|
1252
|
+
this._fieldQuoted = false;
|
|
1253
|
+
} else this._field += char;
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
}));
|
|
1257
|
+
//#endregion
|
|
1258
|
+
//#region src/resources/bulk.ts
|
|
1259
|
+
/** Wait options that abort on the call's signal as well as on the wait options' own signal. */
|
|
1260
|
+
function withSignal(wait, signal) {
|
|
1261
|
+
const signals = [signal, wait.signal].filter((item) => item !== void 0);
|
|
1262
|
+
return {
|
|
1263
|
+
...wait,
|
|
1264
|
+
signal: signals.length > 1 ? AbortSignal.any(signals) : signals[0]
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
async function waitForJob(job, options) {
|
|
1268
|
+
const interval = options.pollIntervalMs ?? 2e3;
|
|
1269
|
+
const deadline = Date.now() + (options.timeoutMs ?? 6e5);
|
|
1270
|
+
for (;;) {
|
|
1271
|
+
const info = await job.refresh({ signal: options.signal });
|
|
1272
|
+
if (info.state === "JobComplete") return info;
|
|
1273
|
+
if (info.state === "Failed" || info.state === "Aborted") throw new SalesforceBulkJobError(`Bulk job ${job.id} ended in state ${info.state}${info.errorMessage ? `: ${info.errorMessage}` : ""}`, job.id, info.state, info);
|
|
1274
|
+
if (Date.now() + interval > deadline) throw new SalesforceBulkJobError(`Bulk job ${job.id} did not complete in time (state ${info.state}).`, job.id, info.state, info);
|
|
1275
|
+
await sleep(interval, options.signal);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
var MAX_UPLOAD_BYTES$1, encoder$2, BulkIngestJob, BulkQueryJob, BulkApi;
|
|
1279
|
+
var init_bulk = __esmMin((() => {
|
|
1280
|
+
init_errors();
|
|
1281
|
+
init_connection();
|
|
1282
|
+
init_csv();
|
|
1283
|
+
MAX_UPLOAD_BYTES$1 = 104857600;
|
|
1284
|
+
encoder$2 = new TextEncoder();
|
|
1285
|
+
BulkIngestJob = class {
|
|
1286
|
+
_connection;
|
|
1287
|
+
info;
|
|
1288
|
+
constructor(_connection, info) {
|
|
1289
|
+
this._connection = _connection;
|
|
1290
|
+
this.info = info;
|
|
1291
|
+
}
|
|
1292
|
+
get id() {
|
|
1293
|
+
return this.info.id;
|
|
1294
|
+
}
|
|
1295
|
+
get path() {
|
|
1296
|
+
return `/jobs/ingest/${segment(this.id)}`;
|
|
1297
|
+
}
|
|
1298
|
+
get delimiter() {
|
|
1299
|
+
return DELIMITERS[this.info.columnDelimiter] ?? ",";
|
|
1300
|
+
}
|
|
1301
|
+
/** Uploads CSV data, or records that are converted to CSV. Only allowed while the job is `Open`. */
|
|
1302
|
+
async upload(data, options = {}) {
|
|
1303
|
+
const csv = typeof data === "string" ? data : toCsv(data, {
|
|
1304
|
+
delimiter: this.delimiter,
|
|
1305
|
+
lineEnding: this.info.lineEnding
|
|
1306
|
+
});
|
|
1307
|
+
const size = encoder$2.encode(csv).byteLength;
|
|
1308
|
+
if (size > MAX_UPLOAD_BYTES$1) throw new Error(`Bulk upload of ${size} bytes exceeds the ${MAX_UPLOAD_BYTES$1} byte limit; split the data.`);
|
|
1309
|
+
await this._connection.request({
|
|
1310
|
+
method: "PUT",
|
|
1311
|
+
path: `${this.path}/batches`,
|
|
1312
|
+
body: csv,
|
|
1313
|
+
headers: { "Content-Type": "text/csv" },
|
|
1314
|
+
signal: options.signal
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
/** Marks the upload as complete so Salesforce starts processing. */
|
|
1318
|
+
close(options = {}) {
|
|
1319
|
+
return this.setState("UploadComplete", options.signal);
|
|
1320
|
+
}
|
|
1321
|
+
abort(options = {}) {
|
|
1322
|
+
return this.setState("Aborted", options.signal);
|
|
1323
|
+
}
|
|
1324
|
+
async delete(options = {}) {
|
|
1325
|
+
await this._connection.request({
|
|
1326
|
+
method: "DELETE",
|
|
1327
|
+
path: this.path,
|
|
1328
|
+
signal: options.signal
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
/** Fetches the current job status. */
|
|
1332
|
+
async refresh(options = {}) {
|
|
1333
|
+
this.info = await this._connection.request({
|
|
1334
|
+
path: this.path,
|
|
1335
|
+
signal: options.signal
|
|
1336
|
+
});
|
|
1337
|
+
return this.info;
|
|
1338
|
+
}
|
|
1339
|
+
/** Polls until the job is `JobComplete`. Throws `SalesforceBulkJobError` on `Failed`, `Aborted` or timeout. */
|
|
1340
|
+
waitForCompletion(options = {}) {
|
|
1341
|
+
return waitForJob(this, options);
|
|
1342
|
+
}
|
|
1343
|
+
/** Successfully processed rows: `sf__Id`, `sf__Created` and the uploaded columns. */
|
|
1344
|
+
successfulResults(options = {}) {
|
|
1345
|
+
return this.results("successfulResults", options.signal);
|
|
1346
|
+
}
|
|
1347
|
+
/** Failed rows: `sf__Id`, `sf__Error` and the uploaded columns. */
|
|
1348
|
+
failedResults(options = {}) {
|
|
1349
|
+
return this.results("failedResults", options.signal);
|
|
1350
|
+
}
|
|
1351
|
+
/** Rows that were not processed (e.g. because the job was aborted). */
|
|
1352
|
+
unprocessedRecords(options = {}) {
|
|
1353
|
+
return this.results("unprocessedrecords", options.signal);
|
|
1354
|
+
}
|
|
1355
|
+
async results(resource, signal) {
|
|
1356
|
+
const csv = await this._connection.request({
|
|
1357
|
+
path: `${this.path}/${resource}/`,
|
|
1358
|
+
headers: { Accept: "text/csv" },
|
|
1359
|
+
responseType: "text",
|
|
1360
|
+
signal
|
|
1361
|
+
});
|
|
1362
|
+
return csv ? parseCsv(csv, this.delimiter) : [];
|
|
1363
|
+
}
|
|
1364
|
+
async setState(state, signal) {
|
|
1365
|
+
this.info = await this._connection.request({
|
|
1366
|
+
method: "PATCH",
|
|
1367
|
+
path: this.path,
|
|
1368
|
+
body: { state },
|
|
1369
|
+
signal
|
|
1370
|
+
});
|
|
1371
|
+
return this.info;
|
|
1372
|
+
}
|
|
1373
|
+
};
|
|
1374
|
+
BulkQueryJob = class {
|
|
1375
|
+
_connection;
|
|
1376
|
+
info;
|
|
1377
|
+
constructor(_connection, info) {
|
|
1378
|
+
this._connection = _connection;
|
|
1379
|
+
this.info = info;
|
|
1380
|
+
}
|
|
1381
|
+
get id() {
|
|
1382
|
+
return this.info.id;
|
|
1383
|
+
}
|
|
1384
|
+
get path() {
|
|
1385
|
+
return `/jobs/query/${segment(this.id)}`;
|
|
1386
|
+
}
|
|
1387
|
+
async refresh(options = {}) {
|
|
1388
|
+
this.info = await this._connection.request({
|
|
1389
|
+
path: this.path,
|
|
1390
|
+
signal: options.signal
|
|
1391
|
+
});
|
|
1392
|
+
return this.info;
|
|
1393
|
+
}
|
|
1394
|
+
/** Polls until the job is `JobComplete`. Throws `SalesforceBulkJobError` on `Failed`, `Aborted` or timeout. */
|
|
1395
|
+
waitForCompletion(options = {}) {
|
|
1396
|
+
return waitForJob(this, options);
|
|
1397
|
+
}
|
|
1398
|
+
async abort(options = {}) {
|
|
1399
|
+
this.info = await this._connection.request({
|
|
1400
|
+
method: "PATCH",
|
|
1401
|
+
path: this.path,
|
|
1402
|
+
body: { state: "Aborted" },
|
|
1403
|
+
signal: options.signal
|
|
1404
|
+
});
|
|
1405
|
+
return this.info;
|
|
1406
|
+
}
|
|
1407
|
+
async delete(options = {}) {
|
|
1408
|
+
await this._connection.request({
|
|
1409
|
+
method: "DELETE",
|
|
1410
|
+
path: this.path,
|
|
1411
|
+
signal: options.signal
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
/** Yields result pages (following `Sforce-Locator`). The job must be complete. */
|
|
1415
|
+
async *pages(options = {}) {
|
|
1416
|
+
const delimiter = DELIMITERS[this.info.columnDelimiter] ?? ",";
|
|
1417
|
+
let locator;
|
|
1418
|
+
do {
|
|
1419
|
+
const response = await this._connection.send({
|
|
1420
|
+
path: `${this.path}/results`,
|
|
1421
|
+
query: {
|
|
1422
|
+
locator,
|
|
1423
|
+
maxRecords: options.maxRecords
|
|
1424
|
+
},
|
|
1425
|
+
headers: { Accept: "text/csv" },
|
|
1426
|
+
responseType: "text",
|
|
1427
|
+
signal: options.signal
|
|
1428
|
+
});
|
|
1429
|
+
yield response.data ? parseCsv(response.data, delimiter) : [];
|
|
1430
|
+
const next = response.headers.get("sforce-locator");
|
|
1431
|
+
locator = next && next !== "null" ? next : void 0;
|
|
1432
|
+
} while (locator);
|
|
1433
|
+
}
|
|
1434
|
+
/** Yields every result row. The job must be complete. */
|
|
1435
|
+
async *records(options = {}) {
|
|
1436
|
+
const delimiter = DELIMITERS[this.info.columnDelimiter] ?? ",";
|
|
1437
|
+
let locator;
|
|
1438
|
+
do {
|
|
1439
|
+
const request = {
|
|
1440
|
+
path: `${this.path}/results`,
|
|
1441
|
+
query: {
|
|
1442
|
+
locator,
|
|
1443
|
+
maxRecords: options.maxRecords
|
|
1444
|
+
},
|
|
1445
|
+
headers: { Accept: "text/csv" },
|
|
1446
|
+
signal: options.signal
|
|
1447
|
+
};
|
|
1448
|
+
const streamed = await this._connection.stream(request);
|
|
1449
|
+
if (!streamed) {
|
|
1450
|
+
for await (const page of this.pages(options)) yield* page;
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
yield* parseCsvStream(streamed.body, delimiter);
|
|
1454
|
+
const next = streamed.headers.get("sforce-locator");
|
|
1455
|
+
locator = next && next !== "null" ? next : void 0;
|
|
1456
|
+
} while (locator);
|
|
1457
|
+
}
|
|
1458
|
+
};
|
|
1459
|
+
BulkApi = class {
|
|
1460
|
+
_connection;
|
|
1461
|
+
constructor(_connection) {
|
|
1462
|
+
this._connection = _connection;
|
|
1463
|
+
}
|
|
1464
|
+
/** Creates an ingest job in state `Open`. Upload data with `job.upload()` and then call `job.close()`. */
|
|
1465
|
+
async createIngestJob(options) {
|
|
1466
|
+
if (options.operation === "upsert" && !options.externalIdFieldName) throw new Error("Bulk upsert requires externalIdFieldName.");
|
|
1467
|
+
const info = await this._connection.request({
|
|
1468
|
+
method: "POST",
|
|
1469
|
+
path: "/jobs/ingest",
|
|
1470
|
+
body: {
|
|
1471
|
+
object: options.object,
|
|
1472
|
+
operation: options.operation,
|
|
1473
|
+
externalIdFieldName: options.externalIdFieldName,
|
|
1474
|
+
assignmentRuleId: options.assignmentRuleId,
|
|
1475
|
+
contentType: "CSV",
|
|
1476
|
+
columnDelimiter: options.columnDelimiter ?? "COMMA",
|
|
1477
|
+
lineEnding: options.lineEnding ?? "LF"
|
|
1478
|
+
},
|
|
1479
|
+
signal: options.signal
|
|
1480
|
+
});
|
|
1481
|
+
return new BulkIngestJob(this._connection, info);
|
|
1482
|
+
}
|
|
1483
|
+
/** Attaches to an existing ingest job. */
|
|
1484
|
+
async ingestJob(id, options = {}) {
|
|
1485
|
+
const info = await this._connection.request({
|
|
1486
|
+
path: `/jobs/ingest/${segment(id)}`,
|
|
1487
|
+
signal: options.signal
|
|
1488
|
+
});
|
|
1489
|
+
return new BulkIngestJob(this._connection, info);
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Creates an ingest job, uploads the data, closes the job and (by default) waits for it.
|
|
1493
|
+
* Check `job.info.numberRecordsFailed` and `job.failedResults()` afterwards.
|
|
1494
|
+
*/
|
|
1495
|
+
async ingest(options) {
|
|
1496
|
+
if (options.csv !== void 0 && options.records !== void 0) throw new Error("bulk.ingest() takes either records or csv, not both.");
|
|
1497
|
+
const data = options.csv ?? options.records;
|
|
1498
|
+
if (data === void 0 || data.length === 0 || typeof data === "string" && data.trim().length === 0) throw new Error("bulk.ingest() requires non-empty records or csv.");
|
|
1499
|
+
const job = await this.createIngestJob(options);
|
|
1500
|
+
try {
|
|
1501
|
+
await job.upload(data, { signal: options.signal });
|
|
1502
|
+
await job.close({ signal: options.signal });
|
|
1503
|
+
} catch (error) {
|
|
1504
|
+
await job.abort().catch(() => void 0);
|
|
1505
|
+
throw error;
|
|
1506
|
+
}
|
|
1507
|
+
if (options.wait !== false) await job.waitForCompletion(withSignal(typeof options.wait === "object" ? options.wait : {}, options.signal));
|
|
1508
|
+
return job;
|
|
1509
|
+
}
|
|
1510
|
+
createQueryJob(query, options = {}) {
|
|
1511
|
+
return this.startQueryJob(query, options);
|
|
1512
|
+
}
|
|
1513
|
+
async startQueryJob(query, options) {
|
|
1514
|
+
if (typeof query !== "string" && query.usesTypeOf) throw new Error("Bulk API 2.0 does not support TYPEOF; use sf.query()/sf.collect() instead.");
|
|
1515
|
+
const operation = options.includeDeleted ? "queryAll" : "query";
|
|
1516
|
+
const info = await this._connection.request({
|
|
1517
|
+
method: "POST",
|
|
1518
|
+
path: "/jobs/query",
|
|
1519
|
+
body: {
|
|
1520
|
+
operation,
|
|
1521
|
+
query: typeof query === "string" ? query : query.build(),
|
|
1522
|
+
contentType: "CSV",
|
|
1523
|
+
columnDelimiter: options.columnDelimiter ?? "COMMA",
|
|
1524
|
+
lineEnding: options.lineEnding ?? "LF"
|
|
1525
|
+
},
|
|
1526
|
+
signal: options.signal
|
|
1527
|
+
});
|
|
1528
|
+
return new BulkQueryJob(this._connection, info);
|
|
1529
|
+
}
|
|
1530
|
+
/** Attaches to an existing query job. */
|
|
1531
|
+
async queryJob(id, options = {}) {
|
|
1532
|
+
const info = await this._connection.request({
|
|
1533
|
+
path: `/jobs/query/${segment(id)}`,
|
|
1534
|
+
signal: options.signal
|
|
1535
|
+
});
|
|
1536
|
+
return new BulkQueryJob(this._connection, info);
|
|
1537
|
+
}
|
|
1538
|
+
async *query(query, options = {}) {
|
|
1539
|
+
const job = await this.startQueryJob(query, options);
|
|
1540
|
+
await job.waitForCompletion(withSignal(options.wait ?? {}, options.signal));
|
|
1541
|
+
yield* job.records({
|
|
1542
|
+
maxRecords: options.maxRecords,
|
|
1543
|
+
signal: options.signal
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
};
|
|
1547
|
+
}));
|
|
1548
|
+
//#endregion
|
|
1549
|
+
//#region src/resources/collections.ts
|
|
1550
|
+
function chunks(items, size, options) {
|
|
1551
|
+
if (items.length === 0) throw new Error("At least one record or id is required.");
|
|
1552
|
+
if (items.length > size && !options.chunk) throw new Error(`sObject collections accept at most ${size} items per call, got ${items.length}. Pass { chunk: true } or use the Bulk API.`);
|
|
1553
|
+
if (items.length > size && options.allOrNone) throw new Error("allOrNone cannot be guaranteed across chunks. Send at most 200 records or drop allOrNone.");
|
|
1554
|
+
const result = [];
|
|
1555
|
+
for (let index = 0; index < items.length; index += size) result.push(items.slice(index, index + size));
|
|
1556
|
+
return result;
|
|
1557
|
+
}
|
|
1558
|
+
function assertSuccess(message, results, options) {
|
|
1559
|
+
if (options.throwOnError === false) return;
|
|
1560
|
+
const failed = results.filter((result) => !result.success);
|
|
1561
|
+
if (failed.length > 0) throw new SalesforceSaveError(`${message} failed for ${failed.length} of ${results.length} item(s)`, results, failed.flatMap((result) => result.errors));
|
|
1562
|
+
}
|
|
1563
|
+
var MAX_RECORDS, MAX_RETRIEVE_IDS, CollectionsApi;
|
|
1564
|
+
var init_collections = __esmMin((() => {
|
|
1565
|
+
init_errors();
|
|
1566
|
+
init_connection();
|
|
1567
|
+
MAX_RECORDS = 200;
|
|
1568
|
+
MAX_RETRIEVE_IDS = 2e3;
|
|
1569
|
+
CollectionsApi = class {
|
|
1570
|
+
_connection;
|
|
1571
|
+
constructor(_connection) {
|
|
1572
|
+
this._connection = _connection;
|
|
1573
|
+
}
|
|
1574
|
+
create(sobject, records, options = {}) {
|
|
1575
|
+
return this.save("POST", "/composite/sobjects", sobject, records, options, "Creating");
|
|
1576
|
+
}
|
|
1577
|
+
/** Updates records; every record needs its `Id`. */
|
|
1578
|
+
update(sobject, records, options = {}) {
|
|
1579
|
+
return this.save("PATCH", "/composite/sobjects", sobject, records, options, "Updating");
|
|
1580
|
+
}
|
|
1581
|
+
/** Upserts records of one sObject by external id. Every record needs the external id field. */
|
|
1582
|
+
upsert(sobject, externalIdField, records, options = {}) {
|
|
1583
|
+
return this.save("PATCH", `/composite/sobjects/${segment(sobject)}/${segment(externalIdField)}`, sobject, records, options, "Upserting");
|
|
1584
|
+
}
|
|
1585
|
+
/** Deletes records by id (they may be of different sObjects). */
|
|
1586
|
+
async delete(ids, options = {}) {
|
|
1587
|
+
const results = await runChunks(chunks(ids, MAX_RECORDS, options), (chunk) => this._connection.request({
|
|
1588
|
+
method: "DELETE",
|
|
1589
|
+
path: "/composite/sobjects",
|
|
1590
|
+
query: {
|
|
1591
|
+
ids: chunk,
|
|
1592
|
+
allOrNone: options.allOrNone
|
|
1593
|
+
},
|
|
1594
|
+
signal: options.signal
|
|
1595
|
+
}), "Deleting records failed");
|
|
1596
|
+
assertSuccess("Deleting records", results, options);
|
|
1597
|
+
return results;
|
|
1598
|
+
}
|
|
1599
|
+
/** Retrieves records by id with the given fields. Inaccessible or unknown ids yield `null`. */
|
|
1600
|
+
async retrieve(sobject, ids, fields, options = {}) {
|
|
1601
|
+
if (fields.length === 0) throw new Error("retrieve() requires at least one field.");
|
|
1602
|
+
const results = [];
|
|
1603
|
+
for (const chunk of chunks(ids, MAX_RETRIEVE_IDS, { chunk: true })) results.push(...await this._connection.request({
|
|
1604
|
+
method: "POST",
|
|
1605
|
+
path: `/composite/sobjects/${segment(sobject)}`,
|
|
1606
|
+
body: {
|
|
1607
|
+
ids: chunk,
|
|
1608
|
+
fields
|
|
1609
|
+
},
|
|
1610
|
+
signal: options.signal
|
|
1611
|
+
}));
|
|
1612
|
+
return results;
|
|
1613
|
+
}
|
|
1614
|
+
async save(method, path, sobject, records, options, verb) {
|
|
1615
|
+
const results = await runChunks(chunks(records, MAX_RECORDS, options), (chunk) => {
|
|
1616
|
+
const body = {
|
|
1617
|
+
allOrNone: options.allOrNone ?? false,
|
|
1618
|
+
records: chunk.map((record) => ({
|
|
1619
|
+
attributes: { type: sobject },
|
|
1620
|
+
...record
|
|
1621
|
+
}))
|
|
1622
|
+
};
|
|
1623
|
+
return this._connection.request({
|
|
1624
|
+
method,
|
|
1625
|
+
path,
|
|
1626
|
+
body,
|
|
1627
|
+
signal: options.signal
|
|
1628
|
+
});
|
|
1629
|
+
}, `${verb} ${sobject} records failed`);
|
|
1630
|
+
assertSuccess(`${verb} ${sobject} records`, results, options);
|
|
1631
|
+
return results;
|
|
1632
|
+
}
|
|
1633
|
+
};
|
|
1634
|
+
}));
|
|
1635
|
+
//#endregion
|
|
1636
|
+
//#region src/resources/composite.ts
|
|
1637
|
+
/**
|
|
1638
|
+
* Encodes `value` with `encode` but leaves `@{...}` references as they are: Salesforce only
|
|
1639
|
+
* substitutes references it can read literally, so an encoded one would be sent as text.
|
|
1640
|
+
*/
|
|
1641
|
+
function keepReferences(value, encode) {
|
|
1642
|
+
let result = "";
|
|
1643
|
+
let last = 0;
|
|
1644
|
+
for (const match of value.matchAll(REFERENCE)) {
|
|
1645
|
+
result += encode(value.slice(last, match.index)) + match[0];
|
|
1646
|
+
last = match.index + match[0].length;
|
|
1647
|
+
}
|
|
1648
|
+
return result + encode(value.slice(last));
|
|
1649
|
+
}
|
|
1650
|
+
/** A URL path segment that may be (or contain) a composite reference. */
|
|
1651
|
+
function refSegment(value) {
|
|
1652
|
+
return keepReferences(value, segment);
|
|
1653
|
+
}
|
|
1654
|
+
function assertCompositeLimits(builder) {
|
|
1655
|
+
const count = builder.subrequests.length;
|
|
1656
|
+
if (count === 0 || count > MAX_SUBREQUESTS) throw new Error(`A composite request needs 1 to ${MAX_SUBREQUESTS} subrequests, got ${count}.`);
|
|
1657
|
+
if (builder.queryCount > MAX_QUERY_SUBREQUESTS) throw new Error(`A composite request allows at most ${MAX_QUERY_SUBREQUESTS} query/collection subrequests, got ${builder.queryCount}.`);
|
|
1658
|
+
}
|
|
1659
|
+
function throwFirstError(response) {
|
|
1660
|
+
const failed = response.responses.find((item) => item.httpStatusCode >= 400 && !isProcessingHalted(item.body));
|
|
1661
|
+
if (failed) response.get({
|
|
1662
|
+
referenceId: failed.referenceId,
|
|
1663
|
+
ref: () => ""
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
function isProcessingHalted(body) {
|
|
1667
|
+
return Array.isArray(body) && body.some((error) => error.errorCode === "PROCESSING_HALTED");
|
|
1668
|
+
}
|
|
1669
|
+
function countTreeRecords(records) {
|
|
1670
|
+
let total = 0;
|
|
1671
|
+
for (const record of records) {
|
|
1672
|
+
total++;
|
|
1673
|
+
for (const [key, value] of Object.entries(record)) if (key !== "attributes" && isRecordsWrapper(value)) total += countTreeRecords(value.records);
|
|
1674
|
+
}
|
|
1675
|
+
return total;
|
|
1676
|
+
}
|
|
1677
|
+
function isRecordsWrapper(value) {
|
|
1678
|
+
return typeof value === "object" && value !== null && Array.isArray(value.records);
|
|
1679
|
+
}
|
|
1680
|
+
var MAX_SUBREQUESTS, MAX_QUERY_SUBREQUESTS, MAX_GRAPHS, MAX_GRAPH_NODES, MAX_TREE_RECORDS, REFERENCE_ID_PATTERN, REFERENCE, CompositeRequestBuilder, CompositeResponse, CompositeApi;
|
|
1681
|
+
var init_composite = __esmMin((() => {
|
|
1682
|
+
init_errors();
|
|
1683
|
+
init_connection();
|
|
1684
|
+
MAX_SUBREQUESTS = 25;
|
|
1685
|
+
MAX_QUERY_SUBREQUESTS = 5;
|
|
1686
|
+
MAX_GRAPHS = 75;
|
|
1687
|
+
MAX_GRAPH_NODES = 500;
|
|
1688
|
+
MAX_TREE_RECORDS = 200;
|
|
1689
|
+
REFERENCE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_]*$/;
|
|
1690
|
+
REFERENCE = /@\{[^{}]+\}/g;
|
|
1691
|
+
CompositeRequestBuilder = class {
|
|
1692
|
+
_apiVersion;
|
|
1693
|
+
_referencePrefix;
|
|
1694
|
+
_subrequests = [];
|
|
1695
|
+
_queryCount = 0;
|
|
1696
|
+
constructor(_apiVersion, _referencePrefix = "ref") {
|
|
1697
|
+
this._apiVersion = _apiVersion;
|
|
1698
|
+
this._referencePrefix = _referencePrefix;
|
|
1699
|
+
}
|
|
1700
|
+
/** The subrequests collected so far. */
|
|
1701
|
+
get subrequests() {
|
|
1702
|
+
return this._subrequests;
|
|
1703
|
+
}
|
|
1704
|
+
/** Number of query subrequests collected so far (Salesforce allows at most 5 per composite request). */
|
|
1705
|
+
get queryCount() {
|
|
1706
|
+
return this._queryCount;
|
|
1707
|
+
}
|
|
1708
|
+
create(sobject, record, options) {
|
|
1709
|
+
return this.add("POST", `/sobjects/${segment(sobject)}`, record, options);
|
|
1710
|
+
}
|
|
1711
|
+
update(sobject, id, record, options) {
|
|
1712
|
+
return this.add("PATCH", `/sobjects/${segment(sobject)}/${refSegment(id)}`, record, options);
|
|
1713
|
+
}
|
|
1714
|
+
upsert(sobject, externalIdField, externalIdValue, record, options) {
|
|
1715
|
+
return this.add("PATCH", `/sobjects/${segment(sobject)}/${segment(externalIdField)}/${refSegment(externalIdText(externalIdValue))}`, record, options);
|
|
1716
|
+
}
|
|
1717
|
+
delete(sobject, id, options) {
|
|
1718
|
+
return this.add("DELETE", `/sobjects/${segment(sobject)}/${refSegment(id)}`, void 0, options);
|
|
1719
|
+
}
|
|
1720
|
+
get(sobject, id, fields, options) {
|
|
1721
|
+
const query = fields && fields.length > 0 ? `?fields=${fields.map(segment).join(",")}` : "";
|
|
1722
|
+
return this.add("GET", `/sobjects/${segment(sobject)}/${refSegment(id)}${query}`, void 0, options);
|
|
1723
|
+
}
|
|
1724
|
+
query(query, options) {
|
|
1725
|
+
const soql = typeof query === "string" ? query : query.build();
|
|
1726
|
+
this._queryCount++;
|
|
1727
|
+
return this.add("GET", `/query?q=${keepReferences(soql, encodeURIComponent)}`, void 0, options);
|
|
1728
|
+
}
|
|
1729
|
+
/** Adds any other supported subrequest (sObject collections, describe, ...). */
|
|
1730
|
+
request(subrequest, options) {
|
|
1731
|
+
const path = subrequest.path.split("?")[0].replace(/^\/?(services\/data\/)?v[\d.]+(?=\/)/, "");
|
|
1732
|
+
if (/^\/?(tooling\/)?(query|queryAll)(\/|$)|^\/?composite\/sobjects(\/|$)/.test(path)) this._queryCount++;
|
|
1733
|
+
return this.add(subrequest.method, subrequest.path, subrequest.body, options);
|
|
1734
|
+
}
|
|
1735
|
+
/** `ref1`, `ref2`, ...: skips ids already taken by a custom `referenceId`. */
|
|
1736
|
+
nextReferenceId() {
|
|
1737
|
+
const taken = new Set(this._subrequests.map((subrequest) => subrequest.referenceId));
|
|
1738
|
+
for (let index = this._subrequests.length + 1;; index++) {
|
|
1739
|
+
const candidate = `${this._referencePrefix}${index}`;
|
|
1740
|
+
if (!taken.has(candidate)) return candidate;
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
add(method, path, body, options = {}) {
|
|
1744
|
+
const referenceId = options.referenceId ?? this.nextReferenceId();
|
|
1745
|
+
if (!REFERENCE_ID_PATTERN.test(referenceId)) throw new Error(`Invalid composite referenceId "${referenceId}": use letters, digits and underscores.`);
|
|
1746
|
+
if (this._subrequests.some((subrequest) => subrequest.referenceId === referenceId)) throw new Error(`Duplicate composite referenceId "${referenceId}".`);
|
|
1747
|
+
const subrequest = {
|
|
1748
|
+
method,
|
|
1749
|
+
url: path.startsWith("/services/") ? path : `/services/data/${this._apiVersion}${path.startsWith("/") ? path : `/${path}`}`,
|
|
1750
|
+
referenceId
|
|
1751
|
+
};
|
|
1752
|
+
if (body !== void 0) subrequest.body = body;
|
|
1753
|
+
if (options.httpHeaders) subrequest.httpHeaders = options.httpHeaders;
|
|
1754
|
+
this._subrequests.push(subrequest);
|
|
1755
|
+
return {
|
|
1756
|
+
referenceId,
|
|
1757
|
+
ref: (refPath) => `@{${referenceId}.${refPath}}`
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
};
|
|
1761
|
+
CompositeResponse = class {
|
|
1762
|
+
responses;
|
|
1763
|
+
constructor(responses) {
|
|
1764
|
+
this.responses = responses;
|
|
1765
|
+
}
|
|
1766
|
+
/** `true` when any subrequest returned a status of 400 or higher. */
|
|
1767
|
+
get hasErrors() {
|
|
1768
|
+
return this.responses.some((response) => response.httpStatusCode >= 400);
|
|
1769
|
+
}
|
|
1770
|
+
/** The raw subrequest result for `ref`. */
|
|
1771
|
+
response(ref) {
|
|
1772
|
+
const response = this.responses.find((item) => item.referenceId === ref.referenceId);
|
|
1773
|
+
if (!response) throw new Error(`No composite response for referenceId "${ref.referenceId}".`);
|
|
1774
|
+
return response;
|
|
1775
|
+
}
|
|
1776
|
+
/** The body of the subrequest for `ref`. Throws `SalesforceError` when that subrequest failed. */
|
|
1777
|
+
get(ref) {
|
|
1778
|
+
const response = this.response(ref);
|
|
1779
|
+
if (response.httpStatusCode >= 400) throw new SalesforceError({
|
|
1780
|
+
status: response.httpStatusCode,
|
|
1781
|
+
method: "POST",
|
|
1782
|
+
path: `/composite (${ref.referenceId})`,
|
|
1783
|
+
body: response.body,
|
|
1784
|
+
headers: response.httpHeaders
|
|
1785
|
+
});
|
|
1786
|
+
return response.body;
|
|
1787
|
+
}
|
|
1788
|
+
};
|
|
1789
|
+
CompositeApi = class {
|
|
1790
|
+
_connection;
|
|
1791
|
+
constructor(_connection) {
|
|
1792
|
+
this._connection = _connection;
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Runs up to 25 subrequests in one call; later subrequests can reference earlier results.
|
|
1796
|
+
*
|
|
1797
|
+
* ```ts
|
|
1798
|
+
* const result = await sf.composite.execute(c => {
|
|
1799
|
+
* const account = c.create("Account", { Name: "Acme" });
|
|
1800
|
+
* c.create("Contact", { LastName: "Doe", AccountId: account.ref("id") });
|
|
1801
|
+
* return { account };
|
|
1802
|
+
* }, { allOrNone: true });
|
|
1803
|
+
* const accountId = result.get(result.refs.account).id;
|
|
1804
|
+
* ```
|
|
1805
|
+
*/
|
|
1806
|
+
async execute(build, options = {}) {
|
|
1807
|
+
const builder = new CompositeRequestBuilder(this._connection.apiVersion);
|
|
1808
|
+
const refs = build(builder);
|
|
1809
|
+
assertCompositeLimits(builder);
|
|
1810
|
+
const body = { compositeRequest: builder.subrequests };
|
|
1811
|
+
if (options.allOrNone !== void 0) body.allOrNone = options.allOrNone;
|
|
1812
|
+
if (options.collateSubrequests !== void 0) body.collateSubrequests = options.collateSubrequests;
|
|
1813
|
+
const raw = await this._connection.request({
|
|
1814
|
+
method: "POST",
|
|
1815
|
+
path: "/composite",
|
|
1816
|
+
body,
|
|
1817
|
+
signal: options.signal
|
|
1818
|
+
});
|
|
1819
|
+
const response = Object.assign(new CompositeResponse(raw.compositeResponse), { refs });
|
|
1820
|
+
if (options.throwOnError) throwFirstError(response);
|
|
1821
|
+
return response;
|
|
1822
|
+
}
|
|
1823
|
+
/** `/composite/batch`: up to 25 independent subrequests (each counts against API limits). */
|
|
1824
|
+
async batch(requests, options = {}) {
|
|
1825
|
+
if (requests.length === 0 || requests.length > MAX_SUBREQUESTS) throw new Error(`A composite batch needs 1 to ${MAX_SUBREQUESTS} subrequests, got ${requests.length}.`);
|
|
1826
|
+
const version = this._connection.apiVersion;
|
|
1827
|
+
const batchRequests = requests.map((request) => ({
|
|
1828
|
+
method: request.method,
|
|
1829
|
+
url: `${version}${request.path.startsWith("/") ? request.path : `/${request.path}`}`,
|
|
1830
|
+
...request.body === void 0 ? {} : { richInput: request.body }
|
|
1831
|
+
}));
|
|
1832
|
+
return this._connection.request({
|
|
1833
|
+
method: "POST",
|
|
1834
|
+
path: "/composite/batch",
|
|
1835
|
+
body: {
|
|
1836
|
+
haltOnError: options.haltOnError ?? false,
|
|
1837
|
+
batchRequests
|
|
1838
|
+
},
|
|
1839
|
+
signal: options.signal
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
/**
|
|
1843
|
+
* `/composite/tree/{sobject}`: creates up to 200 records with nested children in one
|
|
1844
|
+
* all-or-nothing call. On failure a `SalesforceError` is thrown whose `body` holds the per-record errors.
|
|
1845
|
+
*/
|
|
1846
|
+
tree(sobject, records, options = {}) {
|
|
1847
|
+
const total = countTreeRecords(records);
|
|
1848
|
+
if (total === 0 || total > MAX_TREE_RECORDS) throw new Error(`An sObject tree needs 1 to ${MAX_TREE_RECORDS} records in total, got ${total}.`);
|
|
1849
|
+
return this._connection.request({
|
|
1850
|
+
method: "POST",
|
|
1851
|
+
path: `/composite/tree/${segment(sobject)}`,
|
|
1852
|
+
body: { records },
|
|
1853
|
+
signal: options.signal
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
/** `/composite/graph`: several independent all-or-nothing graphs of subrequests in one call. */
|
|
1857
|
+
async graph(graphs, options = {}) {
|
|
1858
|
+
if (graphs.length === 0 || graphs.length > MAX_GRAPHS) throw new Error(`A composite graph request needs 1 to ${MAX_GRAPHS} graphs, got ${graphs.length}.`);
|
|
1859
|
+
const payload = graphs.map((graph) => {
|
|
1860
|
+
const builder = new CompositeRequestBuilder(this._connection.apiVersion);
|
|
1861
|
+
graph.build(builder);
|
|
1862
|
+
if (builder.subrequests.length === 0) throw new Error(`Graph "${graph.graphId}" has no subrequests.`);
|
|
1863
|
+
return {
|
|
1864
|
+
graphId: graph.graphId,
|
|
1865
|
+
compositeRequest: builder.subrequests
|
|
1866
|
+
};
|
|
1867
|
+
});
|
|
1868
|
+
const nodes = payload.reduce((sum, graph) => sum + graph.compositeRequest.length, 0);
|
|
1869
|
+
if (nodes > MAX_GRAPH_NODES) throw new Error(`A composite graph request allows at most ${MAX_GRAPH_NODES} nodes, got ${nodes}.`);
|
|
1870
|
+
return (await this._connection.request({
|
|
1871
|
+
method: "POST",
|
|
1872
|
+
path: "/composite/graph",
|
|
1873
|
+
body: { graphs: payload },
|
|
1874
|
+
signal: options.signal
|
|
1875
|
+
})).graphs.map((graph) => ({
|
|
1876
|
+
graphId: graph.graphId,
|
|
1877
|
+
isSuccessful: graph.isSuccessful,
|
|
1878
|
+
response: new CompositeResponse(graph.graphResponse.compositeResponse)
|
|
1879
|
+
}));
|
|
1880
|
+
}
|
|
1881
|
+
};
|
|
1882
|
+
}));
|
|
1883
|
+
//#endregion
|
|
1884
|
+
//#region src/resources/events.ts
|
|
1885
|
+
/** Converts a save result, treating the `OPERATION_ENQUEUED` entry as the UUID instead of an error. */
|
|
1886
|
+
function toPublishResult(result) {
|
|
1887
|
+
const errors = result.errors ?? [];
|
|
1888
|
+
const enqueued = errors.find((error) => error.statusCode === "OPERATION_ENQUEUED");
|
|
1889
|
+
const realErrors = errors.filter((error) => error.statusCode !== "OPERATION_ENQUEUED");
|
|
1890
|
+
return {
|
|
1891
|
+
success: result.success && realErrors.length === 0,
|
|
1892
|
+
uuid: enqueued?.message,
|
|
1893
|
+
id: result.id,
|
|
1894
|
+
errors: realErrors
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
function toSaveErrors(body) {
|
|
1898
|
+
if (!Array.isArray(body)) return [{
|
|
1899
|
+
statusCode: "UNKNOWN_EXCEPTION",
|
|
1900
|
+
message: JSON.stringify(body) ?? "",
|
|
1901
|
+
fields: []
|
|
1902
|
+
}];
|
|
1903
|
+
return body.map((error) => ({
|
|
1904
|
+
statusCode: error.errorCode,
|
|
1905
|
+
message: error.message,
|
|
1906
|
+
fields: error.fields ?? []
|
|
1907
|
+
}));
|
|
1908
|
+
}
|
|
1909
|
+
var MAX_PER_COMPOSITE, EventsApi;
|
|
1910
|
+
var init_events = __esmMin((() => {
|
|
1911
|
+
init_errors();
|
|
1912
|
+
init_connection();
|
|
1913
|
+
MAX_PER_COMPOSITE = 25;
|
|
1914
|
+
EventsApi = class {
|
|
1915
|
+
_connection;
|
|
1916
|
+
constructor(_connection) {
|
|
1917
|
+
this._connection = _connection;
|
|
1918
|
+
}
|
|
1919
|
+
async publish(eventName, events, options = {}) {
|
|
1920
|
+
if (typeof eventName !== "string" || !/^[A-Za-z][A-Za-z0-9_]*__e$/.test(eventName)) throw new Error(`"${String(eventName)}" is not a platform event name (it must end with __e).`);
|
|
1921
|
+
const list = Array.isArray(events) ? events : [events];
|
|
1922
|
+
if (list.length === 0) throw new Error("publish() requires at least one event.");
|
|
1923
|
+
const batches = [];
|
|
1924
|
+
for (let index = 0; list.length > 1 && index < list.length; index += MAX_PER_COMPOSITE) batches.push(list.slice(index, index + MAX_PER_COMPOSITE));
|
|
1925
|
+
const results = list.length === 1 ? [await this.publishOne(eventName, list[0], options)] : await runChunks(batches, (batch) => this.publishBatch(eventName, batch, options), `Publishing ${eventName} failed`);
|
|
1926
|
+
if (options.throwOnError !== false && results.some((result) => !result.success)) {
|
|
1927
|
+
const failed = results.filter((result) => !result.success);
|
|
1928
|
+
throw new SalesforceSaveError(`Publishing ${eventName} failed for ${failed.length} of ${results.length} event(s)`, results, failed.flatMap((result) => result.errors));
|
|
1929
|
+
}
|
|
1930
|
+
return results;
|
|
1931
|
+
}
|
|
1932
|
+
async publishOne(eventName, event, options) {
|
|
1933
|
+
try {
|
|
1934
|
+
return toPublishResult(await this._connection.request({
|
|
1935
|
+
method: "POST",
|
|
1936
|
+
path: `/sobjects/${segment(eventName)}`,
|
|
1937
|
+
body: event,
|
|
1938
|
+
signal: options.signal
|
|
1939
|
+
}));
|
|
1940
|
+
} catch (error) {
|
|
1941
|
+
if (error instanceof SalesforceError && error.status === 400 && error.errors.length > 0) return {
|
|
1942
|
+
success: false,
|
|
1943
|
+
uuid: void 0,
|
|
1944
|
+
id: void 0,
|
|
1945
|
+
errors: toSaveErrors(error.body)
|
|
1946
|
+
};
|
|
1947
|
+
throw error;
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
async publishBatch(eventName, events, options) {
|
|
1951
|
+
const url = `/services/data/${this._connection.apiVersion}/sobjects/${segment(eventName)}`;
|
|
1952
|
+
return (await this._connection.request({
|
|
1953
|
+
method: "POST",
|
|
1954
|
+
path: "/composite",
|
|
1955
|
+
body: {
|
|
1956
|
+
allOrNone: false,
|
|
1957
|
+
compositeRequest: events.map((body, index) => ({
|
|
1958
|
+
method: "POST",
|
|
1959
|
+
url,
|
|
1960
|
+
referenceId: `event${index}`,
|
|
1961
|
+
body
|
|
1962
|
+
}))
|
|
1963
|
+
},
|
|
1964
|
+
signal: options.signal
|
|
1965
|
+
})).compositeResponse.map((item) => item.httpStatusCode < 400 ? toPublishResult(item.body) : {
|
|
1966
|
+
success: false,
|
|
1967
|
+
uuid: void 0,
|
|
1968
|
+
id: void 0,
|
|
1969
|
+
errors: toSaveErrors(item.body)
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
};
|
|
1973
|
+
}));
|
|
1974
|
+
//#endregion
|
|
1975
|
+
//#region src/http/multipart.ts
|
|
1976
|
+
/**
|
|
1977
|
+
* Builds a `multipart/form-data` body as bytes, so it can go through any transport without
|
|
1978
|
+
* FormData support.
|
|
1979
|
+
*/
|
|
1980
|
+
function buildMultipart(parts, boundary = randomBoundary()) {
|
|
1981
|
+
if (parts.length === 0) throw new Error("A multipart body needs at least one part.");
|
|
1982
|
+
const chunks = [];
|
|
1983
|
+
for (const part of parts) {
|
|
1984
|
+
const filename = part.filename === void 0 ? "" : `; filename="${escapeQuoted(part.filename)}"`;
|
|
1985
|
+
chunks.push(encoder$1.encode(`--${boundary}\r\nContent-Disposition: form-data; name="${escapeQuoted(part.name)}"${filename}\r\nContent-Type: ${part.contentType}\r\n\r\n`), typeof part.data === "string" ? encoder$1.encode(part.data) : part.data, encoder$1.encode("\r\n"));
|
|
1986
|
+
}
|
|
1987
|
+
chunks.push(encoder$1.encode(`--${boundary}--\r\n`));
|
|
1988
|
+
return {
|
|
1989
|
+
body: concat(chunks),
|
|
1990
|
+
contentType: `multipart/form-data; boundary=${boundary}`
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
function randomBoundary() {
|
|
1994
|
+
return `----sobjectly${globalThis.crypto.randomUUID().replace(/-/g, "")}`;
|
|
1995
|
+
}
|
|
1996
|
+
function escapeQuoted(value) {
|
|
1997
|
+
return value.replace(/["\r\n]/g, "_");
|
|
1998
|
+
}
|
|
1999
|
+
function concat(chunks) {
|
|
2000
|
+
const result = new Uint8Array(chunks.reduce((total, chunk) => total + chunk.byteLength, 0));
|
|
2001
|
+
let offset = 0;
|
|
2002
|
+
for (const chunk of chunks) {
|
|
2003
|
+
result.set(chunk, offset);
|
|
2004
|
+
offset += chunk.byteLength;
|
|
2005
|
+
}
|
|
2006
|
+
return result;
|
|
2007
|
+
}
|
|
2008
|
+
var encoder$1;
|
|
2009
|
+
var init_multipart = __esmMin((() => {
|
|
2010
|
+
encoder$1 = new TextEncoder();
|
|
2011
|
+
}));
|
|
2012
|
+
//#endregion
|
|
2013
|
+
//#region src/resources/files.ts
|
|
2014
|
+
function requireSaved(result, action) {
|
|
2015
|
+
if (!result.success || !result.id) throw new SalesforceSaveError(`${action} failed`, [result], result.errors ?? []);
|
|
2016
|
+
return result.id;
|
|
2017
|
+
}
|
|
2018
|
+
function stripExtension(fileName) {
|
|
2019
|
+
const index = fileName.lastIndexOf(".");
|
|
2020
|
+
return index > 0 ? fileName.slice(0, index) : fileName;
|
|
2021
|
+
}
|
|
2022
|
+
var MAX_JSON_UPLOAD_BYTES, MAX_UPLOAD_BYTES, encoder, FilesApi;
|
|
2023
|
+
var init_files = __esmMin((() => {
|
|
2024
|
+
init_errors();
|
|
2025
|
+
init_connection();
|
|
2026
|
+
init_multipart();
|
|
2027
|
+
MAX_JSON_UPLOAD_BYTES = Math.floor(29491200);
|
|
2028
|
+
MAX_UPLOAD_BYTES = 2147483648;
|
|
2029
|
+
encoder = new TextEncoder();
|
|
2030
|
+
FilesApi = class {
|
|
2031
|
+
_connection;
|
|
2032
|
+
_jsonUploadLimit;
|
|
2033
|
+
constructor(_connection, _jsonUploadLimit = MAX_JSON_UPLOAD_BYTES) {
|
|
2034
|
+
this._connection = _connection;
|
|
2035
|
+
this._jsonUploadLimit = _jsonUploadLimit;
|
|
2036
|
+
}
|
|
2037
|
+
/** Uploads a file and optionally links it to a record. */
|
|
2038
|
+
async upload(options) {
|
|
2039
|
+
const fields = {
|
|
2040
|
+
Title: options.title ?? stripExtension(options.fileName),
|
|
2041
|
+
PathOnClient: options.fileName
|
|
2042
|
+
};
|
|
2043
|
+
if (options.description !== void 0) fields.Description = options.description;
|
|
2044
|
+
const explicitLink = options.linkTo !== void 0 && (options.shareType ?? options.visibility) !== void 0;
|
|
2045
|
+
if (options.linkTo !== void 0 && !explicitLink) fields.FirstPublishLocationId = options.linkTo;
|
|
2046
|
+
const uploaded = await this.createVersion(fields, options.data, options.signal);
|
|
2047
|
+
if (explicitLink && options.linkTo !== void 0) await this.link(uploaded.contentDocumentId, options.linkTo, {
|
|
2048
|
+
shareType: options.shareType,
|
|
2049
|
+
visibility: options.visibility,
|
|
2050
|
+
signal: options.signal
|
|
2051
|
+
});
|
|
2052
|
+
return uploaded;
|
|
2053
|
+
}
|
|
2054
|
+
/** Adds a new version to an existing file. */
|
|
2055
|
+
newVersion(contentDocumentId, options) {
|
|
2056
|
+
const fields = {
|
|
2057
|
+
ContentDocumentId: contentDocumentId,
|
|
2058
|
+
Title: options.title ?? stripExtension(options.fileName),
|
|
2059
|
+
PathOnClient: options.fileName
|
|
2060
|
+
};
|
|
2061
|
+
if (options.reasonForChange !== void 0) fields.ReasonForChange = options.reasonForChange;
|
|
2062
|
+
return this.createVersion(fields, options.data, options.signal);
|
|
2063
|
+
}
|
|
2064
|
+
/** Downloads file content by ContentVersion id (`068...`) or ContentDocument id (`069...`, latest version). */
|
|
2065
|
+
async download(id, options = {}) {
|
|
2066
|
+
let versionId = id;
|
|
2067
|
+
if (id.startsWith("069")) versionId = (await this._connection.request({
|
|
2068
|
+
path: `/sobjects/ContentDocument/${segment(id)}`,
|
|
2069
|
+
query: { fields: "LatestPublishedVersionId" },
|
|
2070
|
+
signal: options.signal
|
|
2071
|
+
})).LatestPublishedVersionId;
|
|
2072
|
+
return this._connection.request({
|
|
2073
|
+
path: `/sobjects/ContentVersion/${segment(versionId)}/VersionData`,
|
|
2074
|
+
responseType: "binary",
|
|
2075
|
+
signal: options.signal
|
|
2076
|
+
});
|
|
2077
|
+
}
|
|
2078
|
+
/** Shares a file with a record, user or library. Returns the ContentDocumentLink id. */
|
|
2079
|
+
async link(contentDocumentId, linkedEntityId, options = {}) {
|
|
2080
|
+
const body = {
|
|
2081
|
+
ContentDocumentId: contentDocumentId,
|
|
2082
|
+
LinkedEntityId: linkedEntityId,
|
|
2083
|
+
ShareType: options.shareType ?? "V"
|
|
2084
|
+
};
|
|
2085
|
+
if (options.visibility) body.Visibility = options.visibility;
|
|
2086
|
+
return requireSaved(await this._connection.request({
|
|
2087
|
+
method: "POST",
|
|
2088
|
+
path: "/sobjects/ContentDocumentLink",
|
|
2089
|
+
body,
|
|
2090
|
+
signal: options.signal
|
|
2091
|
+
}), "Linking the file");
|
|
2092
|
+
}
|
|
2093
|
+
async createVersion(fields, data, signal) {
|
|
2094
|
+
const bytes = typeof data === "string" ? encoder.encode(data) : data;
|
|
2095
|
+
if (bytes.byteLength > 2147483648) throw new Error(`File of ${bytes.byteLength} bytes exceeds the 2 GB ContentVersion limit.`);
|
|
2096
|
+
const contentVersionId = requireSaved(bytes.byteLength <= this._jsonUploadLimit ? await this._connection.request({
|
|
2097
|
+
method: "POST",
|
|
2098
|
+
path: "/sobjects/ContentVersion",
|
|
2099
|
+
body: {
|
|
2100
|
+
...fields,
|
|
2101
|
+
VersionData: Buffer.from(bytes).toString("base64")
|
|
2102
|
+
},
|
|
2103
|
+
signal
|
|
2104
|
+
}) : await this.createVersionMultipart(fields, bytes, signal), "Uploading the file");
|
|
2105
|
+
return {
|
|
2106
|
+
contentVersionId,
|
|
2107
|
+
contentDocumentId: (await this._connection.request({
|
|
2108
|
+
path: `/sobjects/ContentVersion/${segment(contentVersionId)}`,
|
|
2109
|
+
query: { fields: "ContentDocumentId" },
|
|
2110
|
+
signal
|
|
2111
|
+
})).ContentDocumentId
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
2114
|
+
createVersionMultipart(fields, bytes, signal) {
|
|
2115
|
+
const { body, contentType } = buildMultipart([{
|
|
2116
|
+
name: "entity_content",
|
|
2117
|
+
contentType: "application/json",
|
|
2118
|
+
data: JSON.stringify(fields)
|
|
2119
|
+
}, {
|
|
2120
|
+
name: "VersionData",
|
|
2121
|
+
filename: String(fields.PathOnClient),
|
|
2122
|
+
contentType: "application/octet-stream",
|
|
2123
|
+
data: bytes
|
|
2124
|
+
}]);
|
|
2125
|
+
return this._connection.request({
|
|
2126
|
+
method: "POST",
|
|
2127
|
+
path: "/sobjects/ContentVersion",
|
|
2128
|
+
body,
|
|
2129
|
+
headers: { "Content-Type": contentType },
|
|
2130
|
+
signal,
|
|
2131
|
+
timeoutMs: 0
|
|
2132
|
+
});
|
|
2133
|
+
}
|
|
2134
|
+
};
|
|
2135
|
+
}));
|
|
2136
|
+
//#endregion
|
|
2137
|
+
//#region src/resources/query.ts
|
|
2138
|
+
function toQueryResult(response) {
|
|
2139
|
+
if (typeof response !== "object" || response === null || !Array.isArray(response.records)) throw new Error("Unexpected query response: missing records array.");
|
|
2140
|
+
return {
|
|
2141
|
+
records: response.records,
|
|
2142
|
+
totalSize: response.totalSize,
|
|
2143
|
+
done: response.done,
|
|
2144
|
+
nextRecordsUrl: response.nextRecordsUrl
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
function batchSizeHeader(batchSize) {
|
|
2148
|
+
if (batchSize === void 0) return;
|
|
2149
|
+
if (!Number.isSafeInteger(batchSize) || batchSize < 200 || batchSize > 2e3) throw new Error("batchSize must be an integer between 200 and 2000.");
|
|
2150
|
+
return { "Sforce-Query-Options": `batchSize=${batchSize}` };
|
|
2151
|
+
}
|
|
2152
|
+
var MAX_QUERY_URL_LENGTH, QueryApi;
|
|
2153
|
+
var init_query = __esmMin((() => {
|
|
2154
|
+
init_errors();
|
|
2155
|
+
MAX_QUERY_URL_LENGTH = 12e3;
|
|
2156
|
+
QueryApi = class {
|
|
2157
|
+
_connection;
|
|
2158
|
+
_basePath;
|
|
2159
|
+
constructor(_connection, _basePath = "") {
|
|
2160
|
+
this._connection = _connection;
|
|
2161
|
+
this._basePath = _basePath;
|
|
2162
|
+
}
|
|
2163
|
+
/** Returns the first page of results. */
|
|
2164
|
+
async page(soql, options = {}) {
|
|
2165
|
+
if (typeof soql !== "string" || soql.trim().length === 0) throw new Error("query() requires a non-blank SOQL string.");
|
|
2166
|
+
const path = `${this._basePath}/${options.includeDeleted ? "queryAll" : "query"}`;
|
|
2167
|
+
const headers = batchSizeHeader(options.batchSize);
|
|
2168
|
+
const encoded = encodeURIComponent(soql);
|
|
2169
|
+
return toQueryResult(encoded.length > 12e3 ? await this.viaComposite(path, encoded, headers, options.signal) : await this._connection.request({
|
|
2170
|
+
path,
|
|
2171
|
+
query: { q: soql },
|
|
2172
|
+
headers,
|
|
2173
|
+
signal: options.signal
|
|
2174
|
+
}));
|
|
2175
|
+
}
|
|
2176
|
+
/** Sends a query that is too long for a URL as a composite subrequest (still one API call). */
|
|
2177
|
+
async viaComposite(path, encodedSoql, headers, signal) {
|
|
2178
|
+
const [response] = (await this._connection.request({
|
|
2179
|
+
method: "POST",
|
|
2180
|
+
path: `${this._basePath}/composite`,
|
|
2181
|
+
body: { compositeRequest: [{
|
|
2182
|
+
method: "GET",
|
|
2183
|
+
referenceId: "query",
|
|
2184
|
+
url: `/services/data/${this._connection.apiVersion}${path}?q=${encodedSoql}`,
|
|
2185
|
+
...headers ? { httpHeaders: headers } : {}
|
|
2186
|
+
}] },
|
|
2187
|
+
retry: true,
|
|
2188
|
+
signal
|
|
2189
|
+
})).compositeResponse;
|
|
2190
|
+
if (!response || response.httpStatusCode >= 400) throw new SalesforceError({
|
|
2191
|
+
status: response?.httpStatusCode ?? 500,
|
|
2192
|
+
method: "GET",
|
|
2193
|
+
path,
|
|
2194
|
+
body: response?.body,
|
|
2195
|
+
headers: response?.httpHeaders
|
|
2196
|
+
});
|
|
2197
|
+
return response.body;
|
|
2198
|
+
}
|
|
2199
|
+
/** Fetches the page a cursor points to. */
|
|
2200
|
+
async more(cursor, options = {}) {
|
|
2201
|
+
if (typeof cursor !== "string" || !cursor.includes("/query")) throw new Error(`queryMore() expects a nextRecordsUrl, got "${String(cursor)}".`);
|
|
2202
|
+
return toQueryResult(await this._connection.request({
|
|
2203
|
+
path: cursor,
|
|
2204
|
+
headers: batchSizeHeader(options.batchSize),
|
|
2205
|
+
signal: options.signal
|
|
2206
|
+
}));
|
|
2207
|
+
}
|
|
2208
|
+
/** Yields every record, fetching further pages on demand. */
|
|
2209
|
+
async *iterate(soql, options = {}) {
|
|
2210
|
+
let result = await this.page(soql, options);
|
|
2211
|
+
yield* result.records;
|
|
2212
|
+
while (!result.done && result.nextRecordsUrl) {
|
|
2213
|
+
result = await this.more(result.nextRecordsUrl, options);
|
|
2214
|
+
yield* result.records;
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
/** Fetches every page and returns all records. */
|
|
2218
|
+
async collect(soql, options = {}) {
|
|
2219
|
+
const records = [];
|
|
2220
|
+
for await (const record of this.iterate(soql, options)) records.push(record);
|
|
2221
|
+
return records;
|
|
2222
|
+
}
|
|
2223
|
+
};
|
|
2224
|
+
}));
|
|
2225
|
+
//#endregion
|
|
2226
|
+
//#region src/resources/quick-actions.ts
|
|
2227
|
+
var GLOBAL_BASE_PATH, QuickActionsApi;
|
|
2228
|
+
var init_quick_actions = __esmMin((() => {
|
|
2229
|
+
init_errors();
|
|
2230
|
+
init_connection();
|
|
2231
|
+
GLOBAL_BASE_PATH = "/quickActions";
|
|
2232
|
+
QuickActionsApi = class {
|
|
2233
|
+
_connection;
|
|
2234
|
+
_basePath;
|
|
2235
|
+
constructor(_connection, _basePath) {
|
|
2236
|
+
this._connection = _connection;
|
|
2237
|
+
this._basePath = _basePath;
|
|
2238
|
+
}
|
|
2239
|
+
list(options = {}) {
|
|
2240
|
+
return this._connection.request({
|
|
2241
|
+
path: `${this._basePath}/`,
|
|
2242
|
+
signal: options.signal
|
|
2243
|
+
});
|
|
2244
|
+
}
|
|
2245
|
+
describe(name, options = {}) {
|
|
2246
|
+
return this.send(name, "/describe/", { signal: options.signal });
|
|
2247
|
+
}
|
|
2248
|
+
/** The default field values the action would pre-fill, optionally for a context record. */
|
|
2249
|
+
defaultValues(name, contextId, options = {}) {
|
|
2250
|
+
const suffix = contextId ? `/${segment(contextId)}` : "";
|
|
2251
|
+
return this.send(name, `/defaultValues${suffix}`, { signal: options.signal });
|
|
2252
|
+
}
|
|
2253
|
+
/** Runs the action with the given record values. */
|
|
2254
|
+
async invoke(name, record, options = {}) {
|
|
2255
|
+
const body = { record };
|
|
2256
|
+
if (options.contextId) body.contextId = options.contextId;
|
|
2257
|
+
const result = await this.send(name, "", {
|
|
2258
|
+
method: "POST",
|
|
2259
|
+
body,
|
|
2260
|
+
signal: options.signal
|
|
2261
|
+
});
|
|
2262
|
+
if (options.throwOnError !== false && !result.success) throw new SalesforceSaveError(`Quick action "${name}" failed`, [result], result.errors ?? []);
|
|
2263
|
+
return result;
|
|
2264
|
+
}
|
|
2265
|
+
/** Calls the action under this sObject, falling back to the global action of that name. */
|
|
2266
|
+
async send(name, suffix, request) {
|
|
2267
|
+
if (typeof name !== "string" || !/^[A-Za-z][A-Za-z0-9_.]*$/.test(name)) throw new Error(`Invalid quick action name "${String(name)}".`);
|
|
2268
|
+
const path = (base) => `${base}/${segment(name)}${suffix}`;
|
|
2269
|
+
try {
|
|
2270
|
+
return await this._connection.request({
|
|
2271
|
+
...request,
|
|
2272
|
+
path: path(this._basePath)
|
|
2273
|
+
});
|
|
2274
|
+
} catch (error) {
|
|
2275
|
+
if (this._basePath === GLOBAL_BASE_PATH || !hasErrorCode(error, "NOT_FOUND")) throw error;
|
|
2276
|
+
return this._connection.request({
|
|
2277
|
+
...request,
|
|
2278
|
+
path: path(GLOBAL_BASE_PATH)
|
|
2279
|
+
});
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
};
|
|
2283
|
+
}));
|
|
2284
|
+
//#endregion
|
|
2285
|
+
//#region src/resources/reports.ts
|
|
2286
|
+
/**
|
|
2287
|
+
* Flattens the detail rows of a report result into objects keyed by detail column API name
|
|
2288
|
+
* (e.g. `ACCOUNT.NAME`), each with the cell's `label` and `value`. Works for tabular, summary
|
|
2289
|
+
* and matrix reports; requires `includeDetails`.
|
|
2290
|
+
*/
|
|
2291
|
+
function reportRows(result) {
|
|
2292
|
+
const columns = result.reportMetadata.detailColumns;
|
|
2293
|
+
const rows = [];
|
|
2294
|
+
const downDepth = result.reportMetadata.groupingsDown?.length ?? 0;
|
|
2295
|
+
const acrossDepth = result.reportMetadata.groupingsAcross?.length ?? 0;
|
|
2296
|
+
for (const [key, fact] of Object.entries(result.factMap)) {
|
|
2297
|
+
const [down = "T", across = "T"] = key.split("!");
|
|
2298
|
+
if (!fact.rows || groupingDepth(down) !== downDepth || groupingDepth(across) !== acrossDepth) continue;
|
|
2299
|
+
for (const row of fact.rows) {
|
|
2300
|
+
const record = {};
|
|
2301
|
+
columns.forEach((column, index) => {
|
|
2302
|
+
const cell = row.dataCells[index];
|
|
2303
|
+
if (cell) record[column] = cell;
|
|
2304
|
+
});
|
|
2305
|
+
rows.push(record);
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
return rows;
|
|
2309
|
+
}
|
|
2310
|
+
/** `"T"` is the total (depth 0); `"0"` is depth 1, `"0_2"` depth 2, ... */
|
|
2311
|
+
function groupingDepth(key) {
|
|
2312
|
+
return key === "T" ? 0 : key.split("_").length;
|
|
2313
|
+
}
|
|
2314
|
+
function runMetadata(options) {
|
|
2315
|
+
if (!options.filters && options.booleanFilter === void 0) return;
|
|
2316
|
+
const metadata = {};
|
|
2317
|
+
if (options.filters) metadata.reportFilters = options.filters;
|
|
2318
|
+
if (options.booleanFilter !== void 0) metadata.reportBooleanFilter = options.booleanFilter;
|
|
2319
|
+
return metadata;
|
|
2320
|
+
}
|
|
2321
|
+
function reportPath(reportId) {
|
|
2322
|
+
if (typeof reportId !== "string" || !/^[A-Za-z0-9]{15,18}$/.test(reportId)) throw new Error(`Invalid report id "${String(reportId)}".`);
|
|
2323
|
+
return `/analytics/reports/${reportId}`;
|
|
2324
|
+
}
|
|
2325
|
+
var ReportsApi;
|
|
2326
|
+
var init_reports = __esmMin((() => {
|
|
2327
|
+
init_connection();
|
|
2328
|
+
ReportsApi = class {
|
|
2329
|
+
_connection;
|
|
2330
|
+
constructor(_connection) {
|
|
2331
|
+
this._connection = _connection;
|
|
2332
|
+
}
|
|
2333
|
+
/** Recently viewed reports. */
|
|
2334
|
+
list(options = {}) {
|
|
2335
|
+
return this._connection.request({
|
|
2336
|
+
path: "/analytics/reports",
|
|
2337
|
+
signal: options.signal
|
|
2338
|
+
});
|
|
2339
|
+
}
|
|
2340
|
+
describe(reportId, options = {}) {
|
|
2341
|
+
return this._connection.request({
|
|
2342
|
+
path: `${reportPath(reportId)}/describe`,
|
|
2343
|
+
signal: options.signal
|
|
2344
|
+
});
|
|
2345
|
+
}
|
|
2346
|
+
/** Runs a report synchronously. */
|
|
2347
|
+
run(reportId, options = {}) {
|
|
2348
|
+
const reportMetadata = runMetadata(options);
|
|
2349
|
+
return this._connection.request({
|
|
2350
|
+
method: reportMetadata ? "POST" : "GET",
|
|
2351
|
+
path: reportPath(reportId),
|
|
2352
|
+
query: { includeDetails: options.includeDetails ?? true },
|
|
2353
|
+
body: reportMetadata ? { reportMetadata } : void 0,
|
|
2354
|
+
signal: options.signal,
|
|
2355
|
+
timeoutMs: 0
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
/** Starts an asynchronous run; results stay available for 24 hours. */
|
|
2359
|
+
runAsync(reportId, options = {}) {
|
|
2360
|
+
const reportMetadata = runMetadata(options);
|
|
2361
|
+
return this._connection.request({
|
|
2362
|
+
method: "POST",
|
|
2363
|
+
path: `${reportPath(reportId)}/instances`,
|
|
2364
|
+
query: { includeDetails: options.includeDetails ?? true },
|
|
2365
|
+
body: reportMetadata ? { reportMetadata } : {},
|
|
2366
|
+
signal: options.signal
|
|
2367
|
+
});
|
|
2368
|
+
}
|
|
2369
|
+
/** Status (and, when finished, results) of an asynchronous run. */
|
|
2370
|
+
instance(reportId, instanceId, options = {}) {
|
|
2371
|
+
return this._connection.request({
|
|
2372
|
+
path: `${reportPath(reportId)}/instances/${segment(instanceId)}`,
|
|
2373
|
+
signal: options.signal
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2376
|
+
/** Polls an asynchronous run until it succeeds, then returns the results. */
|
|
2377
|
+
async waitForInstance(reportId, instanceId, options = {}) {
|
|
2378
|
+
const interval = options.pollIntervalMs ?? 2e3;
|
|
2379
|
+
const deadline = Date.now() + (options.timeoutMs ?? 3e5);
|
|
2380
|
+
for (;;) {
|
|
2381
|
+
const result = await this.instance(reportId, instanceId, { signal: options.signal });
|
|
2382
|
+
const status = result.attributes.status;
|
|
2383
|
+
if (status === "Success") return result;
|
|
2384
|
+
if (status === "Error") throw new Error(`Report run ${instanceId} of report ${reportId} failed.`);
|
|
2385
|
+
if (Date.now() + interval > deadline) throw new Error(`Report run ${instanceId} of report ${reportId} did not finish in time (status ${status}).`);
|
|
2386
|
+
await sleep(interval, options.signal);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
/** Flattens detail rows of a report result; see `reportRows`. */
|
|
2390
|
+
toRows(result) {
|
|
2391
|
+
return reportRows(result);
|
|
2392
|
+
}
|
|
2393
|
+
};
|
|
2394
|
+
}));
|
|
2395
|
+
//#endregion
|
|
2396
|
+
//#region src/resources/search.ts
|
|
2397
|
+
var SearchApi;
|
|
2398
|
+
var init_search = __esmMin((() => {
|
|
2399
|
+
SearchApi = class {
|
|
2400
|
+
_connection;
|
|
2401
|
+
constructor(_connection) {
|
|
2402
|
+
this._connection = _connection;
|
|
2403
|
+
}
|
|
2404
|
+
/**
|
|
2405
|
+
* Runs a SOSL query, e.g. `FIND {Acme*} IN NAME FIELDS RETURNING Account(Id, Name)`.
|
|
2406
|
+
* Escape user input in the `FIND {...}` term with `soslEscape`.
|
|
2407
|
+
*/
|
|
2408
|
+
sosl(query, options = {}) {
|
|
2409
|
+
if (typeof query !== "string" || query.trim().length === 0) throw new Error("sosl() requires a non-blank SOSL string.");
|
|
2410
|
+
return this._connection.request({
|
|
2411
|
+
path: "/search",
|
|
2412
|
+
query: { q: query },
|
|
2413
|
+
signal: options.signal
|
|
2414
|
+
});
|
|
2415
|
+
}
|
|
2416
|
+
/** `POST /parameterizedSearch`: search without writing SOSL. */
|
|
2417
|
+
parameterized(request, options = {}) {
|
|
2418
|
+
return this._connection.request({
|
|
2419
|
+
method: "POST",
|
|
2420
|
+
path: "/parameterizedSearch",
|
|
2421
|
+
body: request,
|
|
2422
|
+
signal: options.signal
|
|
2423
|
+
});
|
|
2424
|
+
}
|
|
2425
|
+
/** `GET /search/suggestions`: auto-suggest records matching the start of a term. */
|
|
2426
|
+
suggestions(options) {
|
|
2427
|
+
const { signal, ...query } = options;
|
|
2428
|
+
return this._connection.request({
|
|
2429
|
+
path: "/search/suggestions",
|
|
2430
|
+
query,
|
|
2431
|
+
signal
|
|
2432
|
+
});
|
|
2433
|
+
}
|
|
2434
|
+
};
|
|
2435
|
+
}));
|
|
2436
|
+
//#endregion
|
|
2437
|
+
//#region src/soql/query-builder.ts
|
|
2438
|
+
/**
|
|
2439
|
+
* Narrows a record by its `attributes.type`, e.g. the union returned for a `selectTypeOf` field.
|
|
2440
|
+
* TypeScript doesn't narrow on nested properties, so `record.attributes.type === "Account"` alone
|
|
2441
|
+
* won't narrow; this guard does:
|
|
2442
|
+
*
|
|
2443
|
+
* ```ts
|
|
2444
|
+
* if (isSObjectType(task.What, "Account")) task.What.Phone;
|
|
2445
|
+
* ```
|
|
2446
|
+
*/
|
|
2447
|
+
function isSObjectType(record, type) {
|
|
2448
|
+
return record?.attributes.type === type;
|
|
2449
|
+
}
|
|
2450
|
+
/** Returns a factory for builders bound to registry `R`: `const soql = soqlFor<SObjectRegistry>(); soql("Account")`. */
|
|
2451
|
+
function soqlFor() {
|
|
2452
|
+
return (sobjectName) => SoqlQueryBuilder.from(sobjectName);
|
|
2453
|
+
}
|
|
2454
|
+
function fieldList(fields) {
|
|
2455
|
+
fields.forEach((field) => assertIdentifier("TYPEOF field", field));
|
|
2456
|
+
return [...new Set(fields)].join(", ");
|
|
2457
|
+
}
|
|
2458
|
+
function assertParentPath(path) {
|
|
2459
|
+
const segments = typeof path === "string" ? path.split(".") : [];
|
|
2460
|
+
if (segments.length === 0 || segments.some((segment) => !IDENTIFIER.test(segment))) throw new Error(`Invalid relationship path "${String(path)}".`);
|
|
2461
|
+
if (segments.length > MAX_PARENT_DEPTH) throw new Error(`Relationship path "${path}" exceeds Salesforce's maximum of ${MAX_PARENT_DEPTH} levels.`);
|
|
2462
|
+
}
|
|
2463
|
+
function assertField(context, field) {
|
|
2464
|
+
if (typeof field !== "string" || !FIELD_PATH.test(field)) throw new Error(`${context}: invalid field name "${String(field)}".`);
|
|
2465
|
+
}
|
|
2466
|
+
function assertOperator(operator) {
|
|
2467
|
+
if (!OPERATORS.has(operator)) throw new Error(`Invalid SOQL operator "${String(operator)}". Use one of: ${[...OPERATORS].join(", ")}.`);
|
|
2468
|
+
}
|
|
2469
|
+
function orderSuffix(direction, nullOrder) {
|
|
2470
|
+
if (!DIRECTIONS.has(direction)) throw new Error(`Invalid sort direction "${String(direction)}". Use ASC or DESC.`);
|
|
2471
|
+
if (nullOrder !== void 0 && !NULL_ORDERS.has(nullOrder)) throw new Error(`Invalid null order "${String(nullOrder)}". Use NULLS FIRST or NULLS LAST.`);
|
|
2472
|
+
return `${direction}${nullOrder ? ` ${nullOrder}` : ""}`;
|
|
2473
|
+
}
|
|
2474
|
+
function assertIdentifier(context, value) {
|
|
2475
|
+
if (typeof value !== "string" || !IDENTIFIER.test(value)) throw new Error(`${context}: invalid name "${String(value)}".`);
|
|
2476
|
+
}
|
|
2477
|
+
function assertNonBlank(method, value) {
|
|
2478
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${method}() requires a non-blank SOQL fragment.`);
|
|
2479
|
+
}
|
|
2480
|
+
function assertNonNegativeInteger(method, count) {
|
|
2481
|
+
if (!Number.isSafeInteger(count) || count < 0) throw new Error(`${method}() requires a non-negative safe integer.`);
|
|
2482
|
+
}
|
|
2483
|
+
var FIELDS_ALL_MAX_LIMIT, MAX_OFFSET, MAX_PARENT_DEPTH, ALIAS_PATTERN, IDENTIFIER, FIELD_PATH, OPERATORS, DIRECTIONS, NULL_ORDERS, RESERVED_WORDS, TypeOfBuilder, SoqlQueryBuilder;
|
|
2484
|
+
var init_query_builder = __esmMin((() => {
|
|
2485
|
+
init_escape();
|
|
2486
|
+
FIELDS_ALL_MAX_LIMIT = 200;
|
|
2487
|
+
MAX_OFFSET = 2e3;
|
|
2488
|
+
MAX_PARENT_DEPTH = 5;
|
|
2489
|
+
ALIAS_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
2490
|
+
IDENTIFIER = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
2491
|
+
FIELD_PATH = /^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)*$/;
|
|
2492
|
+
OPERATORS = /* @__PURE__ */ new Set([
|
|
2493
|
+
"=",
|
|
2494
|
+
"!=",
|
|
2495
|
+
">",
|
|
2496
|
+
"<",
|
|
2497
|
+
">=",
|
|
2498
|
+
"<=",
|
|
2499
|
+
"LIKE"
|
|
2500
|
+
]);
|
|
2501
|
+
DIRECTIONS = /* @__PURE__ */ new Set(["ASC", "DESC"]);
|
|
2502
|
+
NULL_ORDERS = /* @__PURE__ */ new Set(["NULLS FIRST", "NULLS LAST"]);
|
|
2503
|
+
RESERVED_WORDS = new Set([
|
|
2504
|
+
"AND",
|
|
2505
|
+
"ASC",
|
|
2506
|
+
"AT",
|
|
2507
|
+
"BY",
|
|
2508
|
+
"CUBE",
|
|
2509
|
+
"DESC",
|
|
2510
|
+
"ELSE",
|
|
2511
|
+
"END",
|
|
2512
|
+
"EXCLUDES",
|
|
2513
|
+
"FALSE",
|
|
2514
|
+
"FIRST",
|
|
2515
|
+
"FOR",
|
|
2516
|
+
"FORMAT",
|
|
2517
|
+
"FROM",
|
|
2518
|
+
"GROUP",
|
|
2519
|
+
"HAVING",
|
|
2520
|
+
"IN",
|
|
2521
|
+
"INCLUDES",
|
|
2522
|
+
"LAST",
|
|
2523
|
+
"LIKE",
|
|
2524
|
+
"LIMIT",
|
|
2525
|
+
"NOT",
|
|
2526
|
+
"NULL",
|
|
2527
|
+
"NULLS",
|
|
2528
|
+
"OFFSET",
|
|
2529
|
+
"OR",
|
|
2530
|
+
"ORDER",
|
|
2531
|
+
"REFERENCE",
|
|
2532
|
+
"ROLLUP",
|
|
2533
|
+
"SELECT",
|
|
2534
|
+
"THEN",
|
|
2535
|
+
"TRACKING",
|
|
2536
|
+
"TRUE",
|
|
2537
|
+
"TYPEOF",
|
|
2538
|
+
"UPDATE",
|
|
2539
|
+
"USING",
|
|
2540
|
+
"VIEW",
|
|
2541
|
+
"WHEN",
|
|
2542
|
+
"WHERE",
|
|
2543
|
+
"WITH"
|
|
2544
|
+
].map((word) => word.toLowerCase()));
|
|
2545
|
+
TypeOfBuilder = class {
|
|
2546
|
+
_whens = [];
|
|
2547
|
+
_types = /* @__PURE__ */ new Set();
|
|
2548
|
+
_else;
|
|
2549
|
+
/** `WHEN type THEN fields`. */
|
|
2550
|
+
when(sobject, firstField, ...additionalFields) {
|
|
2551
|
+
assertIdentifier("TYPEOF when()", sobject);
|
|
2552
|
+
if (this._types.has(sobject)) throw new Error(`TYPEOF already has a WHEN ${sobject} clause.`);
|
|
2553
|
+
if (this._else !== void 0) throw new Error("TYPEOF when() must come before else().");
|
|
2554
|
+
this._types.add(sobject);
|
|
2555
|
+
this._whens.push(`WHEN ${sobject} THEN ${fieldList([firstField, ...additionalFields])}`);
|
|
2556
|
+
return this;
|
|
2557
|
+
}
|
|
2558
|
+
/** `ELSE fields`: the Name fields for every type without a `when`. */
|
|
2559
|
+
else(firstField, ...additionalFields) {
|
|
2560
|
+
this._else = fieldList([firstField, ...additionalFields]);
|
|
2561
|
+
return this;
|
|
2562
|
+
}
|
|
2563
|
+
/** @internal */
|
|
2564
|
+
toSoql(relationship) {
|
|
2565
|
+
if (this._whens.length === 0) throw new Error(`selectTypeOf("${relationship}") requires at least one when().`);
|
|
2566
|
+
const elseClause = this._else === void 0 ? "" : ` ELSE ${this._else}`;
|
|
2567
|
+
return `TYPEOF ${relationship} ${this._whens.join(" ")}${elseClause} END`;
|
|
2568
|
+
}
|
|
2569
|
+
};
|
|
2570
|
+
SoqlQueryBuilder = class SoqlQueryBuilder {
|
|
2571
|
+
_fromClause;
|
|
2572
|
+
_fields = [];
|
|
2573
|
+
_conditions = [];
|
|
2574
|
+
_groupBy = [];
|
|
2575
|
+
_having = [];
|
|
2576
|
+
_orderBy = [];
|
|
2577
|
+
_limit;
|
|
2578
|
+
_offset;
|
|
2579
|
+
_withUserMode = false;
|
|
2580
|
+
_forClause;
|
|
2581
|
+
_usesTypeOf = false;
|
|
2582
|
+
_countAll = false;
|
|
2583
|
+
_usesAggregates = false;
|
|
2584
|
+
constructor(fromClause) {
|
|
2585
|
+
this._fromClause = fromClause;
|
|
2586
|
+
}
|
|
2587
|
+
/** Returns an independent copy, so the original can be reused as a base for other queries. */
|
|
2588
|
+
clone() {
|
|
2589
|
+
const copy = new SoqlQueryBuilder(this._fromClause);
|
|
2590
|
+
copy._fields = [...this._fields];
|
|
2591
|
+
copy._conditions = [...this._conditions];
|
|
2592
|
+
copy._groupBy = [...this._groupBy];
|
|
2593
|
+
copy._having = [...this._having];
|
|
2594
|
+
copy._orderBy = [...this._orderBy];
|
|
2595
|
+
copy._limit = this._limit;
|
|
2596
|
+
copy._offset = this._offset;
|
|
2597
|
+
copy._withUserMode = this._withUserMode;
|
|
2598
|
+
copy._forClause = this._forClause;
|
|
2599
|
+
copy._usesTypeOf = this._usesTypeOf;
|
|
2600
|
+
copy._countAll = this._countAll;
|
|
2601
|
+
copy._usesAggregates = this._usesAggregates;
|
|
2602
|
+
return copy;
|
|
2603
|
+
}
|
|
2604
|
+
/** Starts a query on `sobjectName`. Prefer `client.soql()` or `soqlFor<R>()`, which infer `R`. */
|
|
2605
|
+
static from(sobjectName) {
|
|
2606
|
+
if (typeof sobjectName !== "string" || !IDENTIFIER.test(sobjectName)) throw new Error(`Invalid sObject name "${String(sobjectName)}".`);
|
|
2607
|
+
return new SoqlQueryBuilder(sobjectName);
|
|
2608
|
+
}
|
|
2609
|
+
/** The sObject (or child relationship, in a subquery) this query selects from. */
|
|
2610
|
+
get sobjectName() {
|
|
2611
|
+
return this._fromClause;
|
|
2612
|
+
}
|
|
2613
|
+
/** `true` when the query contains `TYPEOF`, which Bulk API, semi-joins and GROUP BY don't support. */
|
|
2614
|
+
get usesTypeOf() {
|
|
2615
|
+
return this._usesTypeOf;
|
|
2616
|
+
}
|
|
2617
|
+
/** Adds fields to the select list. The result type is narrowed to the selected fields. */
|
|
2618
|
+
select(firstField, ...additionalFields) {
|
|
2619
|
+
if (typeof firstField !== "string" || firstField.length === 0) throw new Error("select() requires at least one field.");
|
|
2620
|
+
const fields = [firstField, ...additionalFields];
|
|
2621
|
+
fields.forEach((field) => assertField("select()", field));
|
|
2622
|
+
this.addFields(fields);
|
|
2623
|
+
return this.cast();
|
|
2624
|
+
}
|
|
2625
|
+
/** Escape hatch for select expressions the typed helpers can't express. The result gains an index signature. */
|
|
2626
|
+
selectRaw(firstExpression, ...additionalExpressions) {
|
|
2627
|
+
const expressions = [firstExpression, ...additionalExpressions];
|
|
2628
|
+
expressions.forEach((expression) => assertNonBlank("selectRaw", expression));
|
|
2629
|
+
this.addFields(expressions);
|
|
2630
|
+
return this.cast();
|
|
2631
|
+
}
|
|
2632
|
+
/**
|
|
2633
|
+
* Selects fields through a child-to-parent path, e.g. `selectRelated("Account", "Name")` or
|
|
2634
|
+
* `selectRelated("Account.Owner", "Email")` from `Contact`. Each level is `| null` in the result.
|
|
2635
|
+
*/
|
|
2636
|
+
selectRelated(path, firstField, ...additionalFields) {
|
|
2637
|
+
assertParentPath(path);
|
|
2638
|
+
const fields = [firstField, ...additionalFields];
|
|
2639
|
+
fields.forEach((field) => assertIdentifier("selectRelated()", field));
|
|
2640
|
+
this.addFields(fields.map((field) => `${String(path)}.${field}`));
|
|
2641
|
+
return this.cast();
|
|
2642
|
+
}
|
|
2643
|
+
/**
|
|
2644
|
+
* Selects Name fields of a polymorphic lookup without TYPEOF, e.g. `selectPolymorphic("Owner", "Name")`
|
|
2645
|
+
* from `Case`. `attributes.type` tells which sObject it is.
|
|
2646
|
+
*/
|
|
2647
|
+
selectPolymorphic(relationship, firstField, ...additionalFields) {
|
|
2648
|
+
assertIdentifier("selectPolymorphic()", relationship);
|
|
2649
|
+
[firstField, ...additionalFields].forEach((field) => assertIdentifier("selectPolymorphic()", field));
|
|
2650
|
+
this.addFields([firstField, ...additionalFields].map((field) => `${String(relationship)}.${field}`));
|
|
2651
|
+
return this.cast();
|
|
2652
|
+
}
|
|
2653
|
+
/**
|
|
2654
|
+
* Adds a `TYPEOF` expression for a polymorphic lookup. The result is a union discriminated on
|
|
2655
|
+
* `attributes.type`:
|
|
2656
|
+
*
|
|
2657
|
+
* ```ts
|
|
2658
|
+
* sf.soql("Task").select("Id").selectTypeOf("What", (t) =>
|
|
2659
|
+
* t.when("Account", "Phone").when("Opportunity", "Amount").else("Name"),
|
|
2660
|
+
* );
|
|
2661
|
+
* ```
|
|
2662
|
+
*/
|
|
2663
|
+
selectTypeOf(relationship, build) {
|
|
2664
|
+
assertIdentifier("selectTypeOf()", relationship);
|
|
2665
|
+
const builder = build(new TypeOfBuilder());
|
|
2666
|
+
this._fields.push(builder.toSoql(relationship));
|
|
2667
|
+
this._usesTypeOf = true;
|
|
2668
|
+
return this.cast();
|
|
2669
|
+
}
|
|
2670
|
+
/**
|
|
2671
|
+
* Adds a parent-to-child subquery, e.g. `selectChild("Contacts", c => c.select("Id", "Email"))`
|
|
2672
|
+
* from `Account` builds `(SELECT Id, Email FROM Contacts)`.
|
|
2673
|
+
*/
|
|
2674
|
+
selectChild(relationship, build) {
|
|
2675
|
+
assertIdentifier("selectChild()", relationship);
|
|
2676
|
+
const sub = new SoqlQueryBuilder(relationship);
|
|
2677
|
+
if (build(sub) !== sub) throw new Error(`selectChild("${String(relationship)}") callback must return the provided query builder.`);
|
|
2678
|
+
if (sub._fields.length === 0) throw new Error(`selectChild("${String(relationship)}") requires select() with at least one field.`);
|
|
2679
|
+
const invalid = [
|
|
2680
|
+
sub._usesTypeOf ? "TYPEOF" : "",
|
|
2681
|
+
sub._usesAggregates || sub._countAll ? "aggregate functions" : "",
|
|
2682
|
+
sub._groupBy.length > 0 || sub._having.length > 0 ? "GROUP BY/HAVING" : "",
|
|
2683
|
+
sub._withUserMode ? "WITH USER_MODE" : "",
|
|
2684
|
+
sub._forClause ? `FOR ${sub._forClause}` : ""
|
|
2685
|
+
].filter((item) => item.length > 0);
|
|
2686
|
+
if (invalid.length > 0) throw new Error(`selectChild("${String(relationship)}"): ${invalid.join(", ")} can't be used in a subquery.`);
|
|
2687
|
+
this._fields.push(`(${sub.build()})`);
|
|
2688
|
+
return this.cast();
|
|
2689
|
+
}
|
|
2690
|
+
count(field, alias) {
|
|
2691
|
+
if (field === void 0) {
|
|
2692
|
+
this._countAll = true;
|
|
2693
|
+
this.addFields(["COUNT()"]);
|
|
2694
|
+
} else this.selectAggregate("COUNT", field, alias);
|
|
2695
|
+
return this.cast();
|
|
2696
|
+
}
|
|
2697
|
+
countDistinct(field, alias) {
|
|
2698
|
+
this.selectAggregate("COUNT_DISTINCT", field, alias);
|
|
2699
|
+
return this.cast();
|
|
2700
|
+
}
|
|
2701
|
+
sum(field, alias) {
|
|
2702
|
+
this.selectAggregate("SUM", field, alias);
|
|
2703
|
+
return this.cast();
|
|
2704
|
+
}
|
|
2705
|
+
avg(field, alias) {
|
|
2706
|
+
this.selectAggregate("AVG", field, alias);
|
|
2707
|
+
return this.cast();
|
|
2708
|
+
}
|
|
2709
|
+
min(field, alias) {
|
|
2710
|
+
this.selectAggregate("MIN", field, alias);
|
|
2711
|
+
return this.cast();
|
|
2712
|
+
}
|
|
2713
|
+
max(field, alias) {
|
|
2714
|
+
this.selectAggregate("MAX", field, alias);
|
|
2715
|
+
return this.cast();
|
|
2716
|
+
}
|
|
2717
|
+
/**
|
|
2718
|
+
* Adds `field operator value`. The value is escaped and type-checked against the field:
|
|
2719
|
+
* date fields take `soqlDate()`/`soqlDateLiteral()`, datetime fields a `Date` or literal,
|
|
2720
|
+
* `LIKE` only works on text, and `null` only on nillable fields with `=`/`!=`.
|
|
2721
|
+
*/
|
|
2722
|
+
where(field, operator, value) {
|
|
2723
|
+
assertField("where()", field);
|
|
2724
|
+
assertOperator(operator);
|
|
2725
|
+
this._conditions.push(`${field} ${operator} ${soqlEscape(value)}`);
|
|
2726
|
+
return this;
|
|
2727
|
+
}
|
|
2728
|
+
/**
|
|
2729
|
+
* Adds a raw condition, wrapped in parentheses so an `OR` inside it can't change how it combines
|
|
2730
|
+
* with other conditions. Escape interpolated values with `soqlEscape`.
|
|
2731
|
+
*/
|
|
2732
|
+
whereRaw(conditionSql) {
|
|
2733
|
+
assertNonBlank("whereRaw", conditionSql);
|
|
2734
|
+
this._conditions.push(`(${conditionSql})`);
|
|
2735
|
+
return this;
|
|
2736
|
+
}
|
|
2737
|
+
/** `field IN (...)` with a list of values or a single-field semi-join subquery. */
|
|
2738
|
+
whereIn(field, values) {
|
|
2739
|
+
return this.whereSet(field, "IN", values);
|
|
2740
|
+
}
|
|
2741
|
+
/** `field NOT IN (...)` with a list of values or a single-field anti-join subquery. */
|
|
2742
|
+
whereNotIn(field, values) {
|
|
2743
|
+
return this.whereSet(field, "NOT IN", values);
|
|
2744
|
+
}
|
|
2745
|
+
/**
|
|
2746
|
+
* `field INCLUDES (...)` for multi-select picklists. Each item matches when all its values are
|
|
2747
|
+
* selected: `whereIncludes("Interests__c", [["Golf", "Tennis"], "Chess"])` builds
|
|
2748
|
+
* `INCLUDES ('Golf;Tennis', 'Chess')`.
|
|
2749
|
+
*/
|
|
2750
|
+
whereIncludes(field, values) {
|
|
2751
|
+
return this.whereMultiPicklist(String(field), "INCLUDES", values);
|
|
2752
|
+
}
|
|
2753
|
+
/** `field EXCLUDES (...)` for multi-select picklists; items combine like `whereIncludes`. */
|
|
2754
|
+
whereExcludes(field, values) {
|
|
2755
|
+
return this.whereMultiPicklist(String(field), "EXCLUDES", values);
|
|
2756
|
+
}
|
|
2757
|
+
/**
|
|
2758
|
+
* Wraps conditions in parentheses, joined by `join` (default `OR`). Groups nest, so
|
|
2759
|
+
* `(A OR B) AND (C OR (D AND E))` is expressible. Top-level conditions are always joined with AND.
|
|
2760
|
+
*/
|
|
2761
|
+
whereGroup(build, join = "OR") {
|
|
2762
|
+
const conditions = this.groupConditions(build);
|
|
2763
|
+
if (conditions.length > 0) this._conditions.push(`(${conditions.join(` ${join} `)})`);
|
|
2764
|
+
return this;
|
|
2765
|
+
}
|
|
2766
|
+
/** Negates a group of conditions: `whereNot(g => g.where("Type", "=", "Prospect"))` builds `(NOT (Type = 'Prospect'))`. */
|
|
2767
|
+
whereNot(build, join = "AND") {
|
|
2768
|
+
const conditions = this.groupConditions(build);
|
|
2769
|
+
if (conditions.length === 0) throw new Error("whereNot() requires at least one condition.");
|
|
2770
|
+
this._conditions.push(`(NOT (${conditions.join(` ${join} `)}))`);
|
|
2771
|
+
return this;
|
|
2772
|
+
}
|
|
2773
|
+
/**
|
|
2774
|
+
* Condition on a parent field, e.g. `whereRelated("Account", "Name", "=", "Acme")` or
|
|
2775
|
+
* `whereRelated("Account.Owner", "IsActive", "=", true)` from `Contact`.
|
|
2776
|
+
*/
|
|
2777
|
+
whereRelated(path, field, operator, value) {
|
|
2778
|
+
assertParentPath(path);
|
|
2779
|
+
assertIdentifier("whereRelated()", field);
|
|
2780
|
+
assertOperator(operator);
|
|
2781
|
+
this._conditions.push(`${String(path)}.${field} ${operator} ${soqlEscape(value)}`);
|
|
2782
|
+
return this;
|
|
2783
|
+
}
|
|
2784
|
+
groupBy(...fields) {
|
|
2785
|
+
fields.forEach((field) => assertField("groupBy()", field));
|
|
2786
|
+
this._groupBy.push(...fields);
|
|
2787
|
+
return this;
|
|
2788
|
+
}
|
|
2789
|
+
/** Raw `HAVING` condition, e.g. `"COUNT(Id) > 1"`, wrapped in parentheses. */
|
|
2790
|
+
havingRaw(conditionSql) {
|
|
2791
|
+
assertNonBlank("havingRaw", conditionSql);
|
|
2792
|
+
this._having.push(`(${conditionSql})`);
|
|
2793
|
+
return this;
|
|
2794
|
+
}
|
|
2795
|
+
havingGroup(build, join = "OR") {
|
|
2796
|
+
const group = new SoqlQueryBuilder(this._fromClause);
|
|
2797
|
+
build(group);
|
|
2798
|
+
group.assertOnly("havingGroup()", "having");
|
|
2799
|
+
if (group._having.length > 0) this._having.push(`(${group._having.join(` ${join} `)})`);
|
|
2800
|
+
return this;
|
|
2801
|
+
}
|
|
2802
|
+
orderBy(field, direction = "ASC", nullOrder) {
|
|
2803
|
+
assertField("orderBy()", field);
|
|
2804
|
+
this._orderBy.push(`${field} ${orderSuffix(direction, nullOrder)}`);
|
|
2805
|
+
return this;
|
|
2806
|
+
}
|
|
2807
|
+
/** Orders by a parent field, e.g. `orderByRelated("Account", "Name", "DESC")`. */
|
|
2808
|
+
orderByRelated(path, field, direction = "ASC", nullOrder) {
|
|
2809
|
+
assertParentPath(path);
|
|
2810
|
+
assertIdentifier("orderByRelated()", field);
|
|
2811
|
+
this._orderBy.push(`${String(path)}.${field} ${orderSuffix(direction, nullOrder)}`);
|
|
2812
|
+
return this;
|
|
2813
|
+
}
|
|
2814
|
+
/** Raw `ORDER BY` item, e.g. `"Account.Name DESC"`. */
|
|
2815
|
+
orderByRaw(expression) {
|
|
2816
|
+
assertNonBlank("orderByRaw", expression);
|
|
2817
|
+
this._orderBy.push(expression);
|
|
2818
|
+
return this;
|
|
2819
|
+
}
|
|
2820
|
+
/** Adds `WITH USER_MODE`, so sharing rules and field-level security of the running user apply. */
|
|
2821
|
+
withUserMode() {
|
|
2822
|
+
this._withUserMode = true;
|
|
2823
|
+
return this;
|
|
2824
|
+
}
|
|
2825
|
+
limit(count) {
|
|
2826
|
+
assertNonNegativeInteger("limit", count);
|
|
2827
|
+
this._limit = count;
|
|
2828
|
+
return this;
|
|
2829
|
+
}
|
|
2830
|
+
offset(count) {
|
|
2831
|
+
assertNonNegativeInteger("offset", count);
|
|
2832
|
+
if (count > MAX_OFFSET) throw new Error(`offset() cannot exceed Salesforce's maximum of ${MAX_OFFSET}.`);
|
|
2833
|
+
this._offset = count;
|
|
2834
|
+
return this;
|
|
2835
|
+
}
|
|
2836
|
+
/** Adds `FOR VIEW`, `FOR REFERENCE` or `FOR UPDATE`. */
|
|
2837
|
+
for(clause) {
|
|
2838
|
+
this._forClause = clause;
|
|
2839
|
+
return this;
|
|
2840
|
+
}
|
|
2841
|
+
/** Returns the SOQL string. Throws when the query is invalid. */
|
|
2842
|
+
build() {
|
|
2843
|
+
this.assertValidQuery();
|
|
2844
|
+
const clauses = [`SELECT ${this._fields.length > 0 ? this._fields.join(", ") : "FIELDS(ALL)"} FROM ${this._fromClause}`];
|
|
2845
|
+
if (this._conditions.length > 0) clauses.push(`WHERE ${this._conditions.join(" AND ")}`);
|
|
2846
|
+
if (this._withUserMode) clauses.push("WITH USER_MODE");
|
|
2847
|
+
if (this._groupBy.length > 0) clauses.push(`GROUP BY ${this._groupBy.join(", ")}`);
|
|
2848
|
+
if (this._having.length > 0) clauses.push(`HAVING ${this._having.join(" AND ")}`);
|
|
2849
|
+
if (this._orderBy.length > 0) clauses.push(`ORDER BY ${this._orderBy.join(", ")}`);
|
|
2850
|
+
if (this._limit !== void 0) clauses.push(`LIMIT ${this._limit}`);
|
|
2851
|
+
if (this._offset !== void 0) clauses.push(`OFFSET ${this._offset}`);
|
|
2852
|
+
if (this._forClause) clauses.push(`FOR ${this._forClause}`);
|
|
2853
|
+
return clauses.join(" ");
|
|
2854
|
+
}
|
|
2855
|
+
toString() {
|
|
2856
|
+
return this.build();
|
|
2857
|
+
}
|
|
2858
|
+
cast() {
|
|
2859
|
+
return this;
|
|
2860
|
+
}
|
|
2861
|
+
/** Adds fields, skipping duplicates. Field names are case-insensitive in SOQL. */
|
|
2862
|
+
addFields(fields) {
|
|
2863
|
+
const existing = new Set(this._fields.map((field) => field.toLowerCase()));
|
|
2864
|
+
for (const field of fields) if (!existing.has(field.toLowerCase())) {
|
|
2865
|
+
existing.add(field.toLowerCase());
|
|
2866
|
+
this._fields.push(field);
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
groupConditions(build) {
|
|
2870
|
+
const group = new SoqlQueryBuilder(this._fromClause);
|
|
2871
|
+
build(group);
|
|
2872
|
+
group.assertOnly("whereGroup()/whereNot()", "conditions");
|
|
2873
|
+
return group._conditions;
|
|
2874
|
+
}
|
|
2875
|
+
/** A group callback may only add conditions (or HAVING conditions); anything else would be lost. */
|
|
2876
|
+
assertOnly(method, allowed) {
|
|
2877
|
+
const used = [
|
|
2878
|
+
this._fields.length > 0 ? "select" : "",
|
|
2879
|
+
allowed !== "conditions" && this._conditions.length > 0 ? "where" : "",
|
|
2880
|
+
allowed !== "having" && this._having.length > 0 ? "having" : "",
|
|
2881
|
+
this._groupBy.length > 0 ? "groupBy" : "",
|
|
2882
|
+
this._orderBy.length > 0 ? "orderBy" : "",
|
|
2883
|
+
this._limit !== void 0 || this._offset !== void 0 ? "limit/offset" : "",
|
|
2884
|
+
this._withUserMode || this._forClause ? "withUserMode/for" : ""
|
|
2885
|
+
].filter((item) => item.length > 0);
|
|
2886
|
+
if (used.length > 0) throw new Error(`${method} callbacks can only add ${allowed === "conditions" ? "where" : "having"} conditions, not ${used.join(", ")}.`);
|
|
2887
|
+
}
|
|
2888
|
+
whereSet(field, operator, values) {
|
|
2889
|
+
const method = operator === "IN" ? "whereIn" : "whereNotIn";
|
|
2890
|
+
assertField(`${method}()`, field);
|
|
2891
|
+
if (values instanceof SoqlQueryBuilder) {
|
|
2892
|
+
if (values._fields.length !== 1) throw new Error(`${method}("${field}", subquery) requires the subquery to select exactly one field, got ${values._fields.length}.`);
|
|
2893
|
+
if (values._usesTypeOf) throw new Error(`${method}("${field}", subquery): TYPEOF is not allowed in a semi-join.`);
|
|
2894
|
+
this._conditions.push(`${field} ${operator} (${values.build()})`);
|
|
2895
|
+
return this;
|
|
2896
|
+
}
|
|
2897
|
+
if (values.length === 0) throw new Error(`${method}("${field}", []) would build an invalid "${operator} ()" SOQL clause.`);
|
|
2898
|
+
this._conditions.push(`${field} ${operator} (${values.map((value) => soqlEscape(value)).join(", ")})`);
|
|
2899
|
+
return this;
|
|
2900
|
+
}
|
|
2901
|
+
whereMultiPicklist(field, operator, values) {
|
|
2902
|
+
assertField(operator === "INCLUDES" ? "whereIncludes()" : "whereExcludes()", field);
|
|
2903
|
+
if (values.length === 0) throw new Error(`${operator.toLowerCase()}("${field}", []) requires at least one value.`);
|
|
2904
|
+
const items = values.map((value) => soqlEscape(typeof value === "string" ? value : value.join(";")));
|
|
2905
|
+
this._conditions.push(`${field} ${operator} (${items.join(", ")})`);
|
|
2906
|
+
return this;
|
|
2907
|
+
}
|
|
2908
|
+
selectAggregate(fn, field, alias) {
|
|
2909
|
+
assertField(`${fn}()`, field);
|
|
2910
|
+
if (alias === void 0 || !ALIAS_PATTERN.test(alias)) throw new Error(`Aggregate alias "${String(alias)}" must be a valid SOQL identifier.`);
|
|
2911
|
+
if (RESERVED_WORDS.has(alias.toLowerCase())) throw new Error(`Aggregate alias "${alias}" is a reserved SOQL keyword.`);
|
|
2912
|
+
this._usesAggregates = true;
|
|
2913
|
+
this._fields.push(`${fn}(${field}) ${alias}`);
|
|
2914
|
+
}
|
|
2915
|
+
assertValidQuery() {
|
|
2916
|
+
if (this._usesTypeOf && this._groupBy.length > 0) throw new Error("TYPEOF can't be combined with groupBy().");
|
|
2917
|
+
if (this._countAll && (this._fields.length > 1 || this._groupBy.length > 0 || this._orderBy.length > 0)) throw new Error("count() must be the only selected item and can't be combined with groupBy() or orderBy().");
|
|
2918
|
+
if (this._fields.length > 0) return;
|
|
2919
|
+
if (this._groupBy.length > 0) throw new Error("groupBy() requires select()/selectRaw(): FIELDS(ALL) can't be used in an aggregate query.");
|
|
2920
|
+
if (this._limit === void 0 || this._limit > FIELDS_ALL_MAX_LIMIT) throw new Error(`Queries using FIELDS(ALL) (select() was never called) require limit() of at most ${FIELDS_ALL_MAX_LIMIT}.`);
|
|
2921
|
+
}
|
|
2922
|
+
};
|
|
2923
|
+
}));
|
|
2924
|
+
//#endregion
|
|
2925
|
+
//#region src/resources/ui-api.ts
|
|
2926
|
+
var UiApi;
|
|
2927
|
+
var init_ui_api = __esmMin((() => {
|
|
2928
|
+
init_connection();
|
|
2929
|
+
UiApi = class {
|
|
2930
|
+
_connection;
|
|
2931
|
+
_cache;
|
|
2932
|
+
constructor(_connection, _cache) {
|
|
2933
|
+
this._connection = _connection;
|
|
2934
|
+
this._cache = _cache;
|
|
2935
|
+
}
|
|
2936
|
+
/** `GET /ui-api/object-info/{name}`: fields, record types and defaults as the UI sees them. Cached. */
|
|
2937
|
+
objectInfo(sobject, options = {}) {
|
|
2938
|
+
const load = (signal) => this._connection.request({
|
|
2939
|
+
path: `/ui-api/object-info/${segment(sobject)}`,
|
|
2940
|
+
signal
|
|
2941
|
+
});
|
|
2942
|
+
return this._cache ? this._cache.getOrLoad(`ui-object-info:${sobject}`, load, options.signal) : load(options.signal);
|
|
2943
|
+
}
|
|
2944
|
+
async picklistValues(sobject, recordTypeId, fieldOrOptions, maybeOptions = {}) {
|
|
2945
|
+
const base = `/ui-api/object-info/${segment(sobject)}/picklist-values/${segment(recordTypeId)}`;
|
|
2946
|
+
if (typeof fieldOrOptions === "string") return this._connection.request({
|
|
2947
|
+
path: `${base}/${segment(fieldOrOptions)}`,
|
|
2948
|
+
signal: maybeOptions.signal
|
|
2949
|
+
});
|
|
2950
|
+
return (await this._connection.request({
|
|
2951
|
+
path: base,
|
|
2952
|
+
signal: fieldOrOptions?.signal
|
|
2953
|
+
})).picklistFieldValues;
|
|
2954
|
+
}
|
|
2955
|
+
/**
|
|
2956
|
+
* `GET /ui-api/records/{id}` with typed field names. At least one of `fields`,
|
|
2957
|
+
* `optionalFields` or `layoutTypes` is required.
|
|
2958
|
+
*
|
|
2959
|
+
* ```ts
|
|
2960
|
+
* const record = await sf.uiApi.record("Account", id, { fields: ["Name", "Industry"] });
|
|
2961
|
+
* record.fields.Name.displayValue;
|
|
2962
|
+
* ```
|
|
2963
|
+
*/
|
|
2964
|
+
record(sobject, id, options) {
|
|
2965
|
+
const qualify = (fields) => fields && fields.length > 0 ? fields.map((field) => `${sobject}.${field}`) : void 0;
|
|
2966
|
+
const fields = qualify(options.fields);
|
|
2967
|
+
const optionalFields = qualify(options.optionalFields);
|
|
2968
|
+
if (!fields && !optionalFields && !options.layoutTypes?.length) throw new Error("uiApi.record() requires fields, optionalFields or layoutTypes.");
|
|
2969
|
+
return this._connection.request({
|
|
2970
|
+
path: `/ui-api/records/${segment(id)}`,
|
|
2971
|
+
query: {
|
|
2972
|
+
fields,
|
|
2973
|
+
optionalFields,
|
|
2974
|
+
layoutTypes: options.layoutTypes,
|
|
2975
|
+
modes: options.modes
|
|
2976
|
+
},
|
|
2977
|
+
signal: options.signal
|
|
2978
|
+
});
|
|
2979
|
+
}
|
|
2980
|
+
/** `GET /ui-api/layout/{name}`: the page layout sections, rows and fields. */
|
|
2981
|
+
layout(sobject, options = {}) {
|
|
2982
|
+
const { signal, ...query } = options;
|
|
2983
|
+
return this._connection.request({
|
|
2984
|
+
path: `/ui-api/layout/${segment(sobject)}`,
|
|
2985
|
+
query: { ...query },
|
|
2986
|
+
signal
|
|
2987
|
+
});
|
|
2988
|
+
}
|
|
2989
|
+
};
|
|
2990
|
+
}));
|
|
2991
|
+
//#endregion
|
|
2992
|
+
//#region src/resources/sobject.ts
|
|
2993
|
+
function requireId(id) {
|
|
2994
|
+
if (typeof id !== "string" || id.trim().length === 0) throw new Error("A record id is required.");
|
|
2995
|
+
return id;
|
|
2996
|
+
}
|
|
2997
|
+
/** Formats a date as `yyyy-MM-ddTHH:mm:ss+00:00`, the format the deleted/updated resources expect. */
|
|
2998
|
+
function formatDateTime(date) {
|
|
2999
|
+
return `${date.toISOString().slice(0, 19)}+00:00`;
|
|
3000
|
+
}
|
|
3001
|
+
var SObjectResource;
|
|
3002
|
+
var init_sobject = __esmMin((() => {
|
|
3003
|
+
init_errors();
|
|
3004
|
+
init_connection();
|
|
3005
|
+
init_query_builder();
|
|
3006
|
+
init_quick_actions();
|
|
3007
|
+
init_ui_api();
|
|
3008
|
+
SObjectResource = class {
|
|
3009
|
+
_connection;
|
|
3010
|
+
_queries;
|
|
3011
|
+
name;
|
|
3012
|
+
_basePath;
|
|
3013
|
+
_cache;
|
|
3014
|
+
constructor(_connection, _queries, name, _basePath = "", _cache) {
|
|
3015
|
+
this._connection = _connection;
|
|
3016
|
+
this._queries = _queries;
|
|
3017
|
+
this.name = name;
|
|
3018
|
+
this._basePath = _basePath;
|
|
3019
|
+
this._cache = _cache;
|
|
3020
|
+
if (typeof name !== "string" || !/^[A-Za-z][A-Za-z0-9_]*$/.test(name)) throw new Error(`Invalid sObject name "${String(name)}".`);
|
|
3021
|
+
}
|
|
3022
|
+
get path() {
|
|
3023
|
+
return `${this._basePath}/sobjects/${this.name}`;
|
|
3024
|
+
}
|
|
3025
|
+
/** `GET /sobjects/{name}/describe`: fields, relationships, record types, ... Cached per client. */
|
|
3026
|
+
describe(options = {}) {
|
|
3027
|
+
const load = (signal) => this._connection.request({
|
|
3028
|
+
path: `${this.path}/describe`,
|
|
3029
|
+
signal
|
|
3030
|
+
});
|
|
3031
|
+
return this._cache ? this._cache.getOrLoad(`describe:${this._basePath}:${this.name}`, load, options.signal) : load(options.signal);
|
|
3032
|
+
}
|
|
3033
|
+
/**
|
|
3034
|
+
* Returns the id of a record type by DeveloperName, e.g. `recordTypeId("Complaint")`. Record type
|
|
3035
|
+
* ids differ between orgs, so resolve them at runtime instead of hard-coding them.
|
|
3036
|
+
*/
|
|
3037
|
+
async recordTypeId(developerName, options = {}) {
|
|
3038
|
+
const describe = await this.describe(options);
|
|
3039
|
+
const match = describe.recordTypeInfos.find((info) => info.developerName === developerName);
|
|
3040
|
+
if (!match) {
|
|
3041
|
+
const available = describe.recordTypeInfos.map((info) => info.developerName).join(", ");
|
|
3042
|
+
throw new Error(`${this.name} has no record type "${developerName}". Available: ${available || "none"}.`);
|
|
3043
|
+
}
|
|
3044
|
+
return match.recordTypeId;
|
|
3045
|
+
}
|
|
3046
|
+
/**
|
|
3047
|
+
* Active picklist values of a field. With `recordType`, only the values available for that
|
|
3048
|
+
* record type are returned (via the UI API).
|
|
3049
|
+
*/
|
|
3050
|
+
async picklistValues(field, options = {}) {
|
|
3051
|
+
if (options.recordType !== void 0) {
|
|
3052
|
+
const recordTypeId = await this.recordTypeId(options.recordType, options);
|
|
3053
|
+
const values = await new UiApi(this._connection).picklistValues(this.name, recordTypeId, field, options);
|
|
3054
|
+
return values.values.map((entry) => ({
|
|
3055
|
+
value: entry.value,
|
|
3056
|
+
label: entry.label,
|
|
3057
|
+
isDefault: values.defaultValue?.value === entry.value
|
|
3058
|
+
}));
|
|
3059
|
+
}
|
|
3060
|
+
const describeField = (await this.describe(options)).fields.find((item) => item.name === field);
|
|
3061
|
+
if (!describeField) throw new Error(`${this.name} has no field "${field}".`);
|
|
3062
|
+
return describeField.picklistValues.filter((entry) => entry.active).map((entry) => ({
|
|
3063
|
+
value: entry.value,
|
|
3064
|
+
label: entry.label ?? entry.value,
|
|
3065
|
+
isDefault: entry.defaultValue
|
|
3066
|
+
}));
|
|
3067
|
+
}
|
|
3068
|
+
/** Quick actions of this sObject. */
|
|
3069
|
+
get quickActions() {
|
|
3070
|
+
return new QuickActionsApi(this._connection, `${this.path}/quickActions`);
|
|
3071
|
+
}
|
|
3072
|
+
/** `GET /sobjects/{name}`: basic metadata and recently viewed records. */
|
|
3073
|
+
basicInfo(options = {}) {
|
|
3074
|
+
return this._connection.request({
|
|
3075
|
+
path: this.path,
|
|
3076
|
+
signal: options.signal
|
|
3077
|
+
});
|
|
3078
|
+
}
|
|
3079
|
+
/** Creates a record and returns its id. Throws `SalesforceSaveError` if Salesforce reports `success: false`. */
|
|
3080
|
+
async create(record, options = {}) {
|
|
3081
|
+
const result = await this._connection.request({
|
|
3082
|
+
method: "POST",
|
|
3083
|
+
path: this.path,
|
|
3084
|
+
body: record,
|
|
3085
|
+
signal: options.signal
|
|
3086
|
+
});
|
|
3087
|
+
if (!result.success || !result.id) throw new SalesforceSaveError(`Creating ${this.name} failed`, [result], result.errors);
|
|
3088
|
+
return result.id;
|
|
3089
|
+
}
|
|
3090
|
+
get(id, fieldsOrOptions, maybeOptions = {}) {
|
|
3091
|
+
const fields = Array.isArray(fieldsOrOptions) ? fieldsOrOptions : void 0;
|
|
3092
|
+
const options = Array.isArray(fieldsOrOptions) ? maybeOptions : fieldsOrOptions ?? {};
|
|
3093
|
+
return this._connection.request({
|
|
3094
|
+
path: `${this.path}/${segment(requireId(id))}`,
|
|
3095
|
+
query: { fields: fields && fields.length > 0 ? fields : void 0 },
|
|
3096
|
+
signal: options.signal
|
|
3097
|
+
});
|
|
3098
|
+
}
|
|
3099
|
+
/** Updates fields of a record. */
|
|
3100
|
+
async update(id, record, options = {}) {
|
|
3101
|
+
await this._connection.request({
|
|
3102
|
+
method: "PATCH",
|
|
3103
|
+
path: `${this.path}/${segment(requireId(id))}`,
|
|
3104
|
+
body: record,
|
|
3105
|
+
signal: options.signal
|
|
3106
|
+
});
|
|
3107
|
+
}
|
|
3108
|
+
/** Deletes a record. */
|
|
3109
|
+
async delete(id, options = {}) {
|
|
3110
|
+
await this._connection.request({
|
|
3111
|
+
method: "DELETE",
|
|
3112
|
+
path: `${this.path}/${segment(requireId(id))}`,
|
|
3113
|
+
signal: options.signal
|
|
3114
|
+
});
|
|
3115
|
+
}
|
|
3116
|
+
/**
|
|
3117
|
+
* Inserts or updates a record by external id (`PATCH /sobjects/{name}/{field}/{value}`).
|
|
3118
|
+
* Salesforce answers 300 (a `SalesforceError`) when the value matches more than one record.
|
|
3119
|
+
*/
|
|
3120
|
+
async upsert(externalIdField, externalIdValue, record, options = {}) {
|
|
3121
|
+
const response = await this._connection.send({
|
|
3122
|
+
method: "PATCH",
|
|
3123
|
+
path: `${this.path}/${segment(externalIdField)}/${segment(externalIdText(externalIdValue))}`,
|
|
3124
|
+
query: { updateOnly: options.updateOnly ? true : void 0 },
|
|
3125
|
+
body: record,
|
|
3126
|
+
signal: options.signal
|
|
3127
|
+
});
|
|
3128
|
+
const result = response.data;
|
|
3129
|
+
if (result && (!result.success || !result.id)) throw new SalesforceSaveError(`Upserting ${this.name} failed`, [result], result.errors);
|
|
3130
|
+
return {
|
|
3131
|
+
id: result?.id ?? "",
|
|
3132
|
+
created: result?.created ?? response.status === 201
|
|
3133
|
+
};
|
|
3134
|
+
}
|
|
3135
|
+
/** Retrieves a record by external id. */
|
|
3136
|
+
getByExternalId(externalIdField, externalIdValue, fields, options = {}) {
|
|
3137
|
+
return this._connection.request({
|
|
3138
|
+
path: `${this.path}/${segment(externalIdField)}/${segment(externalIdText(externalIdValue))}`,
|
|
3139
|
+
query: { fields: fields && fields.length > 0 ? fields : void 0 },
|
|
3140
|
+
signal: options.signal
|
|
3141
|
+
});
|
|
3142
|
+
}
|
|
3143
|
+
/** Ids of records deleted between `start` and `end` (UTC, at most 15 days back). */
|
|
3144
|
+
getDeleted(start, end, options = {}) {
|
|
3145
|
+
return this._connection.request({
|
|
3146
|
+
path: `${this.path}/deleted/`,
|
|
3147
|
+
query: {
|
|
3148
|
+
start: formatDateTime(start),
|
|
3149
|
+
end: formatDateTime(end)
|
|
3150
|
+
},
|
|
3151
|
+
signal: options.signal
|
|
3152
|
+
});
|
|
3153
|
+
}
|
|
3154
|
+
/** Ids of records updated between `start` and `end` (UTC, at most 30 days back). */
|
|
3155
|
+
getUpdated(start, end, options = {}) {
|
|
3156
|
+
return this._connection.request({
|
|
3157
|
+
path: `${this.path}/updated/`,
|
|
3158
|
+
query: {
|
|
3159
|
+
start: formatDateTime(start),
|
|
3160
|
+
end: formatDateTime(end)
|
|
3161
|
+
},
|
|
3162
|
+
signal: options.signal
|
|
3163
|
+
});
|
|
3164
|
+
}
|
|
3165
|
+
/** Downloads a blob field, e.g. `sobject("ContentVersion").getBlob(id, "VersionData")`. */
|
|
3166
|
+
getBlob(id, field, options = {}) {
|
|
3167
|
+
return this._connection.request({
|
|
3168
|
+
path: `${this.path}/${segment(requireId(id))}/${segment(field)}`,
|
|
3169
|
+
responseType: "binary",
|
|
3170
|
+
signal: options.signal
|
|
3171
|
+
});
|
|
3172
|
+
}
|
|
3173
|
+
/** Starts a typed query on this sObject. */
|
|
3174
|
+
soql() {
|
|
3175
|
+
return SoqlQueryBuilder.from(this.name);
|
|
3176
|
+
}
|
|
3177
|
+
/** Runs a query built on this sObject and returns the first page. */
|
|
3178
|
+
query(build, options) {
|
|
3179
|
+
return this._queries.page(build(this.soql()).build(), options);
|
|
3180
|
+
}
|
|
3181
|
+
/** Runs a query built on this sObject and returns the records of all pages. */
|
|
3182
|
+
collect(build, options) {
|
|
3183
|
+
return this._queries.collect(build(this.soql()).build(), options);
|
|
3184
|
+
}
|
|
3185
|
+
/** Runs a query built on this sObject and yields records of all pages lazily. */
|
|
3186
|
+
iterate(build, options) {
|
|
3187
|
+
return this._queries.iterate(build(this.soql()).build(), options);
|
|
3188
|
+
}
|
|
3189
|
+
};
|
|
3190
|
+
}));
|
|
3191
|
+
//#endregion
|
|
3192
|
+
//#region src/resources/debug-logs.ts
|
|
3193
|
+
/** The trace flag was removed meanwhile (by a person or another tool): nothing left to restore. */
|
|
3194
|
+
async function ignoreMissing(operation) {
|
|
3195
|
+
try {
|
|
3196
|
+
await operation;
|
|
3197
|
+
} catch (error) {
|
|
3198
|
+
if (!hasErrorCode(error, "NOT_FOUND") && !hasErrorCode(error, "ENTITY_IS_DELETED")) throw error;
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
function toEntry(row) {
|
|
3202
|
+
return {
|
|
3203
|
+
id: row.Id,
|
|
3204
|
+
operation: row.Operation,
|
|
3205
|
+
request: row.Request,
|
|
3206
|
+
status: row.Status,
|
|
3207
|
+
startTime: row.StartTime,
|
|
3208
|
+
durationMilliseconds: row.DurationMilliseconds,
|
|
3209
|
+
logLength: row.LogLength
|
|
3210
|
+
};
|
|
3211
|
+
}
|
|
3212
|
+
var DEFAULT_DEBUG_LEVELS, DEBUG_LEVEL_NAME, DebugLogCaptureError, LOG_FIELDS, DebugLogsApi;
|
|
3213
|
+
var init_debug_logs = __esmMin((() => {
|
|
3214
|
+
init_errors();
|
|
3215
|
+
init_connection();
|
|
3216
|
+
init_escape();
|
|
3217
|
+
DEFAULT_DEBUG_LEVELS = {
|
|
3218
|
+
ApexCode: "FINEST",
|
|
3219
|
+
ApexProfiling: "INFO",
|
|
3220
|
+
Callout: "INFO",
|
|
3221
|
+
Database: "INFO",
|
|
3222
|
+
System: "DEBUG",
|
|
3223
|
+
Validation: "INFO",
|
|
3224
|
+
Visualforce: "INFO",
|
|
3225
|
+
Workflow: "INFO"
|
|
3226
|
+
};
|
|
3227
|
+
DEBUG_LEVEL_NAME = "sobjectly";
|
|
3228
|
+
DebugLogCaptureError = class extends Error {
|
|
3229
|
+
logs;
|
|
3230
|
+
cause;
|
|
3231
|
+
cleanupError;
|
|
3232
|
+
name = "DebugLogCaptureError";
|
|
3233
|
+
constructor(logs, cause, cleanupError) {
|
|
3234
|
+
super(`${cause instanceof Error ? cause.message : String(cause)} (${logs.length} Apex log(s) captured)`);
|
|
3235
|
+
this.logs = logs;
|
|
3236
|
+
this.cause = cause;
|
|
3237
|
+
this.cleanupError = cleanupError;
|
|
3238
|
+
}
|
|
3239
|
+
};
|
|
3240
|
+
LOG_FIELDS = "Id, Operation, Request, Status, StartTime, DurationMilliseconds, LogLength";
|
|
3241
|
+
DebugLogsApi = class {
|
|
3242
|
+
_tooling;
|
|
3243
|
+
/** The latest capture per user: a user has one trace flag, so captures for a user take turns. */
|
|
3244
|
+
_captures = /* @__PURE__ */ new Map();
|
|
3245
|
+
constructor(_tooling) {
|
|
3246
|
+
this._tooling = _tooling;
|
|
3247
|
+
}
|
|
3248
|
+
/** The id of the authenticated user (from `/services/oauth2/userinfo`). */
|
|
3249
|
+
async currentUserId(options = {}) {
|
|
3250
|
+
return (await this._tooling.connection.request({
|
|
3251
|
+
path: "/services/oauth2/userinfo",
|
|
3252
|
+
signal: options.signal
|
|
3253
|
+
})).user_id;
|
|
3254
|
+
}
|
|
3255
|
+
/** The most recent Apex logs, newest first. */
|
|
3256
|
+
async list(options = {}) {
|
|
3257
|
+
const where = options.userId ? ` WHERE LogUserId = ${soqlEscape(options.userId)}` : "";
|
|
3258
|
+
return (await this._tooling.collect(`SELECT ${LOG_FIELDS} FROM ApexLog${where} ORDER BY StartTime DESC LIMIT ${options.limit ?? 50}`, { signal: options.signal })).map(toEntry);
|
|
3259
|
+
}
|
|
3260
|
+
/** The text of one Apex log. */
|
|
3261
|
+
body(logId, options = {}) {
|
|
3262
|
+
return this._tooling.connection.request({
|
|
3263
|
+
path: `/tooling/sobjects/ApexLog/${segment(logId)}/Body/`,
|
|
3264
|
+
responseType: "text",
|
|
3265
|
+
signal: options.signal
|
|
3266
|
+
});
|
|
3267
|
+
}
|
|
3268
|
+
/**
|
|
3269
|
+
* Runs `work` with debug logging enabled for the user, then returns its result together with
|
|
3270
|
+
* the Apex logs it produced (e.g. from triggers, flows or Apex REST calls):
|
|
3271
|
+
*
|
|
3272
|
+
* ```ts
|
|
3273
|
+
* const { result, logs } = await sf.tooling.debugLogs.capture(() => sf.sobject("Case").create({ Subject: "x" }));
|
|
3274
|
+
* console.log(logs.map((log) => log.body).join("\n"));
|
|
3275
|
+
* ```
|
|
3276
|
+
*
|
|
3277
|
+
* Salesforce allows one trace flag per user; an existing flag is reused and restored afterwards,
|
|
3278
|
+
* and captures for the same user through this client run one after another. Only work that runs
|
|
3279
|
+
* while the callback does is traced: async Apex it enqueues (queueable, batch, future) usually
|
|
3280
|
+
* runs later. At most the 200 newest logs of the user are collected.
|
|
3281
|
+
*/
|
|
3282
|
+
async capture(work, options = {}) {
|
|
3283
|
+
const userId = options.userId ?? await this.currentUserId({ signal: options.signal });
|
|
3284
|
+
const current = (this._captures.get(userId) ?? Promise.resolve()).then(() => this.captureFor(userId, work, options));
|
|
3285
|
+
const settled = current.then(() => void 0, () => void 0);
|
|
3286
|
+
this._captures.set(userId, settled);
|
|
3287
|
+
try {
|
|
3288
|
+
return await current;
|
|
3289
|
+
} finally {
|
|
3290
|
+
if (this._captures.get(userId) === settled) this._captures.delete(userId);
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
async captureFor(userId, work, options) {
|
|
3294
|
+
const { signal } = options;
|
|
3295
|
+
signal?.throwIfAborted();
|
|
3296
|
+
const before = new Set((await this.list({
|
|
3297
|
+
userId,
|
|
3298
|
+
limit: 200,
|
|
3299
|
+
signal
|
|
3300
|
+
})).map((log) => log.id));
|
|
3301
|
+
const debugLevelId = await this.ensureDebugLevel({
|
|
3302
|
+
...DEFAULT_DEBUG_LEVELS,
|
|
3303
|
+
...options.levels
|
|
3304
|
+
}, signal);
|
|
3305
|
+
const restore = await this.enableTraceFlag(userId, debugLevelId, options.expirationMinutes ?? 30, signal);
|
|
3306
|
+
let result;
|
|
3307
|
+
let failure;
|
|
3308
|
+
let cleanupError;
|
|
3309
|
+
try {
|
|
3310
|
+
result = await work();
|
|
3311
|
+
} catch (error) {
|
|
3312
|
+
failure = { error };
|
|
3313
|
+
}
|
|
3314
|
+
try {
|
|
3315
|
+
await restore();
|
|
3316
|
+
} catch (error) {
|
|
3317
|
+
cleanupError = error;
|
|
3318
|
+
}
|
|
3319
|
+
await sleep(options.settleMs ?? 1e3, signal);
|
|
3320
|
+
const logs = (await this.list({
|
|
3321
|
+
userId,
|
|
3322
|
+
limit: 200,
|
|
3323
|
+
signal
|
|
3324
|
+
})).filter((log) => !before.has(log.id)).reverse();
|
|
3325
|
+
if (options.includeBodies !== false) for (const log of logs) log.body = await this.body(log.id, { signal });
|
|
3326
|
+
if (failure) throw new DebugLogCaptureError(logs, failure.error, cleanupError);
|
|
3327
|
+
return cleanupError === void 0 ? {
|
|
3328
|
+
result,
|
|
3329
|
+
logs
|
|
3330
|
+
} : {
|
|
3331
|
+
result,
|
|
3332
|
+
logs,
|
|
3333
|
+
cleanupError
|
|
3334
|
+
};
|
|
3335
|
+
}
|
|
3336
|
+
async ensureDebugLevel(levels, signal) {
|
|
3337
|
+
const [existing] = await this._tooling.collect(`SELECT Id FROM DebugLevel WHERE DeveloperName = ${soqlEscape(DEBUG_LEVEL_NAME)}`, { signal });
|
|
3338
|
+
const levelSObject = this._tooling.sobject("DebugLevel");
|
|
3339
|
+
if (existing) {
|
|
3340
|
+
await levelSObject.update(existing.Id, { ...levels }, { signal });
|
|
3341
|
+
return existing.Id;
|
|
3342
|
+
}
|
|
3343
|
+
return levelSObject.create({
|
|
3344
|
+
DeveloperName: DEBUG_LEVEL_NAME,
|
|
3345
|
+
MasterLabel: DEBUG_LEVEL_NAME,
|
|
3346
|
+
...levels
|
|
3347
|
+
}, { signal });
|
|
3348
|
+
}
|
|
3349
|
+
/** Enables tracing and returns a function that restores the previous state. */
|
|
3350
|
+
async enableTraceFlag(userId, debugLevelId, minutes, signal) {
|
|
3351
|
+
const expiration = new Date(Date.now() + Math.min(Math.max(minutes, 1), 1439) * 6e4).toISOString();
|
|
3352
|
+
const flags = this._tooling.sobject("TraceFlag");
|
|
3353
|
+
const [existing] = await this._tooling.collect(`SELECT Id, DebugLevelId, StartDate, ExpirationDate FROM TraceFlag WHERE TracedEntityId = ${soqlEscape(userId)} AND LogType = 'USER_DEBUG'`, { signal });
|
|
3354
|
+
if (!existing) {
|
|
3355
|
+
const id = await flags.create({
|
|
3356
|
+
TracedEntityId: userId,
|
|
3357
|
+
LogType: "USER_DEBUG",
|
|
3358
|
+
DebugLevelId: debugLevelId,
|
|
3359
|
+
StartDate: null,
|
|
3360
|
+
ExpirationDate: expiration
|
|
3361
|
+
}, { signal });
|
|
3362
|
+
return () => ignoreMissing(flags.delete(id));
|
|
3363
|
+
}
|
|
3364
|
+
await flags.update(existing.Id, {
|
|
3365
|
+
DebugLevelId: debugLevelId,
|
|
3366
|
+
StartDate: null,
|
|
3367
|
+
ExpirationDate: expiration
|
|
3368
|
+
}, { signal });
|
|
3369
|
+
return async () => {
|
|
3370
|
+
if (Date.parse(existing.ExpirationDate) > Date.now()) await ignoreMissing(flags.update(existing.Id, {
|
|
3371
|
+
DebugLevelId: existing.DebugLevelId,
|
|
3372
|
+
StartDate: existing.StartDate,
|
|
3373
|
+
ExpirationDate: existing.ExpirationDate
|
|
3374
|
+
}));
|
|
3375
|
+
else await ignoreMissing(flags.delete(existing.Id));
|
|
3376
|
+
};
|
|
3377
|
+
}
|
|
3378
|
+
};
|
|
3379
|
+
}));
|
|
3380
|
+
//#endregion
|
|
3381
|
+
//#region src/resources/tooling.ts
|
|
3382
|
+
var ApexExecutionError, ToolingApi;
|
|
3383
|
+
var init_tooling = __esmMin((() => {
|
|
3384
|
+
init_debug_logs();
|
|
3385
|
+
init_query();
|
|
3386
|
+
init_sobject();
|
|
3387
|
+
ApexExecutionError = class extends Error {
|
|
3388
|
+
result;
|
|
3389
|
+
logs;
|
|
3390
|
+
name = "ApexExecutionError";
|
|
3391
|
+
constructor(result, logs = []) {
|
|
3392
|
+
super(result.compiled ? `Anonymous Apex threw: ${result.exceptionMessage ?? "unknown error"}` : `Anonymous Apex failed to compile at line ${result.line}, column ${result.column}: ${result.compileProblem ?? "unknown problem"}`);
|
|
3393
|
+
this.result = result;
|
|
3394
|
+
this.logs = logs;
|
|
3395
|
+
}
|
|
3396
|
+
};
|
|
3397
|
+
ToolingApi = class {
|
|
3398
|
+
_connection;
|
|
3399
|
+
_queries;
|
|
3400
|
+
/** Apex debug logs: capture the logs of a block of work, list and read logs. */
|
|
3401
|
+
debugLogs;
|
|
3402
|
+
constructor(_connection) {
|
|
3403
|
+
this._connection = _connection;
|
|
3404
|
+
this._queries = new QueryApi(_connection, "/tooling");
|
|
3405
|
+
this.debugLogs = new DebugLogsApi(this);
|
|
3406
|
+
}
|
|
3407
|
+
/** The connection this API uses. */
|
|
3408
|
+
get connection() {
|
|
3409
|
+
return this._connection;
|
|
3410
|
+
}
|
|
3411
|
+
/** Runs a Tooling SOQL query and returns the first page. */
|
|
3412
|
+
query(soql, options) {
|
|
3413
|
+
return this._queries.page(soql, options);
|
|
3414
|
+
}
|
|
3415
|
+
queryMore(cursor, options) {
|
|
3416
|
+
return this._queries.more(cursor, options);
|
|
3417
|
+
}
|
|
3418
|
+
iterate(soql, options) {
|
|
3419
|
+
return this._queries.iterate(soql, options);
|
|
3420
|
+
}
|
|
3421
|
+
collect(soql, options) {
|
|
3422
|
+
return this._queries.collect(soql, options);
|
|
3423
|
+
}
|
|
3424
|
+
/** CRUD and describe for a Tooling sObject, e.g. `tooling.sobject("TraceFlag")`. */
|
|
3425
|
+
sobject(name) {
|
|
3426
|
+
return new SObjectResource(this._connection, this._queries, name, "/tooling");
|
|
3427
|
+
}
|
|
3428
|
+
describeGlobal(options = {}) {
|
|
3429
|
+
return this._connection.request({
|
|
3430
|
+
path: "/tooling/sobjects",
|
|
3431
|
+
signal: options.signal
|
|
3432
|
+
});
|
|
3433
|
+
}
|
|
3434
|
+
async executeAnonymous(apex, options = {}) {
|
|
3435
|
+
const encodedLength = encodeURIComponent(apex).length;
|
|
3436
|
+
if (encodedLength > 12e3) throw new Error(`Anonymous Apex is too long: ${encodedLength} characters once URL-encoded, the limit is about ${MAX_QUERY_URL_LENGTH}. Salesforce only accepts it in the URL; move the logic into an Apex class.`);
|
|
3437
|
+
if (options.captureLog) {
|
|
3438
|
+
const { result, logs } = await this.debugLogs.capture(() => this.executeAnonymous(apex, {
|
|
3439
|
+
signal: options.signal,
|
|
3440
|
+
throwOnError: false
|
|
3441
|
+
}), {
|
|
3442
|
+
...options.logOptions,
|
|
3443
|
+
signal: options.signal
|
|
3444
|
+
});
|
|
3445
|
+
if (options.throwOnError !== false && (!result.compiled || !result.success)) throw new ApexExecutionError(result, logs);
|
|
3446
|
+
return {
|
|
3447
|
+
...result,
|
|
3448
|
+
logs
|
|
3449
|
+
};
|
|
3450
|
+
}
|
|
3451
|
+
const result = await this._connection.request({
|
|
3452
|
+
path: "/tooling/executeAnonymous/",
|
|
3453
|
+
query: { anonymousBody: apex },
|
|
3454
|
+
signal: options.signal
|
|
3455
|
+
});
|
|
3456
|
+
if (options.throwOnError !== false && (!result.compiled || !result.success)) throw new ApexExecutionError(result);
|
|
3457
|
+
return result;
|
|
3458
|
+
}
|
|
3459
|
+
/** Runs Apex tests synchronously (a single class) and returns the results. */
|
|
3460
|
+
runTestsSynchronous(request, options = {}) {
|
|
3461
|
+
return this._connection.request({
|
|
3462
|
+
method: "POST",
|
|
3463
|
+
path: "/tooling/runTestsSynchronous/",
|
|
3464
|
+
body: request,
|
|
3465
|
+
signal: options.signal,
|
|
3466
|
+
timeoutMs: 0
|
|
3467
|
+
});
|
|
3468
|
+
}
|
|
3469
|
+
/** Enqueues Apex tests and returns the AsyncApexJob id. */
|
|
3470
|
+
runTestsAsynchronous(request, options = {}) {
|
|
3471
|
+
return this._connection.request({
|
|
3472
|
+
method: "POST",
|
|
3473
|
+
path: "/tooling/runTestsAsynchronous/",
|
|
3474
|
+
body: request,
|
|
3475
|
+
signal: options.signal
|
|
3476
|
+
});
|
|
3477
|
+
}
|
|
3478
|
+
/** Any other Tooling resource; `path` is relative to `/tooling`. */
|
|
3479
|
+
request(request) {
|
|
3480
|
+
const path = request.path.startsWith("/") ? request.path : `/${request.path}`;
|
|
3481
|
+
return this._connection.request({
|
|
3482
|
+
...request,
|
|
3483
|
+
path: `/tooling${path}`
|
|
3484
|
+
});
|
|
3485
|
+
}
|
|
3486
|
+
};
|
|
3487
|
+
}));
|
|
3488
|
+
//#endregion
|
|
3489
|
+
//#region src/client.ts
|
|
3490
|
+
function toSoql(query) {
|
|
3491
|
+
return typeof query === "string" ? query : query.build();
|
|
3492
|
+
}
|
|
3493
|
+
var SalesforceClient;
|
|
3494
|
+
var init_client = __esmMin((() => {
|
|
3495
|
+
init_cache();
|
|
3496
|
+
init_connection();
|
|
3497
|
+
init_fetch_transport();
|
|
3498
|
+
init_actions();
|
|
3499
|
+
init_approvals();
|
|
3500
|
+
init_bulk();
|
|
3501
|
+
init_collections();
|
|
3502
|
+
init_composite();
|
|
3503
|
+
init_events();
|
|
3504
|
+
init_files();
|
|
3505
|
+
init_query();
|
|
3506
|
+
init_quick_actions();
|
|
3507
|
+
init_reports();
|
|
3508
|
+
init_search();
|
|
3509
|
+
init_sobject();
|
|
3510
|
+
init_tooling();
|
|
3511
|
+
init_ui_api();
|
|
3512
|
+
init_query_builder();
|
|
3513
|
+
SalesforceClient = class {
|
|
3514
|
+
/** The low-level executor, for advanced use. */
|
|
3515
|
+
connection;
|
|
3516
|
+
/** SOSL, parameterized search and search suggestions. */
|
|
3517
|
+
search;
|
|
3518
|
+
/** Composite, composite batch, sObject tree and composite graph. */
|
|
3519
|
+
composite;
|
|
3520
|
+
/** sObject collections: up to 200 records per call. */
|
|
3521
|
+
collections;
|
|
3522
|
+
/** Invocable actions, including Flows and Apex `@InvocableMethod`s. */
|
|
3523
|
+
actions;
|
|
3524
|
+
/** The Tooling API. */
|
|
3525
|
+
tooling;
|
|
3526
|
+
/** Bulk API 2.0 ingest and query jobs. */
|
|
3527
|
+
bulk;
|
|
3528
|
+
/** Publish platform events. */
|
|
3529
|
+
events;
|
|
3530
|
+
/** Approval processes: submit, approve, reject. */
|
|
3531
|
+
approvals;
|
|
3532
|
+
/** Global quick actions (per-sObject actions are on `sobject(name).quickActions`). */
|
|
3533
|
+
quickActions;
|
|
3534
|
+
/** User Interface API: object info, picklist values per record type, records, layouts. */
|
|
3535
|
+
uiApi;
|
|
3536
|
+
/** Salesforce Files: upload, download, new versions, sharing. */
|
|
3537
|
+
files;
|
|
3538
|
+
/** Reports: run synchronously or asynchronously and flatten the rows. */
|
|
3539
|
+
reports;
|
|
3540
|
+
_queries;
|
|
3541
|
+
_cache;
|
|
3542
|
+
constructor(options) {
|
|
3543
|
+
if (!options?.auth) throw new TypeError("SalesforceClient requires an auth provider.");
|
|
3544
|
+
this.connection = new SalesforceConnection({
|
|
3545
|
+
auth: options.auth,
|
|
3546
|
+
transport: options.transport ?? fetchTransport(),
|
|
3547
|
+
apiVersion: options.apiVersion,
|
|
3548
|
+
timeoutMs: options.timeoutMs,
|
|
3549
|
+
retry: options.retry,
|
|
3550
|
+
allowedOrigins: options.allowedOrigins,
|
|
3551
|
+
headers: options.headers,
|
|
3552
|
+
hooks: options.hooks
|
|
3553
|
+
});
|
|
3554
|
+
this._queries = new QueryApi(this.connection);
|
|
3555
|
+
this.search = new SearchApi(this.connection);
|
|
3556
|
+
this.composite = new CompositeApi(this.connection);
|
|
3557
|
+
this.collections = new CollectionsApi(this.connection);
|
|
3558
|
+
this.actions = new ActionsApi(this.connection);
|
|
3559
|
+
this.tooling = new ToolingApi(this.connection);
|
|
3560
|
+
this.bulk = new BulkApi(this.connection);
|
|
3561
|
+
this.events = new EventsApi(this.connection);
|
|
3562
|
+
this._cache = new MetadataCache(options.cache !== false);
|
|
3563
|
+
this.approvals = new ApprovalsApi(this.connection, this._queries);
|
|
3564
|
+
this.quickActions = new QuickActionsApi(this.connection, "/quickActions");
|
|
3565
|
+
this.uiApi = new UiApi(this.connection, this._cache);
|
|
3566
|
+
this.files = new FilesApi(this.connection);
|
|
3567
|
+
this.reports = new ReportsApi(this.connection);
|
|
3568
|
+
}
|
|
3569
|
+
/** Drops cached metadata (describe results, object info, record type ids). */
|
|
3570
|
+
clearCache() {
|
|
3571
|
+
this._cache.clear();
|
|
3572
|
+
}
|
|
3573
|
+
get apiVersion() {
|
|
3574
|
+
return this.connection.apiVersion;
|
|
3575
|
+
}
|
|
3576
|
+
/** API usage as reported by the last response (`Sforce-Limit-Info`). */
|
|
3577
|
+
get apiUsage() {
|
|
3578
|
+
return this.connection.apiUsage;
|
|
3579
|
+
}
|
|
3580
|
+
/** The instance URL of the current access token (authenticates if needed). */
|
|
3581
|
+
instanceUrl(options = {}) {
|
|
3582
|
+
return this.connection.instanceUrl(options.signal);
|
|
3583
|
+
}
|
|
3584
|
+
/**
|
|
3585
|
+
* Calls any REST resource. `path` is relative to `/services/data/{apiVersion}` unless it
|
|
3586
|
+
* starts with `/services/` or is an absolute URL on the instance.
|
|
3587
|
+
*
|
|
3588
|
+
* ```ts
|
|
3589
|
+
* const limits = await sf.request<OrgLimits>({ path: "/limits" });
|
|
3590
|
+
* ```
|
|
3591
|
+
*/
|
|
3592
|
+
request(request) {
|
|
3593
|
+
return this.connection.request(request);
|
|
3594
|
+
}
|
|
3595
|
+
/** Like `request`, but also returns the status and response headers. */
|
|
3596
|
+
requestWithResponse(request) {
|
|
3597
|
+
return this.connection.send(request);
|
|
3598
|
+
}
|
|
3599
|
+
/** Calls an Apex REST endpoint (`/services/apexrest/...`). */
|
|
3600
|
+
apexRest(request) {
|
|
3601
|
+
const path = request.path.replace(/^\/+/, "").replace(/^services\/apexrest\//, "");
|
|
3602
|
+
return this.connection.request({
|
|
3603
|
+
...request,
|
|
3604
|
+
path: `/services/apexrest/${path}`
|
|
3605
|
+
});
|
|
3606
|
+
}
|
|
3607
|
+
/** Typed operations on one sObject. */
|
|
3608
|
+
sobject(name) {
|
|
3609
|
+
return new SObjectResource(this.connection, this._queries, name, "", this._cache);
|
|
3610
|
+
}
|
|
3611
|
+
/** Starts a typed SOQL query. Run it with `query`, `queryAll`, `iterate` or `collect`. */
|
|
3612
|
+
soql(sobjectName) {
|
|
3613
|
+
return SoqlQueryBuilder.from(sobjectName);
|
|
3614
|
+
}
|
|
3615
|
+
query(query, options) {
|
|
3616
|
+
return this._queries.page(toSoql(query), options);
|
|
3617
|
+
}
|
|
3618
|
+
/** Fetches the next page of a query. */
|
|
3619
|
+
queryMore(cursor, options) {
|
|
3620
|
+
return this._queries.more(cursor, options);
|
|
3621
|
+
}
|
|
3622
|
+
iterate(query, options) {
|
|
3623
|
+
return this._queries.iterate(toSoql(query), options);
|
|
3624
|
+
}
|
|
3625
|
+
collect(query, options) {
|
|
3626
|
+
return this._queries.collect(toSoql(query), options);
|
|
3627
|
+
}
|
|
3628
|
+
/** `GET /services/data`: every API version the org supports. Does not count against limits. */
|
|
3629
|
+
versions(options = {}) {
|
|
3630
|
+
return this.connection.request({
|
|
3631
|
+
path: "/services/data/",
|
|
3632
|
+
signal: options.signal
|
|
3633
|
+
});
|
|
3634
|
+
}
|
|
3635
|
+
/** `GET /services/data/{version}`: the resources available in this version. */
|
|
3636
|
+
resources(options = {}) {
|
|
3637
|
+
return this.connection.request({
|
|
3638
|
+
path: "/",
|
|
3639
|
+
signal: options.signal
|
|
3640
|
+
});
|
|
3641
|
+
}
|
|
3642
|
+
/** `GET /limits`: org limits such as `DailyApiRequests`. */
|
|
3643
|
+
limits(options = {}) {
|
|
3644
|
+
return this.connection.request({
|
|
3645
|
+
path: "/limits",
|
|
3646
|
+
signal: options.signal
|
|
3647
|
+
});
|
|
3648
|
+
}
|
|
3649
|
+
/** `GET /limits/recordCount`: approximate record counts per sObject. */
|
|
3650
|
+
recordCount(sobjects, options = {}) {
|
|
3651
|
+
return this.connection.request({
|
|
3652
|
+
path: "/limits/recordCount",
|
|
3653
|
+
query: { sObjects: sobjects },
|
|
3654
|
+
signal: options.signal
|
|
3655
|
+
});
|
|
3656
|
+
}
|
|
3657
|
+
/** `GET /sobjects`: every sObject visible to the user. */
|
|
3658
|
+
describeGlobal(options = {}) {
|
|
3659
|
+
return this._cache.getOrLoad("describe-global", (signal) => this.connection.request({
|
|
3660
|
+
path: "/sobjects",
|
|
3661
|
+
signal
|
|
3662
|
+
}), options.signal);
|
|
3663
|
+
}
|
|
3664
|
+
/**
|
|
3665
|
+
* Asks Salesforce how it would run a query (`GET /query?explain=`, beta), e.g. to check a
|
|
3666
|
+
* filter is selective. Plans are sorted best first; `relativeCost` above 1 means not selective.
|
|
3667
|
+
*/
|
|
3668
|
+
async explain(query, options = {}) {
|
|
3669
|
+
return (await this.connection.request({
|
|
3670
|
+
path: "/query",
|
|
3671
|
+
query: { explain: toSoql(query) },
|
|
3672
|
+
signal: options.signal
|
|
3673
|
+
})).plans;
|
|
3674
|
+
}
|
|
3675
|
+
};
|
|
3676
|
+
}));
|
|
3677
|
+
//#endregion
|
|
3678
|
+
//#region src/auth/caching-provider.ts
|
|
3679
|
+
function isExpired(token, fetchedAt) {
|
|
3680
|
+
if (token.expiresAt === void 0) return false;
|
|
3681
|
+
const margin = Math.min(EXPIRY_MARGIN_MS, Math.max(0, (token.expiresAt - fetchedAt) / 2));
|
|
3682
|
+
return token.expiresAt - margin <= Date.now();
|
|
3683
|
+
}
|
|
3684
|
+
var EXPIRY_MARGIN_MS, CachingAuthProvider;
|
|
3685
|
+
var init_caching_provider = __esmMin((() => {
|
|
3686
|
+
init_shared();
|
|
3687
|
+
EXPIRY_MARGIN_MS = 6e4;
|
|
3688
|
+
CachingAuthProvider = class {
|
|
3689
|
+
_fetchToken;
|
|
3690
|
+
_pending;
|
|
3691
|
+
_token;
|
|
3692
|
+
constructor(_fetchToken) {
|
|
3693
|
+
this._fetchToken = _fetchToken;
|
|
3694
|
+
}
|
|
3695
|
+
getToken(context) {
|
|
3696
|
+
if (this._token && !isExpired(this._token.value, this._token.fetchedAt)) return Promise.resolve(this._token.value);
|
|
3697
|
+
if (!this._pending || this._pending.abandoned) {
|
|
3698
|
+
const pending = share((signal) => this._fetchToken({
|
|
3699
|
+
transport: context.transport,
|
|
3700
|
+
signal
|
|
3701
|
+
}).then((token) => {
|
|
3702
|
+
this._token = {
|
|
3703
|
+
value: token,
|
|
3704
|
+
fetchedAt: Date.now()
|
|
3705
|
+
};
|
|
3706
|
+
return token;
|
|
3707
|
+
}));
|
|
3708
|
+
this._pending = pending;
|
|
3709
|
+
pending.promise.finally(() => {
|
|
3710
|
+
if (this._pending === pending) this._pending = void 0;
|
|
3711
|
+
}).catch(() => void 0);
|
|
3712
|
+
}
|
|
3713
|
+
return this._pending.join(context.signal);
|
|
3714
|
+
}
|
|
3715
|
+
invalidate(token) {
|
|
3716
|
+
if (this._token?.value.accessToken === token.accessToken) this._token = void 0;
|
|
3717
|
+
}
|
|
3718
|
+
};
|
|
3719
|
+
}));
|
|
3720
|
+
//#endregion
|
|
3721
|
+
//#region src/auth/oauth.ts
|
|
3722
|
+
/**
|
|
3723
|
+
* POSTs a form to `{loginUrl}/services/oauth2/token` and turns the response into an
|
|
3724
|
+
* `AccessToken`. Errors never include the submitted parameters.
|
|
3725
|
+
*/
|
|
3726
|
+
async function requestOAuthToken(transport, loginUrl, params, signal) {
|
|
3727
|
+
const url = new URL("/services/oauth2/token", normalizeLoginUrl(loginUrl));
|
|
3728
|
+
const response = await transport.send({
|
|
3729
|
+
method: "POST",
|
|
3730
|
+
url,
|
|
3731
|
+
headers: new Headers({
|
|
3732
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
3733
|
+
Accept: "application/json"
|
|
3734
|
+
}),
|
|
3735
|
+
body: new URLSearchParams(params).toString(),
|
|
3736
|
+
signal
|
|
3737
|
+
});
|
|
3738
|
+
const json = parseJson(decoder.decode(response.body));
|
|
3739
|
+
if (response.status < 200 || response.status >= 300) {
|
|
3740
|
+
const error = typeof json?.error === "string" ? json.error : void 0;
|
|
3741
|
+
const errorDescription = typeof json?.error_description === "string" ? json.error_description : void 0;
|
|
3742
|
+
throw new SalesforceAuthError(`Salesforce token request to ${url.origin} failed with status ${response.status}${error ? ` - ${error}${errorDescription ? `: ${errorDescription}` : ""}` : ""}`, {
|
|
3743
|
+
status: response.status,
|
|
3744
|
+
error,
|
|
3745
|
+
errorDescription
|
|
3746
|
+
});
|
|
3747
|
+
}
|
|
3748
|
+
if (typeof json?.access_token !== "string" || typeof json.instance_url !== "string") throw new SalesforceAuthError(`Salesforce token response from ${url.origin} has no access_token/instance_url.`, { status: response.status });
|
|
3749
|
+
const tokenResponse = json;
|
|
3750
|
+
return {
|
|
3751
|
+
token: {
|
|
3752
|
+
accessToken: tokenResponse.access_token,
|
|
3753
|
+
instanceUrl: tokenResponse.instance_url
|
|
3754
|
+
},
|
|
3755
|
+
response: tokenResponse
|
|
3756
|
+
};
|
|
3757
|
+
}
|
|
3758
|
+
/** Accepts `https://x.my.salesforce.com`, with or without a trailing slash or path. */
|
|
3759
|
+
function normalizeLoginUrl(loginUrl) {
|
|
3760
|
+
let url;
|
|
3761
|
+
try {
|
|
3762
|
+
url = new URL(loginUrl);
|
|
3763
|
+
} catch {
|
|
3764
|
+
throw new TypeError(`Invalid Salesforce login URL: "${loginUrl}".`);
|
|
3765
|
+
}
|
|
3766
|
+
if (url.protocol !== "https:" && url.hostname !== "localhost" && url.hostname !== "127.0.0.1") throw new TypeError(`Salesforce login URL must use https: "${url.origin}".`);
|
|
3767
|
+
return url.origin;
|
|
3768
|
+
}
|
|
3769
|
+
function parseJson(text) {
|
|
3770
|
+
try {
|
|
3771
|
+
const value = JSON.parse(text);
|
|
3772
|
+
return typeof value === "object" && value !== null ? value : void 0;
|
|
3773
|
+
} catch {
|
|
3774
|
+
return;
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
var decoder;
|
|
3778
|
+
var init_oauth = __esmMin((() => {
|
|
3779
|
+
init_errors();
|
|
3780
|
+
decoder = new TextDecoder();
|
|
3781
|
+
}));
|
|
3782
|
+
//#endregion
|
|
3783
|
+
//#region src/auth/providers.ts
|
|
3784
|
+
/** Uses a fixed access token, e.g. one issued by the Salesforce CLI (`sf org display`). */
|
|
3785
|
+
function accessToken(token) {
|
|
3786
|
+
if (!token.accessToken || !token.instanceUrl) throw new TypeError("accessToken() requires both accessToken and instanceUrl.");
|
|
3787
|
+
return { getToken: () => Promise.resolve(token) };
|
|
3788
|
+
}
|
|
3789
|
+
/**
|
|
3790
|
+
* Wraps your own token source. The result is cached until Salesforce answers 401, after which
|
|
3791
|
+
* `fetchToken` is called again.
|
|
3792
|
+
*/
|
|
3793
|
+
function tokenProvider(fetchToken) {
|
|
3794
|
+
return new CachingAuthProvider(fetchToken);
|
|
3795
|
+
}
|
|
3796
|
+
/**
|
|
3797
|
+
* OAuth 2.0 client credentials flow (server-to-server, runs as the app's "Run As" user).
|
|
3798
|
+
* The token is cached in memory and refreshed once when Salesforce answers 401.
|
|
3799
|
+
*/
|
|
3800
|
+
function clientCredentials(options) {
|
|
3801
|
+
requireOptions("clientCredentials", options, [
|
|
3802
|
+
"loginUrl",
|
|
3803
|
+
"clientId",
|
|
3804
|
+
"clientSecret"
|
|
3805
|
+
]);
|
|
3806
|
+
return new CachingAuthProvider(async (context) => {
|
|
3807
|
+
const params = {
|
|
3808
|
+
grant_type: "client_credentials",
|
|
3809
|
+
client_id: options.clientId,
|
|
3810
|
+
client_secret: options.clientSecret
|
|
3811
|
+
};
|
|
3812
|
+
if (options.scope) params.scope = options.scope;
|
|
3813
|
+
const { token } = await requestOAuthToken(context.transport, options.loginUrl, params, context.signal);
|
|
3814
|
+
return token;
|
|
3815
|
+
});
|
|
3816
|
+
}
|
|
3817
|
+
/** OAuth 2.0 JWT bearer flow. The assertion is signed locally with RS256; no extra dependencies. */
|
|
3818
|
+
function jwtBearer(options) {
|
|
3819
|
+
requireOptions("jwtBearer", options, [
|
|
3820
|
+
"loginUrl",
|
|
3821
|
+
"clientId",
|
|
3822
|
+
"username",
|
|
3823
|
+
"privateKey"
|
|
3824
|
+
]);
|
|
3825
|
+
return new CachingAuthProvider(async (context) => {
|
|
3826
|
+
const assertion = createJwtAssertion(options);
|
|
3827
|
+
const { token } = await requestOAuthToken(context.transport, options.loginUrl, {
|
|
3828
|
+
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
3829
|
+
assertion
|
|
3830
|
+
}, context.signal);
|
|
3831
|
+
return token;
|
|
3832
|
+
});
|
|
3833
|
+
}
|
|
3834
|
+
/** Exchanges a refresh token for access tokens. */
|
|
3835
|
+
function refreshToken(options) {
|
|
3836
|
+
requireOptions("refreshToken", options, [
|
|
3837
|
+
"loginUrl",
|
|
3838
|
+
"clientId",
|
|
3839
|
+
"refreshToken"
|
|
3840
|
+
]);
|
|
3841
|
+
let currentRefreshToken = options.refreshToken;
|
|
3842
|
+
return new CachingAuthProvider(async (context) => {
|
|
3843
|
+
const params = {
|
|
3844
|
+
grant_type: "refresh_token",
|
|
3845
|
+
client_id: options.clientId,
|
|
3846
|
+
refresh_token: currentRefreshToken
|
|
3847
|
+
};
|
|
3848
|
+
if (options.clientSecret) params.client_secret = options.clientSecret;
|
|
3849
|
+
const { token, response } = await requestOAuthToken(context.transport, options.loginUrl, params, context.signal);
|
|
3850
|
+
if (response.refresh_token && response.refresh_token !== currentRefreshToken) {
|
|
3851
|
+
currentRefreshToken = response.refresh_token;
|
|
3852
|
+
try {
|
|
3853
|
+
options.onRefreshTokenRotated?.(response.refresh_token);
|
|
3854
|
+
} catch {}
|
|
3855
|
+
}
|
|
3856
|
+
return token;
|
|
3857
|
+
});
|
|
3858
|
+
}
|
|
3859
|
+
/** Builds a signed RS256 JWT assertion for the JWT bearer flow. Exported for testing. */
|
|
3860
|
+
function createJwtAssertion(options, now = Date.now()) {
|
|
3861
|
+
const expiresIn = Math.min(Math.max(Math.floor(options.expiresInSeconds ?? 180), 1), 180);
|
|
3862
|
+
const header = {
|
|
3863
|
+
alg: "RS256",
|
|
3864
|
+
typ: "JWT"
|
|
3865
|
+
};
|
|
3866
|
+
const claims = {
|
|
3867
|
+
iss: options.clientId,
|
|
3868
|
+
sub: options.username,
|
|
3869
|
+
aud: options.audience ?? defaultAudience(options.loginUrl),
|
|
3870
|
+
exp: Math.floor(now / 1e3) + expiresIn
|
|
3871
|
+
};
|
|
3872
|
+
const unsigned = `${base64Url(JSON.stringify(header))}.${base64Url(JSON.stringify(claims))}`;
|
|
3873
|
+
return `${unsigned}.${base64Url(createSign("RSA-SHA256").update(unsigned).sign(options.privateKey))}`;
|
|
3874
|
+
}
|
|
3875
|
+
function defaultAudience(loginUrl) {
|
|
3876
|
+
const host = new URL(loginUrl).hostname;
|
|
3877
|
+
return host === "test.salesforce.com" || host.includes(".sandbox.") || host.includes(".scratch.") ? "https://test.salesforce.com" : "https://login.salesforce.com";
|
|
3878
|
+
}
|
|
3879
|
+
function base64Url(value) {
|
|
3880
|
+
return Buffer.from(value).toString("base64url");
|
|
3881
|
+
}
|
|
3882
|
+
function requireOptions(provider, options, keys) {
|
|
3883
|
+
const missing = keys.filter((key) => !options[key]);
|
|
3884
|
+
if (missing.length > 0) throw new TypeError(`${provider}() is missing required option(s): ${missing.join(", ")}.`);
|
|
3885
|
+
}
|
|
3886
|
+
var init_providers = __esmMin((() => {
|
|
3887
|
+
init_caching_provider();
|
|
3888
|
+
init_oauth();
|
|
3889
|
+
}));
|
|
3890
|
+
//#endregion
|
|
3891
|
+
//#region src/auth/sf-cli.ts
|
|
3892
|
+
/**
|
|
3893
|
+
* Uses an org you are logged into with the Salesforce CLI (`sf org login web --alias my-org`).
|
|
3894
|
+
* Handy for local development: no connected app or secrets needed.
|
|
3895
|
+
*
|
|
3896
|
+
* The token comes from `sf org auth show-access-token`, the instance URL from `sf org display`
|
|
3897
|
+
* (which no longer returns tokens). Both run without a shell. On a 401 the CLI is asked again;
|
|
3898
|
+
* it refreshes the token itself.
|
|
3899
|
+
*/
|
|
3900
|
+
function sfCli(options = {}) {
|
|
3901
|
+
if (options.targetOrg !== void 0 && !ALIAS.test(options.targetOrg)) throw new TypeError(`sfCli(): invalid targetOrg "${options.targetOrg}".`);
|
|
3902
|
+
const command = options.command ?? "sf";
|
|
3903
|
+
const run = options.runCommand ?? runCommand;
|
|
3904
|
+
const target = options.targetOrg ? ["--target-org", options.targetOrg] : [];
|
|
3905
|
+
return new CachingAuthProvider(async () => {
|
|
3906
|
+
const display = await callSf(run, command, [
|
|
3907
|
+
"org",
|
|
3908
|
+
"display",
|
|
3909
|
+
"--json",
|
|
3910
|
+
...target
|
|
3911
|
+
]);
|
|
3912
|
+
const instanceUrl = display.instanceUrl;
|
|
3913
|
+
if (!instanceUrl) throw new SalesforceAuthError("`sf org display` returned no instanceUrl.");
|
|
3914
|
+
const token = await callSf(run, command, [
|
|
3915
|
+
"org",
|
|
3916
|
+
"auth",
|
|
3917
|
+
"show-access-token",
|
|
3918
|
+
"--json",
|
|
3919
|
+
...target
|
|
3920
|
+
]).catch((error) => {
|
|
3921
|
+
if (usableToken(display.accessToken)) return { accessToken: display.accessToken };
|
|
3922
|
+
throw error;
|
|
3923
|
+
});
|
|
3924
|
+
if (!usableToken(token.accessToken)) throw new SalesforceAuthError("The Salesforce CLI returned no usable access token. Log in again with `sf org login web`.");
|
|
3925
|
+
return {
|
|
3926
|
+
accessToken: token.accessToken,
|
|
3927
|
+
instanceUrl
|
|
3928
|
+
};
|
|
3929
|
+
});
|
|
3930
|
+
}
|
|
3931
|
+
async function callSf(run, command, args) {
|
|
3932
|
+
let stdout;
|
|
3933
|
+
try {
|
|
3934
|
+
stdout = await run(command, args);
|
|
3935
|
+
} catch (error) {
|
|
3936
|
+
if (error.code === "ENOENT") throw new SalesforceAuthError("The Salesforce CLI (sf) was not found. Install it from https://developer.salesforce.com/tools/salesforcecli and log in with `sf org login web`.");
|
|
3937
|
+
throw error;
|
|
3938
|
+
}
|
|
3939
|
+
let json;
|
|
3940
|
+
try {
|
|
3941
|
+
json = JSON.parse(stdout.slice(Math.max(0, stdout.indexOf("{"))));
|
|
3942
|
+
} catch {
|
|
3943
|
+
throw new SalesforceAuthError(`\`sf ${args.slice(0, 3).join(" ")}\` did not return JSON: ${stdout.trim().slice(0, 200)}`);
|
|
3944
|
+
}
|
|
3945
|
+
if (json.status !== 0 || !json.result) throw new SalesforceAuthError(`\`sf ${args.slice(0, 3).join(" ")}\` failed: ${json.message ?? json.name ?? "unknown error"}`);
|
|
3946
|
+
return json.result;
|
|
3947
|
+
}
|
|
3948
|
+
function usableToken(token) {
|
|
3949
|
+
return typeof token === "string" && token.length > 0 && !token.includes("REDACTED");
|
|
3950
|
+
}
|
|
3951
|
+
function quoteForCmd(value) {
|
|
3952
|
+
return /[\s"]/.test(value) ? `"${value.replace(/"/g, "\"\"")}"` : value;
|
|
3953
|
+
}
|
|
3954
|
+
var ALIAS, CMD_NOT_FOUND, runCommand;
|
|
3955
|
+
var init_sf_cli = __esmMin((() => {
|
|
3956
|
+
init_errors();
|
|
3957
|
+
init_caching_provider();
|
|
3958
|
+
ALIAS = /^[\w.@+-]+$/;
|
|
3959
|
+
CMD_NOT_FOUND = 9009;
|
|
3960
|
+
runCommand = (command, args) => new Promise((resolve, reject) => {
|
|
3961
|
+
const windows = process.platform === "win32";
|
|
3962
|
+
const [file, fileArgs] = windows ? ["cmd.exe", [
|
|
3963
|
+
"/d",
|
|
3964
|
+
"/s",
|
|
3965
|
+
"/c",
|
|
3966
|
+
`"${[command, ...args].map(quoteForCmd).join(" ")}"`
|
|
3967
|
+
]] : [command, [...args]];
|
|
3968
|
+
execFile(file, fileArgs, {
|
|
3969
|
+
windowsHide: true,
|
|
3970
|
+
windowsVerbatimArguments: windows,
|
|
3971
|
+
maxBuffer: 10485760,
|
|
3972
|
+
env: {
|
|
3973
|
+
...process.env,
|
|
3974
|
+
SF_JSON_TO_STDOUT: "true"
|
|
3975
|
+
}
|
|
3976
|
+
}, (error, stdout, stderr) => {
|
|
3977
|
+
if (error && (error.code === "ENOENT" || windows && (error.code === CMD_NOT_FOUND || /is not recognized/i.test(stderr)))) reject(Object.assign(/* @__PURE__ */ new Error(`${command} was not found`), { code: "ENOENT" }));
|
|
3978
|
+
else if (error && !stdout.includes("{")) reject(/* @__PURE__ */ new Error(`${error.message}${stderr ? `\n${stderr.trim()}` : ""}`));
|
|
3979
|
+
else resolve(stdout);
|
|
3980
|
+
});
|
|
3981
|
+
});
|
|
3982
|
+
}));
|
|
3983
|
+
//#endregion
|
|
3984
|
+
export { BulkApi as $, init_search as A, init_errors as At, MAX_UPLOAD_BYTES as B, init_ui_api as C, soslEscape as Ct, isSObjectType as D, SalesforcePartialFailureError as Dt, init_query_builder as E, SalesforceError as Et, init_quick_actions as F, MetadataCache as Ft, init_events as G, buildMultipart as H, QueryApi as I, init_cache as It, CompositeRequestBuilder as J, toPublishResult as K, init_query as L, __commonJSMin as Lt, init_reports as M, init_api_version as Mt, reportRows as N, isApiVersion as Nt, soqlFor as O, SalesforceSaveError as Ot, QuickActionsApi as P, normalizeApiVersion as Pt, init_collections as Q, FilesApi as R, __esmMin as Rt, UiApi as S, soqlLiteral as St, TypeOfBuilder as T, SalesforceBulkJobError as Tt, init_multipart as U, init_files as V, EventsApi as W, init_composite as X, CompositeResponse as Y, CollectionsApi as Z, DebugLogCaptureError as _, init_escape as _t, createJwtAssertion as a, parseCsv as at, SObjectResource as b, soqlEscape as bt, refreshToken as c, toCsv as ct, init_client as d, ActionsApi as dt, BulkIngestJob as et, ApexExecutionError as f, init_actions as ft, DEFAULT_DEBUG_LEVELS as g, init_connection as gt, DEBUG_LEVEL_NAME as h, SalesforceConnection as ht, clientCredentials as i, init_csv as it, ReportsApi as j, isSalesforceError as jt, SearchApi as k, hasErrorCode as kt, tokenProvider as l, ApprovalsApi as lt, init_tooling as m, init_fetch_transport as mt, sfCli as n, init_bulk as nt, init_providers as o, parseCsvRows as ot, ToolingApi as p, fetchTransport as pt, CompositeApi as q, accessToken as r, CsvRowParser as rt, jwtBearer as s, parseCsvStream as st, init_sf_cli as t, BulkQueryJob as tt, SalesforceClient as u, init_approvals as ut, DebugLogsApi as v, soqlDate as vt, SoqlQueryBuilder as w, SalesforceAuthError as wt, init_sobject as x, soqlEscapeDateOnly as xt, init_debug_logs as y, soqlDateLiteral as yt, MAX_JSON_UPLOAD_BYTES as z };
|
|
3985
|
+
|
|
3986
|
+
//# sourceMappingURL=sf-cli-CtWtdkJ0.mjs.map
|