appwrite-utils-cli 1.8.9 → 1.9.1

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.
@@ -105,6 +105,15 @@ export interface CreateAttributeParams {
105
105
  default?: any;
106
106
  array?: boolean;
107
107
  encrypt?: boolean;
108
+ min?: number;
109
+ max?: number;
110
+ elements?: string[];
111
+ relatedCollection?: string;
112
+ relationType?: 'oneToOne' | 'manyToOne' | 'oneToMany' | 'manyToMany';
113
+ twoWay?: boolean;
114
+ twoWayKey?: string;
115
+ onDelete?: 'setNull' | 'cascade' | 'restrict';
116
+ side?: 'parent' | 'child';
108
117
  [key: string]: any;
109
118
  }
110
119
  export interface UpdateAttributeParams {
@@ -316,7 +316,7 @@ export class LegacyAdapter extends BaseAdapter {
316
316
  relatedCollectionId: params.relatedCollection || '',
317
317
  type: (params.type || 'oneToOne'),
318
318
  twoWay: params.twoWay ?? false,
319
- onDelete: params.onDelete || 'restrict'
319
+ onDelete: (params.onDelete || 'restrict')
320
320
  });
321
321
  break;
322
322
  default:
@@ -308,10 +308,10 @@ export class TablesDBAdapter extends BaseAdapter {
308
308
  tableId: params.tableId,
309
309
  key: params.key,
310
310
  relatedTableId: params.relatedCollection || '',
311
- type: (params.type || 'oneToOne'),
311
+ type: (params.relationType || 'oneToOne'),
312
312
  twoWay: params.twoWay ?? false,
313
313
  twoWayKey: params.twoWayKey,
314
- onDelete: params.onDelete || 'restrict'
314
+ onDelete: (params.onDelete || 'restrict')
315
315
  });
316
316
  break;
317
317
  default:
@@ -505,7 +505,21 @@ export class TablesDBAdapter extends BaseAdapter {
505
505
  let queries;
506
506
  // Wipe mode: use Query.limit for deleting without fetching
507
507
  if (params.rowIds.length === 0) {
508
- const batchSize = params.batchSize || 250;
508
+ const batchSize = params.batchSize || 1000;
509
+ // Validation: Ensure table exists before wiping
510
+ try {
511
+ await this.getTable({
512
+ databaseId: params.databaseId,
513
+ tableId: params.tableId
514
+ });
515
+ }
516
+ catch (error) {
517
+ const errorMessage = error?.message || String(error);
518
+ if (errorMessage.toLowerCase().includes('not found') || error?.code === 404) {
519
+ throw new AdapterError(`Table '${params.tableId}' not found in database '${params.databaseId}'`, 'TABLE_NOT_FOUND', error);
520
+ }
521
+ throw error; // Re-throw other errors
522
+ }
509
523
  queries = [Query.limit(batchSize)];
510
524
  }
511
525
  // Specific IDs mode: chunk into batches of 80-90 to stay within Appwrite limits
@@ -526,7 +540,18 @@ export class TablesDBAdapter extends BaseAdapter {
526
540
  };
527
541
  }
528
542
  catch (error) {
529
- throw new AdapterError(`Failed to bulk delete rows: ${error instanceof Error ? error.message : 'Unknown error'}`, 'BULK_DELETE_ROWS_FAILED', error instanceof Error ? error : undefined);
543
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
544
+ // Enhanced error categorization
545
+ if (errorMessage.toLowerCase().includes('not found') || error?.code === 404) {
546
+ throw new AdapterError(`Table or rows not found: ${errorMessage}`, 'NOT_FOUND', error instanceof Error ? error : undefined);
547
+ }
548
+ else if (errorMessage.toLowerCase().includes('permission') || errorMessage.toLowerCase().includes('unauthorized') || error?.code === 403) {
549
+ throw new AdapterError(`Permission denied for bulk delete: ${errorMessage}`, 'PERMISSION_DENIED', error instanceof Error ? error : undefined);
550
+ }
551
+ else if (errorMessage.toLowerCase().includes('rate limit') || error?.code === 429) {
552
+ throw new AdapterError(`Rate limit exceeded during bulk delete: ${errorMessage}`, 'RATE_LIMIT', error instanceof Error ? error : undefined);
553
+ }
554
+ throw new AdapterError(`Failed to bulk delete rows: ${errorMessage}`, 'BULK_DELETE_ROWS_FAILED', error instanceof Error ? error : undefined);
530
555
  }
531
556
  }
532
557
  // Metadata and Capabilities
@@ -10,4 +10,5 @@ export declare const databaseCommands: {
10
10
  executeUnifiedBackup(cli: InteractiveCLI, trackingDatabaseId: string, scope: any): Promise<void>;
11
11
  wipeDatabase(cli: InteractiveCLI): Promise<void>;
12
12
  wipeCollections(cli: InteractiveCLI): Promise<void>;
13
+ wipeTablesData(cli: InteractiveCLI): Promise<void>;
13
14
  };
@@ -1,6 +1,7 @@
1
1
  import inquirer from "inquirer";
2
2
  import chalk from "chalk";
3
3
  import { join } from "node:path";
4
+ import { Query } from "node-appwrite";
4
5
  import { MessageFormatter } from "../../shared/messageFormatter.js";
5
6
  import { ConfirmationDialogs } from "../../shared/confirmationDialogs.js";
6
7
  import { SelectionDialogs } from "../../shared/selectionDialogs.js";
@@ -8,6 +9,7 @@ import { logger } from "../../shared/logging.js";
8
9
  import { fetchAllDatabases } from "../../databases/methods.js";
9
10
  import { listBuckets } from "../../storage/methods.js";
10
11
  import { getFunction, downloadLatestFunctionDeployment } from "../../functions/methods.js";
12
+ import { wipeTableRows } from "../../collections/wipeOperations.js";
11
13
  export const databaseCommands = {
12
14
  async syncDb(cli) {
13
15
  MessageFormatter.progress("Pushing local configuration to Appwrite...", { prefix: "Database" });
@@ -550,5 +552,93 @@ export const databaseCommands = {
550
552
  }
551
553
  }
552
554
  MessageFormatter.success("Wipe collections operation completed", { prefix: "Wipe" });
555
+ },
556
+ async wipeTablesData(cli) {
557
+ const controller = cli.controller;
558
+ if (!controller?.adapter) {
559
+ throw new Error("Database adapter is not initialized. TablesDB operations require adapter support.");
560
+ }
561
+ try {
562
+ // Step 1: Select database (single selection for clearer UX)
563
+ const databases = await fetchAllDatabases(controller.database);
564
+ if (!databases || databases.length === 0) {
565
+ MessageFormatter.warning("No databases found", { prefix: "Wipe" });
566
+ return;
567
+ }
568
+ const { selectedDatabase } = await inquirer.prompt([
569
+ {
570
+ type: "list",
571
+ name: "selectedDatabase",
572
+ message: "Select database containing tables to wipe:",
573
+ choices: databases.map((db) => ({
574
+ name: `${db.name} (${db.$id})`,
575
+ value: db
576
+ }))
577
+ }
578
+ ]);
579
+ const database = selectedDatabase;
580
+ // Step 2: Get available tables
581
+ const adapter = controller.adapter;
582
+ const tablesResponse = await adapter.listTables({
583
+ databaseId: database.$id,
584
+ queries: [Query.limit(500)]
585
+ });
586
+ const availableTables = tablesResponse.tables || [];
587
+ if (availableTables.length === 0) {
588
+ MessageFormatter.warning(`No tables found in database: ${database.name}`, { prefix: "Wipe" });
589
+ return;
590
+ }
591
+ // Step 3: Select tables using existing SelectionDialogs
592
+ const selectedTableIds = await SelectionDialogs.selectTablesForDatabase(database.$id, database.name, availableTables, [], // No configured tables context needed for wipe
593
+ {
594
+ showSelectAll: true,
595
+ allowNewOnly: false,
596
+ defaultSelected: []
597
+ });
598
+ if (selectedTableIds.length === 0) {
599
+ MessageFormatter.warning("No tables selected. Operation cancelled.", { prefix: "Wipe" });
600
+ return;
601
+ }
602
+ // Step 4: Show confirmation with table details
603
+ const selectedTables = availableTables.filter((t) => selectedTableIds.includes(t.$id));
604
+ const tableNames = selectedTables.map((t) => t.name);
605
+ console.log(chalk.yellow.bold("\n⚠️ WARNING: Table Row Wipe Operation"));
606
+ console.log(chalk.yellow("This will delete ALL ROWS from the selected tables."));
607
+ console.log(chalk.yellow("The table structures will remain intact.\n"));
608
+ console.log(chalk.cyan("Database:"), chalk.white(database.name));
609
+ console.log(chalk.cyan("Tables to wipe:"));
610
+ tableNames.forEach((name) => console.log(chalk.white(` • ${name}`)));
611
+ console.log();
612
+ const { confirmed } = await inquirer.prompt([
613
+ {
614
+ type: "confirm",
615
+ name: "confirmed",
616
+ message: chalk.red.bold("Are you ABSOLUTELY SURE you want to wipe these table rows?"),
617
+ default: false
618
+ }
619
+ ]);
620
+ if (!confirmed) {
621
+ MessageFormatter.info("Wipe operation cancelled by user", { prefix: "Wipe" });
622
+ return;
623
+ }
624
+ // Step 5: Execute wipe using existing wipeTableRows function
625
+ MessageFormatter.progress("Starting table row wipe operation...", { prefix: "Wipe" });
626
+ for (const table of selectedTables) {
627
+ try {
628
+ MessageFormatter.info(`Wiping rows from table: ${table.name}`, { prefix: "Wipe" });
629
+ // Use existing wipeTableRows from wipeOperations.ts
630
+ await wipeTableRows(adapter, database.$id, table.$id);
631
+ MessageFormatter.success(`Successfully wiped rows from table: ${table.name}`, { prefix: "Wipe" });
632
+ }
633
+ catch (error) {
634
+ MessageFormatter.error(`Failed to wipe table ${table.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Wipe" });
635
+ }
636
+ }
637
+ MessageFormatter.success(`Wipe operation completed for ${selectedTables.length} table(s)`, { prefix: "Wipe" });
638
+ }
639
+ catch (error) {
640
+ MessageFormatter.error("Table wipe operation failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Wipe" });
641
+ throw error;
642
+ }
553
643
  }
554
644
  };
@@ -54,6 +54,11 @@ export interface ConfigLoadOptions {
54
54
  * Override session authentication (used for preserving session on reload)
55
55
  */
56
56
  sessionOverride?: SessionAuthInfo;
57
+ /**
58
+ * Prefer loading from appwrite.config.json over config.yaml
59
+ * @default false
60
+ */
61
+ preferJson?: boolean;
57
62
  }
58
63
  /**
59
64
  * Filter function for collections (accepts both Collection and CollectionCreate)
@@ -139,7 +139,7 @@ export class ConfigManager {
139
139
  }
140
140
  logger.debug("Loading config from file", { prefix: "ConfigManager", options });
141
141
  // 2. Discover config file
142
- const configPath = this.discoveryService.findConfig(options.configDir || process.cwd());
142
+ const configPath = await this.discoveryService.findConfig(options.configDir || process.cwd(), options.preferJson || false);
143
143
  if (!configPath) {
144
144
  const searchDir = options.configDir || process.cwd();
145
145
  throw new Error(`No Appwrite configuration found in "${searchDir}".\n` +
@@ -10,74 +10,76 @@ export interface DiscoveryResult {
10
10
  /**
11
11
  * Service for discovering Appwrite configuration files and collection/table definitions.
12
12
  *
13
+ * Uses find-up for intelligent searching with git repository boundary detection:
14
+ * 1. Finds .git directory to establish repo root boundary
15
+ * 2. Searches UP from current directory to repo root
16
+ * 3. Searches DOWN recursively within repo root
17
+ *
13
18
  * Search Priority:
14
19
  * 1. YAML configs (.appwrite/config.yaml, .appwrite/config.yml, etc.)
15
- * 2. TypeScript configs (appwriteConfig.ts)
16
- * 3. JSON configs (appwrite.json, appwrite.config.json)
17
- *
18
- * Features:
19
- * - Searches up directory tree (max 5 levels)
20
- * - Ignores common directories (node_modules, .git, etc.)
21
- * - Discovers both collections/ and tables/ directories
22
- * - Recursive subdirectory scanning for .appwrite folders
20
+ * 2. JSON configs (appwrite.config.json, appwrite.json)
21
+ * 3. TypeScript configs (appwriteConfig.ts)
23
22
  */
24
23
  export declare class ConfigDiscoveryService {
25
24
  /**
26
25
  * YAML configuration file names to search for
27
26
  */
28
27
  private readonly YAML_FILENAMES;
28
+ /**
29
+ * JSON configuration file names to search for
30
+ */
31
+ private readonly JSON_FILENAMES;
29
32
  /**
30
33
  * TypeScript configuration file names to search for
31
34
  */
32
35
  private readonly TS_FILENAMES;
33
36
  /**
34
- * JSON configuration file names to search for
37
+ * Finds the git repository root directory
38
+ * @param startDir The directory to start searching from
39
+ * @returns Path to the repository root, or startDir if no .git found
35
40
  */
36
- private readonly JSON_FILENAMES;
41
+ private findRepoRoot;
37
42
  /**
38
- * Maximum levels to search up the directory tree
43
+ * Recursively searches downward for files matching patterns
44
+ * @param dir Directory to search in
45
+ * @param patterns File patterns to match
46
+ * @param maxDepth Maximum depth to search
47
+ * @param currentDepth Current recursion depth
48
+ * @returns First matching file path or null
39
49
  */
40
- private readonly MAX_SEARCH_DEPTH;
50
+ private searchDownward;
41
51
  /**
42
- * Finds any configuration file with priority: YAML → TypeScript → JSON
52
+ * Finds any configuration file with configurable priority
43
53
  * @param startDir The directory to start searching from
54
+ * @param preferJson If true, prioritizes appwrite.config.json over YAML (default: false)
44
55
  * @returns Path to the configuration file or null if not found
56
+ *
57
+ * Default priority: YAML → JSON → TypeScript
58
+ * With preferJson=true: JSON → YAML → TypeScript
45
59
  */
46
- findConfig(startDir: string): string | null;
60
+ findConfig(startDir: string, preferJson?: boolean): Promise<string | null>;
47
61
  /**
48
62
  * Finds YAML configuration files
49
- * Searches current directory, subdirectories, and parent directory
63
+ * Searches UP to repo root, then DOWN from repo root
50
64
  * @param startDir The directory to start searching from
65
+ * @param repoRoot The repository root boundary
51
66
  * @returns Path to the YAML config file or null if not found
52
67
  */
53
- findYamlConfig(startDir: string): string | null;
68
+ findYamlConfig(startDir: string, repoRoot?: string): Promise<string | null>;
54
69
  /**
55
- * Recursively searches for YAML configs in .appwrite subdirectories
56
- * @param dir The directory to search
57
- * @param depth Current search depth
58
- * @returns Path to YAML config or null
70
+ * Finds JSON project configuration files (appwrite.config.json, appwrite.json)
71
+ * @param startDir The directory to start searching from
72
+ * @param repoRoot The repository root boundary
73
+ * @returns Path to the JSON config file or null if not found
59
74
  */
60
- private findYamlConfigRecursive;
75
+ findProjectConfig(startDir: string, repoRoot?: string): Promise<string | null>;
61
76
  /**
62
77
  * Finds TypeScript configuration files (appwriteConfig.ts)
63
78
  * @param startDir The directory to start searching from
79
+ * @param repoRoot The repository root boundary
64
80
  * @returns Path to the TypeScript config file or null if not found
65
81
  */
66
- findTypeScriptConfig(startDir: string): string | null;
67
- /**
68
- * Recursively searches for TypeScript configuration files
69
- * @param dir The directory to search
70
- * @param depth Current search depth
71
- * @returns Path to TypeScript config or null
72
- */
73
- private findTypeScriptConfigRecursive;
74
- /**
75
- * Finds project configuration JSON files (appwrite.json, appwrite.config.json)
76
- * Searches up to 5 levels up the directory tree
77
- * @param startDir The directory to start searching from
78
- * @returns Path to the JSON config file or null if not found
79
- */
80
- findProjectConfig(startDir?: string): string | null;
82
+ findTypeScriptConfig(startDir: string, repoRoot?: string): Promise<string | null>;
81
83
  /**
82
84
  * Discovers collection YAML files in a collections/ directory
83
85
  * @param collectionsDir Path to the collections directory
@@ -95,32 +97,26 @@ export declare class ConfigDiscoveryService {
95
97
  * @param startDir The directory to start searching from
96
98
  * @returns Path to .appwrite directory or null if not found
97
99
  */
98
- findAppwriteDirectory(startDir: string): string | null;
100
+ findAppwriteDirectory(startDir: string): Promise<string | null>;
99
101
  /**
100
102
  * Finds the functions directory
101
103
  * @param startDir The directory to start searching from
102
104
  * @returns Path to functions directory or null if not found
103
105
  */
104
- findFunctionsDirectory(startDir: string): string | null;
105
- /**
106
- * Recursively searches for the functions directory
107
- * @param dir The directory to search
108
- * @param depth Current search depth
109
- * @returns Path to functions directory or null
110
- */
111
- private findFunctionsDirectoryRecursive;
106
+ findFunctionsDirectory(startDir: string): Promise<string | null>;
112
107
  /**
113
108
  * Gets a summary of all discoverable configuration files
114
109
  * Useful for debugging configuration issues
115
110
  * @param startDir The directory to start searching from
116
111
  * @returns Object containing paths to all discovered config types
117
112
  */
118
- getConfigurationSummary(startDir: string): {
113
+ getConfigurationSummary(startDir: string): Promise<{
119
114
  yaml: string | null;
120
115
  typescript: string | null;
121
116
  json: string | null;
122
117
  appwriteDirectory: string | null;
123
118
  functionsDirectory: string | null;
124
119
  selectedConfig: string | null;
125
- };
120
+ repoRoot: string;
121
+ }>;
126
122
  }