@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.js
CHANGED
|
@@ -96,7 +96,8 @@ function normalizeRecordFields(fields) {
|
|
|
96
96
|
}
|
|
97
97
|
function splitUpdateRecord(record) {
|
|
98
98
|
const { record_id: recordId, ...fields } = record;
|
|
99
|
-
|
|
99
|
+
const normalizedFields = Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
|
|
100
|
+
return { recordId, fields: normalizedFields };
|
|
100
101
|
}
|
|
101
102
|
async function toBuffer(file) {
|
|
102
103
|
if (typeof file === "string") {
|
|
@@ -149,18 +150,44 @@ function isFileLike(value) {
|
|
|
149
150
|
}
|
|
150
151
|
|
|
151
152
|
// src/client.ts
|
|
153
|
+
var DEFAULT_LOGGER = {
|
|
154
|
+
info(message, meta) {
|
|
155
|
+
if (meta) {
|
|
156
|
+
console.info(`[feishu-bitable] ${message}`, meta);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
console.info(`[feishu-bitable] ${message}`);
|
|
160
|
+
},
|
|
161
|
+
warn(message, meta) {
|
|
162
|
+
if (meta) {
|
|
163
|
+
console.warn(`[feishu-bitable] ${message}`, meta);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
console.warn(`[feishu-bitable] ${message}`);
|
|
167
|
+
},
|
|
168
|
+
error(message, meta) {
|
|
169
|
+
if (meta) {
|
|
170
|
+
console.error(`[feishu-bitable] ${message}`, meta);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
console.error(`[feishu-bitable] ${message}`);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
152
177
|
class Bitable {
|
|
153
178
|
client;
|
|
154
179
|
defaultAppToken;
|
|
155
180
|
maxRetries;
|
|
156
181
|
retryDelayMs;
|
|
157
182
|
defaultConcurrency;
|
|
183
|
+
logger;
|
|
158
184
|
constructor(optionsOrToken, appId, appSecret) {
|
|
159
185
|
const options = this.resolveConstructorOptions(optionsOrToken, appId, appSecret);
|
|
160
186
|
this.defaultAppToken = options.defaultAppToken;
|
|
161
187
|
this.maxRetries = Math.max(1, options.maxRetries ?? 5);
|
|
162
188
|
this.retryDelayMs = Math.max(100, options.retryDelayMs ?? 1000);
|
|
163
189
|
this.defaultConcurrency = Math.max(1, options.defaultConcurrency ?? 1);
|
|
190
|
+
this.logger = options.logger === null ? null : options.logger ?? DEFAULT_LOGGER;
|
|
164
191
|
this.client = options.sdkClient ?? new lark.Client({
|
|
165
192
|
appId: options.appId,
|
|
166
193
|
appSecret: options.appSecret,
|
|
@@ -176,159 +203,295 @@ class Bitable {
|
|
|
176
203
|
});
|
|
177
204
|
}
|
|
178
205
|
async fetchAllRecords(tableId, options = {}, appToken) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
206
|
+
return this.runLogged("fetchAllRecords", {
|
|
207
|
+
tableId,
|
|
208
|
+
pageSize: options.pageSize ?? FEISHU_BATCH_LIMIT,
|
|
209
|
+
hasViewId: Boolean(options.viewId),
|
|
210
|
+
fieldCount: options.fieldNames?.length ?? 0
|
|
211
|
+
}, async () => {
|
|
212
|
+
const token = this.resolveAppToken(appToken);
|
|
213
|
+
const pageSize = Math.max(1, Math.min(options.pageSize ?? FEISHU_BATCH_LIMIT, FEISHU_BATCH_LIMIT));
|
|
214
|
+
const request = {
|
|
215
|
+
path: {
|
|
216
|
+
app_token: token,
|
|
217
|
+
table_id: tableId
|
|
218
|
+
},
|
|
219
|
+
params: {
|
|
220
|
+
page_size: pageSize,
|
|
221
|
+
user_id_type: "open_id"
|
|
222
|
+
},
|
|
223
|
+
data: {
|
|
224
|
+
view_id: options.viewId,
|
|
225
|
+
field_names: options.fieldNames,
|
|
226
|
+
filter: options.filter,
|
|
227
|
+
sort: options.sort,
|
|
228
|
+
automatic_fields: options.automaticFields
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
const iterator = await this.client.bitable.v1.appTableRecord.searchWithIterator(this.logRequest("fetchAllRecords request", request));
|
|
232
|
+
const allRecords = [];
|
|
233
|
+
for await (const page of iterator) {
|
|
234
|
+
const items = page?.items ?? [];
|
|
235
|
+
allRecords.push(...items.map((item) => normalizeRecord(item.record_id, item.fields, options.normalizeFields !== false)));
|
|
196
236
|
}
|
|
237
|
+
this.logInfo("fetchAllRecords completed", {
|
|
238
|
+
tableId,
|
|
239
|
+
recordCount: allRecords.length
|
|
240
|
+
});
|
|
241
|
+
return allRecords;
|
|
197
242
|
});
|
|
198
|
-
const allRecords = [];
|
|
199
|
-
for await (const page of iterator) {
|
|
200
|
-
const items = page?.items ?? [];
|
|
201
|
-
allRecords.push(...items.map((item) => normalizeRecord(item.record_id, item.fields, options.normalizeFields !== false)));
|
|
202
|
-
}
|
|
203
|
-
return allRecords;
|
|
204
243
|
}
|
|
205
244
|
async insertList(tableId, records, options = {}) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
records: chunk.map((fields) => ({ fields }))
|
|
245
|
+
return this.runLogged("insertList", {
|
|
246
|
+
tableId,
|
|
247
|
+
recordCount: records.length,
|
|
248
|
+
chunkSize: options.chunkSize ?? FEISHU_BATCH_LIMIT,
|
|
249
|
+
concurrency: options.concurrency ?? this.defaultConcurrency
|
|
250
|
+
}, async () => {
|
|
251
|
+
if (records.length === 0) {
|
|
252
|
+
return [];
|
|
215
253
|
}
|
|
216
|
-
|
|
254
|
+
const token = this.resolveAppToken(options.appToken);
|
|
255
|
+
const chunks = chunkArray(records, options.chunkSize ?? FEISHU_BATCH_LIMIT);
|
|
256
|
+
const responses = await runWithConcurrency(chunks, options.concurrency ?? this.defaultConcurrency, async (chunk) => this.withRetry("insert records", async () => {
|
|
257
|
+
const request = {
|
|
258
|
+
path: { app_token: token, table_id: tableId },
|
|
259
|
+
data: {
|
|
260
|
+
records: chunk.map((fields) => ({ fields }))
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
return assertFeishuResponse(await this.client.bitable.v1.appTableRecord.batchCreate(this.logRequest("insertList request", request)), "insert records");
|
|
264
|
+
}));
|
|
265
|
+
this.logInfo("insertList completed", {
|
|
266
|
+
tableId,
|
|
267
|
+
chunkCount: chunks.length
|
|
268
|
+
});
|
|
269
|
+
return responses;
|
|
270
|
+
});
|
|
217
271
|
}
|
|
218
272
|
async batchUpdateRecords(payload) {
|
|
219
|
-
return this.
|
|
273
|
+
return this.runLogged("batchUpdateRecords", {
|
|
274
|
+
tableId: payload.path.table_id,
|
|
275
|
+
recordCount: payload.data.records.length
|
|
276
|
+
}, async () => this.executeBatchUpdateRecords(payload));
|
|
220
277
|
}
|
|
221
278
|
async updateRecords(tableId, records, options = {}) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
279
|
+
return this.runLogged("updateRecords", {
|
|
280
|
+
tableId,
|
|
281
|
+
inputRecordCount: records.length,
|
|
282
|
+
chunkSize: options.chunkSize ?? FEISHU_BATCH_LIMIT,
|
|
283
|
+
concurrency: options.concurrency ?? this.defaultConcurrency
|
|
284
|
+
}, async () => {
|
|
285
|
+
if (records.length === 0) {
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
const token = this.resolveAppToken(options.appToken);
|
|
289
|
+
const chunks = chunkArray(records, options.chunkSize ?? FEISHU_BATCH_LIMIT);
|
|
290
|
+
const responses = await runWithConcurrency(chunks, options.concurrency ?? this.defaultConcurrency, async (chunk, index) => {
|
|
291
|
+
const batchRecords = chunk.map((record, recordIndex) => {
|
|
292
|
+
const { recordId, fields } = splitUpdateRecord(record);
|
|
293
|
+
if (!recordId || !recordId.trim()) {
|
|
294
|
+
throw new FeishuBitableError(`updateRecords failed: record_id is required for chunk ${index}, item ${recordIndex}`, {
|
|
295
|
+
details: {
|
|
296
|
+
tableId,
|
|
297
|
+
chunkIndex: index,
|
|
298
|
+
recordIndex,
|
|
299
|
+
record
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
if (Object.keys(fields).length === 0) {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
record_id: recordId,
|
|
308
|
+
fields
|
|
309
|
+
};
|
|
310
|
+
}).filter((record) => Boolean(record));
|
|
311
|
+
if (batchRecords.length === 0) {
|
|
312
|
+
this.logInfo("updateRecords skipped empty chunk", {
|
|
313
|
+
tableId,
|
|
314
|
+
chunkIndex: index,
|
|
315
|
+
inputChunkSize: chunk.length
|
|
316
|
+
});
|
|
317
|
+
return {
|
|
318
|
+
code: 0,
|
|
319
|
+
msg: "ok",
|
|
320
|
+
data: {
|
|
321
|
+
records: []
|
|
322
|
+
}
|
|
323
|
+
};
|
|
241
324
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
325
|
+
const payload = {
|
|
326
|
+
path: {
|
|
327
|
+
app_token: token,
|
|
328
|
+
table_id: tableId
|
|
329
|
+
},
|
|
330
|
+
data: {
|
|
331
|
+
records: batchRecords
|
|
332
|
+
}
|
|
247
333
|
};
|
|
248
|
-
|
|
249
|
-
|
|
334
|
+
if (options.userIdType || options.ignoreConsistencyCheck !== undefined) {
|
|
335
|
+
payload.params = {
|
|
336
|
+
user_id_type: options.userIdType,
|
|
337
|
+
ignore_consistency_check: options.ignoreConsistencyCheck
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
return this.executeBatchUpdateRecords(payload);
|
|
341
|
+
});
|
|
342
|
+
this.logInfo("updateRecords completed", {
|
|
343
|
+
tableId,
|
|
344
|
+
chunkCount: chunks.length
|
|
345
|
+
});
|
|
346
|
+
return responses;
|
|
250
347
|
});
|
|
251
348
|
}
|
|
252
349
|
async deleteList(tableId, recordIds, options = {}) {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
records: chunk
|
|
350
|
+
return this.runLogged("deleteList", {
|
|
351
|
+
tableId,
|
|
352
|
+
recordCount: recordIds.length,
|
|
353
|
+
chunkSize: options.chunkSize ?? FEISHU_BATCH_LIMIT,
|
|
354
|
+
concurrency: options.concurrency ?? this.defaultConcurrency
|
|
355
|
+
}, async () => {
|
|
356
|
+
if (recordIds.length === 0) {
|
|
357
|
+
return [];
|
|
262
358
|
}
|
|
263
|
-
|
|
359
|
+
const token = this.resolveAppToken(options.appToken);
|
|
360
|
+
const chunks = chunkArray(recordIds, options.chunkSize ?? FEISHU_BATCH_LIMIT);
|
|
361
|
+
const responses = await runWithConcurrency(chunks, options.concurrency ?? this.defaultConcurrency, async (chunk) => this.withRetry("delete records", async () => {
|
|
362
|
+
const request = {
|
|
363
|
+
path: { app_token: token, table_id: tableId },
|
|
364
|
+
data: {
|
|
365
|
+
records: chunk
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
return assertFeishuResponse(await this.client.bitable.v1.appTableRecord.batchDelete(this.logRequest("deleteList request", request)), "delete records");
|
|
369
|
+
}));
|
|
370
|
+
this.logInfo("deleteList completed", {
|
|
371
|
+
tableId,
|
|
372
|
+
chunkCount: chunks.length
|
|
373
|
+
});
|
|
374
|
+
return responses;
|
|
375
|
+
});
|
|
264
376
|
}
|
|
265
377
|
async uploadFile(options) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
})) ?? {};
|
|
279
|
-
}
|
|
280
|
-
const prepare = assertFeishuResponse(await this.withRetry("prepare multipart upload", async () => this.client.drive.v1.media.uploadPrepare({
|
|
281
|
-
data: {
|
|
282
|
-
file_name: fileName,
|
|
283
|
-
parent_type: options.parentType,
|
|
284
|
-
parent_node: options.parentNode,
|
|
285
|
-
size: buffer.byteLength
|
|
286
|
-
}
|
|
287
|
-
})), "prepare multipart upload");
|
|
288
|
-
const uploadId = prepare.data?.upload_id;
|
|
289
|
-
const blockSize = prepare.data?.block_size;
|
|
290
|
-
const blockNum = prepare.data?.block_num;
|
|
291
|
-
if (!uploadId || !blockSize || !blockNum) {
|
|
292
|
-
throw new FeishuBitableError("prepare multipart upload failed: missing upload metadata", {
|
|
293
|
-
details: prepare
|
|
378
|
+
return this.runLogged("uploadFile", {
|
|
379
|
+
parentType: options.parentType,
|
|
380
|
+
parentNode: options.parentNode,
|
|
381
|
+
hasExtra: Boolean(options.extra)
|
|
382
|
+
}, async () => {
|
|
383
|
+
const buffer = await toBuffer(options.file);
|
|
384
|
+
const fileName = options.fileName ?? inferFileName(options.file);
|
|
385
|
+
this.logInfo("uploadFile resolved input", {
|
|
386
|
+
fileName,
|
|
387
|
+
size: buffer.byteLength,
|
|
388
|
+
mode: buffer.byteLength <= FEISHU_SIMPLE_UPLOAD_LIMIT ? "simple" : "multipart"
|
|
294
389
|
});
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
390
|
+
if (buffer.byteLength <= FEISHU_SIMPLE_UPLOAD_LIMIT) {
|
|
391
|
+
return await this.withRetry("upload file", async () => {
|
|
392
|
+
const request = {
|
|
393
|
+
data: {
|
|
394
|
+
file_name: fileName,
|
|
395
|
+
parent_type: options.parentType,
|
|
396
|
+
parent_node: options.parentNode,
|
|
397
|
+
size: buffer.byteLength,
|
|
398
|
+
extra: options.extra,
|
|
399
|
+
file: buffer
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
this.logRequest("uploadFile request", {
|
|
403
|
+
data: {
|
|
404
|
+
...request.data,
|
|
405
|
+
file: `[Buffer ${buffer.byteLength} bytes]`
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
return this.client.drive.v1.media.uploadAll(request);
|
|
409
|
+
}) ?? {};
|
|
313
410
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
411
|
+
const prepare = assertFeishuResponse(await this.withRetry("prepare multipart upload", async () => {
|
|
412
|
+
const request = {
|
|
413
|
+
data: {
|
|
414
|
+
file_name: fileName,
|
|
415
|
+
parent_type: options.parentType,
|
|
416
|
+
parent_node: options.parentNode,
|
|
417
|
+
size: buffer.byteLength
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
return this.client.drive.v1.media.uploadPrepare(this.logRequest("uploadPrepare request", request));
|
|
421
|
+
}), "prepare multipart upload");
|
|
422
|
+
const uploadId = prepare.data?.upload_id;
|
|
423
|
+
const blockSize = prepare.data?.block_size;
|
|
424
|
+
const blockNum = prepare.data?.block_num;
|
|
425
|
+
if (!uploadId || !blockSize || !blockNum) {
|
|
426
|
+
throw new FeishuBitableError("prepare multipart upload failed: missing upload metadata", {
|
|
427
|
+
details: prepare
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
for (let index = 0;index < blockNum; index++) {
|
|
431
|
+
const start = index * blockSize;
|
|
432
|
+
const end = Math.min(start + blockSize, buffer.byteLength);
|
|
433
|
+
const chunk = buffer.subarray(start, end);
|
|
434
|
+
await this.withRetry(`upload file chunk ${index + 1}/${blockNum}`, async () => {
|
|
435
|
+
const request = {
|
|
436
|
+
data: {
|
|
437
|
+
upload_id: uploadId,
|
|
438
|
+
seq: index,
|
|
439
|
+
size: chunk.byteLength,
|
|
440
|
+
file: chunk
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
this.logRequest("uploadPart request", {
|
|
444
|
+
data: {
|
|
445
|
+
...request.data,
|
|
446
|
+
file: `[Buffer ${chunk.byteLength} bytes]`
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
return this.client.drive.v1.media.uploadPart(request);
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
const finish = assertFeishuResponse(await this.withRetry("finish multipart upload", async () => {
|
|
453
|
+
const request = {
|
|
454
|
+
data: {
|
|
455
|
+
upload_id: uploadId,
|
|
456
|
+
block_num: blockNum
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
return this.client.drive.v1.media.uploadFinish(this.logRequest("uploadFinish request", request));
|
|
460
|
+
}), "finish multipart upload");
|
|
461
|
+
return {
|
|
462
|
+
file_token: finish.data?.file_token
|
|
463
|
+
};
|
|
464
|
+
});
|
|
318
465
|
}
|
|
319
466
|
async downloadFile(fileToken, extra) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
467
|
+
return this.runLogged("downloadFile", {
|
|
468
|
+
fileToken,
|
|
469
|
+
hasExtra: Boolean(extra)
|
|
470
|
+
}, async () => {
|
|
471
|
+
const response = await this.withRetry("download file", async () => {
|
|
472
|
+
const request = {
|
|
473
|
+
path: {
|
|
474
|
+
file_token: fileToken
|
|
475
|
+
},
|
|
476
|
+
params: {
|
|
477
|
+
extra
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
return this.client.drive.v1.media.download(this.logRequest("downloadFile request", request));
|
|
481
|
+
});
|
|
482
|
+
const buffer = await readableToBuffer(response.getReadableStream());
|
|
483
|
+
this.logInfo("downloadFile completed", {
|
|
484
|
+
fileToken,
|
|
485
|
+
size: buffer.byteLength
|
|
486
|
+
});
|
|
487
|
+
return buffer;
|
|
488
|
+
});
|
|
329
489
|
}
|
|
330
490
|
async downLoadFile(fileToken, extra) {
|
|
331
|
-
return this.
|
|
491
|
+
return this.runLogged("downLoadFile", {
|
|
492
|
+
fileToken,
|
|
493
|
+
hasExtra: Boolean(extra)
|
|
494
|
+
}, async () => this.downloadFile(fileToken, extra));
|
|
332
495
|
}
|
|
333
496
|
resolveConstructorOptions(optionsOrToken, appIdArg, appSecretArg) {
|
|
334
497
|
const objectMode = typeof optionsOrToken === "object" && optionsOrToken !== null && !Array.isArray(optionsOrToken);
|
|
@@ -356,6 +519,9 @@ class Bitable {
|
|
|
356
519
|
}
|
|
357
520
|
return token;
|
|
358
521
|
}
|
|
522
|
+
async executeBatchUpdateRecords(payload) {
|
|
523
|
+
return this.withRetry("batch update records", async () => assertFeishuResponse(await this.client.bitable.v1.appTableRecord.batchUpdate(this.logRequest("batchUpdateRecords request", payload)), "batch update records"));
|
|
524
|
+
}
|
|
359
525
|
async withRetry(label, task) {
|
|
360
526
|
let lastError;
|
|
361
527
|
for (let attempt = 1;attempt <= this.maxRetries; attempt++) {
|
|
@@ -363,15 +529,75 @@ class Bitable {
|
|
|
363
529
|
return await task();
|
|
364
530
|
} catch (error) {
|
|
365
531
|
lastError = error;
|
|
532
|
+
const meta = {
|
|
533
|
+
label,
|
|
534
|
+
attempt,
|
|
535
|
+
maxRetries: this.maxRetries,
|
|
536
|
+
retryDelayMs: this.retryDelayMs,
|
|
537
|
+
...this.getErrorMeta(error)
|
|
538
|
+
};
|
|
366
539
|
if (attempt === this.maxRetries) {
|
|
540
|
+
this.logError("request exhausted retries", meta);
|
|
367
541
|
break;
|
|
368
542
|
}
|
|
543
|
+
this.logWarn("request attempt failed, retrying", meta);
|
|
369
544
|
await sleep(this.retryDelayMs * attempt);
|
|
370
545
|
}
|
|
371
546
|
}
|
|
372
|
-
|
|
373
|
-
|
|
547
|
+
const causeMessage = lastError instanceof Error ? `: ${lastError.message}` : "";
|
|
548
|
+
throw new FeishuBitableError(`${label} failed after ${this.maxRetries} attempts${causeMessage}`, {
|
|
549
|
+
cause: lastError,
|
|
550
|
+
code: lastError instanceof FeishuBitableError ? lastError.code : undefined,
|
|
551
|
+
details: lastError instanceof FeishuBitableError ? lastError.details : undefined
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
async runLogged(action, meta, task) {
|
|
555
|
+
this.logInfo(`${action} started`, meta);
|
|
556
|
+
try {
|
|
557
|
+
const result = await task();
|
|
558
|
+
this.logInfo(`${action} succeeded`, meta);
|
|
559
|
+
return result;
|
|
560
|
+
} catch (error) {
|
|
561
|
+
this.logError(`${action} failed`, {
|
|
562
|
+
...meta,
|
|
563
|
+
...this.getErrorMeta(error)
|
|
564
|
+
});
|
|
565
|
+
throw error;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
logInfo(message, meta) {
|
|
569
|
+
this.logger?.info(message, meta);
|
|
570
|
+
}
|
|
571
|
+
logWarn(message, meta) {
|
|
572
|
+
this.logger?.warn?.(message, meta);
|
|
573
|
+
}
|
|
574
|
+
logError(message, meta) {
|
|
575
|
+
this.logger?.error?.(message, meta);
|
|
576
|
+
}
|
|
577
|
+
getErrorMeta(error) {
|
|
578
|
+
if (error instanceof FeishuBitableError) {
|
|
579
|
+
return {
|
|
580
|
+
errorName: error.name,
|
|
581
|
+
errorMessage: error.message,
|
|
582
|
+
errorCode: error.code,
|
|
583
|
+
errorDetails: error.details
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
if (error instanceof Error) {
|
|
587
|
+
return {
|
|
588
|
+
errorName: error.name,
|
|
589
|
+
errorMessage: error.message
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
return {
|
|
593
|
+
errorMessage: String(error)
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
logRequest(message, payload) {
|
|
597
|
+
this.logInfo(message, {
|
|
598
|
+
payload
|
|
374
599
|
});
|
|
600
|
+
return payload;
|
|
375
601
|
}
|
|
376
602
|
}
|
|
377
603
|
|
package/lib/types.d.ts
CHANGED
|
@@ -62,6 +62,11 @@ export type BitableFilterGroup = {
|
|
|
62
62
|
};
|
|
63
63
|
export type MediaParentType = "doc_image" | "docx_image" | "sheet_image" | "doc_file" | "docx_file" | "sheet_file" | "vc_virtual_background" | "bitable_image" | "bitable_file" | "moments" | "ccm_import_open" | "calendar" | "base_global" | "lark_ai_media_analysis";
|
|
64
64
|
export type UploadableFile = string | Buffer | Uint8Array | ArrayBuffer | Blob | BunFileLike;
|
|
65
|
+
export type BitableLogger = {
|
|
66
|
+
info: (message: string, meta?: Record<string, unknown>) => void;
|
|
67
|
+
warn?: (message: string, meta?: Record<string, unknown>) => void;
|
|
68
|
+
error?: (message: string, meta?: Record<string, unknown>) => void;
|
|
69
|
+
};
|
|
65
70
|
export type BunFileLike = {
|
|
66
71
|
arrayBuffer(): Promise<ArrayBuffer>;
|
|
67
72
|
name?: string;
|
|
@@ -76,6 +81,7 @@ export interface BitableConstructorOptions {
|
|
|
76
81
|
retryDelayMs?: number;
|
|
77
82
|
defaultConcurrency?: number;
|
|
78
83
|
sdkClient?: lark.Client;
|
|
84
|
+
logger?: BitableLogger | null;
|
|
79
85
|
}
|
|
80
86
|
export interface FetchAllRecordsOptions {
|
|
81
87
|
viewId?: string;
|