appwrite-utils-cli 1.7.7 → 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.
@@ -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.7",
4
+ "version": "1.7.8",
5
5
  "main": "src/main.ts",
6
6
  "type": "module",
7
7
  "repository": {
@@ -3,6 +3,9 @@ import chalk from "chalk";
3
3
  import { join } from "node:path";
4
4
  import { MessageFormatter } from "../../shared/messageFormatter.js";
5
5
  import { ConfirmationDialogs } from "../../shared/confirmationDialogs.js";
6
+ import { SelectionDialogs } from "../../shared/selectionDialogs.js";
7
+ import type { DatabaseSelection, BucketSelection } from "../../shared/selectionDialogs.js";
8
+ import { logger } from "../../shared/logging.js";
6
9
  import { fetchAllDatabases } from "../../databases/methods.js";
7
10
  import { listBuckets } from "../../storage/methods.js";
8
11
  import { getFunction, downloadLatestFunctionDeployment } from "../../functions/methods.js";
@@ -12,23 +15,41 @@ export const databaseCommands = {
12
15
  async syncDb(cli: InteractiveCLI): Promise<void> {
13
16
  MessageFormatter.progress("Pushing local configuration to Appwrite...", { prefix: "Database" });
14
17
 
15
- const databases = await (cli as any).selectDatabases(
16
- (cli as any).getLocalDatabases(),
17
- chalk.blue("Select local databases to push:"),
18
- true
19
- );
18
+ try {
19
+ // Initialize controller
20
+ await (cli as any).controller!.init();
20
21
 
21
- if (!databases.length) {
22
- MessageFormatter.warning("No databases selected. Skipping database sync.", { prefix: "Database" });
23
- return;
24
- }
22
+ // Get available and configured databases
23
+ const availableDatabases = await fetchAllDatabases((cli as any).controller!.database!);
24
+ const configuredDatabases = (cli as any).controller!.config?.databases || [];
25
25
 
26
- try {
27
- // Loop through each database and prompt for collections specific to that database
28
- for (const database of databases) {
29
- MessageFormatter.info(`\n📦 Configuring push for database: ${database.name}`, { prefix: "Database" });
26
+ // Get local collections for selection
27
+ const localCollections = (cli as any).getLocalCollections();
28
+
29
+ // Prompt about existing configuration
30
+ const { syncExisting, modifyConfiguration } = await SelectionDialogs.promptForExistingConfig(configuredDatabases);
31
+
32
+ // Select databases
33
+ const selectedDatabaseIds = await SelectionDialogs.selectDatabases(
34
+ availableDatabases,
35
+ configuredDatabases,
36
+ { showSelectAll: true, allowNewOnly: !syncExisting }
37
+ );
38
+
39
+ if (selectedDatabaseIds.length === 0) {
40
+ MessageFormatter.warning("No databases selected. Skipping database sync.", { prefix: "Database" });
41
+ return;
42
+ }
43
+
44
+ // Select tables/collections for each database using the existing method
45
+ const tableSelectionsMap = new Map<string, string[]>();
46
+ const availableTablesMap = new Map<string, any[]>();
30
47
 
31
- const collections = await (cli as any).selectCollectionsAndTables(
48
+ for (const databaseId of selectedDatabaseIds) {
49
+ const database = availableDatabases.find(db => db.$id === databaseId)!;
50
+
51
+ // Use the existing selectCollectionsAndTables method
52
+ const selectedCollections = await (cli as any).selectCollectionsAndTables(
32
53
  database,
33
54
  (cli as any).controller!.database!,
34
55
  chalk.blue(`Select collections/tables to push to "${database.name}":`),
@@ -36,19 +57,93 @@ export const databaseCommands = {
36
57
  true // prefer local
37
58
  );
38
59
 
39
- if (collections.length === 0) {
60
+ // Map selected collections to table IDs
61
+ const selectedTableIds = selectedCollections.map((c: any) => c.$id || c.id);
62
+
63
+ // Store selections
64
+ tableSelectionsMap.set(databaseId, selectedTableIds);
65
+ availableTablesMap.set(databaseId, selectedCollections);
66
+
67
+ if (selectedCollections.length === 0) {
40
68
  MessageFormatter.warning(`No collections selected for database "${database.name}". Skipping.`, { prefix: "Database" });
41
69
  continue;
42
70
  }
71
+ }
43
72
 
44
- // Push selected collections to this specific database
45
- await (cli as any).controller!.syncDb([database], collections);
46
- MessageFormatter.success(
47
- `Pushed ${collections.length} collection(s) to database "${database.name}"`,
48
- { prefix: "Database" }
49
- );
73
+ // Ask if user wants to select buckets
74
+ const { selectBuckets } = await inquirer.prompt([
75
+ {
76
+ type: "confirm",
77
+ name: "selectBuckets",
78
+ message: "Do you want to select storage buckets to sync as well?",
79
+ default: false,
80
+ },
81
+ ]);
82
+
83
+ let bucketSelections: BucketSelection[] = [];
84
+
85
+ if (selectBuckets) {
86
+ // Get available and configured buckets
87
+ try {
88
+ const availableBucketsResponse = await listBuckets((cli as any).controller!.storage!);
89
+ const availableBuckets = availableBucketsResponse.buckets || [];
90
+ const configuredBuckets = (cli as any).controller!.config?.buckets || [];
91
+
92
+ if (availableBuckets.length === 0) {
93
+ MessageFormatter.warning("No storage buckets available in remote instance.", { prefix: "Database" });
94
+ } else {
95
+ // Select buckets using SelectionDialogs
96
+ const selectedBucketIds = await SelectionDialogs.selectBucketsForDatabases(
97
+ selectedDatabaseIds,
98
+ availableBuckets,
99
+ configuredBuckets,
100
+ { showSelectAll: true, groupByDatabase: true }
101
+ );
102
+
103
+ if (selectedBucketIds.length > 0) {
104
+ // Create BucketSelection objects
105
+ bucketSelections = SelectionDialogs.createBucketSelection(
106
+ selectedBucketIds,
107
+ availableBuckets,
108
+ configuredBuckets,
109
+ availableDatabases
110
+ );
111
+
112
+ MessageFormatter.info(`Selected ${bucketSelections.length} storage bucket(s)`, { prefix: "Database" });
113
+ }
114
+ }
115
+ } catch (error) {
116
+ MessageFormatter.warning("Failed to fetch storage buckets. Continuing with databases only.", { prefix: "Database" });
117
+ logger.warn("Storage bucket fetch failed during syncDb", { error: error instanceof Error ? error.message : String(error) });
118
+ }
50
119
  }
51
120
 
121
+ // Create DatabaseSelection objects
122
+ const databaseSelections = SelectionDialogs.createDatabaseSelection(
123
+ selectedDatabaseIds,
124
+ availableDatabases,
125
+ tableSelectionsMap,
126
+ configuredDatabases,
127
+ availableTablesMap
128
+ );
129
+
130
+ // Show confirmation summary
131
+ const selectionSummary = SelectionDialogs.createSyncSelectionSummary(
132
+ databaseSelections,
133
+ bucketSelections
134
+ );
135
+
136
+ const confirmed = await SelectionDialogs.confirmSyncSelection(selectionSummary);
137
+
138
+ if (!confirmed) {
139
+ MessageFormatter.info("Sync operation cancelled by user", { prefix: "Database" });
140
+ return;
141
+ }
142
+
143
+ // Perform selective sync using the controller
144
+ MessageFormatter.progress("Starting selective sync...", { prefix: "Database" });
145
+ await (cli as any).controller!.selectiveSync(databaseSelections, bucketSelections);
146
+
52
147
  MessageFormatter.success("\n✅ All database configurations pushed successfully!", { prefix: "Database" });
53
148
 
54
149
  // Then handle functions if requested
@@ -104,23 +199,28 @@ export const databaseCommands = {
104
199
  (cli as any).controller!.database!
105
200
  );
106
201
 
107
- // Use the controller's synchronizeConfigurations method which handles collections properly
108
- MessageFormatter.progress("Pulling collections and generating collection files...", { prefix: "Collections" });
109
- await (cli as any).controller!.synchronizeConfigurations(remoteDatabases);
110
-
111
- // Also configure buckets for any new databases
202
+ // First, prepare the combined database list for bucket configuration
112
203
  const localDatabases = (cli as any).controller!.config?.databases || [];
113
- const updatedConfig = await (cli as any).configureBuckets({
204
+ const allDatabases = [
205
+ ...localDatabases,
206
+ ...remoteDatabases.filter(
207
+ (rd: any) => !localDatabases.some((ld: any) => ld.name === rd.name)
208
+ ),
209
+ ];
210
+
211
+ // Configure buckets FIRST to get user selections before writing config
212
+ MessageFormatter.progress("Configuring storage buckets...", { prefix: "Buckets" });
213
+ const configWithBuckets = await (cli as any).configureBuckets({
114
214
  ...(cli as any).controller!.config!,
115
- databases: [
116
- ...localDatabases,
117
- ...remoteDatabases.filter(
118
- (rd: any) => !localDatabases.some((ld: any) => ld.name === rd.name)
119
- ),
120
- ],
215
+ databases: allDatabases,
121
216
  });
122
217
 
123
- (cli as any).controller!.config = updatedConfig;
218
+ // Update controller config with bucket selections
219
+ (cli as any).controller!.config = configWithBuckets;
220
+
221
+ // Now synchronize configurations with the updated config that includes bucket selections
222
+ MessageFormatter.progress("Pulling collections and generating collection files...", { prefix: "Collections" });
223
+ await (cli as any).controller!.synchronizeConfigurations(remoteDatabases, configWithBuckets);
124
224
  }
125
225
 
126
226
  // Then sync functions