@2bad/bitrix 3.0.0-beta.0 → 3.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/README.md +93 -186
- package/build/bin.d.ts +1 -0
- package/build/bin.js +160 -0
- package/build/client-Bk76ySik.js +769 -0
- package/build/index.d.ts +2881 -0
- package/build/index.js +1 -0
- package/package.json +43 -64
- package/build/bitrix.d.ts +0 -65
- package/build/bitrix.js +0 -55
- package/build/client/hooks/addAccessToken.d.ts +0 -6
- package/build/client/hooks/addAccessToken.js +0 -9
- package/build/client/index.d.ts +0 -15
- package/build/client/index.js +0 -53
- package/build/client/methods/batch.d.ts +0 -40
- package/build/client/methods/batch.js +0 -75
- package/build/client/methods/call.d.ts +0 -17
- package/build/client/methods/call.js +0 -27
- package/build/client/methods/list.d.ts +0 -30
- package/build/client/methods/list.js +0 -58
- package/build/commands.d.ts +0 -10
- package/build/commands.js +0 -1
- package/build/methods.d.ts +0 -92
- package/build/methods.js +0 -38
- package/build/payloads.d.ts +0 -40
- package/build/payloads.js +0 -1
- package/build/services/common.d.ts +0 -22
- package/build/services/common.js +0 -2
- package/build/services/companies/entities.d.ts +0 -54
- package/build/services/companies/entities.js +0 -1
- package/build/services/companies/index.d.ts +0 -15
- package/build/services/companies/index.js +0 -17
- package/build/services/companies/methods.d.ts +0 -44
- package/build/services/companies/methods.js +0 -1
- package/build/services/contacts/entities.d.ts +0 -45
- package/build/services/contacts/entities.js +0 -1
- package/build/services/contacts/index.d.ts +0 -15
- package/build/services/contacts/index.js +0 -17
- package/build/services/contacts/methods.d.ts +0 -44
- package/build/services/contacts/methods.js +0 -1
- package/build/services/currencies/entities.d.ts +0 -15
- package/build/services/currencies/entities.js +0 -1
- package/build/services/deals/entities.d.ts +0 -43
- package/build/services/deals/entities.js +0 -1
- package/build/services/deals/index.d.ts +0 -15
- package/build/services/deals/index.js +0 -17
- package/build/services/deals/methods.d.ts +0 -44
- package/build/services/deals/methods.js +0 -1
- package/build/services/leads/entities.d.ts +0 -49
- package/build/services/leads/entities.js +0 -1
- package/build/services/leads/index.d.ts +0 -15
- package/build/services/leads/index.js +0 -17
- package/build/services/leads/methods.d.ts +0 -44
- package/build/services/leads/methods.js +0 -1
- package/build/services/statuses/entities.d.ts +0 -16
- package/build/services/statuses/entities.js +0 -1
- package/build/services/statuses/index.d.ts +0 -14
- package/build/services/statuses/index.js +0 -19
- package/build/services/statuses/methods.d.ts +0 -51
- package/build/services/statuses/methods.js +0 -1
- package/build/services/users/entities.d.ts +0 -40
- package/build/services/users/entities.js +0 -1
- package/build/services/users/index.d.ts +0 -9
- package/build/services/users/index.js +0 -7
- package/build/services/users/methods.d.ts +0 -17
- package/build/services/users/methods.js +0 -1
- package/build/utils/Diff.d.ts +0 -1
- package/build/utils/Diff.js +0 -7
- package/build/utils/ExtractValue.d.ts +0 -6
- package/build/utils/ExtractValue.js +0 -5
- package/build/utils/isArray.d.ts +0 -2
- package/build/utils/isArray.js +0 -2
|
@@ -0,0 +1,769 @@
|
|
|
1
|
+
// source/encode.ts
|
|
2
|
+
const encodeValue = (value) => {
|
|
3
|
+
if (value === null) return "";
|
|
4
|
+
if (value === true) return "1";
|
|
5
|
+
if (value === false) return "0";
|
|
6
|
+
if (value instanceof Date) return encodeURIComponent(value.toISOString());
|
|
7
|
+
return encodeURIComponent(String(value));
|
|
8
|
+
};
|
|
9
|
+
const pairs = (key, value) => {
|
|
10
|
+
if (value === void 0) return [];
|
|
11
|
+
if (Array.isArray(value)) return value.flatMap((item, index) => pairs(`${key}[${index}]`, item));
|
|
12
|
+
if (typeof value === "object" && value !== null && !(value instanceof Date)) return Object.entries(value).flatMap(([name, item]) => pairs(`${key}[${name}]`, item));
|
|
13
|
+
return [`${encodeURIComponent(key)}=${encodeValue(value)}`];
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Serializes params into a PHP style query string (`filter[>ID]=5&select[0]=ID`), the format Bitrix expects
|
|
17
|
+
* for commands inside a `batch` call
|
|
18
|
+
* @param params params of a single command
|
|
19
|
+
*/
|
|
20
|
+
const encode = (params) => Object.entries(params).flatMap(([key, value]) => pairs(key, value)).join("&");
|
|
21
|
+
// source/errors.ts
|
|
22
|
+
var BitrixError = class extends Error {
|
|
23
|
+
name = "BitrixError";
|
|
24
|
+
code;
|
|
25
|
+
description;
|
|
26
|
+
method;
|
|
27
|
+
status;
|
|
28
|
+
command;
|
|
29
|
+
constructor({ code, description, method, status, command, cause }) {
|
|
30
|
+
const target = command === void 0 ? method : `${method} (command ${command})`;
|
|
31
|
+
super(`${target}: ${[code, description].filter(Boolean).join(" ")}`, { cause });
|
|
32
|
+
this.code = code;
|
|
33
|
+
this.description = description;
|
|
34
|
+
this.method = method;
|
|
35
|
+
this.status = status;
|
|
36
|
+
this.command = command;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
// source/transport.ts
|
|
40
|
+
const RETRYABLE_CODES = /* @__PURE__ */ new Set([
|
|
41
|
+
"QUERY_LIMIT_EXCEEDED",
|
|
42
|
+
"OPERATION_TIME_LIMIT",
|
|
43
|
+
"INTERNAL_SERVER_ERROR"
|
|
44
|
+
]);
|
|
45
|
+
const sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
46
|
+
const onAbort = () => {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
reject(signal?.reason);
|
|
49
|
+
};
|
|
50
|
+
const timer = setTimeout(() => {
|
|
51
|
+
signal?.removeEventListener("abort", onAbort);
|
|
52
|
+
resolve();
|
|
53
|
+
}, ms);
|
|
54
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
55
|
+
});
|
|
56
|
+
/**
|
|
57
|
+
* Time to wait before retrying an operating time limit error: until the limit resets when Bitrix said when,
|
|
58
|
+
* exponential backoff otherwise
|
|
59
|
+
* @param resetAt when the limit resets, in milliseconds since the epoch
|
|
60
|
+
* @param delay base backoff delay in milliseconds
|
|
61
|
+
* @param attempt zero based retry attempt
|
|
62
|
+
*/
|
|
63
|
+
const operatingDelay = (resetAt, delay, attempt) => resetAt !== void 0 && resetAt > Date.now() ? resetAt - Date.now() : delay * 2 ** attempt;
|
|
64
|
+
const isRetryable = (error) => error instanceof BitrixError ? RETRYABLE_CODES.has(error.code) || error.status !== void 0 && error.status >= 500 : error instanceof TypeError;
|
|
65
|
+
const parse = async (response, method) => {
|
|
66
|
+
const text = await response.text();
|
|
67
|
+
let body;
|
|
68
|
+
try {
|
|
69
|
+
body = JSON.parse(text);
|
|
70
|
+
} catch (cause) {
|
|
71
|
+
throw new BitrixError({
|
|
72
|
+
code: "INVALID_RESPONSE",
|
|
73
|
+
description: `expected JSON, received ${text.slice(0, 100)}`,
|
|
74
|
+
method,
|
|
75
|
+
status: response.status,
|
|
76
|
+
cause
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (!response.ok || "error" in body) throw new BitrixError({
|
|
80
|
+
code: typeof body["error"] === "string" && body["error"] !== "" ? body["error"] : `HTTP_${response.status}`,
|
|
81
|
+
description: typeof body["error_description"] === "string" ? body["error_description"] : response.statusText,
|
|
82
|
+
method,
|
|
83
|
+
status: response.status
|
|
84
|
+
});
|
|
85
|
+
return body;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Sends a single REST method call as a JSON POST, applying auth, rate limiting and retries
|
|
89
|
+
* @param options endpoint, auth, fetch and hook settings
|
|
90
|
+
* @param limiter rate limiter every request waits for
|
|
91
|
+
* @param retry how often and how long to wait before retrying
|
|
92
|
+
* @param retry.retries attempts after the first one
|
|
93
|
+
* @param retry.delay base backoff delay in milliseconds
|
|
94
|
+
*/
|
|
95
|
+
const createTransport = (options, limiter, { retries, delay }) => {
|
|
96
|
+
const { auth, onAuthExpired } = options;
|
|
97
|
+
const fetcher = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
98
|
+
const base = new URL(options.url.endsWith("/") ? options.url : `${options.url}/`);
|
|
99
|
+
const operatingResetAt = /* @__PURE__ */ new Map();
|
|
100
|
+
let refreshing;
|
|
101
|
+
const refreshAuth = async () => {
|
|
102
|
+
refreshing ??= (async () => {
|
|
103
|
+
try {
|
|
104
|
+
await onAuthExpired?.();
|
|
105
|
+
} finally {
|
|
106
|
+
refreshing = void 0;
|
|
107
|
+
}
|
|
108
|
+
})();
|
|
109
|
+
await refreshing;
|
|
110
|
+
};
|
|
111
|
+
const request = async (method, params, signal) => {
|
|
112
|
+
const token = typeof auth === "function" ? await auth() : auth;
|
|
113
|
+
const response = await fetcher(new URL(method, base), {
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers: {
|
|
116
|
+
"content-type": "application/json",
|
|
117
|
+
accept: "application/json"
|
|
118
|
+
},
|
|
119
|
+
body: JSON.stringify(token === void 0 ? params : {
|
|
120
|
+
...params,
|
|
121
|
+
auth: token
|
|
122
|
+
}),
|
|
123
|
+
...signal && { signal }
|
|
124
|
+
});
|
|
125
|
+
return parse(response, method);
|
|
126
|
+
};
|
|
127
|
+
const send = async (method, params, signal, attempt) => {
|
|
128
|
+
await limiter.acquire(signal);
|
|
129
|
+
options.onRequest?.({
|
|
130
|
+
method,
|
|
131
|
+
params,
|
|
132
|
+
attempt
|
|
133
|
+
});
|
|
134
|
+
const started = Date.now();
|
|
135
|
+
let envelope;
|
|
136
|
+
try {
|
|
137
|
+
envelope = await request(method, params, signal);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
options.onResponse?.({
|
|
140
|
+
method,
|
|
141
|
+
attempt,
|
|
142
|
+
duration: Date.now() - started,
|
|
143
|
+
error
|
|
144
|
+
});
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
options.onResponse?.({
|
|
148
|
+
method,
|
|
149
|
+
attempt,
|
|
150
|
+
duration: Date.now() - started,
|
|
151
|
+
...envelope.time && { time: envelope.time }
|
|
152
|
+
});
|
|
153
|
+
if (envelope.time?.operating_reset_at !== void 0) operatingResetAt.set(method, envelope.time.operating_reset_at * 1e3);
|
|
154
|
+
return envelope;
|
|
155
|
+
};
|
|
156
|
+
const backoff = (error, method, attempt) => {
|
|
157
|
+
if (error instanceof BitrixError && error.code === "QUERY_LIMIT_EXCEEDED") {
|
|
158
|
+
limiter.exhaust();
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
if (error instanceof BitrixError && error.code === "OPERATION_TIME_LIMIT") return operatingDelay(operatingResetAt.get(method), delay, attempt);
|
|
162
|
+
return delay * 2 ** attempt;
|
|
163
|
+
};
|
|
164
|
+
return async (method, params = {}, callSignal) => {
|
|
165
|
+
const signals = [options.signal, callSignal].filter((signal) => signal !== void 0);
|
|
166
|
+
const signal = signals.length > 1 ? AbortSignal.any(signals) : signals[0];
|
|
167
|
+
let refreshed = false;
|
|
168
|
+
let sent = 0;
|
|
169
|
+
for (let attempt = 0;; attempt++) try {
|
|
170
|
+
return await send(method, params, signal, sent++);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (signal?.aborted) throw signal.reason;
|
|
173
|
+
if (error instanceof BitrixError && error.code === "expired_token" && onAuthExpired && !refreshed) {
|
|
174
|
+
refreshed = true;
|
|
175
|
+
await refreshAuth();
|
|
176
|
+
attempt--;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (attempt >= retries || !isRetryable(error)) throw error;
|
|
180
|
+
await sleep(backoff(error, method, attempt), signal);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
const OPERATION_TIME_LIMIT = "OPERATION_TIME_LIMIT";
|
|
185
|
+
const chunk = (items, size) => Array.from({ length: Math.ceil(items.length / size) }, (_, index) => items.slice(index * size, (index + 1) * size));
|
|
186
|
+
const toCommand = ({ method, params }) => {
|
|
187
|
+
const query = params ? encode(params) : "";
|
|
188
|
+
return query === "" ? method : `${method}?${query}`;
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* Runs any number of commands through `batch`, 50 per request. Keyed commands give keyed results,
|
|
192
|
+
* arrays give arrays. A failed command lands in `errors` and does not reject the whole call.
|
|
193
|
+
* Commands that hit the operating time limit are sent again once the limit resets, unless they
|
|
194
|
+
* reference results of other commands with `$result`
|
|
195
|
+
* @param transport sends the `batch` requests
|
|
196
|
+
* @param retry how often and how long to wait before sending limited commands again
|
|
197
|
+
* @param retry.retries attempts after the first one
|
|
198
|
+
* @param retry.delay base backoff delay in milliseconds
|
|
199
|
+
*/
|
|
200
|
+
const createBatch = (transport, { retries, delay }) => {
|
|
201
|
+
const send = async (entries, { halt = false, signal }) => {
|
|
202
|
+
const cmd = Object.fromEntries(entries.map(([key, command]) => [key, toCommand(command)]));
|
|
203
|
+
const { result, time } = await transport("batch", {
|
|
204
|
+
halt: halt ? 1 : 0,
|
|
205
|
+
cmd
|
|
206
|
+
}, signal);
|
|
207
|
+
const methods = new Map(entries.map(([key, command]) => [key, command.method]));
|
|
208
|
+
const errors = Object.fromEntries(Object.entries(result.result_error).map(([key, raw]) => [key, new BitrixError({
|
|
209
|
+
code: raw.error || "COMMAND_FAILED",
|
|
210
|
+
description: raw.error_description ?? "",
|
|
211
|
+
method: methods.get(key) ?? "batch",
|
|
212
|
+
command: key
|
|
213
|
+
})]));
|
|
214
|
+
const resetAt = time?.operating_reset_at === void 0 ? void 0 : time.operating_reset_at * 1e3;
|
|
215
|
+
return {
|
|
216
|
+
results: { ...result.result },
|
|
217
|
+
errors,
|
|
218
|
+
resetAt
|
|
219
|
+
};
|
|
220
|
+
};
|
|
221
|
+
const retryable = (entries, { results, errors }, halt) => {
|
|
222
|
+
const limited = entries.filter(([key]) => errors[key]?.code === OPERATION_TIME_LIMIT);
|
|
223
|
+
if (limited.length === 0) return [];
|
|
224
|
+
const skipped = halt ? entries.filter(([key]) => !(key in results) && !(key in errors)) : [];
|
|
225
|
+
return [...limited, ...skipped].filter(([, command]) => !JSON.stringify(command.params ?? {}).includes("$result"));
|
|
226
|
+
};
|
|
227
|
+
const run = async (entries, options) => {
|
|
228
|
+
let response = await send(entries, options);
|
|
229
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
230
|
+
const again = retryable(entries, response, options.halt ?? false);
|
|
231
|
+
if (again.length === 0) break;
|
|
232
|
+
await sleep(operatingDelay(response.resetAt, delay, attempt), options.signal);
|
|
233
|
+
const retried = await send(again, options);
|
|
234
|
+
const keys = new Set(again.map(([key]) => key));
|
|
235
|
+
response = {
|
|
236
|
+
results: {
|
|
237
|
+
...response.results,
|
|
238
|
+
...retried.results
|
|
239
|
+
},
|
|
240
|
+
errors: {
|
|
241
|
+
...Object.fromEntries(Object.entries(response.errors).filter(([key]) => !keys.has(key))),
|
|
242
|
+
...retried.errors
|
|
243
|
+
},
|
|
244
|
+
resetAt: retried.resetAt
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
return response;
|
|
248
|
+
};
|
|
249
|
+
return (async (commands, options = {}) => {
|
|
250
|
+
const entries = Object.entries(commands);
|
|
251
|
+
const chunks = chunk(entries, 50);
|
|
252
|
+
const responses = [];
|
|
253
|
+
if (options.halt) for (const group of chunks) {
|
|
254
|
+
const response = await run(group, options);
|
|
255
|
+
responses.push(response);
|
|
256
|
+
if (Object.keys(response.errors).length > 0) break;
|
|
257
|
+
}
|
|
258
|
+
else responses.push(...await Promise.all(chunks.map((group) => run(group, options))));
|
|
259
|
+
const results = Object.assign({}, ...responses.map((response) => response.results));
|
|
260
|
+
const errors = Object.assign({}, ...responses.map((response) => response.errors));
|
|
261
|
+
return Array.isArray(commands) ? {
|
|
262
|
+
results: entries.map(([key]) => results[key]),
|
|
263
|
+
errors: entries.map(([key]) => errors[key])
|
|
264
|
+
} : {
|
|
265
|
+
results,
|
|
266
|
+
errors
|
|
267
|
+
};
|
|
268
|
+
});
|
|
269
|
+
};
|
|
270
|
+
// source/limiter.ts
|
|
271
|
+
/**
|
|
272
|
+
* Leaky bucket matching the Bitrix request limit: up to `burst` requests at once, then `refillPerSecond`.
|
|
273
|
+
* Requests over the limit reserve a token in advance and wait until it refills
|
|
274
|
+
* @param options bucket size and refill rate
|
|
275
|
+
* @param options.burst requests allowed at once, 50 by default
|
|
276
|
+
* @param options.refillPerSecond requests added back every second, 2 by default
|
|
277
|
+
*/
|
|
278
|
+
const createLimiter = ({ burst = 50, refillPerSecond = 2 } = {}) => {
|
|
279
|
+
let tokens = burst;
|
|
280
|
+
let updatedAt = Date.now();
|
|
281
|
+
const refill = () => {
|
|
282
|
+
const now = Date.now();
|
|
283
|
+
tokens = Math.min(burst, tokens + (now - updatedAt) / 1e3 * refillPerSecond);
|
|
284
|
+
updatedAt = now;
|
|
285
|
+
};
|
|
286
|
+
const acquire = async (signal) => {
|
|
287
|
+
signal?.throwIfAborted();
|
|
288
|
+
refill();
|
|
289
|
+
tokens -= 1;
|
|
290
|
+
if (tokens >= 0) return;
|
|
291
|
+
try {
|
|
292
|
+
await sleep(Math.ceil(-tokens / refillPerSecond * 1e3), signal);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
tokens += 1;
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
const exhaust = () => {
|
|
299
|
+
refill();
|
|
300
|
+
tokens = Math.min(tokens, 0);
|
|
301
|
+
};
|
|
302
|
+
return {
|
|
303
|
+
acquire,
|
|
304
|
+
exhaust
|
|
305
|
+
};
|
|
306
|
+
};
|
|
307
|
+
/**
|
|
308
|
+
* Finds the rows in a list result: either the result itself or the single array a catalog style result wraps
|
|
309
|
+
* @param method method name used in the error message
|
|
310
|
+
* @param result result of a list method
|
|
311
|
+
*/
|
|
312
|
+
const unwrap = (method, result) => {
|
|
313
|
+
if (Array.isArray(result)) return {
|
|
314
|
+
rows: result,
|
|
315
|
+
key: void 0
|
|
316
|
+
};
|
|
317
|
+
const entries = typeof result === "object" && result !== null ? Object.entries(result) : [];
|
|
318
|
+
const [entry] = entries;
|
|
319
|
+
if (entries.length === 1 && entry && Array.isArray(entry[1])) return {
|
|
320
|
+
rows: entry[1],
|
|
321
|
+
key: entry[0]
|
|
322
|
+
};
|
|
323
|
+
throw new TypeError(`${method} did not return a list`);
|
|
324
|
+
};
|
|
325
|
+
/**
|
|
326
|
+
* Batches start with one page and double up to 50, so a consumer that stops early wastes few requests
|
|
327
|
+
* @param size pages in the previous batch
|
|
328
|
+
*/
|
|
329
|
+
const grow = (size) => Math.min(size * 2, 50);
|
|
330
|
+
/**
|
|
331
|
+
* Adds the fields pagination relies on to an explicit select. Wildcards and no select already include them
|
|
332
|
+
* @param select select of the caller
|
|
333
|
+
* @param fields fields to add
|
|
334
|
+
*/
|
|
335
|
+
const withFields = (select, fields) => select && !select.includes("*") ? [.../* @__PURE__ */ new Set([...select, ...fields])] : select;
|
|
336
|
+
const createPaginate = (transport, batch) => {
|
|
337
|
+
const runBatch = async function* (method, pages, signal, resumable) {
|
|
338
|
+
const { results, errors } = await batch(pages.map((params) => ({
|
|
339
|
+
method,
|
|
340
|
+
params
|
|
341
|
+
})), signal && { signal });
|
|
342
|
+
for (const [index, error] of errors.entries()) {
|
|
343
|
+
if (error && resumable && index > 0 && error.code === "OPERATION_TIME_LIMIT") return;
|
|
344
|
+
if (error) throw error;
|
|
345
|
+
yield { rows: unwrap(method, results[index]).rows };
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
const cursor = async function* (method, params, idField, count, signal) {
|
|
349
|
+
if (Object.keys(params.order ?? {}).some((field) => field !== idField)) throw new TypeError(`cursor pagination orders by ${idField}, use { mode: 'offset' } for a custom order`);
|
|
350
|
+
const { start: _start, ...rest } = params;
|
|
351
|
+
const select = withFields(rest.select, [idField]);
|
|
352
|
+
const page = (filter) => ({
|
|
353
|
+
...rest,
|
|
354
|
+
...select && { select },
|
|
355
|
+
filter: {
|
|
356
|
+
...rest.filter,
|
|
357
|
+
...filter
|
|
358
|
+
},
|
|
359
|
+
order: { [idField]: "ASC" },
|
|
360
|
+
start: -1
|
|
361
|
+
});
|
|
362
|
+
const { start: _counted, ...counted } = page({});
|
|
363
|
+
const first = await transport(method, count ? counted : page({}), signal);
|
|
364
|
+
let { rows, key } = unwrap(method, first.result);
|
|
365
|
+
yield {
|
|
366
|
+
rows,
|
|
367
|
+
total: count ? first.total : void 0
|
|
368
|
+
};
|
|
369
|
+
for (let size = 1; rows.length === 50; size = grow(size)) {
|
|
370
|
+
const lastId = rows.at(-1)[idField];
|
|
371
|
+
const pages = Array.from({ length: size }, (_, index) => page({ [`>${idField}`]: index === 0 ? lastId : `$result[${index - 1}]${key ? `[${key}]` : ""}[49][${idField}]` }));
|
|
372
|
+
for await ({rows} of runBatch(method, pages, signal, true)) {
|
|
373
|
+
yield { rows };
|
|
374
|
+
if (rows.length < 50) return;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
const modified = async function* (method, params, idField, modifiedField, since, count, signal) {
|
|
379
|
+
if (Object.keys(params.order ?? {}).some((field) => field !== modifiedField && field !== idField)) throw new TypeError(`modified pagination orders by ${modifiedField} and ${idField}`);
|
|
380
|
+
const { start: _start, order: _order, ...rest } = params;
|
|
381
|
+
const select = withFields(rest.select, [idField, modifiedField]);
|
|
382
|
+
let after = { [`>=${modifiedField}`]: since instanceof Date ? since.toISOString() : since };
|
|
383
|
+
for (let counting = count;; counting = false) {
|
|
384
|
+
const page = await transport(method, {
|
|
385
|
+
...rest,
|
|
386
|
+
...select && { select },
|
|
387
|
+
filter: {
|
|
388
|
+
...rest.filter,
|
|
389
|
+
...after
|
|
390
|
+
},
|
|
391
|
+
order: {
|
|
392
|
+
[modifiedField]: "ASC",
|
|
393
|
+
[idField]: "ASC"
|
|
394
|
+
},
|
|
395
|
+
...!counting && { start: -1 }
|
|
396
|
+
}, signal);
|
|
397
|
+
const { rows } = unwrap(method, page.result);
|
|
398
|
+
yield {
|
|
399
|
+
rows,
|
|
400
|
+
total: counting ? page.total : void 0
|
|
401
|
+
};
|
|
402
|
+
if (rows.length < 50) return;
|
|
403
|
+
const last = rows.at(-1);
|
|
404
|
+
yield* cursor(method, {
|
|
405
|
+
...rest,
|
|
406
|
+
...select && { select },
|
|
407
|
+
filter: {
|
|
408
|
+
...rest.filter,
|
|
409
|
+
[`=${modifiedField}`]: last[modifiedField],
|
|
410
|
+
[`>${idField}`]: last[idField]
|
|
411
|
+
}
|
|
412
|
+
}, idField, false, signal);
|
|
413
|
+
after = { [`>${modifiedField}`]: last[modifiedField] };
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
const offset = async function* (method, params, signal) {
|
|
417
|
+
const first = await transport(method, {
|
|
418
|
+
...params,
|
|
419
|
+
start: params.start ?? 0
|
|
420
|
+
}, signal);
|
|
421
|
+
yield {
|
|
422
|
+
rows: unwrap(method, first.result).rows,
|
|
423
|
+
total: first.total
|
|
424
|
+
};
|
|
425
|
+
if (first.next === void 0) return;
|
|
426
|
+
const starts = [];
|
|
427
|
+
for (let start = first.next; start < (first.total ?? 0); start += 50) starts.push(start);
|
|
428
|
+
for (let index = 0, size = 1; index < starts.length; index += size, size = grow(size)) {
|
|
429
|
+
const pages = starts.slice(index, index + size).map((start) => ({
|
|
430
|
+
...params,
|
|
431
|
+
start
|
|
432
|
+
}));
|
|
433
|
+
yield* runBatch(method, pages, signal, false);
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
const pages = (method, params, options) => {
|
|
437
|
+
const { idField = "ID", signal, onProgress } = options;
|
|
438
|
+
const count = onProgress !== void 0;
|
|
439
|
+
if (options.mode === "modified") return modified(method, params, idField, options.modifiedField ?? "DATE_MODIFY", options.since, count, signal);
|
|
440
|
+
return options.mode === "offset" ? offset(method, params, signal) : cursor(method, params, idField, count, signal);
|
|
441
|
+
};
|
|
442
|
+
return async function* (method, params = {}, options = {}) {
|
|
443
|
+
let read = 0;
|
|
444
|
+
let total;
|
|
445
|
+
for await (const page of pages(method, params, options)) {
|
|
446
|
+
read += page.rows.length;
|
|
447
|
+
total ??= page.total;
|
|
448
|
+
options.onProgress?.({
|
|
449
|
+
read,
|
|
450
|
+
...total !== void 0 && { total }
|
|
451
|
+
});
|
|
452
|
+
yield* page.rows;
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
};
|
|
456
|
+
// source/services/crm.ts
|
|
457
|
+
/**
|
|
458
|
+
* CRM entities share one set of `crm.<entity>.*` methods, so their services only differ by name
|
|
459
|
+
* @param core client methods the service is built on
|
|
460
|
+
* @param core.call calls a single REST method
|
|
461
|
+
* @param core.batch runs many commands in as few requests as possible
|
|
462
|
+
* @param core.list reads every page of a list method
|
|
463
|
+
* @param core.paginate iterates over every page of a list method
|
|
464
|
+
* @param name entity name in the method names, like `deal` in `crm.deal.get`
|
|
465
|
+
*/
|
|
466
|
+
const createCrmService = ({ call, batch, list, paginate }, name) => {
|
|
467
|
+
const send = call;
|
|
468
|
+
const sendMany = batch;
|
|
469
|
+
const method = `crm.${name}`;
|
|
470
|
+
const modifiedField = name === "activity" ? "LAST_UPDATED" : "DATE_MODIFY";
|
|
471
|
+
return {
|
|
472
|
+
fields: () => send(`${method}.fields`),
|
|
473
|
+
get: (id) => send(`${method}.get`, { id }),
|
|
474
|
+
list: (params, options) => list(`${method}.list`, params, options),
|
|
475
|
+
paginate: (params, options) => paginate(`${method}.list`, params, options),
|
|
476
|
+
changedSince: (since, params, options) => paginate(`${method}.list`, params, {
|
|
477
|
+
...options,
|
|
478
|
+
mode: "modified",
|
|
479
|
+
since,
|
|
480
|
+
modifiedField
|
|
481
|
+
}),
|
|
482
|
+
create: (fields, params) => send(`${method}.add`, {
|
|
483
|
+
fields,
|
|
484
|
+
...params && { params }
|
|
485
|
+
}),
|
|
486
|
+
update: (id, fields, params) => send(`${method}.update`, {
|
|
487
|
+
id,
|
|
488
|
+
fields,
|
|
489
|
+
...params && { params }
|
|
490
|
+
}),
|
|
491
|
+
delete: (id) => send(`${method}.delete`, { id }),
|
|
492
|
+
createMany: (items, params) => sendMany(items.map((fields) => ({
|
|
493
|
+
method: `${method}.add`,
|
|
494
|
+
params: {
|
|
495
|
+
fields,
|
|
496
|
+
...params && { params }
|
|
497
|
+
}
|
|
498
|
+
}))),
|
|
499
|
+
updateMany: (items, params) => sendMany(items.map(({ id, fields }) => ({
|
|
500
|
+
method: `${method}.update`,
|
|
501
|
+
params: {
|
|
502
|
+
id,
|
|
503
|
+
fields,
|
|
504
|
+
...params && { params }
|
|
505
|
+
}
|
|
506
|
+
}))),
|
|
507
|
+
deleteMany: (ids) => sendMany(ids.map((id) => ({
|
|
508
|
+
method: `${method}.delete`,
|
|
509
|
+
params: { id }
|
|
510
|
+
})))
|
|
511
|
+
};
|
|
512
|
+
};
|
|
513
|
+
/**
|
|
514
|
+
* Product rows of a deal or lead
|
|
515
|
+
* @param core client methods the service is built on
|
|
516
|
+
* @param core.call calls a single REST method
|
|
517
|
+
* @param name entity name in the method names
|
|
518
|
+
*/
|
|
519
|
+
const createProductRows = ({ call }, name) => ({
|
|
520
|
+
get: (id) => call(`crm.${name}.productrows.get`, { id }),
|
|
521
|
+
set: (id, rows) => call(`crm.${name}.productrows.set`, {
|
|
522
|
+
id,
|
|
523
|
+
rows
|
|
524
|
+
})
|
|
525
|
+
});
|
|
526
|
+
/**
|
|
527
|
+
* Links between CRM entities through `crm.<owner>.<linked>.*`, like the contacts of a deal
|
|
528
|
+
* @param core client methods the service is built on
|
|
529
|
+
* @param core.call calls a single REST method
|
|
530
|
+
* @param owner entity the links belong to
|
|
531
|
+
* @param linked entity that is linked
|
|
532
|
+
*/
|
|
533
|
+
const createLinks = ({ call }, owner, linked) => {
|
|
534
|
+
const send = call;
|
|
535
|
+
const method = `crm.${owner}.${linked}`;
|
|
536
|
+
const key = `${linked.toUpperCase()}_ID`;
|
|
537
|
+
return {
|
|
538
|
+
get: (id) => send(`${method}.items.get`, { id }),
|
|
539
|
+
set: (id, links) => send(`${method}.items.set`, {
|
|
540
|
+
id,
|
|
541
|
+
items: links
|
|
542
|
+
}),
|
|
543
|
+
add: (id, link) => send(`${method}.add`, {
|
|
544
|
+
id,
|
|
545
|
+
fields: link
|
|
546
|
+
}),
|
|
547
|
+
delete: (id, linkedId) => send(`${method}.delete`, {
|
|
548
|
+
id,
|
|
549
|
+
fields: { [key]: linkedId }
|
|
550
|
+
}),
|
|
551
|
+
clear: (id) => send(`${method}.items.delete`, { id })
|
|
552
|
+
};
|
|
553
|
+
};
|
|
554
|
+
// source/services/currencies.ts
|
|
555
|
+
/**
|
|
556
|
+
* Currencies are identified by their code like `USD`, and `crm.currency.list` returns every currency at once
|
|
557
|
+
* @param core client methods the service is built on
|
|
558
|
+
* @param core.call calls a single REST method
|
|
559
|
+
*/
|
|
560
|
+
const createCurrenciesService = ({ call }) => ({
|
|
561
|
+
fields: () => call("crm.currency.fields"),
|
|
562
|
+
get: (id) => call("crm.currency.get", { id }),
|
|
563
|
+
list: (params = {}) => call("crm.currency.list", params),
|
|
564
|
+
create: (fields) => call("crm.currency.add", { fields }),
|
|
565
|
+
update: (id, fields) => call("crm.currency.update", {
|
|
566
|
+
id,
|
|
567
|
+
fields
|
|
568
|
+
}),
|
|
569
|
+
delete: (id) => call("crm.currency.delete", { id }),
|
|
570
|
+
base: () => call("crm.currency.base.get"),
|
|
571
|
+
setBase: (id) => call("crm.currency.base.set", { id })
|
|
572
|
+
});
|
|
573
|
+
// source/services/items.ts
|
|
574
|
+
/**
|
|
575
|
+
* `crm.item.*` works for every CRM type by its `entityTypeId`, smart processes included, and wraps its results
|
|
576
|
+
* in `item`, `items` or `fields`
|
|
577
|
+
* @param core client methods the service is built on
|
|
578
|
+
* @param core.call calls a single REST method
|
|
579
|
+
* @param core.list reads every page of a list method
|
|
580
|
+
* @param core.paginate iterates over every page of a list method
|
|
581
|
+
* @param entityTypeId type id, like 2 for deals or the id of a smart process from `crm.type.list`
|
|
582
|
+
*/
|
|
583
|
+
const createItemsService = ({ call, list, paginate }, entityTypeId) => ({
|
|
584
|
+
fields: async () => (await call("crm.item.fields", { entityTypeId })).fields,
|
|
585
|
+
get: async (id) => (await call("crm.item.get", {
|
|
586
|
+
entityTypeId,
|
|
587
|
+
id
|
|
588
|
+
})).item,
|
|
589
|
+
list: (params = {}, options = {}) => list("crm.item.list", {
|
|
590
|
+
...params,
|
|
591
|
+
entityTypeId
|
|
592
|
+
}, {
|
|
593
|
+
...options,
|
|
594
|
+
idField: "id"
|
|
595
|
+
}),
|
|
596
|
+
paginate: (params = {}, options = {}) => paginate("crm.item.list", {
|
|
597
|
+
...params,
|
|
598
|
+
entityTypeId
|
|
599
|
+
}, {
|
|
600
|
+
...options,
|
|
601
|
+
idField: "id"
|
|
602
|
+
}),
|
|
603
|
+
changedSince: (since, params = {}, options = {}) => paginate("crm.item.list", {
|
|
604
|
+
...params,
|
|
605
|
+
entityTypeId
|
|
606
|
+
}, {
|
|
607
|
+
...options,
|
|
608
|
+
mode: "modified",
|
|
609
|
+
since,
|
|
610
|
+
idField: "id",
|
|
611
|
+
modifiedField: "updatedTime"
|
|
612
|
+
}),
|
|
613
|
+
create: async (fields) => (await call("crm.item.add", {
|
|
614
|
+
entityTypeId,
|
|
615
|
+
fields
|
|
616
|
+
})).item,
|
|
617
|
+
update: async (id, fields) => (await call("crm.item.update", {
|
|
618
|
+
entityTypeId,
|
|
619
|
+
id,
|
|
620
|
+
fields
|
|
621
|
+
})).item,
|
|
622
|
+
delete: async (id) => {
|
|
623
|
+
await call("crm.item.delete", {
|
|
624
|
+
entityTypeId,
|
|
625
|
+
id
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
// source/services/products.ts
|
|
630
|
+
/**
|
|
631
|
+
* `catalog.product.list` rejects requests that do not select `id` and `iblockId`
|
|
632
|
+
* @param params list params from the caller
|
|
633
|
+
*/
|
|
634
|
+
const withRequiredSelect = (params) => ({
|
|
635
|
+
...params,
|
|
636
|
+
select: [.../* @__PURE__ */ new Set([
|
|
637
|
+
...params.select ?? ["name"],
|
|
638
|
+
"id",
|
|
639
|
+
"iblockId"
|
|
640
|
+
])]
|
|
641
|
+
});
|
|
642
|
+
const createProductsService = ({ call, list, paginate }) => ({
|
|
643
|
+
fields: async (iblockId) => (await call("catalog.product.getFieldsByFilter", { filter: { iblockId } })).product,
|
|
644
|
+
get: async (id) => (await call("catalog.product.get", { id })).product,
|
|
645
|
+
list: (params, options = {}) => list("catalog.product.list", withRequiredSelect(params), {
|
|
646
|
+
...options,
|
|
647
|
+
idField: "id"
|
|
648
|
+
}),
|
|
649
|
+
paginate: (params, options = {}) => paginate("catalog.product.list", withRequiredSelect(params), {
|
|
650
|
+
...options,
|
|
651
|
+
idField: "id"
|
|
652
|
+
}),
|
|
653
|
+
create: async (fields) => (await call("catalog.product.add", { fields })).element,
|
|
654
|
+
update: async (id, fields) => (await call("catalog.product.update", {
|
|
655
|
+
id,
|
|
656
|
+
fields
|
|
657
|
+
})).element,
|
|
658
|
+
delete: (id) => call("catalog.product.delete", { id })
|
|
659
|
+
});
|
|
660
|
+
// source/services/statuses.ts
|
|
661
|
+
/**
|
|
662
|
+
* `crm.status.list` returns every status at once, so there is no pagination here
|
|
663
|
+
* @param core client methods the service is built on
|
|
664
|
+
* @param core.call calls a single REST method
|
|
665
|
+
*/
|
|
666
|
+
const createStatusesService = ({ call }) => ({
|
|
667
|
+
fields: () => call("crm.status.fields"),
|
|
668
|
+
get: (id) => call("crm.status.get", { id }),
|
|
669
|
+
list: (params = {}) => call("crm.status.list", params),
|
|
670
|
+
create: (fields) => call("crm.status.add", { fields }),
|
|
671
|
+
update: (id, fields) => call("crm.status.update", {
|
|
672
|
+
id,
|
|
673
|
+
fields
|
|
674
|
+
}),
|
|
675
|
+
delete: (id, { forced = false } = {}) => call("crm.status.delete", {
|
|
676
|
+
id,
|
|
677
|
+
params: { FORCED: forced ? "Y" : "N" }
|
|
678
|
+
})
|
|
679
|
+
});
|
|
680
|
+
// source/services/users.ts
|
|
681
|
+
/**
|
|
682
|
+
* `user.get` takes `sort` and `order` as plain strings instead of an order map, so users paginate by offset
|
|
683
|
+
* @param core client methods the service is built on
|
|
684
|
+
* @param core.call calls a single REST method
|
|
685
|
+
* @param core.list reads every page of a list method
|
|
686
|
+
* @param core.paginate iterates over every page of a list method
|
|
687
|
+
*/
|
|
688
|
+
const createUsersService = ({ call, list, paginate }) => ({
|
|
689
|
+
fields: () => call("user.fields"),
|
|
690
|
+
current: () => call("user.current"),
|
|
691
|
+
get: async (id) => {
|
|
692
|
+
const [user] = await call("user.get", { ID: id });
|
|
693
|
+
if (!user) throw new BitrixError({
|
|
694
|
+
code: "NOT_FOUND",
|
|
695
|
+
description: `user ${id} not found`,
|
|
696
|
+
method: "user.get"
|
|
697
|
+
});
|
|
698
|
+
return user;
|
|
699
|
+
},
|
|
700
|
+
list: (params = {}, options = {}) => list("user.get", params, {
|
|
701
|
+
...options,
|
|
702
|
+
mode: "offset"
|
|
703
|
+
}),
|
|
704
|
+
paginate: (params = {}, options = {}) => paginate("user.get", params, {
|
|
705
|
+
...options,
|
|
706
|
+
mode: "offset"
|
|
707
|
+
})
|
|
708
|
+
});
|
|
709
|
+
// source/client.ts
|
|
710
|
+
/**
|
|
711
|
+
* Creates a Bitrix24 REST client
|
|
712
|
+
* @param options endpoint, auth, limiter and retry settings
|
|
713
|
+
* @example
|
|
714
|
+
* const bitrix = createClient({ url: 'https://portal.bitrix24.com/rest/1/WEBHOOK_TOKEN' })
|
|
715
|
+
* const deal = await bitrix.deals.get(77)
|
|
716
|
+
*/
|
|
717
|
+
const createClient = (options) => {
|
|
718
|
+
const retry = {
|
|
719
|
+
retries: 3,
|
|
720
|
+
delay: 1e3,
|
|
721
|
+
...options.retry
|
|
722
|
+
};
|
|
723
|
+
const transport = createTransport(options, createLimiter(options.limiter), retry);
|
|
724
|
+
const call = (async (method, params, { raw = false, signal } = {}) => {
|
|
725
|
+
const envelope = await transport(method, params, signal);
|
|
726
|
+
return raw ? envelope : envelope.result;
|
|
727
|
+
});
|
|
728
|
+
const batch = createBatch(transport, retry);
|
|
729
|
+
const paginate = createPaginate(transport, batch);
|
|
730
|
+
const list = (async (method, params, listOptions) => {
|
|
731
|
+
const rows = [];
|
|
732
|
+
for await (const row of paginate(method, params, listOptions)) rows.push(row);
|
|
733
|
+
return rows;
|
|
734
|
+
});
|
|
735
|
+
const core = {
|
|
736
|
+
call,
|
|
737
|
+
batch,
|
|
738
|
+
paginate,
|
|
739
|
+
list
|
|
740
|
+
};
|
|
741
|
+
return {
|
|
742
|
+
...core,
|
|
743
|
+
activities: createCrmService(core, "activity"),
|
|
744
|
+
companies: {
|
|
745
|
+
...createCrmService(core, "company"),
|
|
746
|
+
contacts: createLinks(core, "company", "contact")
|
|
747
|
+
},
|
|
748
|
+
contacts: {
|
|
749
|
+
...createCrmService(core, "contact"),
|
|
750
|
+
companies: createLinks(core, "contact", "company")
|
|
751
|
+
},
|
|
752
|
+
currencies: createCurrenciesService(core),
|
|
753
|
+
deals: {
|
|
754
|
+
...createCrmService(core, "deal"),
|
|
755
|
+
productRows: createProductRows(core, "deal"),
|
|
756
|
+
contacts: createLinks(core, "deal", "contact")
|
|
757
|
+
},
|
|
758
|
+
items: (entityTypeId) => createItemsService(core, entityTypeId),
|
|
759
|
+
leads: {
|
|
760
|
+
...createCrmService(core, "lead"),
|
|
761
|
+
productRows: createProductRows(core, "lead"),
|
|
762
|
+
contacts: createLinks(core, "lead", "contact")
|
|
763
|
+
},
|
|
764
|
+
products: createProductsService(core),
|
|
765
|
+
statuses: createStatusesService(core),
|
|
766
|
+
users: createUsersService(core)
|
|
767
|
+
};
|
|
768
|
+
};
|
|
769
|
+
export { BitrixError as n, createClient as t };
|