appwrite-utils-cli 1.7.4 → 1.7.6

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.
@@ -182,7 +182,7 @@ export class AdapterFactory {
182
182
  // Prefer session with admin mode, fallback to API key with default mode
183
183
  if (config.sessionCookie && isValidSessionCookie(config.sessionCookie)) {
184
184
  client.setSession(config.sessionCookie);
185
- client.setHeader('X-Appwrite-Mode', 'admin');
185
+ client.headers['X-Appwrite-Mode'] = 'admin';
186
186
  logger.debug('Using session authentication for TablesDB adapter', {
187
187
  project: config.appwriteProject,
188
188
  operation: 'createTablesDBAdapter'
@@ -190,7 +190,7 @@ export class AdapterFactory {
190
190
  }
191
191
  else if (config.appwriteKey) {
192
192
  client.setKey(config.appwriteKey);
193
- client.setHeader('X-Appwrite-Mode', 'default');
193
+ client.headers['X-Appwrite-Mode'] = 'default';
194
194
  logger.debug('Using API key authentication for TablesDB adapter', {
195
195
  project: config.appwriteProject,
196
196
  operation: 'createTablesDBAdapter'
@@ -256,7 +256,7 @@ export class AdapterFactory {
256
256
  // Prefer session with admin mode, fallback to API key with default mode
257
257
  if (config.sessionCookie && isValidSessionCookie(config.sessionCookie)) {
258
258
  client.setSession(config.sessionCookie);
259
- client.setHeader('X-Appwrite-Mode', 'admin');
259
+ client.headers['X-Appwrite-Mode'] = 'admin';
260
260
  logger.debug('Using session authentication for Legacy adapter', {
261
261
  project: config.appwriteProject,
262
262
  operation: 'createLegacyAdapter'
@@ -264,7 +264,7 @@ export class AdapterFactory {
264
264
  }
265
265
  else if (config.appwriteKey) {
266
266
  client.setKey(config.appwriteKey);
267
- client.setHeader('X-Appwrite-Mode', 'default');
267
+ client.headers['X-Appwrite-Mode'] = 'default';
268
268
  logger.debug('Using API key authentication for Legacy adapter', {
269
269
  project: config.appwriteProject,
270
270
  operation: 'createLegacyAdapter'
@@ -6,6 +6,7 @@
6
6
  * code can use modern TablesDB patterns while maintaining compatibility with
7
7
  * older Appwrite instances.
8
8
  */
9
+ import { Query } from "node-appwrite";
9
10
  import { BaseAdapter, AdapterError, UnsupportedOperationError } from './DatabaseAdapter.js';
10
11
  /**
11
12
  * LegacyAdapter - Translates TablesDB calls to legacy Databases API
@@ -308,30 +309,51 @@ export class LegacyAdapter extends BaseAdapter {
308
309
  throw new UnsupportedOperationError('bulkUpsertRows', 'legacy');
309
310
  }
310
311
  async bulkDeleteRows(params) {
311
- // Legacy doesn't support bulk operations, fallback to individual deletes
312
- const results = [];
313
- const errors = [];
314
- for (const rowId of params.rowIds) {
315
- try {
316
- await this.deleteRow({
317
- databaseId: params.databaseId,
318
- tableId: params.tableId,
319
- id: rowId
320
- });
321
- results.push({ id: rowId, deleted: true });
312
+ try {
313
+ // Try to use deleteDocuments with queries first (more efficient)
314
+ const queries = params.rowIds.map(id => Query.equal('$id', id));
315
+ const result = await this.databases.deleteDocuments(params.databaseId, params.tableId, // Maps tableId to collectionId
316
+ queries);
317
+ return {
318
+ data: result,
319
+ total: params.rowIds.length
320
+ };
321
+ }
322
+ catch (error) {
323
+ // If deleteDocuments with queries fails, fall back to individual deletes
324
+ const errorMessage = error instanceof Error ? error.message : String(error);
325
+ // Check if the error indicates that deleteDocuments with queries is not supported
326
+ if (errorMessage.includes('not supported') || errorMessage.includes('invalid') || errorMessage.includes('queries')) {
327
+ // Fall back to individual deletions
328
+ const results = [];
329
+ const errors = [];
330
+ for (const rowId of params.rowIds) {
331
+ try {
332
+ await this.deleteRow({
333
+ databaseId: params.databaseId,
334
+ tableId: params.tableId,
335
+ id: rowId
336
+ });
337
+ results.push({ id: rowId, deleted: true });
338
+ }
339
+ catch (individualError) {
340
+ errors.push({
341
+ rowId,
342
+ error: individualError instanceof Error ? individualError.message : 'Unknown error'
343
+ });
344
+ }
345
+ }
346
+ return {
347
+ data: results,
348
+ total: results.length,
349
+ errors: errors.length > 0 ? errors : undefined
350
+ };
322
351
  }
323
- catch (error) {
324
- errors.push({
325
- rowId,
326
- error: error instanceof Error ? error.message : 'Unknown error'
327
- });
352
+ else {
353
+ // Re-throw the original error if it's not a support issue
354
+ throw new AdapterError(`Failed to bulk delete rows (legacy): ${errorMessage}`, 'BULK_DELETE_ROWS_FAILED', error instanceof Error ? error : undefined);
328
355
  }
329
356
  }
330
- return {
331
- data: results,
332
- total: results.length,
333
- errors: errors.length > 0 ? errors : undefined
334
- };
335
357
  }
336
358
  // Metadata and Capabilities
337
359
  getMetadata() {
@@ -5,6 +5,7 @@
5
5
  * without any translation layer. It uses object notation parameters
6
6
  * and returns Models.Row instead of Models.Document.
7
7
  */
8
+ import { Query } from "node-appwrite";
8
9
  import { BaseAdapter, AdapterError } from './DatabaseAdapter.js';
9
10
  /**
10
11
  * TablesDBAdapter implementation for native TablesDB API
@@ -269,7 +270,13 @@ export class TablesDBAdapter extends BaseAdapter {
269
270
  }
270
271
  async bulkDeleteRows(params) {
271
272
  try {
272
- const result = await this.tablesDB.bulkDeleteRows(params);
273
+ // Convert rowIds to queries for the deleteRows API
274
+ const queries = params.rowIds.map(id => Query.equal('$id', id));
275
+ const result = await this.tablesDB.deleteRows({
276
+ databaseId: params.databaseId,
277
+ tableId: params.tableId,
278
+ queries: queries
279
+ });
273
280
  return {
274
281
  data: result,
275
282
  total: params.rowIds.length
@@ -208,8 +208,8 @@ export const wipeTableRows = async (adapter, databaseId, tableId) => {
208
208
  if (adapter.bulkDeleteRows) {
209
209
  try {
210
210
  // Attempt bulk deletion (available in TablesDB)
211
- await tryBulkDeletion(adapter, databaseId, tableId, rowIds, BULK_DELETE_BATCH_SIZE, MAX_CONCURRENT_OPERATIONS);
212
- totalDeleted += rows.length;
211
+ const deletedCount = await tryBulkDeletion(adapter, databaseId, tableId, rowIds, BULK_DELETE_BATCH_SIZE, MAX_CONCURRENT_OPERATIONS);
212
+ totalDeleted += deletedCount;
213
213
  progress.update(totalDeleted);
214
214
  }
215
215
  catch (bulkError) {
@@ -224,15 +224,15 @@ export const wipeTableRows = async (adapter, databaseId, tableId) => {
224
224
  else {
225
225
  MessageFormatter.progress(`Bulk deletion failed (${errorMessage}), falling back to individual deletion for ${rows.length} rows`, { prefix: "Wipe" });
226
226
  }
227
- await tryIndividualDeletion(adapter, databaseId, tableId, rows, INDIVIDUAL_DELETE_BATCH_SIZE, MAX_CONCURRENT_OPERATIONS, progress, totalDeleted);
228
- totalDeleted += rows.length;
227
+ const deletedCount = await tryIndividualDeletion(adapter, databaseId, tableId, rows, INDIVIDUAL_DELETE_BATCH_SIZE, MAX_CONCURRENT_OPERATIONS, progress, totalDeleted);
228
+ totalDeleted += deletedCount;
229
229
  }
230
230
  }
231
231
  else {
232
232
  // Bulk deletion not available, use optimized individual deletion
233
233
  MessageFormatter.progress(`Using individual deletion for ${rows.length} rows (bulk deletion not available)`, { prefix: "Wipe" });
234
- await tryIndividualDeletion(adapter, databaseId, tableId, rows, INDIVIDUAL_DELETE_BATCH_SIZE, MAX_CONCURRENT_OPERATIONS, progress, totalDeleted);
235
- totalDeleted += rows.length;
234
+ const deletedCount = await tryIndividualDeletion(adapter, databaseId, tableId, rows, INDIVIDUAL_DELETE_BATCH_SIZE, MAX_CONCURRENT_OPERATIONS, progress, totalDeleted);
235
+ totalDeleted += deletedCount;
236
236
  }
237
237
  // Set up cursor for next iteration
238
238
  if (rows.length < FETCH_BATCH_SIZE) {
@@ -268,9 +268,11 @@ async function tryBulkDeletion(adapter, databaseId, tableId, rowIds, batchSize,
268
268
  }
269
269
  const limit = pLimit(maxConcurrent);
270
270
  const batches = chunk(rowIds, batchSize);
271
+ let successfullyDeleted = 0;
271
272
  const deletePromises = batches.map((batch) => limit(async () => {
272
273
  try {
273
- await tryAwaitWithRetry(async () => adapter.bulkDeleteRows({ databaseId, tableId, rowIds: batch }));
274
+ const result = await tryAwaitWithRetry(async () => adapter.bulkDeleteRows({ databaseId, tableId, rowIds: batch }));
275
+ successfullyDeleted += batch.length; // Assume success if no error thrown
274
276
  }
275
277
  catch (error) {
276
278
  const errorMessage = error.message || String(error);
@@ -286,6 +288,7 @@ async function tryBulkDeletion(adapter, databaseId, tableId, rowIds, batchSize,
286
288
  }
287
289
  }));
288
290
  await Promise.all(deletePromises);
291
+ return successfullyDeleted;
289
292
  }
290
293
  /**
291
294
  * Helper function for fallback individual deletion
@@ -294,16 +297,19 @@ async function tryIndividualDeletion(adapter, databaseId, tableId, rows, batchSi
294
297
  const limit = pLimit(maxConcurrent);
295
298
  const batches = chunk(rows, batchSize);
296
299
  let processedInBatch = 0;
300
+ let successfullyDeleted = 0;
297
301
  const deletePromises = batches.map((batch) => limit(async () => {
298
302
  const batchDeletePromises = batch.map(async (row) => {
299
303
  try {
300
304
  await tryAwaitWithRetry(async () => adapter.deleteRow({ databaseId, tableId, id: row.$id }));
305
+ successfullyDeleted++;
301
306
  }
302
307
  catch (error) {
303
308
  const errorMessage = error.message || String(error);
304
309
  // Enhanced error handling for row deletion
305
310
  if (errorMessage.includes("Row with the requested ID could not be found")) {
306
- // Row already deleted, skip silently
311
+ // Row already deleted, count as success since it's gone
312
+ successfullyDeleted++;
307
313
  }
308
314
  else if (isCriticalError(errorMessage)) {
309
315
  // Critical error, log and rethrow to stop operation
@@ -320,9 +326,10 @@ async function tryIndividualDeletion(adapter, databaseId, tableId, rows, batchSi
320
326
  }
321
327
  }
322
328
  processedInBatch++;
323
- progress.update(baseDeleted + processedInBatch);
329
+ progress.update(baseDeleted + successfullyDeleted);
324
330
  });
325
331
  await Promise.all(batchDeletePromises);
326
332
  }));
327
333
  await Promise.all(deletePromises);
334
+ return successfullyDeleted;
328
335
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "appwrite-utils-cli",
3
3
  "description": "Appwrite Utility Functions to help with database management, data conversion, data import, migrations, and much more. Meant to be used as a CLI tool, I do not recommend installing this in frontend environments.",
4
- "version": "1.7.4",
4
+ "version": "1.7.6",
5
5
  "main": "src/main.ts",
6
6
  "type": "module",
7
7
  "repository": {
@@ -234,15 +234,15 @@ export class AdapterFactory {
234
234
  // Set authentication method with mode headers
235
235
  // Prefer session with admin mode, fallback to API key with default mode
236
236
  if (config.sessionCookie && isValidSessionCookie(config.sessionCookie)) {
237
- (client as any).setSession(config.sessionCookie);
238
- client.setHeader('X-Appwrite-Mode', 'admin');
237
+ client.setSession(config.sessionCookie);
238
+ client.headers['X-Appwrite-Mode'] = 'admin';
239
239
  logger.debug('Using session authentication for TablesDB adapter', {
240
240
  project: config.appwriteProject,
241
241
  operation: 'createTablesDBAdapter'
242
242
  });
243
243
  } else if (config.appwriteKey) {
244
244
  client.setKey(config.appwriteKey);
245
- client.setHeader('X-Appwrite-Mode', 'default');
245
+ client.headers['X-Appwrite-Mode'] = 'default';
246
246
  logger.debug('Using API key authentication for TablesDB adapter', {
247
247
  project: config.appwriteProject,
248
248
  operation: 'createTablesDBAdapter'
@@ -320,14 +320,14 @@ export class AdapterFactory {
320
320
  // Prefer session with admin mode, fallback to API key with default mode
321
321
  if (config.sessionCookie && isValidSessionCookie(config.sessionCookie)) {
322
322
  (client as any).setSession(config.sessionCookie);
323
- client.setHeader('X-Appwrite-Mode', 'admin');
323
+ client.headers['X-Appwrite-Mode'] = 'admin';
324
324
  logger.debug('Using session authentication for Legacy adapter', {
325
325
  project: config.appwriteProject,
326
326
  operation: 'createLegacyAdapter'
327
327
  });
328
328
  } else if (config.appwriteKey) {
329
329
  client.setKey(config.appwriteKey);
330
- client.setHeader('X-Appwrite-Mode', 'default');
330
+ client.headers['X-Appwrite-Mode'] = 'default';
331
331
  logger.debug('Using API key authentication for Legacy adapter', {
332
332
  project: config.appwriteProject,
333
333
  operation: 'createLegacyAdapter'
@@ -7,31 +7,32 @@
7
7
  * older Appwrite instances.
8
8
  */
9
9
 
10
- import {
11
- BaseAdapter,
12
- type CreateRowParams,
13
- type UpdateRowParams,
14
- type ListRowsParams,
15
- type DeleteRowParams,
16
- type CreateTableParams,
17
- type UpdateTableParams,
18
- type ListTablesParams,
19
- type DeleteTableParams,
20
- type GetTableParams,
21
- type BulkCreateRowsParams,
22
- type BulkUpsertRowsParams,
23
- type BulkDeleteRowsParams,
24
- type CreateIndexParams,
25
- type ListIndexesParams,
26
- type DeleteIndexParams,
27
- type CreateAttributeParams,
28
- type UpdateAttributeParams,
29
- type DeleteAttributeParams,
30
- type ApiResponse,
31
- type AdapterMetadata,
32
- AdapterError,
33
- UnsupportedOperationError
34
- } from './DatabaseAdapter.js';
10
+ import { Query } from "node-appwrite";
11
+ import {
12
+ BaseAdapter,
13
+ type CreateRowParams,
14
+ type UpdateRowParams,
15
+ type ListRowsParams,
16
+ type DeleteRowParams,
17
+ type CreateTableParams,
18
+ type UpdateTableParams,
19
+ type ListTablesParams,
20
+ type DeleteTableParams,
21
+ type GetTableParams,
22
+ type BulkCreateRowsParams,
23
+ type BulkUpsertRowsParams,
24
+ type BulkDeleteRowsParams,
25
+ type CreateIndexParams,
26
+ type ListIndexesParams,
27
+ type DeleteIndexParams,
28
+ type CreateAttributeParams,
29
+ type UpdateAttributeParams,
30
+ type DeleteAttributeParams,
31
+ type ApiResponse,
32
+ type AdapterMetadata,
33
+ AdapterError,
34
+ UnsupportedOperationError
35
+ } from './DatabaseAdapter.js';
35
36
 
36
37
  /**
37
38
  * LegacyAdapter - Translates TablesDB calls to legacy Databases API
@@ -586,33 +587,62 @@ export class LegacyAdapter extends BaseAdapter {
586
587
  throw new UnsupportedOperationError('bulkUpsertRows', 'legacy');
587
588
  }
588
589
 
589
- async bulkDeleteRows(params: BulkDeleteRowsParams): Promise<ApiResponse> {
590
- // Legacy doesn't support bulk operations, fallback to individual deletes
591
- const results = [];
592
- const errors = [];
593
-
594
- for (const rowId of params.rowIds) {
595
- try {
596
- await this.deleteRow({
597
- databaseId: params.databaseId,
598
- tableId: params.tableId,
599
- id: rowId
600
- });
601
- results.push({ id: rowId, deleted: true });
602
- } catch (error) {
603
- errors.push({
604
- rowId,
605
- error: error instanceof Error ? error.message : 'Unknown error'
606
- });
607
- }
608
- }
609
-
610
- return {
611
- data: results,
612
- total: results.length,
613
- errors: errors.length > 0 ? errors : undefined
614
- };
615
- }
590
+ async bulkDeleteRows(params: BulkDeleteRowsParams): Promise<ApiResponse> {
591
+ try {
592
+ // Try to use deleteDocuments with queries first (more efficient)
593
+ const queries = params.rowIds.map(id => Query.equal('$id', id));
594
+
595
+ const result = await this.databases.deleteDocuments(
596
+ params.databaseId,
597
+ params.tableId, // Maps tableId to collectionId
598
+ queries
599
+ );
600
+
601
+ return {
602
+ data: result,
603
+ total: params.rowIds.length
604
+ };
605
+ } catch (error) {
606
+ // If deleteDocuments with queries fails, fall back to individual deletes
607
+ const errorMessage = error instanceof Error ? error.message : String(error);
608
+
609
+ // Check if the error indicates that deleteDocuments with queries is not supported
610
+ if (errorMessage.includes('not supported') || errorMessage.includes('invalid') || errorMessage.includes('queries')) {
611
+ // Fall back to individual deletions
612
+ const results = [];
613
+ const errors = [];
614
+
615
+ for (const rowId of params.rowIds) {
616
+ try {
617
+ await this.deleteRow({
618
+ databaseId: params.databaseId,
619
+ tableId: params.tableId,
620
+ id: rowId
621
+ });
622
+ results.push({ id: rowId, deleted: true });
623
+ } catch (individualError) {
624
+ errors.push({
625
+ rowId,
626
+ error: individualError instanceof Error ? individualError.message : 'Unknown error'
627
+ });
628
+ }
629
+ }
630
+
631
+ return {
632
+ data: results,
633
+ total: results.length,
634
+ errors: errors.length > 0 ? errors : undefined
635
+ };
636
+ } else {
637
+ // Re-throw the original error if it's not a support issue
638
+ throw new AdapterError(
639
+ `Failed to bulk delete rows (legacy): ${errorMessage}`,
640
+ 'BULK_DELETE_ROWS_FAILED',
641
+ error instanceof Error ? error : undefined
642
+ );
643
+ }
644
+ }
645
+ }
616
646
 
617
647
  // Metadata and Capabilities
618
648
 
@@ -6,31 +6,32 @@
6
6
  * and returns Models.Row instead of Models.Document.
7
7
  */
8
8
 
9
- import {
10
- BaseAdapter,
11
- type DatabaseAdapter,
12
- type CreateRowParams,
13
- type UpdateRowParams,
14
- type ListRowsParams,
15
- type DeleteRowParams,
16
- type CreateTableParams,
17
- type UpdateTableParams,
18
- type ListTablesParams,
19
- type DeleteTableParams,
20
- type GetTableParams,
21
- type BulkCreateRowsParams,
22
- type BulkUpsertRowsParams,
23
- type BulkDeleteRowsParams,
24
- type CreateIndexParams,
25
- type ListIndexesParams,
26
- type DeleteIndexParams,
27
- type CreateAttributeParams,
28
- type UpdateAttributeParams,
29
- type DeleteAttributeParams,
30
- type ApiResponse,
31
- type AdapterMetadata,
32
- AdapterError
33
- } from './DatabaseAdapter.js';
9
+ import { Query } from "node-appwrite";
10
+ import {
11
+ BaseAdapter,
12
+ type DatabaseAdapter,
13
+ type CreateRowParams,
14
+ type UpdateRowParams,
15
+ type ListRowsParams,
16
+ type DeleteRowParams,
17
+ type CreateTableParams,
18
+ type UpdateTableParams,
19
+ type ListTablesParams,
20
+ type DeleteTableParams,
21
+ type GetTableParams,
22
+ type BulkCreateRowsParams,
23
+ type BulkUpsertRowsParams,
24
+ type BulkDeleteRowsParams,
25
+ type CreateIndexParams,
26
+ type ListIndexesParams,
27
+ type DeleteIndexParams,
28
+ type CreateAttributeParams,
29
+ type UpdateAttributeParams,
30
+ type DeleteAttributeParams,
31
+ type ApiResponse,
32
+ type AdapterMetadata,
33
+ AdapterError
34
+ } from './DatabaseAdapter.js';
34
35
 
35
36
  /**
36
37
  * TablesDBAdapter implementation for native TablesDB API
@@ -513,21 +514,27 @@ export class TablesDBAdapter extends BaseAdapter {
513
514
  }
514
515
  }
515
516
 
516
- async bulkDeleteRows(params: BulkDeleteRowsParams): Promise<ApiResponse> {
517
- try {
518
- const result = await this.tablesDB.bulkDeleteRows(params);
519
- return {
520
- data: result,
521
- total: params.rowIds.length
522
- };
523
- } catch (error) {
524
- throw new AdapterError(
525
- `Failed to bulk delete rows: ${error instanceof Error ? error.message : 'Unknown error'}`,
526
- 'BULK_DELETE_ROWS_FAILED',
527
- error instanceof Error ? error : undefined
528
- );
529
- }
530
- }
517
+ async bulkDeleteRows(params: BulkDeleteRowsParams): Promise<ApiResponse> {
518
+ try {
519
+ // Convert rowIds to queries for the deleteRows API
520
+ const queries = params.rowIds.map(id => Query.equal('$id', id));
521
+ const result = await this.tablesDB.deleteRows({
522
+ databaseId: params.databaseId,
523
+ tableId: params.tableId,
524
+ queries: queries
525
+ });
526
+ return {
527
+ data: result,
528
+ total: params.rowIds.length
529
+ };
530
+ } catch (error) {
531
+ throw new AdapterError(
532
+ `Failed to bulk delete rows: ${error instanceof Error ? error.message : 'Unknown error'}`,
533
+ 'BULK_DELETE_ROWS_FAILED',
534
+ error instanceof Error ? error : undefined
535
+ );
536
+ }
537
+ }
531
538
 
532
539
  // Metadata and Capabilities
533
540
  getMetadata(): AdapterMetadata {
@@ -296,65 +296,65 @@ export const wipeTableRows = async (
296
296
  // Try to use bulk deletion first, fall back to individual deletion
297
297
  const rowIds = rows.map((row: any) => row.$id);
298
298
 
299
- // Check if bulk deletion is available and try it first
300
- if (adapter.bulkDeleteRows) {
301
- try {
302
- // Attempt bulk deletion (available in TablesDB)
303
- await tryBulkDeletion(adapter, databaseId, tableId, rowIds, BULK_DELETE_BATCH_SIZE, MAX_CONCURRENT_OPERATIONS);
304
- totalDeleted += rows.length;
305
- progress.update(totalDeleted);
306
- } catch (bulkError) {
307
- // Enhanced error handling: categorize the error and decide on fallback strategy
308
- const errorMessage = bulkError instanceof Error ? bulkError.message : String(bulkError);
309
-
310
- if (isRetryableError(errorMessage)) {
311
- MessageFormatter.progress(
312
- `Bulk deletion encountered retryable error, retrying with individual deletion for ${rows.length} rows`,
313
- { prefix: "Wipe" }
314
- );
315
- } else if (isBulkNotSupportedError(errorMessage)) {
316
- MessageFormatter.progress(
317
- `Bulk deletion not supported by server, switching to individual deletion for ${rows.length} rows`,
318
- { prefix: "Wipe" }
319
- );
320
- } else {
321
- MessageFormatter.progress(
322
- `Bulk deletion failed (${errorMessage}), falling back to individual deletion for ${rows.length} rows`,
323
- { prefix: "Wipe" }
324
- );
325
- }
326
-
327
- await tryIndividualDeletion(
328
- adapter,
329
- databaseId,
330
- tableId,
331
- rows,
332
- INDIVIDUAL_DELETE_BATCH_SIZE,
333
- MAX_CONCURRENT_OPERATIONS,
334
- progress,
335
- totalDeleted
336
- );
337
- totalDeleted += rows.length;
338
- }
339
- } else {
340
- // Bulk deletion not available, use optimized individual deletion
341
- MessageFormatter.progress(
342
- `Using individual deletion for ${rows.length} rows (bulk deletion not available)`,
343
- { prefix: "Wipe" }
344
- );
345
-
346
- await tryIndividualDeletion(
347
- adapter,
348
- databaseId,
349
- tableId,
350
- rows,
351
- INDIVIDUAL_DELETE_BATCH_SIZE,
352
- MAX_CONCURRENT_OPERATIONS,
353
- progress,
354
- totalDeleted
355
- );
356
- totalDeleted += rows.length;
357
- }
299
+ // Check if bulk deletion is available and try it first
300
+ if (adapter.bulkDeleteRows) {
301
+ try {
302
+ // Attempt bulk deletion (available in TablesDB)
303
+ const deletedCount = await tryBulkDeletion(adapter, databaseId, tableId, rowIds, BULK_DELETE_BATCH_SIZE, MAX_CONCURRENT_OPERATIONS);
304
+ totalDeleted += deletedCount;
305
+ progress.update(totalDeleted);
306
+ } catch (bulkError) {
307
+ // Enhanced error handling: categorize the error and decide on fallback strategy
308
+ const errorMessage = bulkError instanceof Error ? bulkError.message : String(bulkError);
309
+
310
+ if (isRetryableError(errorMessage)) {
311
+ MessageFormatter.progress(
312
+ `Bulk deletion encountered retryable error, retrying with individual deletion for ${rows.length} rows`,
313
+ { prefix: "Wipe" }
314
+ );
315
+ } else if (isBulkNotSupportedError(errorMessage)) {
316
+ MessageFormatter.progress(
317
+ `Bulk deletion not supported by server, switching to individual deletion for ${rows.length} rows`,
318
+ { prefix: "Wipe" }
319
+ );
320
+ } else {
321
+ MessageFormatter.progress(
322
+ `Bulk deletion failed (${errorMessage}), falling back to individual deletion for ${rows.length} rows`,
323
+ { prefix: "Wipe" }
324
+ );
325
+ }
326
+
327
+ const deletedCount = await tryIndividualDeletion(
328
+ adapter,
329
+ databaseId,
330
+ tableId,
331
+ rows,
332
+ INDIVIDUAL_DELETE_BATCH_SIZE,
333
+ MAX_CONCURRENT_OPERATIONS,
334
+ progress,
335
+ totalDeleted
336
+ );
337
+ totalDeleted += deletedCount;
338
+ }
339
+ } else {
340
+ // Bulk deletion not available, use optimized individual deletion
341
+ MessageFormatter.progress(
342
+ `Using individual deletion for ${rows.length} rows (bulk deletion not available)`,
343
+ { prefix: "Wipe" }
344
+ );
345
+
346
+ const deletedCount = await tryIndividualDeletion(
347
+ adapter,
348
+ databaseId,
349
+ tableId,
350
+ rows,
351
+ INDIVIDUAL_DELETE_BATCH_SIZE,
352
+ MAX_CONCURRENT_OPERATIONS,
353
+ progress,
354
+ totalDeleted
355
+ );
356
+ totalDeleted += deletedCount;
357
+ }
358
358
 
359
359
  // Set up cursor for next iteration
360
360
  if (rows.length < FETCH_BATCH_SIZE) {
@@ -393,109 +393,116 @@ export const wipeTableRows = async (
393
393
  /**
394
394
  * Helper function to attempt bulk deletion of row IDs
395
395
  */
396
- async function tryBulkDeletion(
397
- adapter: DatabaseAdapter,
398
- databaseId: string,
399
- tableId: string,
400
- rowIds: string[],
401
- batchSize: number,
402
- maxConcurrent: number
403
- ): Promise<void> {
404
- if (!adapter.bulkDeleteRows) {
405
- throw new Error("Bulk deletion not available on this adapter");
406
- }
407
-
408
- const limit = pLimit(maxConcurrent);
409
- const batches = chunk(rowIds, batchSize);
410
-
411
- const deletePromises = batches.map((batch) =>
412
- limit(async () => {
413
- try {
414
- await tryAwaitWithRetry(async () =>
415
- adapter.bulkDeleteRows!({ databaseId, tableId, rowIds: batch })
416
- );
417
- } catch (error: any) {
418
- const errorMessage = error.message || String(error);
419
-
420
- // Enhanced error handling for bulk deletion
421
- if (isCriticalError(errorMessage)) {
422
- MessageFormatter.error(
423
- `Critical error in bulk deletion batch: ${errorMessage}`,
424
- error,
425
- { prefix: "Wipe" }
426
- );
427
- throw error;
428
- } else {
429
- // For non-critical errors in bulk deletion, re-throw to trigger fallback
430
- throw new Error(`Bulk deletion batch failed: ${errorMessage}`);
431
- }
432
- }
433
- })
434
- );
435
-
436
- await Promise.all(deletePromises);
437
- }
396
+ async function tryBulkDeletion(
397
+ adapter: DatabaseAdapter,
398
+ databaseId: string,
399
+ tableId: string,
400
+ rowIds: string[],
401
+ batchSize: number,
402
+ maxConcurrent: number
403
+ ): Promise<number> {
404
+ if (!adapter.bulkDeleteRows) {
405
+ throw new Error("Bulk deletion not available on this adapter");
406
+ }
407
+
408
+ const limit = pLimit(maxConcurrent);
409
+ const batches = chunk(rowIds, batchSize);
410
+ let successfullyDeleted = 0;
411
+
412
+ const deletePromises = batches.map((batch) =>
413
+ limit(async () => {
414
+ try {
415
+ const result = await tryAwaitWithRetry(async () =>
416
+ adapter.bulkDeleteRows!({ databaseId, tableId, rowIds: batch })
417
+ );
418
+ successfullyDeleted += batch.length; // Assume success if no error thrown
419
+ } catch (error: any) {
420
+ const errorMessage = error.message || String(error);
421
+
422
+ // Enhanced error handling for bulk deletion
423
+ if (isCriticalError(errorMessage)) {
424
+ MessageFormatter.error(
425
+ `Critical error in bulk deletion batch: ${errorMessage}`,
426
+ error,
427
+ { prefix: "Wipe" }
428
+ );
429
+ throw error;
430
+ } else {
431
+ // For non-critical errors in bulk deletion, re-throw to trigger fallback
432
+ throw new Error(`Bulk deletion batch failed: ${errorMessage}`);
433
+ }
434
+ }
435
+ })
436
+ );
437
+
438
+ await Promise.all(deletePromises);
439
+ return successfullyDeleted;
440
+ }
438
441
 
439
442
  /**
440
443
  * Helper function for fallback individual deletion
441
444
  */
442
- async function tryIndividualDeletion(
443
- adapter: DatabaseAdapter,
444
- databaseId: string,
445
- tableId: string,
446
- rows: any[],
447
- batchSize: number,
448
- maxConcurrent: number,
449
- progress: any,
450
- baseDeleted: number
451
- ): Promise<void> {
452
- const limit = pLimit(maxConcurrent);
453
- const batches = chunk(rows, batchSize);
454
- let processedInBatch = 0;
455
-
456
- const deletePromises = batches.map((batch) =>
457
- limit(async () => {
458
- const batchDeletePromises = batch.map(async (row: any) => {
459
- try {
460
- await tryAwaitWithRetry(async () =>
461
- adapter.deleteRow({ databaseId, tableId, id: row.$id })
462
- );
463
- } catch (error: any) {
464
- const errorMessage = error.message || String(error);
465
-
466
- // Enhanced error handling for row deletion
467
- if (errorMessage.includes("Row with the requested ID could not be found")) {
468
- // Row already deleted, skip silently
469
- } else if (isCriticalError(errorMessage)) {
470
- // Critical error, log and rethrow to stop operation
471
- MessageFormatter.error(
472
- `Critical error deleting row ${row.$id}: ${errorMessage}`,
473
- error,
474
- { prefix: "Wipe" }
475
- );
476
- throw error;
477
- } else if (isRetryableError(errorMessage)) {
478
- // Retryable error, will be handled by tryAwaitWithRetry
479
- MessageFormatter.progress(
480
- `Retryable error for row ${row.$id}, will retry`,
481
- { prefix: "Wipe" }
482
- );
483
- } else {
484
- // Other non-critical errors, log but continue
485
- MessageFormatter.error(
486
- `Failed to delete row ${row.$id}: ${errorMessage}`,
487
- error,
488
- { prefix: "Wipe" }
489
- );
490
- }
491
- }
492
- processedInBatch++;
493
- progress.update(baseDeleted + processedInBatch);
494
- });
495
-
496
- await Promise.all(batchDeletePromises);
497
- })
498
- );
499
-
500
- await Promise.all(deletePromises);
501
- }
445
+ async function tryIndividualDeletion(
446
+ adapter: DatabaseAdapter,
447
+ databaseId: string,
448
+ tableId: string,
449
+ rows: any[],
450
+ batchSize: number,
451
+ maxConcurrent: number,
452
+ progress: any,
453
+ baseDeleted: number
454
+ ): Promise<number> {
455
+ const limit = pLimit(maxConcurrent);
456
+ const batches = chunk(rows, batchSize);
457
+ let processedInBatch = 0;
458
+ let successfullyDeleted = 0;
459
+
460
+ const deletePromises = batches.map((batch) =>
461
+ limit(async () => {
462
+ const batchDeletePromises = batch.map(async (row: any) => {
463
+ try {
464
+ await tryAwaitWithRetry(async () =>
465
+ adapter.deleteRow({ databaseId, tableId, id: row.$id })
466
+ );
467
+ successfullyDeleted++;
468
+ } catch (error: any) {
469
+ const errorMessage = error.message || String(error);
470
+
471
+ // Enhanced error handling for row deletion
472
+ if (errorMessage.includes("Row with the requested ID could not be found")) {
473
+ // Row already deleted, count as success since it's gone
474
+ successfullyDeleted++;
475
+ } else if (isCriticalError(errorMessage)) {
476
+ // Critical error, log and rethrow to stop operation
477
+ MessageFormatter.error(
478
+ `Critical error deleting row ${row.$id}: ${errorMessage}`,
479
+ error,
480
+ { prefix: "Wipe" }
481
+ );
482
+ throw error;
483
+ } else if (isRetryableError(errorMessage)) {
484
+ // Retryable error, will be handled by tryAwaitWithRetry
485
+ MessageFormatter.progress(
486
+ `Retryable error for row ${row.$id}, will retry`,
487
+ { prefix: "Wipe" }
488
+ );
489
+ } else {
490
+ // Other non-critical errors, log but continue
491
+ MessageFormatter.error(
492
+ `Failed to delete row ${row.$id}: ${errorMessage}`,
493
+ error,
494
+ { prefix: "Wipe" }
495
+ );
496
+ }
497
+ }
498
+ processedInBatch++;
499
+ progress.update(baseDeleted + successfullyDeleted);
500
+ });
501
+
502
+ await Promise.all(batchDeletePromises);
503
+ })
504
+ );
505
+
506
+ await Promise.all(deletePromises);
507
+ return successfullyDeleted;
508
+ }