@yuuko1410/feishu-bitable 0.0.2 → 0.0.4
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 +464 -0
- package/lib/client.d.ts +9 -1
- package/lib/index.cjs +359 -133
- package/lib/index.d.ts +1 -1
- package/lib/index.js +359 -133
- package/lib/types.d.ts +6 -0
- package/package.json +1 -1
package/lib/index.cjs
CHANGED
|
@@ -170,7 +170,8 @@ function normalizeRecordFields(fields) {
|
|
|
170
170
|
}
|
|
171
171
|
function splitUpdateRecord(record) {
|
|
172
172
|
const { record_id: recordId, ...fields } = record;
|
|
173
|
-
|
|
173
|
+
const normalizedFields = Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
|
|
174
|
+
return { recordId, fields: normalizedFields };
|
|
174
175
|
}
|
|
175
176
|
async function toBuffer(file) {
|
|
176
177
|
if (typeof file === "string") {
|
|
@@ -223,18 +224,44 @@ function isFileLike(value) {
|
|
|
223
224
|
}
|
|
224
225
|
|
|
225
226
|
// src/client.ts
|
|
227
|
+
var DEFAULT_LOGGER = {
|
|
228
|
+
info(message, meta) {
|
|
229
|
+
if (meta) {
|
|
230
|
+
console.info(`[feishu-bitable] ${message}`, meta);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
console.info(`[feishu-bitable] ${message}`);
|
|
234
|
+
},
|
|
235
|
+
warn(message, meta) {
|
|
236
|
+
if (meta) {
|
|
237
|
+
console.warn(`[feishu-bitable] ${message}`, meta);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
console.warn(`[feishu-bitable] ${message}`);
|
|
241
|
+
},
|
|
242
|
+
error(message, meta) {
|
|
243
|
+
if (meta) {
|
|
244
|
+
console.error(`[feishu-bitable] ${message}`, meta);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
console.error(`[feishu-bitable] ${message}`);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
226
251
|
class Bitable {
|
|
227
252
|
client;
|
|
228
253
|
defaultAppToken;
|
|
229
254
|
maxRetries;
|
|
230
255
|
retryDelayMs;
|
|
231
256
|
defaultConcurrency;
|
|
257
|
+
logger;
|
|
232
258
|
constructor(optionsOrToken, appId, appSecret) {
|
|
233
259
|
const options = this.resolveConstructorOptions(optionsOrToken, appId, appSecret);
|
|
234
260
|
this.defaultAppToken = options.defaultAppToken;
|
|
235
261
|
this.maxRetries = Math.max(1, options.maxRetries ?? 5);
|
|
236
262
|
this.retryDelayMs = Math.max(100, options.retryDelayMs ?? 1000);
|
|
237
263
|
this.defaultConcurrency = Math.max(1, options.defaultConcurrency ?? 1);
|
|
264
|
+
this.logger = options.logger === null ? null : options.logger ?? DEFAULT_LOGGER;
|
|
238
265
|
this.client = options.sdkClient ?? new lark.Client({
|
|
239
266
|
appId: options.appId,
|
|
240
267
|
appSecret: options.appSecret,
|
|
@@ -250,159 +277,295 @@ class Bitable {
|
|
|
250
277
|
});
|
|
251
278
|
}
|
|
252
279
|
async fetchAllRecords(tableId, options = {}, appToken) {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
280
|
+
return this.runLogged("fetchAllRecords", {
|
|
281
|
+
tableId,
|
|
282
|
+
pageSize: options.pageSize ?? FEISHU_BATCH_LIMIT,
|
|
283
|
+
hasViewId: Boolean(options.viewId),
|
|
284
|
+
fieldCount: options.fieldNames?.length ?? 0
|
|
285
|
+
}, async () => {
|
|
286
|
+
const token = this.resolveAppToken(appToken);
|
|
287
|
+
const pageSize = Math.max(1, Math.min(options.pageSize ?? FEISHU_BATCH_LIMIT, FEISHU_BATCH_LIMIT));
|
|
288
|
+
const request = {
|
|
289
|
+
path: {
|
|
290
|
+
app_token: token,
|
|
291
|
+
table_id: tableId
|
|
292
|
+
},
|
|
293
|
+
params: {
|
|
294
|
+
page_size: pageSize,
|
|
295
|
+
user_id_type: "open_id"
|
|
296
|
+
},
|
|
297
|
+
data: {
|
|
298
|
+
view_id: options.viewId,
|
|
299
|
+
field_names: options.fieldNames,
|
|
300
|
+
filter: options.filter,
|
|
301
|
+
sort: options.sort,
|
|
302
|
+
automatic_fields: options.automaticFields
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
const iterator = await this.client.bitable.v1.appTableRecord.searchWithIterator(this.logRequest("fetchAllRecords request", request));
|
|
306
|
+
const allRecords = [];
|
|
307
|
+
for await (const page of iterator) {
|
|
308
|
+
const items = page?.items ?? [];
|
|
309
|
+
allRecords.push(...items.map((item) => normalizeRecord(item.record_id, item.fields, options.normalizeFields !== false)));
|
|
270
310
|
}
|
|
311
|
+
this.logInfo("fetchAllRecords completed", {
|
|
312
|
+
tableId,
|
|
313
|
+
recordCount: allRecords.length
|
|
314
|
+
});
|
|
315
|
+
return allRecords;
|
|
271
316
|
});
|
|
272
|
-
const allRecords = [];
|
|
273
|
-
for await (const page of iterator) {
|
|
274
|
-
const items = page?.items ?? [];
|
|
275
|
-
allRecords.push(...items.map((item) => normalizeRecord(item.record_id, item.fields, options.normalizeFields !== false)));
|
|
276
|
-
}
|
|
277
|
-
return allRecords;
|
|
278
317
|
}
|
|
279
318
|
async insertList(tableId, records, options = {}) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
records: chunk.map((fields) => ({ fields }))
|
|
319
|
+
return this.runLogged("insertList", {
|
|
320
|
+
tableId,
|
|
321
|
+
recordCount: records.length,
|
|
322
|
+
chunkSize: options.chunkSize ?? FEISHU_BATCH_LIMIT,
|
|
323
|
+
concurrency: options.concurrency ?? this.defaultConcurrency
|
|
324
|
+
}, async () => {
|
|
325
|
+
if (records.length === 0) {
|
|
326
|
+
return [];
|
|
289
327
|
}
|
|
290
|
-
|
|
328
|
+
const token = this.resolveAppToken(options.appToken);
|
|
329
|
+
const chunks = chunkArray(records, options.chunkSize ?? FEISHU_BATCH_LIMIT);
|
|
330
|
+
const responses = await runWithConcurrency(chunks, options.concurrency ?? this.defaultConcurrency, async (chunk) => this.withRetry("insert records", async () => {
|
|
331
|
+
const request = {
|
|
332
|
+
path: { app_token: token, table_id: tableId },
|
|
333
|
+
data: {
|
|
334
|
+
records: chunk.map((fields) => ({ fields }))
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
return assertFeishuResponse(await this.client.bitable.v1.appTableRecord.batchCreate(this.logRequest("insertList request", request)), "insert records");
|
|
338
|
+
}));
|
|
339
|
+
this.logInfo("insertList completed", {
|
|
340
|
+
tableId,
|
|
341
|
+
chunkCount: chunks.length
|
|
342
|
+
});
|
|
343
|
+
return responses;
|
|
344
|
+
});
|
|
291
345
|
}
|
|
292
346
|
async batchUpdateRecords(payload) {
|
|
293
|
-
return this.
|
|
347
|
+
return this.runLogged("batchUpdateRecords", {
|
|
348
|
+
tableId: payload.path.table_id,
|
|
349
|
+
recordCount: payload.data.records.length
|
|
350
|
+
}, async () => this.executeBatchUpdateRecords(payload));
|
|
294
351
|
}
|
|
295
352
|
async updateRecords(tableId, records, options = {}) {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
353
|
+
return this.runLogged("updateRecords", {
|
|
354
|
+
tableId,
|
|
355
|
+
inputRecordCount: records.length,
|
|
356
|
+
chunkSize: options.chunkSize ?? FEISHU_BATCH_LIMIT,
|
|
357
|
+
concurrency: options.concurrency ?? this.defaultConcurrency
|
|
358
|
+
}, async () => {
|
|
359
|
+
if (records.length === 0) {
|
|
360
|
+
return [];
|
|
361
|
+
}
|
|
362
|
+
const token = this.resolveAppToken(options.appToken);
|
|
363
|
+
const chunks = chunkArray(records, options.chunkSize ?? FEISHU_BATCH_LIMIT);
|
|
364
|
+
const responses = await runWithConcurrency(chunks, options.concurrency ?? this.defaultConcurrency, async (chunk, index) => {
|
|
365
|
+
const batchRecords = chunk.map((record, recordIndex) => {
|
|
366
|
+
const { recordId, fields } = splitUpdateRecord(record);
|
|
367
|
+
if (!recordId || !recordId.trim()) {
|
|
368
|
+
throw new FeishuBitableError(`updateRecords failed: record_id is required for chunk ${index}, item ${recordIndex}`, {
|
|
369
|
+
details: {
|
|
370
|
+
tableId,
|
|
371
|
+
chunkIndex: index,
|
|
372
|
+
recordIndex,
|
|
373
|
+
record
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
if (Object.keys(fields).length === 0) {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
record_id: recordId,
|
|
382
|
+
fields
|
|
383
|
+
};
|
|
384
|
+
}).filter((record) => Boolean(record));
|
|
385
|
+
if (batchRecords.length === 0) {
|
|
386
|
+
this.logInfo("updateRecords skipped empty chunk", {
|
|
387
|
+
tableId,
|
|
388
|
+
chunkIndex: index,
|
|
389
|
+
inputChunkSize: chunk.length
|
|
390
|
+
});
|
|
391
|
+
return {
|
|
392
|
+
code: 0,
|
|
393
|
+
msg: "ok",
|
|
394
|
+
data: {
|
|
395
|
+
records: []
|
|
396
|
+
}
|
|
397
|
+
};
|
|
315
398
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
399
|
+
const payload = {
|
|
400
|
+
path: {
|
|
401
|
+
app_token: token,
|
|
402
|
+
table_id: tableId
|
|
403
|
+
},
|
|
404
|
+
data: {
|
|
405
|
+
records: batchRecords
|
|
406
|
+
}
|
|
321
407
|
};
|
|
322
|
-
|
|
323
|
-
|
|
408
|
+
if (options.userIdType || options.ignoreConsistencyCheck !== undefined) {
|
|
409
|
+
payload.params = {
|
|
410
|
+
user_id_type: options.userIdType,
|
|
411
|
+
ignore_consistency_check: options.ignoreConsistencyCheck
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
return this.executeBatchUpdateRecords(payload);
|
|
415
|
+
});
|
|
416
|
+
this.logInfo("updateRecords completed", {
|
|
417
|
+
tableId,
|
|
418
|
+
chunkCount: chunks.length
|
|
419
|
+
});
|
|
420
|
+
return responses;
|
|
324
421
|
});
|
|
325
422
|
}
|
|
326
423
|
async deleteList(tableId, recordIds, options = {}) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
records: chunk
|
|
424
|
+
return this.runLogged("deleteList", {
|
|
425
|
+
tableId,
|
|
426
|
+
recordCount: recordIds.length,
|
|
427
|
+
chunkSize: options.chunkSize ?? FEISHU_BATCH_LIMIT,
|
|
428
|
+
concurrency: options.concurrency ?? this.defaultConcurrency
|
|
429
|
+
}, async () => {
|
|
430
|
+
if (recordIds.length === 0) {
|
|
431
|
+
return [];
|
|
336
432
|
}
|
|
337
|
-
|
|
433
|
+
const token = this.resolveAppToken(options.appToken);
|
|
434
|
+
const chunks = chunkArray(recordIds, options.chunkSize ?? FEISHU_BATCH_LIMIT);
|
|
435
|
+
const responses = await runWithConcurrency(chunks, options.concurrency ?? this.defaultConcurrency, async (chunk) => this.withRetry("delete records", async () => {
|
|
436
|
+
const request = {
|
|
437
|
+
path: { app_token: token, table_id: tableId },
|
|
438
|
+
data: {
|
|
439
|
+
records: chunk
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
return assertFeishuResponse(await this.client.bitable.v1.appTableRecord.batchDelete(this.logRequest("deleteList request", request)), "delete records");
|
|
443
|
+
}));
|
|
444
|
+
this.logInfo("deleteList completed", {
|
|
445
|
+
tableId,
|
|
446
|
+
chunkCount: chunks.length
|
|
447
|
+
});
|
|
448
|
+
return responses;
|
|
449
|
+
});
|
|
338
450
|
}
|
|
339
451
|
async uploadFile(options) {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
352
|
-
})) ?? {};
|
|
353
|
-
}
|
|
354
|
-
const prepare = assertFeishuResponse(await this.withRetry("prepare multipart upload", async () => this.client.drive.v1.media.uploadPrepare({
|
|
355
|
-
data: {
|
|
356
|
-
file_name: fileName,
|
|
357
|
-
parent_type: options.parentType,
|
|
358
|
-
parent_node: options.parentNode,
|
|
359
|
-
size: buffer.byteLength
|
|
360
|
-
}
|
|
361
|
-
})), "prepare multipart upload");
|
|
362
|
-
const uploadId = prepare.data?.upload_id;
|
|
363
|
-
const blockSize = prepare.data?.block_size;
|
|
364
|
-
const blockNum = prepare.data?.block_num;
|
|
365
|
-
if (!uploadId || !blockSize || !blockNum) {
|
|
366
|
-
throw new FeishuBitableError("prepare multipart upload failed: missing upload metadata", {
|
|
367
|
-
details: prepare
|
|
452
|
+
return this.runLogged("uploadFile", {
|
|
453
|
+
parentType: options.parentType,
|
|
454
|
+
parentNode: options.parentNode,
|
|
455
|
+
hasExtra: Boolean(options.extra)
|
|
456
|
+
}, async () => {
|
|
457
|
+
const buffer = await toBuffer(options.file);
|
|
458
|
+
const fileName = options.fileName ?? inferFileName(options.file);
|
|
459
|
+
this.logInfo("uploadFile resolved input", {
|
|
460
|
+
fileName,
|
|
461
|
+
size: buffer.byteLength,
|
|
462
|
+
mode: buffer.byteLength <= FEISHU_SIMPLE_UPLOAD_LIMIT ? "simple" : "multipart"
|
|
368
463
|
});
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
464
|
+
if (buffer.byteLength <= FEISHU_SIMPLE_UPLOAD_LIMIT) {
|
|
465
|
+
return await this.withRetry("upload file", async () => {
|
|
466
|
+
const request = {
|
|
467
|
+
data: {
|
|
468
|
+
file_name: fileName,
|
|
469
|
+
parent_type: options.parentType,
|
|
470
|
+
parent_node: options.parentNode,
|
|
471
|
+
size: buffer.byteLength,
|
|
472
|
+
extra: options.extra,
|
|
473
|
+
file: buffer
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
this.logRequest("uploadFile request", {
|
|
477
|
+
data: {
|
|
478
|
+
...request.data,
|
|
479
|
+
file: `[Buffer ${buffer.byteLength} bytes]`
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
return this.client.drive.v1.media.uploadAll(request);
|
|
483
|
+
}) ?? {};
|
|
387
484
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
485
|
+
const prepare = assertFeishuResponse(await this.withRetry("prepare multipart upload", async () => {
|
|
486
|
+
const request = {
|
|
487
|
+
data: {
|
|
488
|
+
file_name: fileName,
|
|
489
|
+
parent_type: options.parentType,
|
|
490
|
+
parent_node: options.parentNode,
|
|
491
|
+
size: buffer.byteLength
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
return this.client.drive.v1.media.uploadPrepare(this.logRequest("uploadPrepare request", request));
|
|
495
|
+
}), "prepare multipart upload");
|
|
496
|
+
const uploadId = prepare.data?.upload_id;
|
|
497
|
+
const blockSize = prepare.data?.block_size;
|
|
498
|
+
const blockNum = prepare.data?.block_num;
|
|
499
|
+
if (!uploadId || !blockSize || !blockNum) {
|
|
500
|
+
throw new FeishuBitableError("prepare multipart upload failed: missing upload metadata", {
|
|
501
|
+
details: prepare
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
for (let index = 0;index < blockNum; index++) {
|
|
505
|
+
const start = index * blockSize;
|
|
506
|
+
const end = Math.min(start + blockSize, buffer.byteLength);
|
|
507
|
+
const chunk = buffer.subarray(start, end);
|
|
508
|
+
await this.withRetry(`upload file chunk ${index + 1}/${blockNum}`, async () => {
|
|
509
|
+
const request = {
|
|
510
|
+
data: {
|
|
511
|
+
upload_id: uploadId,
|
|
512
|
+
seq: index,
|
|
513
|
+
size: chunk.byteLength,
|
|
514
|
+
file: chunk
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
this.logRequest("uploadPart request", {
|
|
518
|
+
data: {
|
|
519
|
+
...request.data,
|
|
520
|
+
file: `[Buffer ${chunk.byteLength} bytes]`
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
return this.client.drive.v1.media.uploadPart(request);
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
const finish = assertFeishuResponse(await this.withRetry("finish multipart upload", async () => {
|
|
527
|
+
const request = {
|
|
528
|
+
data: {
|
|
529
|
+
upload_id: uploadId,
|
|
530
|
+
block_num: blockNum
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
return this.client.drive.v1.media.uploadFinish(this.logRequest("uploadFinish request", request));
|
|
534
|
+
}), "finish multipart upload");
|
|
535
|
+
return {
|
|
536
|
+
file_token: finish.data?.file_token
|
|
537
|
+
};
|
|
538
|
+
});
|
|
392
539
|
}
|
|
393
540
|
async downloadFile(fileToken, extra) {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
541
|
+
return this.runLogged("downloadFile", {
|
|
542
|
+
fileToken,
|
|
543
|
+
hasExtra: Boolean(extra)
|
|
544
|
+
}, async () => {
|
|
545
|
+
const response = await this.withRetry("download file", async () => {
|
|
546
|
+
const request = {
|
|
547
|
+
path: {
|
|
548
|
+
file_token: fileToken
|
|
549
|
+
},
|
|
550
|
+
params: {
|
|
551
|
+
extra
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
return this.client.drive.v1.media.download(this.logRequest("downloadFile request", request));
|
|
555
|
+
});
|
|
556
|
+
const buffer = await readableToBuffer(response.getReadableStream());
|
|
557
|
+
this.logInfo("downloadFile completed", {
|
|
558
|
+
fileToken,
|
|
559
|
+
size: buffer.byteLength
|
|
560
|
+
});
|
|
561
|
+
return buffer;
|
|
562
|
+
});
|
|
403
563
|
}
|
|
404
564
|
async downLoadFile(fileToken, extra) {
|
|
405
|
-
return this.
|
|
565
|
+
return this.runLogged("downLoadFile", {
|
|
566
|
+
fileToken,
|
|
567
|
+
hasExtra: Boolean(extra)
|
|
568
|
+
}, async () => this.downloadFile(fileToken, extra));
|
|
406
569
|
}
|
|
407
570
|
resolveConstructorOptions(optionsOrToken, appIdArg, appSecretArg) {
|
|
408
571
|
const objectMode = typeof optionsOrToken === "object" && optionsOrToken !== null && !Array.isArray(optionsOrToken);
|
|
@@ -430,6 +593,9 @@ class Bitable {
|
|
|
430
593
|
}
|
|
431
594
|
return token;
|
|
432
595
|
}
|
|
596
|
+
async executeBatchUpdateRecords(payload) {
|
|
597
|
+
return this.withRetry("batch update records", async () => assertFeishuResponse(await this.client.bitable.v1.appTableRecord.batchUpdate(this.logRequest("batchUpdateRecords request", payload)), "batch update records"));
|
|
598
|
+
}
|
|
433
599
|
async withRetry(label, task) {
|
|
434
600
|
let lastError;
|
|
435
601
|
for (let attempt = 1;attempt <= this.maxRetries; attempt++) {
|
|
@@ -437,15 +603,75 @@ class Bitable {
|
|
|
437
603
|
return await task();
|
|
438
604
|
} catch (error) {
|
|
439
605
|
lastError = error;
|
|
606
|
+
const meta = {
|
|
607
|
+
label,
|
|
608
|
+
attempt,
|
|
609
|
+
maxRetries: this.maxRetries,
|
|
610
|
+
retryDelayMs: this.retryDelayMs,
|
|
611
|
+
...this.getErrorMeta(error)
|
|
612
|
+
};
|
|
440
613
|
if (attempt === this.maxRetries) {
|
|
614
|
+
this.logError("request exhausted retries", meta);
|
|
441
615
|
break;
|
|
442
616
|
}
|
|
617
|
+
this.logWarn("request attempt failed, retrying", meta);
|
|
443
618
|
await sleep(this.retryDelayMs * attempt);
|
|
444
619
|
}
|
|
445
620
|
}
|
|
446
|
-
|
|
447
|
-
|
|
621
|
+
const causeMessage = lastError instanceof Error ? `: ${lastError.message}` : "";
|
|
622
|
+
throw new FeishuBitableError(`${label} failed after ${this.maxRetries} attempts${causeMessage}`, {
|
|
623
|
+
cause: lastError,
|
|
624
|
+
code: lastError instanceof FeishuBitableError ? lastError.code : undefined,
|
|
625
|
+
details: lastError instanceof FeishuBitableError ? lastError.details : undefined
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
async runLogged(action, meta, task) {
|
|
629
|
+
this.logInfo(`${action} started`, meta);
|
|
630
|
+
try {
|
|
631
|
+
const result = await task();
|
|
632
|
+
this.logInfo(`${action} succeeded`, meta);
|
|
633
|
+
return result;
|
|
634
|
+
} catch (error) {
|
|
635
|
+
this.logError(`${action} failed`, {
|
|
636
|
+
...meta,
|
|
637
|
+
...this.getErrorMeta(error)
|
|
638
|
+
});
|
|
639
|
+
throw error;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
logInfo(message, meta) {
|
|
643
|
+
this.logger?.info(message, meta);
|
|
644
|
+
}
|
|
645
|
+
logWarn(message, meta) {
|
|
646
|
+
this.logger?.warn?.(message, meta);
|
|
647
|
+
}
|
|
648
|
+
logError(message, meta) {
|
|
649
|
+
this.logger?.error?.(message, meta);
|
|
650
|
+
}
|
|
651
|
+
getErrorMeta(error) {
|
|
652
|
+
if (error instanceof FeishuBitableError) {
|
|
653
|
+
return {
|
|
654
|
+
errorName: error.name,
|
|
655
|
+
errorMessage: error.message,
|
|
656
|
+
errorCode: error.code,
|
|
657
|
+
errorDetails: error.details
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
if (error instanceof Error) {
|
|
661
|
+
return {
|
|
662
|
+
errorName: error.name,
|
|
663
|
+
errorMessage: error.message
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
return {
|
|
667
|
+
errorMessage: String(error)
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
logRequest(message, payload) {
|
|
671
|
+
this.logInfo(message, {
|
|
672
|
+
payload
|
|
448
673
|
});
|
|
674
|
+
return payload;
|
|
449
675
|
}
|
|
450
676
|
}
|
|
451
677
|
|
package/lib/index.d.ts
CHANGED
|
@@ -2,5 +2,5 @@ import { Bitable } from "./client";
|
|
|
2
2
|
export { Bitable };
|
|
3
3
|
export { FeishuBitableError } from "./errors";
|
|
4
4
|
export { AppType, Domain, LoggerLevel } from "@larksuiteoapi/node-sdk";
|
|
5
|
-
export type { BatchOperationOptions, BitableBatchUpdatePayload, BitableBatchUpdateResponse, BitableConstructorOptions, BitableFieldValue, BitableFilterCondition, BitableFilterGroup, BitableInsertRecord, BitableLocationValue, BitableMemberValue, BitableRecord, BitableRecordFields, BitableSort, BitableTextValue, BitableUpdateRecord, FetchAllRecordsOptions, MediaParentType, UpdateRecordsOptions, UploadFileOptions, UploadableFile, } from "./types";
|
|
5
|
+
export type { BatchOperationOptions, BitableBatchUpdatePayload, BitableBatchUpdateResponse, BitableConstructorOptions, BitableFieldValue, BitableFilterCondition, BitableFilterGroup, BitableInsertRecord, BitableLocationValue, BitableLogger, BitableMemberValue, BitableRecord, BitableRecordFields, BitableSort, BitableTextValue, BitableUpdateRecord, FetchAllRecordsOptions, MediaParentType, UpdateRecordsOptions, UploadFileOptions, UploadableFile, } from "./types";
|
|
6
6
|
export default Bitable;
|