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
@@ -0,0 +1,152 @@
1
+ import inquirer from "inquirer";
2
+ import chalk from "chalk";
3
+ import { Storage, Permission, Role, Compression, type Models } from "node-appwrite";
4
+ import type { InteractiveCLI } from "../../interactiveCLI.js";
5
+ import { MessageFormatter } from "../../shared/messageFormatter.js";
6
+ import { listBuckets, createBucket as createBucketApi, deleteBucket as deleteBucketApi } from "../../storage/methods.js";
7
+ import { writeYamlConfig } from "../../config/yamlConfig.js";
8
+ import { ConfigManager } from "../../config/ConfigManager.js";
9
+
10
+ export const storageCommands = {
11
+ async createBucket(cli: InteractiveCLI): Promise<void> {
12
+ const storage: Storage = (cli as any).controller!.storage!;
13
+ if (!storage) {
14
+ MessageFormatter.error("Storage client not initialized", undefined, { prefix: "Buckets" });
15
+ return;
16
+ }
17
+
18
+ const answers = await inquirer.prompt([
19
+ { type: 'input', name: 'name', message: 'Bucket name:', validate: (v:string)=> v?.trim()?.length>0 || 'Required' },
20
+ { type: 'input', name: 'id', message: 'Bucket ID (optional):' },
21
+ { type: 'confirm', name: 'publicRead', message: 'Public read access?', default: false },
22
+ { type: 'confirm', name: 'fileSecurity', message: 'Enable file-level security?', default: false },
23
+ { type: 'confirm', name: 'enabled', message: 'Enable bucket?', default: true },
24
+ { type: 'number', name: 'maximumFileSize', message: 'Max file size (bytes, optional):', default: undefined },
25
+ { type: 'input', name: 'allowedFileExtensions', message: 'Allowed extensions (comma separated, optional):', default: '' },
26
+ { type: 'list', name: 'compression', message: 'Compression:', choices: ['none','gzip','zstd'], default: 'none' },
27
+ { type: 'confirm', name: 'encryption', message: 'Enable encryption?', default: false },
28
+ { type: 'confirm', name: 'antivirus', message: 'Enable antivirus?', default: false },
29
+ ]);
30
+
31
+ const permissions: string[] = [];
32
+ if (answers.publicRead) permissions.push(Permission.read(Role.any()));
33
+
34
+ const bucketInput: Omit<Models.Bucket, "$id" | "$createdAt" | "$updatedAt"> = {
35
+ name: answers.name,
36
+ $permissions: permissions,
37
+ fileSecurity: !!answers.fileSecurity,
38
+ enabled: !!answers.enabled,
39
+ maximumFileSize: answers.maximumFileSize || undefined,
40
+ allowedFileExtensions: String(answers.allowedFileExtensions || '')
41
+ .split(',')
42
+ .map((s) => s.trim())
43
+ .filter(Boolean),
44
+ compression: answers.compression as Compression,
45
+ encryption: !!answers.encryption,
46
+ antivirus: !!answers.antivirus,
47
+ } as any;
48
+
49
+ try {
50
+ const created = await createBucketApi(storage, bucketInput, answers.id || undefined);
51
+ MessageFormatter.success(`Bucket '${answers.name}' created`, { prefix: 'Buckets' });
52
+
53
+ // Update in-memory config and persist to YAML as a global bucket entry
54
+ const controller = (cli as any).controller!;
55
+ controller.config.buckets = controller.config.buckets || [];
56
+ controller.config.buckets.push({
57
+ $id: (created as any).$id || answers.id || bucketInput.name,
58
+ name: bucketInput.name,
59
+ permissions: [],
60
+ fileSecurity: bucketInput.fileSecurity,
61
+ enabled: bucketInput.enabled,
62
+ maximumFileSize: bucketInput.maximumFileSize,
63
+ allowedFileExtensions: bucketInput.allowedFileExtensions,
64
+ compression: bucketInput.compression,
65
+ encryption: bucketInput.encryption,
66
+ antivirus: bucketInput.antivirus,
67
+ });
68
+
69
+ const cfgMgr = ConfigManager.getInstance();
70
+ const cfgPath = cfgMgr.getConfigPath();
71
+ if (cfgPath && /\.ya?ml$/i.test(cfgPath)) {
72
+ await writeYamlConfig(cfgPath, controller.config);
73
+ MessageFormatter.info(`Added bucket to config.yaml`, { prefix: 'Buckets' });
74
+ } else {
75
+ MessageFormatter.warning(`Config is not YAML; updated in-memory only. Please update your TypeScript config manually.`, { prefix: 'Buckets' });
76
+ }
77
+ } catch (e) {
78
+ MessageFormatter.error('Failed to create bucket', e instanceof Error ? e : new Error(String(e)), { prefix: 'Buckets' });
79
+ }
80
+ },
81
+
82
+ async deleteBuckets(cli: InteractiveCLI): Promise<void> {
83
+ const storage: Storage = (cli as any).controller!.storage!;
84
+ if (!storage) {
85
+ MessageFormatter.error("Storage client not initialized", undefined, { prefix: "Buckets" });
86
+ return;
87
+ }
88
+
89
+ try {
90
+ const res = await listBuckets(storage);
91
+ const buckets: Models.Bucket[] = res.buckets || [];
92
+ if (buckets.length === 0) {
93
+ MessageFormatter.info('No buckets found', { prefix: 'Buckets' });
94
+ return;
95
+ }
96
+
97
+ const { toDelete } = await inquirer.prompt([
98
+ {
99
+ type: 'checkbox',
100
+ name: 'toDelete',
101
+ message: chalk.red('Select buckets to delete:'),
102
+ choices: buckets.map((b) => ({ name: `${b.name} (${b.$id})`, value: b.$id })),
103
+ pageSize: 10,
104
+ }
105
+ ]);
106
+
107
+ if (!toDelete || toDelete.length === 0) {
108
+ MessageFormatter.info('No buckets selected', { prefix: 'Buckets' });
109
+ return;
110
+ }
111
+
112
+ const { confirm } = await inquirer.prompt([
113
+ { type: 'confirm', name: 'confirm', message: `Delete ${toDelete.length} bucket(s)?`, default: false }
114
+ ]);
115
+ if (!confirm) return;
116
+
117
+ const controller = (cli as any).controller!;
118
+ for (const id of toDelete) {
119
+ try {
120
+ await deleteBucketApi(storage, id);
121
+ MessageFormatter.success(`Deleted bucket ${id}`, { prefix: 'Buckets' });
122
+
123
+ // Remove from in-memory config (global buckets)
124
+ if (Array.isArray(controller.config.buckets)) {
125
+ controller.config.buckets = controller.config.buckets.filter((b: any) => b.$id !== id);
126
+ }
127
+ // Clear database-linked bucket references if matching
128
+ if (Array.isArray(controller.config.databases)) {
129
+ controller.config.databases = controller.config.databases.map((db: any) => ({
130
+ ...db,
131
+ bucket: db.bucket && db.bucket.$id === id ? undefined : db.bucket,
132
+ }));
133
+ }
134
+ } catch (e) {
135
+ MessageFormatter.error(`Failed to delete bucket ${id}`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Buckets' });
136
+ }
137
+ }
138
+
139
+ // Persist YAML changes
140
+ const cfgMgr = ConfigManager.getInstance();
141
+ const cfgPath = cfgMgr.getConfigPath();
142
+ if (cfgPath && /\.ya?ml$/i.test(cfgPath)) {
143
+ await writeYamlConfig(cfgPath, controller.config);
144
+ MessageFormatter.info(`Updated config.yaml after deletion`, { prefix: 'Buckets' });
145
+ } else {
146
+ MessageFormatter.warning(`Config is not YAML; updated in-memory only. Please update your TypeScript config manually.`, { prefix: 'Buckets' });
147
+ }
148
+ } catch (e) {
149
+ MessageFormatter.error('Failed to list buckets', e instanceof Error ? e : new Error(String(e)), { prefix: 'Buckets' });
150
+ }
151
+ }
152
+ };
@@ -325,8 +325,8 @@ const createAttributeViaAdapter = async (
325
325
  ...((attribute as any).size && { size: (attribute as any).size }),
326
326
  ...((attribute as any).xdefault !== undefined &&
327
327
  !attribute.required && { default: (attribute as any).xdefault }),
328
- ...((attribute as any).encrypted && {
329
- encrypt: (attribute as any).encrypted,
328
+ ...((attribute as any).encrypt && {
329
+ encrypt: (attribute as any).encrypt,
330
330
  }),
331
331
  ...((attribute as any).min !== undefined && {
332
332
  min: (attribute as any).min,
@@ -408,7 +408,7 @@ const updateAttributeViaAdapter = async (
408
408
  size: (attribute as any).size,
409
409
  min: (attribute as any).min,
410
410
  max: (attribute as any).max,
411
- encrypt: (attribute as any).encrypted ?? (attribute as any).encrypt,
411
+ encrypt: (attribute as any).encrypt,
412
412
  elements: (attribute as any).elements,
413
413
  relatedCollection: (attribute as any).relatedCollection,
414
414
  relationType: (attribute as any).relationType,
@@ -458,7 +458,7 @@ const createLegacyAttribute = async (
458
458
  ? (attribute as any).xdefault
459
459
  : undefined,
460
460
  array: attribute.array || false,
461
- encrypted: (attribute as any).encrypted,
461
+ encrypt: (attribute as any).encrypt,
462
462
  };
463
463
  logger.debug(`Creating string attribute '${attribute.key}'`, {
464
464
  ...stringParams,
@@ -472,7 +472,7 @@ const createLegacyAttribute = async (
472
472
  stringParams.required,
473
473
  stringParams.defaultValue,
474
474
  stringParams.array,
475
- stringParams.encrypted
475
+ stringParams.encrypt
476
476
  );
477
477
  break;
478
478
  case "integer":
@@ -1055,7 +1055,7 @@ const getComparableFields = (type: string): string[] => {
1055
1055
 
1056
1056
  switch (type) {
1057
1057
  case "string":
1058
- return [...baseFields, "size", "encrypted"];
1058
+ return [...baseFields, "size", "encrypt"];
1059
1059
 
1060
1060
  case "integer":
1061
1061
  case "double":
@@ -1088,7 +1088,7 @@ const getComparableFields = (type: string): string[] => {
1088
1088
  "key",
1089
1089
  "type",
1090
1090
  "array",
1091
- "encrypted",
1091
+ "encrypt",
1092
1092
  "required",
1093
1093
  "size",
1094
1094
  "min",
@@ -176,13 +176,13 @@ export const fetchAndCacheCollectionByName = async (
176
176
  }
177
177
  };
178
178
 
179
- export const generateSchemas = async (
180
- config: AppwriteConfig,
181
- appwriteFolderPath: string
182
- ): Promise<void> => {
183
- const schemaGenerator = new SchemaGenerator(config, appwriteFolderPath);
184
- schemaGenerator.generateSchemas();
185
- };
179
+ export const generateSchemas = async (
180
+ config: AppwriteConfig,
181
+ appwriteFolderPath: string
182
+ ): Promise<void> => {
183
+ const schemaGenerator = new SchemaGenerator(config, appwriteFolderPath);
184
+ await schemaGenerator.generateSchemas();
185
+ };
186
186
 
187
187
  export const createOrUpdateCollections = async (
188
188
  database: Databases,
@@ -319,11 +319,18 @@ export const createOrUpdateCollectionsViaAdapter = async (
319
319
  const minus = plan.toRecreate.map((r: any) => (r.newAttribute as any).key);
320
320
  const skip = plan.unchanged;
321
321
 
322
+ // Compute deletions (remote extras not present locally)
323
+ const desiredKeysForDelete = new Set((attributes || []).map((a: any) => a.key));
324
+ const extraRemoteKeys = (existingCols || [])
325
+ .map((c: any) => c?.key)
326
+ .filter((k: any): k is string => !!k && !desiredKeysForDelete.has(k));
327
+
322
328
  const parts: string[] = [];
323
329
  if (plus.length) parts.push(`➕ ${plus.length} (${plus.join(', ')})`);
324
330
  if (plusminus.length) parts.push(`🔧 ${plusminus.length} (${plusminus.join(', ')})`);
325
331
  if (minus.length) parts.push(`♻️ ${minus.length} (${minus.join(', ')})`);
326
332
  if (skip.length) parts.push(`⏭️ ${skip.length}`);
333
+ parts.push(`🗑️ ${extraRemoteKeys.length}${extraRemoteKeys.length ? ` (${extraRemoteKeys.join(', ')})` : ''}`);
327
334
  MessageFormatter.info(`Plan → ${parts.join(' | ') || 'no changes'}`, { prefix: 'Attributes' });
328
335
 
329
336
  // Execute
@@ -510,10 +517,117 @@ export const createOrUpdateCollectionsViaAdapter = async (
510
517
  } catch (e) {
511
518
  MessageFormatter.error(`Failed to list/create indexes`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Indexes' });
512
519
  }
513
-
514
- // Mark this table as fully processed to prevent re-processing
515
- markCollectionProcessed(tableId, collectionData.name);
516
- }
520
+
521
+ // Deletions for indexes: remove remote indexes not declared in YAML/config
522
+ try {
523
+ const desiredIndexKeys = new Set((indexes || []).map((i: any) => i.key));
524
+ const idxRes = await adapter.listIndexes({ databaseId, tableId });
525
+ const existingIdx: any[] = (idxRes as any).data || (idxRes as any).indexes || [];
526
+ const extraIdx = existingIdx
527
+ .filter((i: any) => i?.key && !desiredIndexKeys.has(i.key))
528
+ .map((i: any) => i.key as string);
529
+ if (extraIdx.length > 0) {
530
+ MessageFormatter.info(`Plan → 🗑️ ${extraIdx.length} indexes (${extraIdx.join(', ')})`, { prefix: 'Indexes' });
531
+ const deleted: string[] = [];
532
+ const errors: Array<{ key: string; error: string }> = [];
533
+ for (const key of extraIdx) {
534
+ try {
535
+ await adapter.deleteIndex({ databaseId, tableId, key });
536
+ // Optionally wait for index to disappear
537
+ const start = Date.now();
538
+ const maxWait = 30000;
539
+ while (Date.now() - start < maxWait) {
540
+ try {
541
+ const li = await adapter.listIndexes({ databaseId, tableId });
542
+ const list: any[] = (li as any).data || (li as any).indexes || [];
543
+ if (!list.find((ix: any) => ix.key === key)) break;
544
+ } catch {}
545
+ await delay(1000);
546
+ }
547
+ deleted.push(key);
548
+ } catch (e: any) {
549
+ errors.push({ key, error: e?.message || String(e) });
550
+ }
551
+ }
552
+ if (deleted.length) {
553
+ MessageFormatter.success(`Deleted ${deleted.length} indexes: ${deleted.join(', ')}`, { prefix: 'Indexes' });
554
+ }
555
+ if (errors.length) {
556
+ MessageFormatter.error(`${errors.length} index deletions failed`, undefined, { prefix: 'Indexes' });
557
+ errors.forEach(er => MessageFormatter.error(` ${er.key}: ${er.error}`, undefined, { prefix: 'Indexes' }));
558
+ }
559
+ } else {
560
+ MessageFormatter.info(`Plan → 🗑️ 0 indexes`, { prefix: 'Indexes' });
561
+ }
562
+ } catch (e) {
563
+ MessageFormatter.warning(`Could not evaluate index deletions: ${(e as Error)?.message || e}`, { prefix: 'Indexes' });
564
+ }
565
+
566
+ // Deletions: remove columns/attributes that are present remotely but not in desired config
567
+ try {
568
+ const desiredKeys = new Set((attributes || []).map((a: any) => a.key));
569
+ const tableInfo3 = await adapter.getTable({ databaseId, tableId });
570
+ const existingCols3: any[] = (tableInfo3 as any).data?.columns || (tableInfo3 as any).data?.attributes || [];
571
+ const toDelete = existingCols3
572
+ .filter((col: any) => col?.key && !desiredKeys.has(col.key))
573
+ .map((col: any) => col.key as string);
574
+
575
+ if (toDelete.length > 0) {
576
+ MessageFormatter.info(`Plan → 🗑️ ${toDelete.length} (${toDelete.join(', ')})`, { prefix: 'Attributes' });
577
+ const deleted: string[] = [];
578
+ const errors: Array<{ key: string; error: string }> = [];
579
+ for (const key of toDelete) {
580
+ try {
581
+ // Drop any indexes that reference this attribute to avoid server errors
582
+ try {
583
+ const idxRes = await adapter.listIndexes({ databaseId, tableId });
584
+ const ilist: any[] = (idxRes as any).data || (idxRes as any).indexes || [];
585
+ for (const idx of ilist) {
586
+ const attrs: string[] = Array.isArray(idx.attributes) ? idx.attributes : [];
587
+ if (attrs.includes(key)) {
588
+ MessageFormatter.info(`🗑️ Deleting index '${idx.key}' referencing '${key}'`, { prefix: 'Indexes' });
589
+ await adapter.deleteIndex({ databaseId, tableId, key: idx.key });
590
+ await delay(500);
591
+ }
592
+ }
593
+ } catch {}
594
+
595
+ await adapter.deleteAttribute({ databaseId, tableId, key });
596
+ // Wait briefly for deletion to settle
597
+ const start = Date.now();
598
+ const maxWaitMs = 60000;
599
+ while (Date.now() - start < maxWaitMs) {
600
+ try {
601
+ const tinfo = await adapter.getTable({ databaseId, tableId });
602
+ const cols = (tinfo as any).data?.columns || (tinfo as any).data?.attributes || [];
603
+ const found = cols.find((c: any) => c.key === key);
604
+ if (!found) break;
605
+ if (found.status && found.status !== 'deleting') break;
606
+ } catch {}
607
+ await delay(1000);
608
+ }
609
+ deleted.push(key);
610
+ } catch (e: any) {
611
+ errors.push({ key, error: e?.message || String(e) });
612
+ }
613
+ }
614
+ if (deleted.length) {
615
+ MessageFormatter.success(`Deleted ${deleted.length} attributes: ${deleted.join(', ')}`, { prefix: 'Attributes' });
616
+ }
617
+ if (errors.length) {
618
+ MessageFormatter.error(`${errors.length} deletions failed`, undefined, { prefix: 'Attributes' });
619
+ errors.forEach(er => MessageFormatter.error(` ${er.key}: ${er.error}`, undefined, { prefix: 'Attributes' }));
620
+ }
621
+ } else {
622
+ MessageFormatter.info(`Plan → 🗑️ 0`, { prefix: 'Attributes' });
623
+ }
624
+ } catch (e) {
625
+ MessageFormatter.warning(`Could not evaluate deletions: ${(e as Error)?.message || e}`, { prefix: 'Attributes' });
626
+ }
627
+
628
+ // Mark this table as fully processed to prevent re-processing
629
+ markCollectionProcessed(tableId, collectionData.name);
630
+ }
517
631
 
518
632
  // Process queued relationships once mapping likely populated
519
633
  if (relQueue.length > 0) {
@@ -115,7 +115,7 @@ export function normalizeAttributeToComparable(attr: Attribute): ComparableColum
115
115
 
116
116
  if (t === 'string') {
117
117
  base.size = (attr as any).size ?? 255;
118
- base.encrypt = !!((attr as any).encrypted ?? (attr as any).encrypt);
118
+ base.encrypt = !!((attr as any).encrypt);
119
119
  }
120
120
  if (t === 'integer' || t === 'float' || t === 'double') {
121
121
  const min = toNumber((attr as any).min);
@@ -261,7 +261,7 @@ function compareColumnProperties(
261
261
  const getNewVal = (prop: string) => {
262
262
  const na = newAttribute as any;
263
263
  if (prop === 'default') return na.xdefault;
264
- if (prop === 'encrypt') return na.encrypted ?? na.encrypt;
264
+ if (prop === 'encrypt') return na.encrypt;
265
265
  return na[prop];
266
266
  };
267
267
  const getOldVal = (prop: string) => {
@@ -42,11 +42,12 @@ import { findYamlConfig } from "./config/yamlConfig.js";
42
42
  // Import command modules
43
43
  import { configCommands } from "./cli/commands/configCommands.js";
44
44
  import { databaseCommands } from "./cli/commands/databaseCommands.js";
45
- import { functionCommands } from "./cli/commands/functionCommands.js";
45
+ import { functionCommands } from "./cli/commands/functionCommands.js";
46
+ import { storageCommands } from "./cli/commands/storageCommands.js";
46
47
  import { transferCommands } from "./cli/commands/transferCommands.js";
47
48
  import { schemaCommands } from "./cli/commands/schemaCommands.js";
48
49
 
49
- enum CHOICES {
50
+ enum CHOICES {
50
51
  MIGRATE_CONFIG = "🔄 Migrate TypeScript config to YAML (.appwrite structure)",
51
52
  VALIDATE_CONFIG = "✅ Validate configuration (collections/tables conflicts)",
52
53
  MIGRATE_COLLECTIONS_TO_TABLES = "🔀 Migrate collections to tables format",
@@ -67,11 +68,12 @@ enum CHOICES {
67
68
  GENERATE_CONSTANTS = "📋 Generate cross-language constants (TypeScript, Python, PHP, Dart, etc.)",
68
69
  IMPORT_DATA = "📥 Import data",
69
70
  RELOAD_CONFIG = "🔄 Reload configuration files",
70
- UPDATE_FUNCTION_SPEC = "⚙️ Update function specifications",
71
- EXIT = "👋 Exit",
72
- }
71
+ UPDATE_FUNCTION_SPEC = "⚙️ Update function specifications",
72
+ MANAGE_BUCKETS = "🪣 Manage storage buckets",
73
+ EXIT = "👋 Exit",
74
+ }
73
75
 
74
- export class InteractiveCLI {
76
+ export class InteractiveCLI {
75
77
  private controller: UtilsController | undefined;
76
78
  private isUsingTypeScriptConfig: boolean = false;
77
79
 
@@ -180,10 +182,13 @@ export class InteractiveCLI {
180
182
  case CHOICES.RELOAD_CONFIG:
181
183
  await configCommands.reloadConfigWithSessionPreservation(this);
182
184
  break;
183
- case CHOICES.UPDATE_FUNCTION_SPEC:
184
- await this.initControllerIfNeeded();
185
- await functionCommands.updateFunctionSpec(this);
186
- break;
185
+ case CHOICES.UPDATE_FUNCTION_SPEC:
186
+ await this.initControllerIfNeeded();
187
+ await functionCommands.updateFunctionSpec(this);
188
+ break;
189
+ case CHOICES.MANAGE_BUCKETS:
190
+ await this.manageBuckets();
191
+ break;
187
192
  case CHOICES.EXIT:
188
193
  MessageFormatter.success("Goodbye!");
189
194
  process.exit(0);
@@ -191,7 +196,7 @@ export class InteractiveCLI {
191
196
  }
192
197
  }
193
198
 
194
- private async initControllerIfNeeded(directConfig?: {
199
+ private async initControllerIfNeeded(directConfig?: {
195
200
  appwriteEndpoint: string;
196
201
  appwriteProject: string;
197
202
  appwriteKey: string;
@@ -222,7 +227,32 @@ export class InteractiveCLI {
222
227
  }
223
228
  // If no directConfig provided, keep existing controller
224
229
  }
225
- }
230
+ }
231
+
232
+ private async manageBuckets(): Promise<void> {
233
+ await this.initControllerIfNeeded();
234
+ while (true) {
235
+ const { action } = await inquirer.prompt([
236
+ {
237
+ type: 'list',
238
+ name: 'action',
239
+ message: chalk.blue('Bucket management'),
240
+ choices: [
241
+ { name: 'Create bucket', value: 'create' },
242
+ { name: 'Delete buckets', value: 'delete' },
243
+ { name: 'Back', value: 'back' },
244
+ ],
245
+ },
246
+ ]);
247
+
248
+ if (action === 'back') break;
249
+ if (action === 'create') {
250
+ await storageCommands.createBucket(this);
251
+ } else if (action === 'delete') {
252
+ await storageCommands.deleteBuckets(this);
253
+ }
254
+ }
255
+ }
226
256
 
227
257
  private async selectDatabases(
228
258
  databases: Models.Database[],
package/src/main.ts CHANGED
@@ -41,7 +41,7 @@ if (!(globalThis as any).require) {
41
41
  (globalThis as any).require = require;
42
42
  }
43
43
 
44
- interface CliOptions {
44
+ interface CliOptions {
45
45
  config?: string;
46
46
  it?: boolean;
47
47
  dbIds?: string;
@@ -86,8 +86,13 @@ interface CliOptions {
86
86
  session?: string;
87
87
  listBackups?: boolean;
88
88
  autoSync?: boolean;
89
- selectBuckets?: boolean;
90
- }
89
+ selectBuckets?: boolean;
90
+ // New schema/constant CLI flags
91
+ generateSchemas?: boolean;
92
+ schemaFormat?: 'zod' | 'json' | 'pydantic' | 'both' | 'all';
93
+ schemaOutDir?: string;
94
+ constantsInclude?: string;
95
+ }
91
96
 
92
97
  type ParsedArgv = ArgumentsCamelCase<CliOptions>;
93
98
 
@@ -567,12 +572,30 @@ const argv = yargs(hideBin(process.argv))
567
572
  "Comma-separated list of languages for constants (typescript,javascript,python,php,dart,json,env)",
568
573
  default: "typescript",
569
574
  })
570
- .option("constantsOutput", {
571
- type: "string",
572
- description:
573
- "Output directory for generated constants files (default: config-folder/constants)",
574
- default: "auto",
575
- })
575
+ .option("constantsOutput", {
576
+ type: "string",
577
+ description:
578
+ "Output directory for generated constants files (default: config-folder/constants)",
579
+ default: "auto",
580
+ })
581
+ .option("constantsInclude", {
582
+ type: "string",
583
+ description:
584
+ "Comma-separated categories to include: databases,collections,buckets,functions",
585
+ })
586
+ .option("generateSchemas", {
587
+ type: "boolean",
588
+ description: "Generate schemas/models without interactive prompts",
589
+ })
590
+ .option("schemaFormat", {
591
+ type: "string",
592
+ choices: ["zod", "json", "pydantic", "both", "all"],
593
+ description: "Schema format: zod, json, pydantic, both (zod+json), or all",
594
+ })
595
+ .option("schemaOutDir", {
596
+ type: "string",
597
+ description: "Output directory for generated schemas (absolute path respected)",
598
+ })
576
599
  .option("migrateCollectionsToTables", {
577
600
  alias: ["migrate-collections"],
578
601
  type: "boolean",
@@ -638,7 +638,7 @@ export class AppwriteToX {
638
638
  }
639
639
 
640
640
  MessageFormatter.info("Generating Zod schemas from synced collections...", { prefix: "Migration" });
641
- generator.generateSchemas();
641
+ await generator.generateSchemas();
642
642
  MessageFormatter.success("Sync-from-Appwrite process completed successfully", { prefix: "Migration" });
643
643
  } catch (error) {
644
644
  MessageFormatter.error(
@@ -562,12 +562,12 @@ export function generateTableSchema(): any {
562
562
  // Add column definition (similar to attribute but with table terminology)
563
563
  tableSchema.$defs.column = JSON.parse(JSON.stringify(tableSchema.$defs.attribute));
564
564
 
565
- // Add encrypted property (table-specific feature)
566
- tableSchema.$defs.column.properties.encrypted = {
567
- "type": "boolean",
568
- "description": "Whether the column should be encrypted",
569
- "default": false
570
- };
565
+ // Add encrypt property (table-specific feature)
566
+ tableSchema.$defs.column.properties.encrypt = {
567
+ "type": "boolean",
568
+ "description": "Whether the column should be encrypted",
569
+ "default": false
570
+ };
571
571
 
572
572
  // Replace relatedCollection with relatedTable for table terminology
573
573
  delete tableSchema.$defs.column.properties.relatedCollection;
@@ -1351,4 +1351,4 @@ options:
1351
1351
  fs.writeFileSync(examplePath, example.content);
1352
1352
  logger.info(`Created import example: ${examplePath}`);
1353
1353
  }
1354
- }
1354
+ }
@@ -328,6 +328,12 @@ export function createYamlValidationSchema(
328
328
  "default": false,
329
329
  "description": `Whether the ${useTables ? 'column' : 'attribute'} is an array`
330
330
  },
331
+ // Encryption flag for string types
332
+ "encrypt": {
333
+ "type": "boolean",
334
+ "default": false,
335
+ "description": `Enable encryption for string ${useTables ? 'columns' : 'attributes'}`
336
+ },
331
337
  ...(useTables ? {
332
338
  "unique": {
333
339
  "type": "boolean",
@@ -20,7 +20,7 @@ export function mapToCreateAttributeParams(
20
20
  const required = !!(attr as any).required;
21
21
  const array = !!(attr as any).array;
22
22
  const xdefault = (attr as any).xdefault;
23
- const encrypt = (attr as any).encrypted ?? (attr as any).encrypt;
23
+ const encrypt = (attr as any).encrypt;
24
24
 
25
25
  // Numeric helpers
26
26
  const rawMin = ensureNumber((attr as any).min);
@@ -184,7 +184,7 @@ export function mapToUpdateAttributeParams(
184
184
  }
185
185
  setIfDefined("array", (attr as any).array);
186
186
  // encrypt only applies to string types
187
- if (type === "string") setIfDefined("encrypt", (attr as any).encrypted ?? (attr as any).encrypt);
187
+ if (type === "string") setIfDefined("encrypt", (attr as any).encrypt);
188
188
 
189
189
  // Numeric normalization
190
190
  const toNum = (n: any) => (n === null || n === undefined ? undefined : (Number(n)));
@@ -236,7 +236,7 @@ export class JsonSchemaGenerator {
236
236
  outputDirectory?: string;
237
237
  verbose?: boolean;
238
238
  } = {}): void {
239
- const { outputFormat = "both", outputDirectory = "schemas", verbose = false } = options;
239
+ const { outputFormat = "both", outputDirectory = "schemas", verbose = false } = options;
240
240
 
241
241
  if (!this.config.collections) {
242
242
  if (verbose) {
@@ -246,7 +246,9 @@ export class JsonSchemaGenerator {
246
246
  }
247
247
 
248
248
  // Create JSON schemas directory using provided outputDirectory
249
- const jsonSchemasPath = path.join(this.appwriteFolderPath, outputDirectory);
249
+ const jsonSchemasPath = path.isAbsolute(outputDirectory)
250
+ ? outputDirectory
251
+ : path.join(this.appwriteFolderPath, outputDirectory);
250
252
  if (!fs.existsSync(jsonSchemasPath)) {
251
253
  fs.mkdirSync(jsonSchemasPath, { recursive: true });
252
254
  }