appwrite-utils-cli 1.8.3 → 1.8.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +6 -1
  2. package/README.md +42 -13
  3. package/dist/adapters/TablesDBAdapter.js +1 -1
  4. package/dist/cli/commands/functionCommands.js +30 -3
  5. package/dist/cli/commands/schemaCommands.js +39 -4
  6. package/dist/cli/commands/storageCommands.d.ts +5 -0
  7. package/dist/cli/commands/storageCommands.js +143 -0
  8. package/dist/collections/attributes.js +7 -7
  9. package/dist/collections/methods.js +122 -1
  10. package/dist/collections/tableOperations.js +2 -2
  11. package/dist/interactiveCLI.d.ts +1 -0
  12. package/dist/interactiveCLI.js +30 -0
  13. package/dist/main.js +17 -0
  14. package/dist/migrations/appwriteToX.js +1 -1
  15. package/dist/migrations/yaml/generateImportSchemas.js +2 -2
  16. package/dist/setupCommands.js +6 -0
  17. package/dist/shared/attributeMapper.js +2 -2
  18. package/dist/shared/jsonSchemaGenerator.js +3 -1
  19. package/dist/shared/pydanticModelGenerator.d.ts +17 -0
  20. package/dist/shared/pydanticModelGenerator.js +615 -0
  21. package/dist/shared/schemaGenerator.d.ts +3 -2
  22. package/dist/shared/schemaGenerator.js +22 -9
  23. package/dist/storage/methods.js +50 -7
  24. package/dist/utils/configDiscovery.js +2 -3
  25. package/dist/utils/constantsGenerator.d.ts +20 -8
  26. package/dist/utils/constantsGenerator.js +37 -25
  27. package/dist/utils/projectConfig.js +1 -1
  28. package/dist/utils/yamlConverter.d.ts +2 -2
  29. package/dist/utils/yamlConverter.js +2 -2
  30. package/dist/utilsController.js +11 -1
  31. package/package.json +1 -1
  32. package/src/adapters/TablesDBAdapter.ts +1 -1
  33. package/src/cli/commands/functionCommands.ts +28 -3
  34. package/src/cli/commands/schemaCommands.ts +59 -22
  35. package/src/cli/commands/storageCommands.ts +152 -0
  36. package/src/collections/attributes.ts +7 -7
  37. package/src/collections/methods.ts +125 -11
  38. package/src/collections/tableOperations.ts +2 -2
  39. package/src/interactiveCLI.ts +42 -12
  40. package/src/main.ts +32 -9
  41. package/src/migrations/appwriteToX.ts +1 -1
  42. package/src/migrations/yaml/generateImportSchemas.ts +7 -7
  43. package/src/setupCommands.ts +6 -0
  44. package/src/shared/attributeMapper.ts +2 -2
  45. package/src/shared/jsonSchemaGenerator.ts +4 -2
  46. package/src/shared/pydanticModelGenerator.ts +618 -0
  47. package/src/shared/schemaGenerator.ts +38 -25
  48. package/src/storage/methods.ts +67 -23
  49. package/src/utils/configDiscovery.ts +40 -41
  50. package/src/utils/constantsGenerator.ts +43 -26
  51. package/src/utils/projectConfig.ts +11 -11
  52. package/src/utils/yamlConverter.ts +40 -40
  53. package/src/utilsController.ts +30 -20
package/CHANGELOG.md CHANGED
@@ -11,4 +11,9 @@ All notable changes to this project will be documented in this file.
11
11
  - Multi-database targeting supported via `databaseIds` (alongside `databaseId`) on table/collection definitions.
12
12
  - Per-function configuration discovery via `.fnconfig.yaml` anywhere in the repository. Relative `dirPath` resolves from the file’s directory; `~` expands to homedir.
13
13
  - TablesDB/legacy detection is fetch-based; no additional SDK packages required.
14
-
14
+ - New: Python (Pydantic) model generation with modern typing (PEP 604 unions) and Appwrite alias mapping. Generates:
15
+ - `base.py` (always overwritten) with aliased $id/$createdAt/$updatedAt/$permissions and helpers
16
+ - One model per collection/table extending `BaseAppwriteModel`
17
+ - Output directory selectable during interactive flow (absolute paths respected)
18
+ - Constants generator now supports selecting which categories to include (databases, collections/tables, buckets, functions).
19
+ - Bucket management: interactive create/delete commands with YAML persistence when active; selective push now diffs and updates bucket settings (name, permissions, security, limits).
package/README.md CHANGED
@@ -34,13 +34,13 @@ Highlights:
34
34
  - **File Operations**: Complete file handling with URL downloads, local file search, and afterImportActions
35
35
  - **Backup Management**: Comprehensive backup system with progress tracking and detailed reporting
36
36
 
37
- ### Development Tools
38
- - **Database Migrations**: Full migration control with progress tracking and operation summaries
39
- - **Schema Generation**: Generate TypeScript and JSON schemas from database configurations
40
- - **Constants Generation**: Generate cross-language constants files (TypeScript, Python, PHP, Dart, JSON, Env) for database, collection, bucket, and function IDs
41
- - **Data Transfer**: Transfer data between databases, collections, and instances with real-time progress
42
- - **Configuration Sync**: Bidirectional synchronization between local YAML configs and Appwrite projects
43
- - **Function Management**: Deploy and manage Appwrite Functions with specification updates
37
+ ### Development Tools
38
+ - **Database Migrations**: Full migration control with progress tracking and operation summaries
39
+ - **Schema Generation**: Generate TypeScript (Zod), JSON, and Python (Pydantic) schemas/models from database configurations
40
+ - **Constants Generation**: Generate cross-language constants files (TypeScript, Python, PHP, Dart, JSON, Env) for database, collection, bucket, and function IDs
41
+ - **Data Transfer**: Transfer data between databases, collections, and instances with real-time progress
42
+ - **Configuration Sync**: Bidirectional synchronization between local YAML configs and Appwrite projects
43
+ - **Function Management**: Deploy and manage Appwrite Functions with specification updates
44
44
 
45
45
  ## Installation
46
46
 
@@ -213,7 +213,7 @@ logging:
213
213
 
214
214
  After installation, you can access the tool directly from your command line using the provided commands.
215
215
 
216
- ### Interactive Mode
216
+ ### Interactive Mode
217
217
 
218
218
  Run the CLI in interactive mode with enhanced visual feedback:
219
219
 
@@ -221,11 +221,28 @@ Run the CLI in interactive mode with enhanced visual feedback:
221
221
  npx --package=appwrite-utils-cli@latest appwrite-migrate --it
222
222
  ```
223
223
 
224
- This provides a professional guided experience with:
225
- - Rich visual feedback and progress tracking
226
- - Smart confirmation dialogs for destructive operations
227
- - Operation summaries with detailed statistics
228
- - Real-time progress bars with ETA calculations
224
+ This provides a professional guided experience with:
225
+ - Rich visual feedback and progress tracking
226
+ - Smart confirmation dialogs for destructive operations
227
+ - Operation summaries with detailed statistics
228
+ - Real-time progress bars with ETA calculations
229
+
230
+ ### Generate Schemas (Zod / JSON / Pydantic)
231
+
232
+ Interactive schema generation lets you pick the format and output directory:
233
+
234
+ ```bash
235
+ npx appwrite-utils-cli appwrite-migrate --it
236
+ # Choose: Generate schemas
237
+ # Select: TypeScript (Zod), JSON, Python (Pydantic), or All
238
+ # Enter output directory (absolute path respected)
239
+ ```
240
+
241
+ - Pydantic models use modern typing (str | None, list[str]) and alias mapping for Appwrite system fields:
242
+ - Base model (written as `base.py`) defines aliases for `$id`, `$createdAt`, `$updatedAt`, `$permissions`, `$databaseId`, `$collectionId`, `$sequence`.
243
+ - Each collection/table generates a model extending `BaseAppwriteModel`.
244
+ - Serialize with aliases via `model_dump(by_alias=True)` and validate from Appwrite docs via `model_validate(...)`.
245
+ - Output directory is selectable; files are written directly to your chosen path (no extra subfolder).
229
246
 
230
247
  ### Push (manual selection)
231
248
 
@@ -1102,3 +1119,15 @@ Rules:
1102
1119
  - `.fnconfig.yaml` definitions merge with central `.appwrite/config.yaml` functions; if the same `$id` exists in both, `.fnconfig.yaml` overrides
1103
1120
 
1104
1121
  Deployment uses the merged function set and resolves paths according to these rules.
1122
+ ### Generate Constants
1123
+
1124
+ Select which languages and which categories to generate (databases, collections/tables, buckets, functions):
1125
+
1126
+ ```bash
1127
+ npx appwrite-utils-cli appwrite-migrate --it
1128
+ # Choose: Generate cross-language constants
1129
+ # Select languages (TS/JS/Python/PHP/Dart/JSON/Env)
1130
+ # Select categories to include
1131
+ ```
1132
+
1133
+ Constants are written to a configurable output directory under `.appwrite/` by default.
@@ -192,7 +192,7 @@ export class TablesDBAdapter extends BaseAdapter {
192
192
  const type = (params.type || "").toLowerCase();
193
193
  const required = params.required ?? false;
194
194
  const array = params.array ?? false;
195
- const encrypt = params.encrypt ?? params.encrypted ?? false;
195
+ const encrypt = params.encrypt ?? false;
196
196
  const normalizedDefault = params.default === null || params.default === undefined
197
197
  ? undefined
198
198
  : params.default;
@@ -111,11 +111,38 @@ export const functionCommands = {
111
111
  MessageFormatter.error("Failed to initialize controller or load config", undefined, { prefix: "Functions" });
112
112
  return;
113
113
  }
114
- // Discover local .fnconfig.yaml functions and merge into controller config
114
+ // Offer choice of function config sources: central YAML, .fnconfig.yaml, or both
115
+ let sourceChoice = 'both';
116
+ try {
117
+ const answer = await inquirer.prompt([
118
+ {
119
+ type: 'list',
120
+ name: 'source',
121
+ message: 'Select function config source:',
122
+ choices: [
123
+ { name: 'config.yaml functions (central)', value: 'central' },
124
+ { name: '.fnconfig.yaml (discovered per-function)', value: 'fnconfig' },
125
+ { name: 'Both (merge; .fnconfig overrides)', value: 'both' },
126
+ ],
127
+ default: 'both'
128
+ }
129
+ ]);
130
+ sourceChoice = answer.source;
131
+ }
132
+ catch { }
115
133
  try {
116
134
  const discovered = discoverFnConfigs(cli.currentDir);
117
- const merged = mergeDiscoveredFunctions(cli.controller.config.functions || [], discovered);
118
- cli.controller.config.functions = merged;
135
+ const central = cli.controller.config.functions || [];
136
+ if (sourceChoice === 'central') {
137
+ cli.controller.config.functions = central;
138
+ }
139
+ else if (sourceChoice === 'fnconfig') {
140
+ cli.controller.config.functions = discovered;
141
+ }
142
+ else {
143
+ const merged = mergeDiscoveredFunctions(central, discovered);
144
+ cli.controller.config.functions = merged;
145
+ }
119
146
  }
120
147
  catch { }
121
148
  const functions = await cli.selectFunctions("Select function(s) to deploy:", true, true);
@@ -17,9 +17,11 @@ export const schemaCommands = {
17
17
  choices: [
18
18
  { name: "TypeScript (Zod) schemas", value: "zod" },
19
19
  { name: "JSON schemas", value: "json" },
20
- { name: "Both TypeScript and JSON schemas", value: "both" },
20
+ { name: "Python (Pydantic) models", value: "pydantic" },
21
+ { name: "TypeScript + JSON", value: "both" },
22
+ { name: "All (Zod, JSON, Pydantic)", value: "all" },
21
23
  ],
22
- default: "both",
24
+ default: "all",
23
25
  },
24
26
  ]);
25
27
  // Get the config folder path (where the config file is located)
@@ -28,9 +30,21 @@ export const schemaCommands = {
28
30
  MessageFormatter.error("Failed to get config folder path", undefined, { prefix: "Schemas" });
29
31
  return;
30
32
  }
33
+ // Prompt for schema output directory (optional override)
34
+ const defaultSchemaOut = path.join(configFolderPath, cli.controller.config?.schemaConfig?.outputDirectory || 'schemas');
35
+ const { schemaOutDir } = await inquirer.prompt([
36
+ {
37
+ type: 'input',
38
+ name: 'schemaOutDir',
39
+ message: 'Output directory for schemas:',
40
+ default: defaultSchemaOut,
41
+ validate: (input) => input && input.trim().length > 0 ? true : 'Please provide an output directory',
42
+ }
43
+ ]);
31
44
  // Create SchemaGenerator with the correct base path and generate schemas
32
45
  const schemaGenerator = new SchemaGenerator(cli.controller.config, configFolderPath);
33
- schemaGenerator.generateSchemas({ format: schemaType, verbose: true });
46
+ const outDirRel = path.isAbsolute(schemaOutDir) ? schemaOutDir : path.relative(configFolderPath, schemaOutDir);
47
+ await schemaGenerator.generateSchemas({ format: schemaType, verbose: true, outputDir: outDirRel });
34
48
  MessageFormatter.success("Schema generation completed", { prefix: "Schemas" });
35
49
  },
36
50
  async generateConstants(cli) {
@@ -62,6 +76,21 @@ export const schemaCommands = {
62
76
  },
63
77
  },
64
78
  ]);
79
+ // Prompt for which constants to include
80
+ const { includeWhat } = await inquirer.prompt([
81
+ {
82
+ type: 'checkbox',
83
+ name: 'includeWhat',
84
+ message: 'Select which constants to generate:',
85
+ choices: [
86
+ { name: 'Databases', value: 'databases', checked: true },
87
+ { name: 'Collections/Tables', value: 'collections', checked: true },
88
+ { name: 'Buckets', value: 'buckets', checked: true },
89
+ { name: 'Functions', value: 'functions', checked: true },
90
+ ],
91
+ validate: (input) => input.length > 0 ? true : 'Select at least one category',
92
+ }
93
+ ]);
65
94
  // Determine default output directory based on config location
66
95
  const configPath = cli.controller.getAppwriteFolderPath();
67
96
  const defaultOutputDir = configPath
@@ -85,8 +114,14 @@ export const schemaCommands = {
85
114
  try {
86
115
  const { ConstantsGenerator } = await import("../../utils/constantsGenerator.js");
87
116
  const generator = new ConstantsGenerator(cli.controller.config);
117
+ const include = {
118
+ databases: includeWhat.includes('databases'),
119
+ collections: includeWhat.includes('collections'),
120
+ buckets: includeWhat.includes('buckets'),
121
+ functions: includeWhat.includes('functions'),
122
+ };
88
123
  MessageFormatter.info(`Generating constants for: ${languages.join(", ")}`, { prefix: "Constants" });
89
- await generator.generateFiles(languages, outputDir);
124
+ await generator.generateFiles(languages, outputDir, include);
90
125
  MessageFormatter.success(`Constants generated in ${outputDir}`, { prefix: "Constants" });
91
126
  }
92
127
  catch (error) {
@@ -0,0 +1,5 @@
1
+ import type { InteractiveCLI } from "../../interactiveCLI.js";
2
+ export declare const storageCommands: {
3
+ createBucket(cli: InteractiveCLI): Promise<void>;
4
+ deleteBuckets(cli: InteractiveCLI): Promise<void>;
5
+ };
@@ -0,0 +1,143 @@
1
+ import inquirer from "inquirer";
2
+ import chalk from "chalk";
3
+ import { Storage, Permission, Role, Compression } from "node-appwrite";
4
+ import { MessageFormatter } from "../../shared/messageFormatter.js";
5
+ import { listBuckets, createBucket as createBucketApi, deleteBucket as deleteBucketApi } from "../../storage/methods.js";
6
+ import { writeYamlConfig } from "../../config/yamlConfig.js";
7
+ import { ConfigManager } from "../../config/ConfigManager.js";
8
+ export const storageCommands = {
9
+ async createBucket(cli) {
10
+ const storage = cli.controller.storage;
11
+ if (!storage) {
12
+ MessageFormatter.error("Storage client not initialized", undefined, { prefix: "Buckets" });
13
+ return;
14
+ }
15
+ const answers = await inquirer.prompt([
16
+ { type: 'input', name: 'name', message: 'Bucket name:', validate: (v) => v?.trim()?.length > 0 || 'Required' },
17
+ { type: 'input', name: 'id', message: 'Bucket ID (optional):' },
18
+ { type: 'confirm', name: 'publicRead', message: 'Public read access?', default: false },
19
+ { type: 'confirm', name: 'fileSecurity', message: 'Enable file-level security?', default: false },
20
+ { type: 'confirm', name: 'enabled', message: 'Enable bucket?', default: true },
21
+ { type: 'number', name: 'maximumFileSize', message: 'Max file size (bytes, optional):', default: undefined },
22
+ { type: 'input', name: 'allowedFileExtensions', message: 'Allowed extensions (comma separated, optional):', default: '' },
23
+ { type: 'list', name: 'compression', message: 'Compression:', choices: ['none', 'gzip', 'zstd'], default: 'none' },
24
+ { type: 'confirm', name: 'encryption', message: 'Enable encryption?', default: false },
25
+ { type: 'confirm', name: 'antivirus', message: 'Enable antivirus?', default: false },
26
+ ]);
27
+ const permissions = [];
28
+ if (answers.publicRead)
29
+ permissions.push(Permission.read(Role.any()));
30
+ const bucketInput = {
31
+ name: answers.name,
32
+ $permissions: permissions,
33
+ fileSecurity: !!answers.fileSecurity,
34
+ enabled: !!answers.enabled,
35
+ maximumFileSize: answers.maximumFileSize || undefined,
36
+ allowedFileExtensions: String(answers.allowedFileExtensions || '')
37
+ .split(',')
38
+ .map((s) => s.trim())
39
+ .filter(Boolean),
40
+ compression: answers.compression,
41
+ encryption: !!answers.encryption,
42
+ antivirus: !!answers.antivirus,
43
+ };
44
+ try {
45
+ const created = await createBucketApi(storage, bucketInput, answers.id || undefined);
46
+ MessageFormatter.success(`Bucket '${answers.name}' created`, { prefix: 'Buckets' });
47
+ // Update in-memory config and persist to YAML as a global bucket entry
48
+ const controller = cli.controller;
49
+ controller.config.buckets = controller.config.buckets || [];
50
+ controller.config.buckets.push({
51
+ $id: created.$id || answers.id || bucketInput.name,
52
+ name: bucketInput.name,
53
+ permissions: [],
54
+ fileSecurity: bucketInput.fileSecurity,
55
+ enabled: bucketInput.enabled,
56
+ maximumFileSize: bucketInput.maximumFileSize,
57
+ allowedFileExtensions: bucketInput.allowedFileExtensions,
58
+ compression: bucketInput.compression,
59
+ encryption: bucketInput.encryption,
60
+ antivirus: bucketInput.antivirus,
61
+ });
62
+ const cfgMgr = ConfigManager.getInstance();
63
+ const cfgPath = cfgMgr.getConfigPath();
64
+ if (cfgPath && /\.ya?ml$/i.test(cfgPath)) {
65
+ await writeYamlConfig(cfgPath, controller.config);
66
+ MessageFormatter.info(`Added bucket to config.yaml`, { prefix: 'Buckets' });
67
+ }
68
+ else {
69
+ MessageFormatter.warning(`Config is not YAML; updated in-memory only. Please update your TypeScript config manually.`, { prefix: 'Buckets' });
70
+ }
71
+ }
72
+ catch (e) {
73
+ MessageFormatter.error('Failed to create bucket', e instanceof Error ? e : new Error(String(e)), { prefix: 'Buckets' });
74
+ }
75
+ },
76
+ async deleteBuckets(cli) {
77
+ const storage = cli.controller.storage;
78
+ if (!storage) {
79
+ MessageFormatter.error("Storage client not initialized", undefined, { prefix: "Buckets" });
80
+ return;
81
+ }
82
+ try {
83
+ const res = await listBuckets(storage);
84
+ const buckets = res.buckets || [];
85
+ if (buckets.length === 0) {
86
+ MessageFormatter.info('No buckets found', { prefix: 'Buckets' });
87
+ return;
88
+ }
89
+ const { toDelete } = await inquirer.prompt([
90
+ {
91
+ type: 'checkbox',
92
+ name: 'toDelete',
93
+ message: chalk.red('Select buckets to delete:'),
94
+ choices: buckets.map((b) => ({ name: `${b.name} (${b.$id})`, value: b.$id })),
95
+ pageSize: 10,
96
+ }
97
+ ]);
98
+ if (!toDelete || toDelete.length === 0) {
99
+ MessageFormatter.info('No buckets selected', { prefix: 'Buckets' });
100
+ return;
101
+ }
102
+ const { confirm } = await inquirer.prompt([
103
+ { type: 'confirm', name: 'confirm', message: `Delete ${toDelete.length} bucket(s)?`, default: false }
104
+ ]);
105
+ if (!confirm)
106
+ return;
107
+ const controller = cli.controller;
108
+ for (const id of toDelete) {
109
+ try {
110
+ await deleteBucketApi(storage, id);
111
+ MessageFormatter.success(`Deleted bucket ${id}`, { prefix: 'Buckets' });
112
+ // Remove from in-memory config (global buckets)
113
+ if (Array.isArray(controller.config.buckets)) {
114
+ controller.config.buckets = controller.config.buckets.filter((b) => b.$id !== id);
115
+ }
116
+ // Clear database-linked bucket references if matching
117
+ if (Array.isArray(controller.config.databases)) {
118
+ controller.config.databases = controller.config.databases.map((db) => ({
119
+ ...db,
120
+ bucket: db.bucket && db.bucket.$id === id ? undefined : db.bucket,
121
+ }));
122
+ }
123
+ }
124
+ catch (e) {
125
+ MessageFormatter.error(`Failed to delete bucket ${id}`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Buckets' });
126
+ }
127
+ }
128
+ // Persist YAML changes
129
+ const cfgMgr = ConfigManager.getInstance();
130
+ const cfgPath = cfgMgr.getConfigPath();
131
+ if (cfgPath && /\.ya?ml$/i.test(cfgPath)) {
132
+ await writeYamlConfig(cfgPath, controller.config);
133
+ MessageFormatter.info(`Updated config.yaml after deletion`, { prefix: 'Buckets' });
134
+ }
135
+ else {
136
+ MessageFormatter.warning(`Config is not YAML; updated in-memory only. Please update your TypeScript config manually.`, { prefix: 'Buckets' });
137
+ }
138
+ }
139
+ catch (e) {
140
+ MessageFormatter.error('Failed to list buckets', e instanceof Error ? e : new Error(String(e)), { prefix: 'Buckets' });
141
+ }
142
+ }
143
+ };
@@ -266,8 +266,8 @@ const createAttributeViaAdapter = async (db, dbId, collectionId, attribute) => {
266
266
  ...(attribute.size && { size: attribute.size }),
267
267
  ...(attribute.xdefault !== undefined &&
268
268
  !attribute.required && { default: attribute.xdefault }),
269
- ...(attribute.encrypted && {
270
- encrypt: attribute.encrypted,
269
+ ...(attribute.encrypt && {
270
+ encrypt: attribute.encrypt,
271
271
  }),
272
272
  ...(attribute.min !== undefined && {
273
273
  min: attribute.min,
@@ -334,7 +334,7 @@ const updateAttributeViaAdapter = async (db, dbId, collectionId, attribute) => {
334
334
  size: attribute.size,
335
335
  min: attribute.min,
336
336
  max: attribute.max,
337
- encrypt: attribute.encrypted ?? attribute.encrypt,
337
+ encrypt: attribute.encrypt,
338
338
  elements: attribute.elements,
339
339
  relatedCollection: attribute.relatedCollection,
340
340
  relationType: attribute.relationType,
@@ -375,13 +375,13 @@ const createLegacyAttribute = async (db, dbId, collectionId, attribute) => {
375
375
  ? attribute.xdefault
376
376
  : undefined,
377
377
  array: attribute.array || false,
378
- encrypted: attribute.encrypted,
378
+ encrypt: attribute.encrypt,
379
379
  };
380
380
  logger.debug(`Creating string attribute '${attribute.key}'`, {
381
381
  ...stringParams,
382
382
  operation: "createLegacyAttribute",
383
383
  });
384
- await db.createStringAttribute(dbId, collectionId, attribute.key, stringParams.size, stringParams.required, stringParams.defaultValue, stringParams.array, stringParams.encrypted);
384
+ await db.createStringAttribute(dbId, collectionId, attribute.key, stringParams.size, stringParams.required, stringParams.defaultValue, stringParams.array, stringParams.encrypt);
385
385
  break;
386
386
  case "integer":
387
387
  const integerParams = {
@@ -686,7 +686,7 @@ const getComparableFields = (type) => {
686
686
  const baseFields = ["key", "type", "array", "required", "xdefault"];
687
687
  switch (type) {
688
688
  case "string":
689
- return [...baseFields, "size", "encrypted"];
689
+ return [...baseFields, "size", "encrypt"];
690
690
  case "integer":
691
691
  case "double":
692
692
  case "float":
@@ -714,7 +714,7 @@ const getComparableFields = (type) => {
714
714
  "key",
715
715
  "type",
716
716
  "array",
717
- "encrypted",
717
+ "encrypt",
718
718
  "required",
719
719
  "size",
720
720
  "min",
@@ -118,7 +118,7 @@ export const fetchAndCacheCollectionByName = async (db, dbId, collectionName) =>
118
118
  };
119
119
  export const generateSchemas = async (config, appwriteFolderPath) => {
120
120
  const schemaGenerator = new SchemaGenerator(config, appwriteFolderPath);
121
- schemaGenerator.generateSchemas();
121
+ await schemaGenerator.generateSchemas();
122
122
  };
123
123
  export const createOrUpdateCollections = async (database, databaseId, config, deletedCollections, selectedCollections = []) => {
124
124
  // Clear processing state at the start of a new operation
@@ -235,6 +235,11 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
235
235
  const plusminus = plan.toUpdate.map((u) => u.attribute.key);
236
236
  const minus = plan.toRecreate.map((r) => r.newAttribute.key);
237
237
  const skip = plan.unchanged;
238
+ // Compute deletions (remote extras not present locally)
239
+ const desiredKeysForDelete = new Set((attributes || []).map((a) => a.key));
240
+ const extraRemoteKeys = (existingCols || [])
241
+ .map((c) => c?.key)
242
+ .filter((k) => !!k && !desiredKeysForDelete.has(k));
238
243
  const parts = [];
239
244
  if (plus.length)
240
245
  parts.push(`➕ ${plus.length} (${plus.join(', ')})`);
@@ -244,6 +249,7 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
244
249
  parts.push(`♻️ ${minus.length} (${minus.join(', ')})`);
245
250
  if (skip.length)
246
251
  parts.push(`⏭️ ${skip.length}`);
252
+ parts.push(`🗑️ ${extraRemoteKeys.length}${extraRemoteKeys.length ? ` (${extraRemoteKeys.join(', ')})` : ''}`);
247
253
  MessageFormatter.info(`Plan → ${parts.join(' | ') || 'no changes'}`, { prefix: 'Attributes' });
248
254
  // Execute
249
255
  const colResults = await executeColumnOperations(adapter, databaseId, tableId, plan);
@@ -458,6 +464,121 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
458
464
  catch (e) {
459
465
  MessageFormatter.error(`Failed to list/create indexes`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Indexes' });
460
466
  }
467
+ // Deletions for indexes: remove remote indexes not declared in YAML/config
468
+ try {
469
+ const desiredIndexKeys = new Set((indexes || []).map((i) => i.key));
470
+ const idxRes = await adapter.listIndexes({ databaseId, tableId });
471
+ const existingIdx = idxRes.data || idxRes.indexes || [];
472
+ const extraIdx = existingIdx
473
+ .filter((i) => i?.key && !desiredIndexKeys.has(i.key))
474
+ .map((i) => i.key);
475
+ if (extraIdx.length > 0) {
476
+ MessageFormatter.info(`Plan → 🗑️ ${extraIdx.length} indexes (${extraIdx.join(', ')})`, { prefix: 'Indexes' });
477
+ const deleted = [];
478
+ const errors = [];
479
+ for (const key of extraIdx) {
480
+ try {
481
+ await adapter.deleteIndex({ databaseId, tableId, key });
482
+ // Optionally wait for index to disappear
483
+ const start = Date.now();
484
+ const maxWait = 30000;
485
+ while (Date.now() - start < maxWait) {
486
+ try {
487
+ const li = await adapter.listIndexes({ databaseId, tableId });
488
+ const list = li.data || li.indexes || [];
489
+ if (!list.find((ix) => ix.key === key))
490
+ break;
491
+ }
492
+ catch { }
493
+ await delay(1000);
494
+ }
495
+ deleted.push(key);
496
+ }
497
+ catch (e) {
498
+ errors.push({ key, error: e?.message || String(e) });
499
+ }
500
+ }
501
+ if (deleted.length) {
502
+ MessageFormatter.success(`Deleted ${deleted.length} indexes: ${deleted.join(', ')}`, { prefix: 'Indexes' });
503
+ }
504
+ if (errors.length) {
505
+ MessageFormatter.error(`${errors.length} index deletions failed`, undefined, { prefix: 'Indexes' });
506
+ errors.forEach(er => MessageFormatter.error(` ${er.key}: ${er.error}`, undefined, { prefix: 'Indexes' }));
507
+ }
508
+ }
509
+ else {
510
+ MessageFormatter.info(`Plan → 🗑️ 0 indexes`, { prefix: 'Indexes' });
511
+ }
512
+ }
513
+ catch (e) {
514
+ MessageFormatter.warning(`Could not evaluate index deletions: ${e?.message || e}`, { prefix: 'Indexes' });
515
+ }
516
+ // Deletions: remove columns/attributes that are present remotely but not in desired config
517
+ try {
518
+ const desiredKeys = new Set((attributes || []).map((a) => a.key));
519
+ const tableInfo3 = await adapter.getTable({ databaseId, tableId });
520
+ const existingCols3 = tableInfo3.data?.columns || tableInfo3.data?.attributes || [];
521
+ const toDelete = existingCols3
522
+ .filter((col) => col?.key && !desiredKeys.has(col.key))
523
+ .map((col) => col.key);
524
+ if (toDelete.length > 0) {
525
+ MessageFormatter.info(`Plan → 🗑️ ${toDelete.length} (${toDelete.join(', ')})`, { prefix: 'Attributes' });
526
+ const deleted = [];
527
+ const errors = [];
528
+ for (const key of toDelete) {
529
+ try {
530
+ // Drop any indexes that reference this attribute to avoid server errors
531
+ try {
532
+ const idxRes = await adapter.listIndexes({ databaseId, tableId });
533
+ const ilist = idxRes.data || idxRes.indexes || [];
534
+ for (const idx of ilist) {
535
+ const attrs = Array.isArray(idx.attributes) ? idx.attributes : [];
536
+ if (attrs.includes(key)) {
537
+ MessageFormatter.info(`🗑️ Deleting index '${idx.key}' referencing '${key}'`, { prefix: 'Indexes' });
538
+ await adapter.deleteIndex({ databaseId, tableId, key: idx.key });
539
+ await delay(500);
540
+ }
541
+ }
542
+ }
543
+ catch { }
544
+ await adapter.deleteAttribute({ databaseId, tableId, key });
545
+ // Wait briefly for deletion to settle
546
+ const start = Date.now();
547
+ const maxWaitMs = 60000;
548
+ while (Date.now() - start < maxWaitMs) {
549
+ try {
550
+ const tinfo = await adapter.getTable({ databaseId, tableId });
551
+ const cols = tinfo.data?.columns || tinfo.data?.attributes || [];
552
+ const found = cols.find((c) => c.key === key);
553
+ if (!found)
554
+ break;
555
+ if (found.status && found.status !== 'deleting')
556
+ break;
557
+ }
558
+ catch { }
559
+ await delay(1000);
560
+ }
561
+ deleted.push(key);
562
+ }
563
+ catch (e) {
564
+ errors.push({ key, error: e?.message || String(e) });
565
+ }
566
+ }
567
+ if (deleted.length) {
568
+ MessageFormatter.success(`Deleted ${deleted.length} attributes: ${deleted.join(', ')}`, { prefix: 'Attributes' });
569
+ }
570
+ if (errors.length) {
571
+ MessageFormatter.error(`${errors.length} deletions failed`, undefined, { prefix: 'Attributes' });
572
+ errors.forEach(er => MessageFormatter.error(` ${er.key}: ${er.error}`, undefined, { prefix: 'Attributes' }));
573
+ }
574
+ }
575
+ else {
576
+ MessageFormatter.info(`Plan → 🗑️ 0`, { prefix: 'Attributes' });
577
+ }
578
+ }
579
+ catch (e) {
580
+ MessageFormatter.warning(`Could not evaluate deletions: ${e?.message || e}`, { prefix: 'Attributes' });
581
+ }
461
582
  // Mark this table as fully processed to prevent re-processing
462
583
  markCollectionProcessed(tableId, collectionData.name);
463
584
  }
@@ -62,7 +62,7 @@ export function normalizeAttributeToComparable(attr) {
62
62
  };
63
63
  if (t === 'string') {
64
64
  base.size = attr.size ?? 255;
65
- base.encrypt = !!(attr.encrypted ?? attr.encrypt);
65
+ base.encrypt = !!(attr.encrypt);
66
66
  }
67
67
  if (t === 'integer' || t === 'float' || t === 'double') {
68
68
  const min = toNumber(attr.min);
@@ -220,7 +220,7 @@ function compareColumnProperties(oldColumn, newAttribute, columnType) {
220
220
  if (prop === 'default')
221
221
  return na.xdefault;
222
222
  if (prop === 'encrypt')
223
- return na.encrypted ?? na.encrypt;
223
+ return na.encrypt;
224
224
  return na[prop];
225
225
  };
226
226
  const getOldVal = (prop) => {
@@ -5,6 +5,7 @@ export declare class InteractiveCLI {
5
5
  constructor(currentDir: string);
6
6
  run(): Promise<void>;
7
7
  private initControllerIfNeeded;
8
+ private manageBuckets;
8
9
  private selectDatabases;
9
10
  private selectCollections;
10
11
  /**
@@ -19,6 +19,7 @@ import { findYamlConfig } from "./config/yamlConfig.js";
19
19
  import { configCommands } from "./cli/commands/configCommands.js";
20
20
  import { databaseCommands } from "./cli/commands/databaseCommands.js";
21
21
  import { functionCommands } from "./cli/commands/functionCommands.js";
22
+ import { storageCommands } from "./cli/commands/storageCommands.js";
22
23
  import { transferCommands } from "./cli/commands/transferCommands.js";
23
24
  import { schemaCommands } from "./cli/commands/schemaCommands.js";
24
25
  var CHOICES;
@@ -44,6 +45,7 @@ var CHOICES;
44
45
  CHOICES["IMPORT_DATA"] = "\uD83D\uDCE5 Import data";
45
46
  CHOICES["RELOAD_CONFIG"] = "\uD83D\uDD04 Reload configuration files";
46
47
  CHOICES["UPDATE_FUNCTION_SPEC"] = "\u2699\uFE0F Update function specifications";
48
+ CHOICES["MANAGE_BUCKETS"] = "\uD83E\uDEA3 Manage storage buckets";
47
49
  CHOICES["EXIT"] = "\uD83D\uDC4B Exit";
48
50
  })(CHOICES || (CHOICES = {}));
49
51
  export class InteractiveCLI {
@@ -152,6 +154,9 @@ export class InteractiveCLI {
152
154
  await this.initControllerIfNeeded();
153
155
  await functionCommands.updateFunctionSpec(this);
154
156
  break;
157
+ case CHOICES.MANAGE_BUCKETS:
158
+ await this.manageBuckets();
159
+ break;
155
160
  case CHOICES.EXIT:
156
161
  MessageFormatter.success("Goodbye!");
157
162
  process.exit(0);
@@ -187,6 +192,31 @@ export class InteractiveCLI {
187
192
  // If no directConfig provided, keep existing controller
188
193
  }
189
194
  }
195
+ async manageBuckets() {
196
+ await this.initControllerIfNeeded();
197
+ while (true) {
198
+ const { action } = await inquirer.prompt([
199
+ {
200
+ type: 'list',
201
+ name: 'action',
202
+ message: chalk.blue('Bucket management'),
203
+ choices: [
204
+ { name: 'Create bucket', value: 'create' },
205
+ { name: 'Delete buckets', value: 'delete' },
206
+ { name: 'Back', value: 'back' },
207
+ ],
208
+ },
209
+ ]);
210
+ if (action === 'back')
211
+ break;
212
+ if (action === 'create') {
213
+ await storageCommands.createBucket(this);
214
+ }
215
+ else if (action === 'delete') {
216
+ await storageCommands.deleteBuckets(this);
217
+ }
218
+ }
219
+ }
190
220
  async selectDatabases(databases, message, multiSelect = true) {
191
221
  await this.initControllerIfNeeded();
192
222
  const configDatabases = this.getLocalDatabases();