appwrite-utils-cli 1.7.6 → 1.7.8

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.
Files changed (35) hide show
  1. package/SELECTION_DIALOGS.md +146 -0
  2. package/dist/adapters/DatabaseAdapter.d.ts +1 -0
  3. package/dist/adapters/LegacyAdapter.js +15 -3
  4. package/dist/adapters/TablesDBAdapter.js +15 -3
  5. package/dist/cli/commands/databaseCommands.js +90 -23
  6. package/dist/collections/wipeOperations.d.ts +2 -2
  7. package/dist/collections/wipeOperations.js +37 -139
  8. package/dist/main.js +175 -4
  9. package/dist/migrations/appwriteToX.d.ts +27 -2
  10. package/dist/migrations/appwriteToX.js +293 -69
  11. package/dist/migrations/yaml/YamlImportConfigLoader.d.ts +1 -1
  12. package/dist/migrations/yaml/generateImportSchemas.js +23 -8
  13. package/dist/shared/schemaGenerator.js +25 -12
  14. package/dist/shared/selectionDialogs.d.ts +214 -0
  15. package/dist/shared/selectionDialogs.js +516 -0
  16. package/dist/utils/configDiscovery.d.ts +4 -4
  17. package/dist/utils/configDiscovery.js +66 -30
  18. package/dist/utils/yamlConverter.d.ts +1 -0
  19. package/dist/utils/yamlConverter.js +26 -3
  20. package/dist/utilsController.d.ts +6 -1
  21. package/dist/utilsController.js +91 -2
  22. package/package.json +1 -1
  23. package/src/adapters/DatabaseAdapter.ts +2 -1
  24. package/src/adapters/LegacyAdapter.ts +95 -82
  25. package/src/adapters/TablesDBAdapter.ts +62 -47
  26. package/src/cli/commands/databaseCommands.ts +134 -34
  27. package/src/collections/wipeOperations.ts +62 -224
  28. package/src/main.ts +276 -34
  29. package/src/migrations/appwriteToX.ts +385 -90
  30. package/src/migrations/yaml/generateImportSchemas.ts +26 -8
  31. package/src/shared/schemaGenerator.ts +29 -12
  32. package/src/shared/selectionDialogs.ts +716 -0
  33. package/src/utils/configDiscovery.ts +83 -39
  34. package/src/utils/yamlConverter.ts +29 -3
  35. package/src/utilsController.ts +116 -4
@@ -15,9 +15,15 @@ export function collectionToYaml(collection, config = {
15
15
  const yamlData = {
16
16
  name: collection.name,
17
17
  id: collection.$id,
18
- documentSecurity: collection.documentSecurity,
19
18
  enabled: collection.enabled,
20
19
  };
20
+ // Use appropriate security field based on terminology
21
+ if (config.useTableTerminology) {
22
+ yamlData.rowSecurity = collection.documentSecurity;
23
+ }
24
+ else {
25
+ yamlData.documentSecurity = collection.documentSecurity;
26
+ }
21
27
  // Convert permissions
22
28
  if (collection.$permissions && collection.$permissions.length > 0) {
23
29
  yamlData.permissions = collection.$permissions.map(p => ({
@@ -158,6 +164,11 @@ export function normalizeYamlData(yamlData) {
158
164
  columns: undefined
159
165
  }));
160
166
  }
167
+ // Normalize security fields - prefer documentSecurity for consistency
168
+ if (yamlData.rowSecurity !== undefined && yamlData.documentSecurity === undefined) {
169
+ normalized.documentSecurity = yamlData.rowSecurity;
170
+ delete normalized.rowSecurity;
171
+ }
161
172
  return normalized;
162
173
  }
163
174
  /**
@@ -165,7 +176,8 @@ export function normalizeYamlData(yamlData) {
165
176
  */
166
177
  export function usesTableTerminology(yamlData) {
167
178
  return !!(yamlData.columns && yamlData.columns.length > 0) ||
168
- !!(yamlData.indexes?.some(idx => !!idx.columns));
179
+ !!(yamlData.indexes?.some(idx => !!idx.columns)) ||
180
+ yamlData.rowSecurity !== undefined;
169
181
  }
170
182
  /**
171
183
  * Converts between attribute and column terminology
@@ -190,6 +202,11 @@ export function convertTerminology(yamlData, toTableTerminology) {
190
202
  attributes: idx.attributes // Keep both for compatibility
191
203
  }));
192
204
  }
205
+ // Convert security field
206
+ if (yamlData.documentSecurity !== undefined && yamlData.rowSecurity === undefined) {
207
+ converted.rowSecurity = yamlData.documentSecurity;
208
+ delete converted.documentSecurity;
209
+ }
193
210
  return converted;
194
211
  }
195
212
  else {
@@ -272,7 +289,6 @@ export function generateYamlTemplate(entityName, config) {
272
289
  const template = {
273
290
  name: entityName,
274
291
  id: entityName.toLowerCase().replace(/\s+/g, '_'),
275
- documentSecurity: false,
276
292
  enabled: true,
277
293
  permissions: [
278
294
  {
@@ -294,6 +310,13 @@ export function generateYamlTemplate(entityName, config) {
294
310
  ],
295
311
  importDefs: []
296
312
  };
313
+ // Use appropriate security field based on terminology
314
+ if (config.useTableTerminology) {
315
+ template.rowSecurity = false;
316
+ }
317
+ else {
318
+ template.documentSecurity = false;
319
+ }
297
320
  // Assign fields with correct property name
298
321
  template[fieldsKey] = fieldsArray;
299
322
  template.indexes = indexesArray;
@@ -4,6 +4,7 @@ import { type AfterImportActions, type ConverterFunctions, type ValidationRules
4
4
  import { type TransferOptions } from "./migrations/transfer.js";
5
5
  import type { DatabaseAdapter } from './adapters/DatabaseAdapter.js';
6
6
  import { type ValidationResult } from "./config/configValidation.js";
7
+ import type { DatabaseSelection, BucketSelection } from "./shared/selectionDialogs.js";
7
8
  export interface SetupOptions {
8
9
  databases?: Models.Database[];
9
10
  collections?: string[];
@@ -60,6 +61,9 @@ export declare class UtilsController {
60
61
  ensureDatabasesExist(databases?: Models.Database[]): Promise<void>;
61
62
  ensureCollectionsExist(database: Models.Database, collections?: Models.Collection[]): Promise<void>;
62
63
  getDatabasesByIds(ids: string[]): Promise<Models.Database[] | undefined>;
64
+ fetchAllBuckets(): Promise<{
65
+ buckets: Models.Bucket[];
66
+ }>;
63
67
  wipeOtherDatabases(databasesToKeep: Models.Database[]): Promise<void>;
64
68
  wipeUsers(): Promise<void>;
65
69
  backupDatabase(database: Models.Database, format?: 'json' | 'zip'): Promise<void>;
@@ -78,7 +82,8 @@ export declare class UtilsController {
78
82
  }[], collections?: Models.Collection[]): Promise<void>;
79
83
  generateSchemas(): Promise<void>;
80
84
  importData(options?: SetupOptions): Promise<void>;
81
- synchronizeConfigurations(databases?: Models.Database[], config?: AppwriteConfig): Promise<void>;
85
+ synchronizeConfigurations(databases?: Models.Database[], config?: AppwriteConfig, databaseSelections?: DatabaseSelection[], bucketSelections?: BucketSelection[]): Promise<void>;
86
+ selectiveSync(databaseSelections: DatabaseSelection[], bucketSelections: BucketSelection[]): Promise<void>;
82
87
  syncDb(databases?: Models.Database[], collections?: Models.Collection[]): Promise<void>;
83
88
  getAppwriteFolderPath(): string | undefined;
84
89
  transferData(options: TransferOptions): Promise<void>;
@@ -25,6 +25,7 @@ import { configureLogging, updateLogger, logger } from "./shared/logging.js";
25
25
  import { MessageFormatter, Messages } from "./shared/messageFormatter.js";
26
26
  import { SchemaGenerator } from "./shared/schemaGenerator.js";
27
27
  import { findYamlConfig } from "./config/yamlConfig.js";
28
+ import { createImportSchemas } from "./migrations/yaml/generateImportSchemas.js";
28
29
  import { validateCollectionsTablesConfig, reportValidationResults, validateWithStrictMode } from "./config/configValidation.js";
29
30
  import { ConfigManager } from "./config/ConfigManager.js";
30
31
  import { ClientFactory } from "./utils/ClientFactory.js";
@@ -225,6 +226,24 @@ export class UtilsController {
225
226
  ]);
226
227
  return dbs.databases;
227
228
  }
229
+ async fetchAllBuckets() {
230
+ await this.init();
231
+ if (!this.storage) {
232
+ MessageFormatter.warning("Storage not initialized - buckets will be empty", { prefix: "Controller" });
233
+ return { buckets: [] };
234
+ }
235
+ try {
236
+ const result = await this.storage.listBuckets([
237
+ Query.limit(1000) // Increase limit to get all buckets
238
+ ]);
239
+ MessageFormatter.success(`Found ${result.buckets.length} buckets`, { prefix: "Controller" });
240
+ return result;
241
+ }
242
+ catch (error) {
243
+ MessageFormatter.error(`Failed to fetch buckets: ${error.message || error}`, error instanceof Error ? error : undefined, { prefix: "Controller" });
244
+ return { buckets: [] };
245
+ }
246
+ }
228
247
  async wipeOtherDatabases(databasesToKeep) {
229
248
  await this.init();
230
249
  if (!this.database) {
@@ -448,7 +467,7 @@ export class UtilsController {
448
467
  const importController = new ImportController(this.config, this.database, this.storage, this.appwriteFolderPath, importDataActions, options, options.databases);
449
468
  await importController.run(options.collections);
450
469
  }
451
- async synchronizeConfigurations(databases, config) {
470
+ async synchronizeConfigurations(databases, config, databaseSelections, bucketSelections) {
452
471
  await this.init();
453
472
  if (!this.storage) {
454
473
  MessageFormatter.error("Storage not initialized", undefined, { prefix: "Controller" });
@@ -463,8 +482,25 @@ export class UtilsController {
463
482
  MessageFormatter.error("Failed to get appwriteFolderPath", undefined, { prefix: "Controller" });
464
483
  return;
465
484
  }
485
+ // If selections are provided, filter the databases accordingly
486
+ let filteredDatabases = databases;
487
+ if (databaseSelections && databaseSelections.length > 0) {
488
+ // Convert selections to Models.Database format
489
+ filteredDatabases = [];
490
+ const allDatabases = databases ? databases : await fetchAllDatabases(this.database);
491
+ for (const selection of databaseSelections) {
492
+ const database = allDatabases.find(db => db.$id === selection.databaseId);
493
+ if (database) {
494
+ filteredDatabases.push(database);
495
+ }
496
+ else {
497
+ MessageFormatter.warning(`Database with ID ${selection.databaseId} not found`, { prefix: "Controller" });
498
+ }
499
+ }
500
+ MessageFormatter.info(`Syncing ${filteredDatabases.length} selected databases out of ${allDatabases.length} available`, { prefix: "Controller" });
501
+ }
466
502
  const appwriteToX = new AppwriteToX(configToUse, this.appwriteFolderPath, this.storage);
467
- await appwriteToX.toSchemas(databases);
503
+ await appwriteToX.toSchemas(filteredDatabases);
468
504
  // Update the controller's config with the synchronized collections
469
505
  this.config = appwriteToX.updatedConfig;
470
506
  // Write the updated config back to disk
@@ -472,6 +508,59 @@ export class UtilsController {
472
508
  const yamlConfigPath = findYamlConfig(this.appwriteFolderPath);
473
509
  const isYamlProject = !!yamlConfigPath;
474
510
  await generator.updateConfig(this.config, isYamlProject);
511
+ // Regenerate JSON schemas to reflect any table terminology fixes
512
+ try {
513
+ MessageFormatter.progress("Regenerating JSON schemas...", { prefix: "Sync" });
514
+ await createImportSchemas(this.appwriteFolderPath);
515
+ MessageFormatter.success("JSON schemas regenerated successfully", { prefix: "Sync" });
516
+ }
517
+ catch (error) {
518
+ // Log error but don't fail the sync process
519
+ const errorMessage = error instanceof Error ? error.message : String(error);
520
+ MessageFormatter.warning(`Failed to regenerate JSON schemas, but sync completed: ${errorMessage}`, { prefix: "Sync" });
521
+ logger.warn("Schema regeneration failed during sync:", error);
522
+ }
523
+ }
524
+ async selectiveSync(databaseSelections, bucketSelections) {
525
+ await this.init();
526
+ if (!this.database) {
527
+ MessageFormatter.error("Database not initialized", undefined, { prefix: "Controller" });
528
+ return;
529
+ }
530
+ MessageFormatter.progress("Starting selective sync...", { prefix: "Controller" });
531
+ // Convert database selections to Models.Database format
532
+ const selectedDatabases = [];
533
+ for (const dbSelection of databaseSelections) {
534
+ // Get the full database object from the controller
535
+ const databases = await fetchAllDatabases(this.database);
536
+ const database = databases.find(db => db.$id === dbSelection.databaseId);
537
+ if (database) {
538
+ selectedDatabases.push(database);
539
+ MessageFormatter.info(`Selected database: ${database.name} (${database.$id})`, { prefix: "Controller" });
540
+ // Log selected tables for this database
541
+ if (dbSelection.tableIds && dbSelection.tableIds.length > 0) {
542
+ MessageFormatter.info(` Tables: ${dbSelection.tableIds.join(', ')}`, { prefix: "Controller" });
543
+ }
544
+ }
545
+ else {
546
+ MessageFormatter.warning(`Database with ID ${dbSelection.databaseId} not found`, { prefix: "Controller" });
547
+ }
548
+ }
549
+ if (selectedDatabases.length === 0) {
550
+ MessageFormatter.warning("No valid databases selected for sync", { prefix: "Controller" });
551
+ return;
552
+ }
553
+ // Log bucket selections if provided
554
+ if (bucketSelections && bucketSelections.length > 0) {
555
+ MessageFormatter.info(`Selected ${bucketSelections.length} buckets:`, { prefix: "Controller" });
556
+ for (const bucketSelection of bucketSelections) {
557
+ const dbInfo = bucketSelection.databaseId ? ` (DB: ${bucketSelection.databaseId})` : '';
558
+ MessageFormatter.info(` - ${bucketSelection.bucketName} (${bucketSelection.bucketId})${dbInfo}`, { prefix: "Controller" });
559
+ }
560
+ }
561
+ // Perform selective sync using the enhanced synchronizeConfigurations method
562
+ await this.synchronizeConfigurations(selectedDatabases, this.config, databaseSelections, bucketSelections);
563
+ MessageFormatter.success("Selective sync completed successfully!", { prefix: "Controller" });
475
564
  }
476
565
  async syncDb(databases = [], collections = []) {
477
566
  await this.init();
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.6",
4
+ "version": "1.7.8",
5
5
  "main": "src/main.ts",
6
6
  "type": "module",
7
7
  "repository": {
@@ -86,7 +86,8 @@ export interface BulkUpsertRowsParams {
86
86
  export interface BulkDeleteRowsParams {
87
87
  databaseId: string;
88
88
  tableId: string;
89
- rowIds: string[];
89
+ rowIds: string[]; // Empty array = wipe mode (use Query.limit), otherwise specific IDs to delete
90
+ batchSize?: number; // Optional batch size for wipe mode (default 250)
90
91
  }
91
92
 
92
93
  // Index operation parameters
@@ -7,32 +7,33 @@
7
7
  * older Appwrite instances.
8
8
  */
9
9
 
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';
10
+ import { Query } from "node-appwrite";
11
+ import { chunk } from "es-toolkit";
12
+ import {
13
+ BaseAdapter,
14
+ type CreateRowParams,
15
+ type UpdateRowParams,
16
+ type ListRowsParams,
17
+ type DeleteRowParams,
18
+ type CreateTableParams,
19
+ type UpdateTableParams,
20
+ type ListTablesParams,
21
+ type DeleteTableParams,
22
+ type GetTableParams,
23
+ type BulkCreateRowsParams,
24
+ type BulkUpsertRowsParams,
25
+ type BulkDeleteRowsParams,
26
+ type CreateIndexParams,
27
+ type ListIndexesParams,
28
+ type DeleteIndexParams,
29
+ type CreateAttributeParams,
30
+ type UpdateAttributeParams,
31
+ type DeleteAttributeParams,
32
+ type ApiResponse,
33
+ type AdapterMetadata,
34
+ AdapterError,
35
+ UnsupportedOperationError
36
+ } from './DatabaseAdapter.js';
36
37
 
37
38
  /**
38
39
  * LegacyAdapter - Translates TablesDB calls to legacy Databases API
@@ -587,62 +588,74 @@ export class LegacyAdapter extends BaseAdapter {
587
588
  throw new UnsupportedOperationError('bulkUpsertRows', 'legacy');
588
589
  }
589
590
 
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
- }
591
+ async bulkDeleteRows(params: BulkDeleteRowsParams): Promise<ApiResponse> {
592
+ try {
593
+ let queries: string[];
594
+
595
+ // Wipe mode: use Query.limit for deleting without fetching
596
+ if (params.rowIds.length === 0) {
597
+ const batchSize = params.batchSize || 250;
598
+ queries = [Query.limit(batchSize)];
599
+ }
600
+ // Specific IDs mode: chunk into batches of 80-90 to stay within Appwrite limits
601
+ // (max 100 IDs per Query.equal, and queries must be < 4096 chars total)
602
+ else {
603
+ const ID_BATCH_SIZE = 85; // Safe batch size for Query.equal
604
+ const idBatches = chunk(params.rowIds, ID_BATCH_SIZE);
605
+ queries = idBatches.map(batch => Query.equal('$id', batch));
606
+ }
607
+
608
+ const result = await this.databases.deleteDocuments(
609
+ params.databaseId,
610
+ params.tableId, // Maps tableId to collectionId
611
+ queries
612
+ );
613
+
614
+ return {
615
+ data: result,
616
+ total: params.rowIds.length || (result as any).total || 0
617
+ };
618
+ } catch (error) {
619
+ // If deleteDocuments with queries fails, fall back to individual deletes
620
+ const errorMessage = error instanceof Error ? error.message : String(error);
621
+
622
+ // Check if the error indicates that deleteDocuments with queries is not supported
623
+ if (errorMessage.includes('not supported') || errorMessage.includes('invalid') || errorMessage.includes('queries')) {
624
+ // Fall back to individual deletions
625
+ const results = [];
626
+ const errors = [];
627
+
628
+ for (const rowId of params.rowIds) {
629
+ try {
630
+ await this.deleteRow({
631
+ databaseId: params.databaseId,
632
+ tableId: params.tableId,
633
+ id: rowId
634
+ });
635
+ results.push({ id: rowId, deleted: true });
636
+ } catch (individualError) {
637
+ errors.push({
638
+ rowId,
639
+ error: individualError instanceof Error ? individualError.message : 'Unknown error'
640
+ });
641
+ }
642
+ }
643
+
644
+ return {
645
+ data: results,
646
+ total: results.length,
647
+ errors: errors.length > 0 ? errors : undefined
648
+ };
649
+ } else {
650
+ // Re-throw the original error if it's not a support issue
651
+ throw new AdapterError(
652
+ `Failed to bulk delete rows (legacy): ${errorMessage}`,
653
+ 'BULK_DELETE_ROWS_FAILED',
654
+ error instanceof Error ? error : undefined
655
+ );
656
+ }
657
+ }
658
+ }
646
659
 
647
660
  // Metadata and Capabilities
648
661
 
@@ -6,32 +6,33 @@
6
6
  * and returns Models.Row instead of Models.Document.
7
7
  */
8
8
 
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';
9
+ import { Query } from "node-appwrite";
10
+ import { chunk } from "es-toolkit";
11
+ import {
12
+ BaseAdapter,
13
+ type DatabaseAdapter,
14
+ type CreateRowParams,
15
+ type UpdateRowParams,
16
+ type ListRowsParams,
17
+ type DeleteRowParams,
18
+ type CreateTableParams,
19
+ type UpdateTableParams,
20
+ type ListTablesParams,
21
+ type DeleteTableParams,
22
+ type GetTableParams,
23
+ type BulkCreateRowsParams,
24
+ type BulkUpsertRowsParams,
25
+ type BulkDeleteRowsParams,
26
+ type CreateIndexParams,
27
+ type ListIndexesParams,
28
+ type DeleteIndexParams,
29
+ type CreateAttributeParams,
30
+ type UpdateAttributeParams,
31
+ type DeleteAttributeParams,
32
+ type ApiResponse,
33
+ type AdapterMetadata,
34
+ AdapterError
35
+ } from './DatabaseAdapter.js';
35
36
 
36
37
  /**
37
38
  * TablesDBAdapter implementation for native TablesDB API
@@ -514,27 +515,41 @@ export class TablesDBAdapter extends BaseAdapter {
514
515
  }
515
516
  }
516
517
 
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
- }
518
+ async bulkDeleteRows(params: BulkDeleteRowsParams): Promise<ApiResponse> {
519
+ try {
520
+ let queries: string[];
521
+
522
+ // Wipe mode: use Query.limit for deleting without fetching
523
+ if (params.rowIds.length === 0) {
524
+ const batchSize = params.batchSize || 250;
525
+ queries = [Query.limit(batchSize)];
526
+ }
527
+ // Specific IDs mode: chunk into batches of 80-90 to stay within Appwrite limits
528
+ // (max 100 IDs per Query.equal, and queries must be < 4096 chars total)
529
+ else {
530
+ const ID_BATCH_SIZE = 85; // Safe batch size for Query.equal
531
+ const idBatches = chunk(params.rowIds, ID_BATCH_SIZE);
532
+ queries = idBatches.map(batch => Query.equal('$id', batch));
533
+ }
534
+
535
+ const result = await this.tablesDB.deleteRows({
536
+ databaseId: params.databaseId,
537
+ tableId: params.tableId,
538
+ queries: queries
539
+ });
540
+
541
+ return {
542
+ data: result,
543
+ total: params.rowIds.length || (result as any).total || 0
544
+ };
545
+ } catch (error) {
546
+ throw new AdapterError(
547
+ `Failed to bulk delete rows: ${error instanceof Error ? error.message : 'Unknown error'}`,
548
+ 'BULK_DELETE_ROWS_FAILED',
549
+ error instanceof Error ? error : undefined
550
+ );
551
+ }
552
+ }
538
553
 
539
554
  // Metadata and Capabilities
540
555
  getMetadata(): AdapterMetadata {