appwrite-utils-cli 1.5.0 → 1.5.2

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.
@@ -131,6 +131,15 @@ const attributesSame = (databaseAttribute, configAttribute) => {
131
131
  (configValue === undefined || configValue === null)) {
132
132
  return true;
133
133
  }
134
+ // Normalize booleans: treat undefined and false as equivalent
135
+ if (typeof dbValue === "boolean" || typeof configValue === "boolean") {
136
+ return Boolean(dbValue) === Boolean(configValue);
137
+ }
138
+ // For numeric comparisons, compare numbers if both are numeric-like
139
+ if ((typeof dbValue === "number" || (typeof dbValue === "string" && dbValue !== "" && !isNaN(Number(dbValue)))) &&
140
+ (typeof configValue === "number" || (typeof configValue === "string" && configValue !== "" && !isNaN(Number(configValue))))) {
141
+ return Number(dbValue) === Number(configValue);
142
+ }
134
143
  return dbValue === configValue;
135
144
  }
136
145
  // If neither has the attribute, consider it the same
@@ -140,10 +149,18 @@ const attributesSame = (databaseAttribute, configAttribute) => {
140
149
  // If one has the attribute and the other doesn't, check if it's undefined or null
141
150
  if (dbHasAttr && !configHasAttr) {
142
151
  const dbValue = databaseAttribute[attr];
152
+ // Consider default-false booleans as equal to missing in config
153
+ if (typeof dbValue === "boolean") {
154
+ return dbValue === false; // missing in config equals false in db
155
+ }
143
156
  return dbValue === undefined || dbValue === null;
144
157
  }
145
158
  if (!dbHasAttr && configHasAttr) {
146
159
  const configValue = configAttribute[attr];
160
+ // Consider default-false booleans as equal to missing in db
161
+ if (typeof configValue === "boolean") {
162
+ return configValue === false; // missing in db equals false in config
163
+ }
147
164
  return configValue === undefined || configValue === null;
148
165
  }
149
166
  // If we reach here, the attributes are different
@@ -355,7 +372,7 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
355
372
  }
356
373
  const minValue = finalAttribute.min !== undefined && finalAttribute.min !== null
357
374
  ? parseInt(finalAttribute.min)
358
- : 9007199254740991;
375
+ : -9007199254740991;
359
376
  const maxValue = finalAttribute.max !== undefined && finalAttribute.max !== null
360
377
  ? parseInt(finalAttribute.max)
361
378
  : 9007199254740991;
@@ -72,7 +72,8 @@ export const ensureDatabasesExist = async (config, databasesToEnsure) => {
72
72
  throw new Error("Appwrite client is not initialized in the config");
73
73
  }
74
74
  const database = new Databases(config.appwriteClient);
75
- const databasesToCreate = databasesToEnsure || config.databases || [];
75
+ // Work on a shallow copy so we don't mutate caller-provided arrays
76
+ const databasesToCreate = [...(databasesToEnsure || config.databases || [])];
76
77
  if (!databasesToCreate.length) {
77
78
  console.log("No databases to create");
78
79
  return;
@@ -81,7 +82,10 @@ export const ensureDatabasesExist = async (config, databasesToEnsure) => {
81
82
  const migrationsDatabase = existingDatabases.databases.find((d) => d.name.toLowerCase().trim().replace(" ", "") === "migrations");
82
83
  if (config.useMigrations && existingDatabases.databases.length !== 0 && migrationsDatabase) {
83
84
  console.log("Creating all databases including migrations");
84
- databasesToCreate.push(migrationsDatabase);
85
+ // Ensure migrations exists, but do not mutate the caller's array
86
+ if (!databasesToCreate.some((d) => d.$id === migrationsDatabase.$id)) {
87
+ databasesToCreate.push(migrationsDatabase);
88
+ }
85
89
  }
86
90
  for (const db of databasesToCreate) {
87
91
  if (!existingDatabases.databases.some((d) => d.name === db.name)) {
@@ -235,7 +235,13 @@ export class InteractiveCLI {
235
235
  ...configCollections.filter((c) => !remoteCollections.some((rc) => rc.name === c.name)),
236
236
  ];
237
237
  if (shouldFilterByDatabase) {
238
- allCollections = allCollections.filter((c) => c.databaseId === database.$id);
238
+ // Keep local entries that don't have databaseId (common in config),
239
+ // but still filter remote collections by selected database.
240
+ allCollections = allCollections.filter((c) => {
241
+ if (!c.databaseId)
242
+ return true;
243
+ return c.databaseId === database.$id;
244
+ });
239
245
  }
240
246
  const hasLocalAndRemote = allCollections.some((coll) => configCollections.some((c) => c.name === coll.name)) &&
241
247
  allCollections.some((coll) => !configCollections.some((c) => c.name === coll.name));
@@ -867,7 +873,8 @@ export class InteractiveCLI {
867
873
  console.log(chalk.yellow("No databases selected. Skipping database sync."));
868
874
  return;
869
875
  }
870
- const collections = await this.selectCollections(databases[0], this.controller.database, chalk.blue("Select local collections to push:"), true, true // prefer local
876
+ const collections = await this.selectCollections(databases[0], this.controller.database, chalk.blue("Select local collections to push:"), true, true, // prefer local
877
+ true // filter by selected database
871
878
  );
872
879
  const { syncFunctions } = await inquirer.prompt([
873
880
  {
@@ -442,9 +442,12 @@ export class UtilsController {
442
442
  const allDatabases = await fetchAllDatabases(this.database);
443
443
  databases = allDatabases;
444
444
  }
445
+ // Ensure DBs exist (this may internally ensure migrations exists)
445
446
  await this.ensureDatabasesExist(databases);
446
447
  await this.ensureDatabaseConfigBucketsExist(databases);
447
- await this.createOrUpdateCollectionsForDatabases(databases, collections);
448
+ // Do not push collections to the migrations database (prevents duplicate runs)
449
+ const dbsForCollections = databases.filter((db) => (this.config?.useMigrations ?? true) ? db.name.toLowerCase() !== "migrations" : true);
450
+ await this.createOrUpdateCollectionsForDatabases(dbsForCollections, collections);
448
451
  }
449
452
  getAppwriteFolderPath() {
450
453
  return this.appwriteFolderPath;
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.5.0",
4
+ "version": "1.5.2",
5
5
  "main": "src/main.ts",
6
6
  "type": "module",
7
7
  "repository": {
@@ -224,33 +224,33 @@ const attributesSame = (
224
224
  databaseAttribute: Attribute,
225
225
  configAttribute: Attribute
226
226
  ): boolean => {
227
- const attributesToCheck = [
228
- "key",
229
- "type",
230
- "array",
231
- "encrypted",
232
- "required",
233
- "size",
234
- "min",
235
- "max",
236
- "xdefault",
237
- "elements",
238
- "relationType",
239
- "twoWay",
240
- "twoWayKey",
241
- "onDelete",
242
- "relatedCollection",
243
- ];
244
-
245
- return attributesToCheck.every((attr) => {
246
- // Check if both objects have the attribute
247
- const dbHasAttr = attr in databaseAttribute;
248
- const configHasAttr = attr in configAttribute;
227
+ const attributesToCheck = [
228
+ "key",
229
+ "type",
230
+ "array",
231
+ "encrypted",
232
+ "required",
233
+ "size",
234
+ "min",
235
+ "max",
236
+ "xdefault",
237
+ "elements",
238
+ "relationType",
239
+ "twoWay",
240
+ "twoWayKey",
241
+ "onDelete",
242
+ "relatedCollection",
243
+ ];
244
+
245
+ return attributesToCheck.every((attr) => {
246
+ // Check if both objects have the attribute
247
+ const dbHasAttr = attr in databaseAttribute;
248
+ const configHasAttr = attr in configAttribute;
249
249
 
250
250
  // If both have the attribute, compare values
251
- if (dbHasAttr && configHasAttr) {
252
- const dbValue = databaseAttribute[attr as keyof typeof databaseAttribute];
253
- const configValue = configAttribute[attr as keyof typeof configAttribute];
251
+ if (dbHasAttr && configHasAttr) {
252
+ const dbValue = databaseAttribute[attr as keyof typeof databaseAttribute];
253
+ const configValue = configAttribute[attr as keyof typeof configAttribute];
254
254
 
255
255
  // Consider undefined and null as equivalent
256
256
  if (
@@ -260,8 +260,19 @@ const attributesSame = (
260
260
  return true;
261
261
  }
262
262
 
263
- return dbValue === configValue;
264
- }
263
+ // Normalize booleans: treat undefined and false as equivalent
264
+ if (typeof dbValue === "boolean" || typeof configValue === "boolean") {
265
+ return Boolean(dbValue) === Boolean(configValue);
266
+ }
267
+ // For numeric comparisons, compare numbers if both are numeric-like
268
+ if (
269
+ (typeof dbValue === "number" || (typeof dbValue === "string" && dbValue !== "" && !isNaN(Number(dbValue)))) &&
270
+ (typeof configValue === "number" || (typeof configValue === "string" && configValue !== "" && !isNaN(Number(configValue))))
271
+ ) {
272
+ return Number(dbValue) === Number(configValue);
273
+ }
274
+ return dbValue === configValue;
275
+ }
265
276
 
266
277
  // If neither has the attribute, consider it the same
267
278
  if (!dbHasAttr && !configHasAttr) {
@@ -269,15 +280,23 @@ const attributesSame = (
269
280
  }
270
281
 
271
282
  // If one has the attribute and the other doesn't, check if it's undefined or null
272
- if (dbHasAttr && !configHasAttr) {
273
- const dbValue = databaseAttribute[attr as keyof typeof databaseAttribute];
274
- return dbValue === undefined || dbValue === null;
275
- }
283
+ if (dbHasAttr && !configHasAttr) {
284
+ const dbValue = databaseAttribute[attr as keyof typeof databaseAttribute];
285
+ // Consider default-false booleans as equal to missing in config
286
+ if (typeof dbValue === "boolean") {
287
+ return dbValue === false; // missing in config equals false in db
288
+ }
289
+ return dbValue === undefined || dbValue === null;
290
+ }
276
291
 
277
- if (!dbHasAttr && configHasAttr) {
278
- const configValue = configAttribute[attr as keyof typeof configAttribute];
279
- return configValue === undefined || configValue === null;
280
- }
292
+ if (!dbHasAttr && configHasAttr) {
293
+ const configValue = configAttribute[attr as keyof typeof configAttribute];
294
+ // Consider default-false booleans as equal to missing in db
295
+ if (typeof configValue === "boolean") {
296
+ return configValue === false; // missing in db equals false in config
297
+ }
298
+ return configValue === undefined || configValue === null;
299
+ }
281
300
 
282
301
  // If we reach here, the attributes are different
283
302
  return false;
@@ -646,27 +665,27 @@ export const createOrUpdateAttribute = async (
646
665
  finalAttribute.array || false
647
666
  )
648
667
  );
649
- } else {
650
- if (
651
- finalAttribute.min &&
652
- BigInt(finalAttribute.min) === BigInt(-9223372036854776000)
653
- ) {
654
- finalAttribute.min = undefined;
655
- }
656
- if (
657
- finalAttribute.max &&
658
- BigInt(finalAttribute.max) === BigInt(9223372036854776000)
659
- ) {
660
- finalAttribute.max = undefined;
661
- }
662
- const minValue =
663
- finalAttribute.min !== undefined && finalAttribute.min !== null
664
- ? parseInt(finalAttribute.min)
665
- : 9007199254740991;
666
- const maxValue =
667
- finalAttribute.max !== undefined && finalAttribute.max !== null
668
- ? parseInt(finalAttribute.max)
669
- : 9007199254740991;
668
+ } else {
669
+ if (
670
+ finalAttribute.min &&
671
+ BigInt(finalAttribute.min) === BigInt(-9223372036854776000)
672
+ ) {
673
+ finalAttribute.min = undefined;
674
+ }
675
+ if (
676
+ finalAttribute.max &&
677
+ BigInt(finalAttribute.max) === BigInt(9223372036854776000)
678
+ ) {
679
+ finalAttribute.max = undefined;
680
+ }
681
+ const minValue =
682
+ finalAttribute.min !== undefined && finalAttribute.min !== null
683
+ ? parseInt(finalAttribute.min)
684
+ : -9007199254740991;
685
+ const maxValue =
686
+ finalAttribute.max !== undefined && finalAttribute.max !== null
687
+ ? parseInt(finalAttribute.max)
688
+ : 9007199254740991;
670
689
  console.log(
671
690
  `DEBUG: Updating integer attribute '${
672
691
  finalAttribute.key
@@ -111,12 +111,13 @@ export const setupMigrationDatabase = async (config: AppwriteConfig) => {
111
111
  console.log("---------------------------------");
112
112
  };
113
113
 
114
- export const ensureDatabasesExist = async (config: AppwriteConfig, databasesToEnsure?: Models.Database[]) => {
114
+ export const ensureDatabasesExist = async (config: AppwriteConfig, databasesToEnsure?: Models.Database[]) => {
115
115
  if (!config.appwriteClient) {
116
116
  throw new Error("Appwrite client is not initialized in the config");
117
117
  }
118
- const database = new Databases(config.appwriteClient);
119
- const databasesToCreate = databasesToEnsure || config.databases || [];
118
+ const database = new Databases(config.appwriteClient);
119
+ // Work on a shallow copy so we don't mutate caller-provided arrays
120
+ const databasesToCreate = [...(databasesToEnsure || config.databases || [])];
120
121
 
121
122
  if (!databasesToCreate.length) {
122
123
  console.log("No databases to create");
@@ -130,10 +131,13 @@ export const ensureDatabasesExist = async (config: AppwriteConfig, databasesToEn
130
131
  const migrationsDatabase = existingDatabases.databases.find(
131
132
  (d) => d.name.toLowerCase().trim().replace(" ", "") === "migrations"
132
133
  );
133
- if (config.useMigrations && existingDatabases.databases.length !== 0 && migrationsDatabase) {
134
- console.log("Creating all databases including migrations");
135
- databasesToCreate.push(migrationsDatabase);
136
- }
134
+ if (config.useMigrations && existingDatabases.databases.length !== 0 && migrationsDatabase) {
135
+ console.log("Creating all databases including migrations");
136
+ // Ensure migrations exists, but do not mutate the caller's array
137
+ if (!databasesToCreate.some((d) => d.$id === migrationsDatabase.$id)) {
138
+ databasesToCreate.push(migrationsDatabase);
139
+ }
140
+ }
137
141
 
138
142
  for (const db of databasesToCreate) {
139
143
  if (!existingDatabases.databases.some((d) => d.name === db.name)) {
@@ -312,11 +312,14 @@ export class InteractiveCLI {
312
312
  ),
313
313
  ];
314
314
 
315
- if (shouldFilterByDatabase) {
316
- allCollections = allCollections.filter(
317
- (c) => c.databaseId === database.$id
318
- );
319
- }
315
+ if (shouldFilterByDatabase) {
316
+ // Keep local entries that don't have databaseId (common in config),
317
+ // but still filter remote collections by selected database.
318
+ allCollections = allCollections.filter((c) => {
319
+ if (!c.databaseId) return true;
320
+ return c.databaseId === database.$id;
321
+ });
322
+ }
320
323
 
321
324
  const hasLocalAndRemote =
322
325
  allCollections.some((coll) =>
@@ -1159,13 +1162,14 @@ export class InteractiveCLI {
1159
1162
  return;
1160
1163
  }
1161
1164
 
1162
- const collections = await this.selectCollections(
1163
- databases[0],
1164
- this.controller!.database!,
1165
- chalk.blue("Select local collections to push:"),
1166
- true,
1167
- true // prefer local
1168
- );
1165
+ const collections = await this.selectCollections(
1166
+ databases[0],
1167
+ this.controller!.database!,
1168
+ chalk.blue("Select local collections to push:"),
1169
+ true,
1170
+ true, // prefer local
1171
+ true // filter by selected database
1172
+ );
1169
1173
 
1170
1174
  const { syncFunctions } = await inquirer.prompt([
1171
1175
  {
@@ -601,10 +601,10 @@ export class UtilsController {
601
601
  await generator.updateConfig(this.config, isYamlProject);
602
602
  }
603
603
 
604
- async syncDb(
605
- databases: Models.Database[] = [],
606
- collections: Models.Collection[] = []
607
- ) {
604
+ async syncDb(
605
+ databases: Models.Database[] = [],
606
+ collections: Models.Collection[] = []
607
+ ) {
608
608
  await this.init();
609
609
  if (!this.database) {
610
610
  MessageFormatter.error("Database not initialized", undefined, { prefix: "Controller" });
@@ -614,10 +614,17 @@ export class UtilsController {
614
614
  const allDatabases = await fetchAllDatabases(this.database);
615
615
  databases = allDatabases;
616
616
  }
617
- await this.ensureDatabasesExist(databases);
618
- await this.ensureDatabaseConfigBucketsExist(databases);
619
- await this.createOrUpdateCollectionsForDatabases(databases, collections);
620
- }
617
+ // Ensure DBs exist (this may internally ensure migrations exists)
618
+ await this.ensureDatabasesExist(databases);
619
+ await this.ensureDatabaseConfigBucketsExist(databases);
620
+
621
+ // Do not push collections to the migrations database (prevents duplicate runs)
622
+ const dbsForCollections = databases.filter(
623
+ (db) => (this.config?.useMigrations ?? true) ? db.name.toLowerCase() !== "migrations" : true
624
+ );
625
+
626
+ await this.createOrUpdateCollectionsForDatabases(dbsForCollections, collections);
627
+ }
621
628
 
622
629
  getAppwriteFolderPath() {
623
630
  return this.appwriteFolderPath;