appwrite-utils-cli 0.9.92 → 0.9.93

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.
package/README.md CHANGED
@@ -125,6 +125,7 @@ This updated CLI ensures that developers have robust tools at their fingertips t
125
125
 
126
126
  ## Changelog
127
127
 
128
+ - 0.9.93: Updated `selectDatabases` and `selectCollections` from the interactive CLI to prefer local collections or databases when synchronizing the databases
128
129
  - 0.9.92: Fixed `createOrUpdateAttributes` so it deletes attributes that don't exist in local config when you are running `syncDb`. Also updated the database and collection selection, so it won't try and fetch the collections and databases that don't exist (ones you picked from local config) and error
129
130
  - 0.9.91: Fixed another webpack error, screw you react (but you're supported now so I guess not-screw-you)
130
131
  - 0.9.90: Fixed Webpack errors (why tf does webpack add an extra `default`...???)
@@ -6,7 +6,7 @@ import { fetchAllCollections } from "./collections/methods.js";
6
6
  import { listBuckets, createBucket } from "./storage/methods.js";
7
7
  import { Databases, Storage, Client, Compression, Query, } from "node-appwrite";
8
8
  import { getClient } from "./utils/getClientFromConfig.js";
9
- import { parseAttribute, PermissionToAppwritePermission } from "appwrite-utils";
9
+ import { parseAttribute, PermissionToAppwritePermission, } from "appwrite-utils";
10
10
  import { ulid } from "ulidx";
11
11
  import chalk from "chalk";
12
12
  import { DateTime } from "luxon";
@@ -106,15 +106,34 @@ export class InteractiveCLI {
106
106
  async selectDatabases(databases, message, multiSelect = true) {
107
107
  await this.initControllerIfNeeded();
108
108
  const configDatabases = this.getLocalDatabases();
109
- const allDatabases = [...databases, ...configDatabases].reduce((acc, db) => {
110
- if (!acc.find(d => d.name === db.name)) {
109
+ const allDatabases = [...databases, ...configDatabases]
110
+ .reduce((acc, db) => {
111
+ // Local config takes precedence - if a database with same name exists, use local version
112
+ const existingIndex = acc.findIndex((d) => d.name === db.name);
113
+ if (existingIndex >= 0) {
114
+ if (configDatabases.some((cdb) => cdb.name === db.name)) {
115
+ acc[existingIndex] = db; // Replace with local version
116
+ }
117
+ }
118
+ else {
111
119
  acc.push(db);
112
120
  }
113
121
  return acc;
114
- }, []);
122
+ }, [])
123
+ .filter((db) => db.name.toLowerCase() !== "migrations");
124
+ const hasLocalAndRemote = allDatabases.some((db) => configDatabases.some((c) => c.name === db.name)) &&
125
+ allDatabases.some((db) => !configDatabases.some((c) => c.name === db.name));
115
126
  const choices = allDatabases
116
127
  .sort((a, b) => a.name.localeCompare(b.name))
117
- .map((db) => ({ name: db.name, value: db }))
128
+ .map((db) => ({
129
+ name: db.name +
130
+ (hasLocalAndRemote
131
+ ? configDatabases.some((c) => c.name === db.name)
132
+ ? " (Local)"
133
+ : " (Remote)"
134
+ : ""),
135
+ value: db,
136
+ }))
118
137
  .filter((db) => db.name.toLowerCase() !== "migrations");
119
138
  const { selectedDatabases } = await inquirer.prompt([
120
139
  {
@@ -128,22 +147,41 @@ export class InteractiveCLI {
128
147
  ]);
129
148
  return selectedDatabases;
130
149
  }
131
- async selectCollections(database, databasesClient, message, multiSelect = true) {
150
+ async selectCollections(database, databasesClient, message, multiSelect = true, preferLocal = false) {
132
151
  await this.initControllerIfNeeded();
133
- const dbExists = await databasesClient.list([Query.equal("name", database.name)]);
134
- let collections = [];
152
+ const configCollections = this.getLocalCollections();
153
+ let remoteCollections = [];
154
+ const dbExists = await databasesClient.list([
155
+ Query.equal("name", database.name),
156
+ ]);
135
157
  if (dbExists.total === 0) {
136
158
  console.log(chalk.red(`Database "${database.name}" does not exist, using only local collection options`));
137
159
  }
138
160
  else {
139
- collections = await fetchAllCollections(database.$id, databasesClient);
161
+ remoteCollections = await fetchAllCollections(database.$id, databasesClient);
140
162
  }
141
- const configCollections = this.getLocalCollections();
142
- const collectionNames = collections.map((c) => c.name).concat(configCollections.map((c) => c.name));
143
- const allCollectionNamesUnique = Array.from(new Set(collectionNames));
144
- const allCollections = allCollectionNamesUnique.map((name) => configCollections.find((c) => c.name === name) ?? collections.find((c) => c.name === name)).filter((v) => v !== undefined);
145
- const choices = allCollections.map((collection) => ({
146
- name: collection.name,
163
+ const allCollections = preferLocal
164
+ ? remoteCollections.reduce((acc, remoteCollection) => {
165
+ if (!acc.some((c) => c.name === remoteCollection.name)) {
166
+ acc.push(remoteCollection);
167
+ }
168
+ return acc;
169
+ }, [...configCollections])
170
+ : [
171
+ ...remoteCollections,
172
+ ...configCollections.filter((c) => !remoteCollections.some((rc) => rc.name === c.name)),
173
+ ];
174
+ const hasLocalAndRemote = allCollections.some((coll) => configCollections.some((c) => c.name === coll.name)) &&
175
+ allCollections.some((coll) => !configCollections.some((c) => c.name === coll.name));
176
+ const choices = allCollections
177
+ .sort((a, b) => a.name.localeCompare(b.name))
178
+ .map((collection) => ({
179
+ name: collection.name +
180
+ (hasLocalAndRemote
181
+ ? configCollections.some((c) => c.name === collection.name)
182
+ ? " (Local)"
183
+ : " (Remote)"
184
+ : ""),
147
185
  value: collection,
148
186
  }));
149
187
  const { selectedCollections } = await inquirer.prompt([
@@ -195,7 +233,14 @@ export class InteractiveCLI {
195
233
  const allBuckets = await listBuckets(storage);
196
234
  // If there are no buckets, ask to create one for each database
197
235
  if (allBuckets.total === 0) {
198
- for (const database of databases ?? config.databases) {
236
+ const databasesToUse = databases ?? config.databases;
237
+ for (const database of databasesToUse) {
238
+ // If database has bucket config in local config, use that
239
+ const localDatabase = this.controller.config?.databases.find((db) => db.name === database.name);
240
+ if (localDatabase?.bucket) {
241
+ database.bucket = localDatabase.bucket;
242
+ continue;
243
+ }
199
244
  const { wantCreateBucket } = await inquirer.prompt([
200
245
  {
201
246
  type: "confirm",
@@ -361,7 +406,8 @@ export class InteractiveCLI {
361
406
  async syncDb() {
362
407
  console.log(chalk.yellow("Syncing database..."));
363
408
  const databases = await this.selectDatabases(await fetchAllDatabases(this.controller.database), chalk.blue("Select databases to synchronize:"), true);
364
- const collections = await this.selectCollections(databases[0], this.controller.database, chalk.blue("Select collections to synchronize:"), true);
409
+ const collections = await this.selectCollections(databases[0], this.controller.database, chalk.blue("Select collections to synchronize:"), true, true // prefer local
410
+ );
365
411
  await this.controller.syncDb(databases, collections);
366
412
  console.log(chalk.green("Database sync completed."));
367
413
  }
@@ -493,7 +539,7 @@ export class InteractiveCLI {
493
539
  ]);
494
540
  const options = {
495
541
  databases,
496
- collections: collections.map(c => c.name),
542
+ collections: collections.map((c) => c.name),
497
543
  doBackup,
498
544
  importData: true,
499
545
  shouldWriteFile,
@@ -562,7 +608,8 @@ export class InteractiveCLI {
562
608
  if (!targetDb) {
563
609
  throw new Error("No target database selected");
564
610
  }
565
- const selectedCollections = await this.selectCollections(fromDb, sourceClient, "Select collections to transfer:");
611
+ const selectedCollections = await this.selectCollections(fromDb, sourceClient, "Select collections to transfer:", true, false // don't prefer local for transfers
612
+ );
566
613
  const { transferStorage } = await inquirer.prompt([
567
614
  {
568
615
  type: "confirm",
@@ -609,7 +656,7 @@ export class InteractiveCLI {
609
656
  getLocalCollections() {
610
657
  const configCollections = this.controller.config?.collections || [];
611
658
  // @ts-expect-error - appwrite invalid types
612
- return configCollections.map(c => ({
659
+ return configCollections.map((c) => ({
613
660
  $id: c.$id || ulid(),
614
661
  $createdAt: DateTime.now().toISO(),
615
662
  $updatedAt: DateTime.now().toISO(),
@@ -624,7 +671,7 @@ export class InteractiveCLI {
624
671
  }
625
672
  getLocalDatabases() {
626
673
  const configDatabases = this.controller.config?.databases || [];
627
- return configDatabases.map(db => ({
674
+ return configDatabases.map((db) => ({
628
675
  $id: db.$id || ulid(),
629
676
  $createdAt: DateTime.now().toISO(),
630
677
  $updatedAt: DateTime.now().toISO(),
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": "0.9.92",
4
+ "version": "0.9.93",
5
5
  "main": "src/main.ts",
6
6
  "type": "module",
7
7
  "repository": {
@@ -14,7 +14,12 @@ import {
14
14
  } from "node-appwrite";
15
15
  import { getClient } from "./utils/getClientFromConfig.js";
16
16
  import type { TransferOptions } from "./migrations/transfer.js";
17
- import { parseAttribute, PermissionToAppwritePermission, type AppwriteConfig, type ConfigDatabases } from "appwrite-utils";
17
+ import {
18
+ parseAttribute,
19
+ PermissionToAppwritePermission,
20
+ type AppwriteConfig,
21
+ type ConfigDatabases,
22
+ } from "appwrite-utils";
18
23
  import { ulid } from "ulidx";
19
24
  import chalk from "chalk";
20
25
  import { DateTime } from "luxon";
@@ -38,12 +43,16 @@ enum CHOICES {
38
43
  export class InteractiveCLI {
39
44
  private controller: UtilsController | undefined;
40
45
 
41
- constructor(private currentDir: string) { }
46
+ constructor(private currentDir: string) {}
42
47
 
43
48
  async run(): Promise<void> {
44
- console.log(chalk.green("Welcome to Appwrite Utils CLI Tool by Zach Handley"));
45
49
  console.log(
46
- chalk.blue("For more information, visit https://github.com/zachhandley/AppwriteUtils")
50
+ chalk.green("Welcome to Appwrite Utils CLI Tool by Zach Handley")
51
+ );
52
+ console.log(
53
+ chalk.blue(
54
+ "For more information, visit https://github.com/zachhandley/AppwriteUtils"
55
+ )
47
56
  );
48
57
 
49
58
  while (true) {
@@ -124,16 +133,41 @@ export class InteractiveCLI {
124
133
  ): Promise<Models.Database[]> {
125
134
  await this.initControllerIfNeeded();
126
135
  const configDatabases = this.getLocalDatabases();
127
- const allDatabases = [...databases, ...configDatabases].reduce((acc, db) => {
128
- if (!acc.find(d => d.name === db.name)) {
129
- acc.push(db);
130
- }
131
- return acc;
132
- }, [] as Models.Database[]);
136
+ const allDatabases = [...databases, ...configDatabases]
137
+ .reduce((acc, db) => {
138
+ // Local config takes precedence - if a database with same name exists, use local version
139
+ const existingIndex = acc.findIndex((d) => d.name === db.name);
140
+ if (existingIndex >= 0) {
141
+ if (configDatabases.some((cdb) => cdb.name === db.name)) {
142
+ acc[existingIndex] = db; // Replace with local version
143
+ }
144
+ } else {
145
+ acc.push(db);
146
+ }
147
+ return acc;
148
+ }, [] as Models.Database[])
149
+ .filter((db) => db.name.toLowerCase() !== "migrations");
150
+
151
+ const hasLocalAndRemote =
152
+ allDatabases.some((db) =>
153
+ configDatabases.some((c) => c.name === db.name)
154
+ ) &&
155
+ allDatabases.some(
156
+ (db) => !configDatabases.some((c) => c.name === db.name)
157
+ );
133
158
 
134
159
  const choices = allDatabases
135
160
  .sort((a, b) => a.name.localeCompare(b.name))
136
- .map((db) => ({ name: db.name, value: db }))
161
+ .map((db) => ({
162
+ name:
163
+ db.name +
164
+ (hasLocalAndRemote
165
+ ? configDatabases.some((c) => c.name === db.name)
166
+ ? " (Local)"
167
+ : " (Remote)"
168
+ : ""),
169
+ value: db,
170
+ }))
137
171
  .filter((db) => db.name.toLowerCase() !== "migrations");
138
172
 
139
173
  const { selectedDatabases } = await inquirer.prompt([
@@ -154,27 +188,67 @@ export class InteractiveCLI {
154
188
  database: Models.Database,
155
189
  databasesClient: Databases,
156
190
  message: string,
157
- multiSelect = true
191
+ multiSelect = true,
192
+ preferLocal = false
158
193
  ): Promise<Models.Collection[]> {
159
194
  await this.initControllerIfNeeded();
160
- const dbExists = await databasesClient.list([Query.equal("name", database.name)]);
161
- let collections: Models.Collection[] = [];
195
+
196
+ const configCollections = this.getLocalCollections();
197
+ let remoteCollections: Models.Collection[] = [];
198
+
199
+ const dbExists = await databasesClient.list([
200
+ Query.equal("name", database.name),
201
+ ]);
162
202
  if (dbExists.total === 0) {
163
- console.log(chalk.red(`Database "${database.name}" does not exist, using only local collection options`));
203
+ console.log(
204
+ chalk.red(
205
+ `Database "${database.name}" does not exist, using only local collection options`
206
+ )
207
+ );
164
208
  } else {
165
- collections = await fetchAllCollections(
209
+ remoteCollections = await fetchAllCollections(
166
210
  database.$id,
167
211
  databasesClient
168
212
  );
169
213
  }
170
- const configCollections = this.getLocalCollections();
171
- const collectionNames = collections.map((c) => c.name).concat(configCollections.map((c) => c.name));
172
- const allCollectionNamesUnique = Array.from(new Set(collectionNames));
173
- const allCollections = allCollectionNamesUnique.map((name) => configCollections.find((c) => c.name === name) ?? collections.find((c) => c.name === name)).filter((v) => v !== undefined);
174
- const choices = allCollections.map((collection) => ({
175
- name: collection.name,
176
- value: collection,
177
- }));
214
+
215
+ const allCollections = preferLocal
216
+ ? remoteCollections.reduce(
217
+ (acc, remoteCollection) => {
218
+ if (!acc.some((c) => c.name === remoteCollection.name)) {
219
+ acc.push(remoteCollection);
220
+ }
221
+ return acc;
222
+ },
223
+ [...configCollections]
224
+ )
225
+ : [
226
+ ...remoteCollections,
227
+ ...configCollections.filter(
228
+ (c) => !remoteCollections.some((rc) => rc.name === c.name)
229
+ ),
230
+ ];
231
+
232
+ const hasLocalAndRemote =
233
+ allCollections.some((coll) =>
234
+ configCollections.some((c) => c.name === coll.name)
235
+ ) &&
236
+ allCollections.some(
237
+ (coll) => !configCollections.some((c) => c.name === coll.name)
238
+ );
239
+
240
+ const choices = allCollections
241
+ .sort((a, b) => a.name.localeCompare(b.name))
242
+ .map((collection) => ({
243
+ name:
244
+ collection.name +
245
+ (hasLocalAndRemote
246
+ ? configCollections.some((c) => c.name === collection.name)
247
+ ? " (Local)"
248
+ : " (Remote)"
249
+ : ""),
250
+ value: collection,
251
+ }));
178
252
 
179
253
  const { selectedCollections } = await inquirer.prompt([
180
254
  {
@@ -224,7 +298,9 @@ export class InteractiveCLI {
224
298
  input.trim() !== "" || "Collection name cannot be empty.",
225
299
  },
226
300
  ]);
227
- console.log(chalk.green(`Creating collection config file for '${collectionName}'...`));
301
+ console.log(
302
+ chalk.green(`Creating collection config file for '${collectionName}'...`)
303
+ );
228
304
  createEmptyCollection(collectionName);
229
305
  }
230
306
 
@@ -243,12 +319,23 @@ export class InteractiveCLI {
243
319
 
244
320
  // If there are no buckets, ask to create one for each database
245
321
  if (allBuckets.total === 0) {
246
- for (const database of databases ?? config.databases) {
322
+ const databasesToUse = databases ?? config.databases;
323
+ for (const database of databasesToUse) {
324
+ // If database has bucket config in local config, use that
325
+ const localDatabase = this.controller!.config?.databases.find(
326
+ (db) => db.name === database.name
327
+ );
328
+ if (localDatabase?.bucket) {
329
+ database.bucket = localDatabase.bucket;
330
+ continue;
331
+ }
247
332
  const { wantCreateBucket } = await inquirer.prompt([
248
333
  {
249
334
  type: "confirm",
250
335
  name: "wantCreateBucket",
251
- message: chalk.blue(`There are no buckets. Do you want to create a bucket for the database "${database.name}"?`),
336
+ message: chalk.blue(
337
+ `There are no buckets. Do you want to create a bucket for the database "${database.name}"?`
338
+ ),
252
339
  default: true,
253
340
  },
254
341
  ]);
@@ -455,13 +542,14 @@ export class InteractiveCLI {
455
542
  const databases = await this.selectDatabases(
456
543
  await fetchAllDatabases(this.controller!.database!),
457
544
  chalk.blue("Select databases to synchronize:"),
458
- true,
545
+ true
459
546
  );
460
547
  const collections = await this.selectCollections(
461
548
  databases[0],
462
549
  this.controller!.database!,
463
550
  chalk.blue("Select collections to synchronize:"),
464
551
  true,
552
+ true // prefer local
465
553
  );
466
554
  await this.controller!.syncDb(databases, collections);
467
555
  console.log(chalk.green("Database sync completed."));
@@ -607,13 +695,19 @@ export class InteractiveCLI {
607
695
  ]);
608
696
 
609
697
  if (confirm) {
610
- console.log(chalk.yellow(`Wiping selected collections from ${database.name}...`));
698
+ console.log(
699
+ chalk.yellow(`Wiping selected collections from ${database.name}...`)
700
+ );
611
701
  for (const collection of collections) {
612
702
  await this.controller!.wipeCollection(database, collection);
613
- console.log(chalk.green(`Collection ${collection.name} wiped successfully.`));
703
+ console.log(
704
+ chalk.green(`Collection ${collection.name} wiped successfully.`)
705
+ );
614
706
  }
615
707
  } else {
616
- console.log(chalk.blue(`Wipe operation cancelled for ${database.name}.`));
708
+ console.log(
709
+ chalk.blue(`Wipe operation cancelled for ${database.name}.`)
710
+ );
617
711
  }
618
712
  }
619
713
  console.log(chalk.green("Wipe collections operation completed."));
@@ -661,7 +755,7 @@ export class InteractiveCLI {
661
755
 
662
756
  const options = {
663
757
  databases,
664
- collections: collections.map(c => c.name),
758
+ collections: collections.map((c) => c.name),
665
759
  doBackup,
666
760
  importData: true,
667
761
  shouldWriteFile,
@@ -697,10 +791,10 @@ export class InteractiveCLI {
697
791
  let targetDatabases: Models.Database[];
698
792
  let remoteOptions:
699
793
  | {
700
- transferEndpoint: string;
701
- transferProject: string;
702
- transferKey: string;
703
- }
794
+ transferEndpoint: string;
795
+ transferProject: string;
796
+ transferKey: string;
797
+ }
704
798
  | undefined;
705
799
 
706
800
  if (isRemote) {
@@ -760,7 +854,9 @@ export class InteractiveCLI {
760
854
  const selectedCollections = await this.selectCollections(
761
855
  fromDb,
762
856
  sourceClient,
763
- "Select collections to transfer:"
857
+ "Select collections to transfer:",
858
+ true,
859
+ false // don't prefer local for transfers
764
860
  );
765
861
 
766
862
  const { transferStorage } = await inquirer.prompt([
@@ -778,12 +874,12 @@ export class InteractiveCLI {
778
874
  const sourceStorage = new Storage(this.controller!.appwriteServer!);
779
875
  const targetStorage = isRemote
780
876
  ? new Storage(
781
- getClient(
782
- remoteOptions!.transferEndpoint,
783
- remoteOptions!.transferProject,
784
- remoteOptions!.transferKey
877
+ getClient(
878
+ remoteOptions!.transferEndpoint,
879
+ remoteOptions!.transferProject,
880
+ remoteOptions!.transferKey
881
+ )
785
882
  )
786
- )
787
883
  : sourceStorage;
788
884
 
789
885
  const sourceBuckets = await listBuckets(sourceStorage);
@@ -829,11 +925,10 @@ export class InteractiveCLI {
829
925
  console.log(chalk.green("Data transfer completed."));
830
926
  }
831
927
 
832
-
833
928
  private getLocalCollections(): Models.Collection[] {
834
929
  const configCollections = this.controller!.config?.collections || [];
835
930
  // @ts-expect-error - appwrite invalid types
836
- return configCollections.map(c => ({
931
+ return configCollections.map((c) => ({
837
932
  $id: c.$id || ulid(),
838
933
  $createdAt: DateTime.now().toISO(),
839
934
  $updatedAt: DateTime.now().toISO(),
@@ -849,7 +944,7 @@ export class InteractiveCLI {
849
944
 
850
945
  private getLocalDatabases(): Models.Database[] {
851
946
  const configDatabases = this.controller!.config?.databases || [];
852
- return configDatabases.map(db => ({
947
+ return configDatabases.map((db) => ({
853
948
  $id: db.$id || ulid(),
854
949
  $createdAt: DateTime.now().toISO(),
855
950
  $updatedAt: DateTime.now().toISO(),
@@ -867,4 +962,4 @@ export class InteractiveCLI {
867
962
  console.error(chalk.red("Error reloading configuration files:"), error);
868
963
  }
869
964
  }
870
- }
965
+ }