appwrite-utils-cli 1.2.16 → 1.2.18

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
@@ -637,7 +637,9 @@ npx appwrite-utils-cli appwrite-migrate --generateConstants --constantsLanguages
637
637
 
638
638
  ### Changelog
639
639
 
640
- - 1.1.15: Fixed various transfer and sync functionalities
640
+ - 1.2.18: Fix users transfer comparison not counting email or phone validation as a reason
641
+ - 1.2.17: Fixed users transfer not keeping validation of email / phone, temporarily disable bulk transfer to see if permissions aren't being updated by it
642
+ - 1.2.15: Fixed various transfer and sync functionalities
641
643
  - 1.0.5: Fixed `.` directories being ignored. Normally a good thing
642
644
  - 1.0.4: Fixed `appwriteConfig.yaml` being the name for the converted config, instead of `config.yaml`
643
645
  - 1.0.3: Fixed appwriteConfig detection for `--it` so it detects when you can migrate your config
@@ -322,7 +322,14 @@ export class ComprehensiveTransfer {
322
322
  }
323
323
  // Handle indexes with enhanced status checking
324
324
  MessageFormatter.info(`Creating indexes for collection ${collection.name} with enhanced monitoring...`, { prefix: "Transfer" });
325
- const indexesSuccess = await this.createCollectionIndexesWithStatusCheck(dbId, this.targetDatabases, targetCollection.$id, targetCollection, collection.indexes);
325
+ let indexesSuccess = true;
326
+ // Check if indexes need to be created ahead of time
327
+ if (collection.indexes.some((index) => !targetCollection.indexes.some((ti) => ti.key === index.key ||
328
+ ti.attributes.sort().join(",") ===
329
+ index.attributes.sort().join(","))) ||
330
+ collection.indexes.length !== targetCollection.indexes.length) {
331
+ indexesSuccess = await this.createCollectionIndexesWithStatusCheck(dbId, this.targetDatabases, targetCollection.$id, targetCollection, collection.indexes);
332
+ }
326
333
  if (!indexesSuccess) {
327
334
  MessageFormatter.error(`Failed to create some indexes for collection ${collection.name}`, undefined, { prefix: "Transfer" });
328
335
  MessageFormatter.warning(`Proceeding with document transfer despite index failures for collection ${collection.name}`, { prefix: "Transfer" });
@@ -810,8 +817,9 @@ export class ComprehensiveTransfer {
810
817
  let totalSkipped = 0;
811
818
  let totalUpdated = 0;
812
819
  // Check if bulk operations are supported
813
- const supportsBulk = this.options.sourceEndpoint.includes("cloud.appwrite.io") ||
814
- this.options.targetEndpoint.includes("cloud.appwrite.io");
820
+ const bulkEnabled = false;
821
+ // Temporarily disable to see if it fixes my permissions issues
822
+ const supportsBulk = bulkEnabled ? this.options.targetEndpoint.includes("cloud.appwrite.io") : false;
815
823
  if (supportsBulk) {
816
824
  MessageFormatter.info(`Using bulk operations for enhanced performance`, {
817
825
  prefix: "Transfer",
@@ -38,7 +38,9 @@ export const transferStorageLocalToLocal = async (storage, fromBucketId, toBucke
38
38
  continue;
39
39
  }
40
40
  const fileToCreate = InputFile.fromBuffer(new Uint8Array(fileData), file.name);
41
- MessageFormatter.progress(`Creating file: ${file.name}`, { prefix: "Transfer" });
41
+ MessageFormatter.progress(`Creating file: ${file.name}`, {
42
+ prefix: "Transfer",
43
+ });
42
44
  try {
43
45
  await tryAwaitWithRetry(async () => await storage.createFile(toBucketId, file.$id, fileToCreate, file.$permissions));
44
46
  }
@@ -114,7 +116,9 @@ export const transferStorageLocalToRemote = async (localStorage, endpoint, proje
114
116
  }
115
117
  catch (error) {
116
118
  // File already exists, so we can skip it
117
- MessageFormatter.warning(`File ${file.$id} already exists, skipping...`, { prefix: "Transfer" });
119
+ MessageFormatter.warning(`File ${file.$id} already exists, skipping...`, {
120
+ prefix: "Transfer",
121
+ });
118
122
  continue;
119
123
  }
120
124
  numberOfFiles++;
@@ -163,7 +167,7 @@ export const transferDatabaseLocalToLocal = async (localDb, fromDbId, targetDbId
163
167
  }
164
168
  // Handle attributes with enhanced status checking
165
169
  console.log(chalk.blue(`Creating attributes for collection ${collection.name} with enhanced monitoring...`));
166
- const allAttributes = collection.attributes.map(attr => parseAttribute(attr));
170
+ const allAttributes = collection.attributes.map((attr) => parseAttribute(attr));
167
171
  const attributeSuccess = await createUpdateCollectionAttributesWithStatusCheck(localDb, targetDbId, targetCollection, allAttributes);
168
172
  if (!attributeSuccess) {
169
173
  console.log(chalk.red(`❌ Failed to create all attributes for collection ${collection.name}, skipping to next collection`));
@@ -223,7 +227,7 @@ export const transferDatabaseLocalToRemote = async (localDb, endpoint, projectId
223
227
  }
224
228
  // Handle attributes with enhanced status checking
225
229
  console.log(chalk.blue(`Creating attributes for collection ${collection.name} with enhanced monitoring...`));
226
- const attributesToCreate = collection.attributes.map(attr => parseAttribute(attr));
230
+ const attributesToCreate = collection.attributes.map((attr) => parseAttribute(attr));
227
231
  const attributesSuccess = await createUpdateCollectionAttributesWithStatusCheck(remoteDb, toDbId, targetCollection, attributesToCreate);
228
232
  if (!attributesSuccess) {
229
233
  console.log(chalk.red(`Failed to create some attributes for collection ${collection.name}`));
@@ -269,10 +273,75 @@ export const transferUsersLocalToRemote = async (localUsers, endpoint, projectId
269
273
  for (const user of usersList.users) {
270
274
  try {
271
275
  // Check if user already exists in remote
276
+ let remoteUser;
272
277
  try {
273
- await tryAwaitWithRetry(async () => remoteUsers.get(user.$id));
274
- console.log(chalk.yellow(`User ${user.$id} already exists, skipping...`));
275
- continue;
278
+ remoteUser = await tryAwaitWithRetry(async () => remoteUsers.get(user.$id));
279
+ // If user exists, update only the differences
280
+ if (remoteUser) {
281
+ console.log(chalk.blue(`User ${user.$id} exists, checking for updates...`));
282
+ let hasUpdates = false;
283
+ // Update name if different
284
+ if (remoteUser.name !== user.name) {
285
+ await tryAwaitWithRetry(async () => remoteUsers.updateName(user.$id, user.name));
286
+ console.log(chalk.green(`Updated name for user ${user.$id}`));
287
+ hasUpdates = true;
288
+ }
289
+ // Update email if different
290
+ if (remoteUser.email !== user.email) {
291
+ await tryAwaitWithRetry(async () => remoteUsers.updateEmail(user.$id, user.email));
292
+ console.log(chalk.green(`Updated email for user ${user.$id}`));
293
+ hasUpdates = true;
294
+ }
295
+ // Update phone if different
296
+ const normalizedLocalPhone = user.phone
297
+ ? converterFunctions.convertPhoneStringToUSInternational(user.phone)
298
+ : undefined;
299
+ if (remoteUser.phone !== normalizedLocalPhone) {
300
+ if (normalizedLocalPhone) {
301
+ await tryAwaitWithRetry(async () => remoteUsers.updatePhone(user.$id, normalizedLocalPhone));
302
+ }
303
+ console.log(chalk.green(`Updated phone for user ${user.$id}`));
304
+ hasUpdates = true;
305
+ }
306
+ // Update preferences if different
307
+ if (JSON.stringify(remoteUser.prefs) !== JSON.stringify(user.prefs)) {
308
+ await tryAwaitWithRetry(async () => remoteUsers.updatePrefs(user.$id, user.prefs));
309
+ console.log(chalk.green(`Updated preferences for user ${user.$id}`));
310
+ hasUpdates = true;
311
+ }
312
+ // Update labels if different
313
+ if (JSON.stringify(remoteUser.labels) !== JSON.stringify(user.labels)) {
314
+ await tryAwaitWithRetry(async () => remoteUsers.updateLabels(user.$id, user.labels));
315
+ console.log(chalk.green(`Updated labels for user ${user.$id}`));
316
+ hasUpdates = true;
317
+ }
318
+ // Update email verification if different
319
+ if (remoteUser.emailVerification !== user.emailVerification) {
320
+ await tryAwaitWithRetry(async () => remoteUsers.updateEmailVerification(user.$id, user.emailVerification));
321
+ console.log(chalk.green(`Updated email verification for user ${user.$id}`));
322
+ hasUpdates = true;
323
+ }
324
+ // Update phone verification if different
325
+ if (remoteUser.phoneVerification !== user.phoneVerification) {
326
+ await tryAwaitWithRetry(async () => remoteUsers.updatePhoneVerification(user.$id, user.phoneVerification));
327
+ console.log(chalk.green(`Updated phone verification for user ${user.$id}`));
328
+ hasUpdates = true;
329
+ }
330
+ // Update status if different
331
+ if (remoteUser.status !== user.status) {
332
+ await tryAwaitWithRetry(async () => remoteUsers.updateStatus(user.$id, user.status));
333
+ console.log(chalk.green(`Updated status for user ${user.$id}`));
334
+ hasUpdates = true;
335
+ }
336
+ if (!hasUpdates) {
337
+ console.log(chalk.yellow(`User ${user.$id} is already up to date, skipping...`));
338
+ }
339
+ else {
340
+ totalTransferred++;
341
+ console.log(chalk.green(`Updated user ${user.$id}`));
342
+ }
343
+ continue;
344
+ }
276
345
  }
277
346
  catch (error) {
278
347
  // User doesn't exist, proceed with creation
@@ -288,55 +357,74 @@ export const transferUsersLocalToRemote = async (localUsers, endpoint, projectId
288
357
  const hashOptions = user.hashOptions || {};
289
358
  try {
290
359
  switch (hashType) {
291
- case 'argon2':
360
+ case "argon2":
292
361
  await tryAwaitWithRetry(async () => remoteUsers.createArgon2User(user.$id, user.email, hashedPassword, user.name));
293
362
  break;
294
- case 'bcrypt':
363
+ case "bcrypt":
295
364
  await tryAwaitWithRetry(async () => remoteUsers.createBcryptUser(user.$id, user.email, hashedPassword, user.name));
296
365
  break;
297
- case 'scrypt':
366
+ case "scrypt":
298
367
  // Scrypt requires additional parameters from hashOptions
299
- const salt = typeof hashOptions.salt === 'string' ? hashOptions.salt : '';
300
- const costCpu = typeof hashOptions.costCpu === 'number' ? hashOptions.costCpu : 32768;
301
- const costMemory = typeof hashOptions.costMemory === 'number' ? hashOptions.costMemory : 14;
302
- const costParallel = typeof hashOptions.costParallel === 'number' ? hashOptions.costParallel : 1;
303
- const length = typeof hashOptions.length === 'number' ? hashOptions.length : 64;
368
+ const salt = typeof hashOptions.salt === "string" ? hashOptions.salt : "";
369
+ const costCpu = typeof hashOptions.costCpu === "number"
370
+ ? hashOptions.costCpu
371
+ : 32768;
372
+ const costMemory = typeof hashOptions.costMemory === "number"
373
+ ? hashOptions.costMemory
374
+ : 14;
375
+ const costParallel = typeof hashOptions.costParallel === "number"
376
+ ? hashOptions.costParallel
377
+ : 1;
378
+ const length = typeof hashOptions.length === "number"
379
+ ? hashOptions.length
380
+ : 64;
304
381
  // Warn if using default values due to missing hash options
305
- if (!hashOptions.salt || typeof hashOptions.costCpu !== 'number') {
382
+ if (!hashOptions.salt ||
383
+ typeof hashOptions.costCpu !== "number") {
306
384
  console.log(chalk.yellow(`User ${user.$id}: Using default Scrypt parameters due to missing hashOptions`));
307
385
  }
308
386
  await tryAwaitWithRetry(async () => remoteUsers.createScryptUser(user.$id, user.email, hashedPassword, salt, costCpu, costMemory, costParallel, length, user.name));
309
387
  break;
310
- case 'scryptmodified':
388
+ case "scryptmodified":
311
389
  // Scrypt Modified (Firebase) requires salt, separator, and signer key
312
- const modSalt = typeof hashOptions.salt === 'string' ? hashOptions.salt : '';
313
- const saltSeparator = typeof hashOptions.saltSeparator === 'string' ? hashOptions.saltSeparator : '';
314
- const signerKey = typeof hashOptions.signerKey === 'string' ? hashOptions.signerKey : '';
390
+ const modSalt = typeof hashOptions.salt === "string" ? hashOptions.salt : "";
391
+ const saltSeparator = typeof hashOptions.saltSeparator === "string"
392
+ ? hashOptions.saltSeparator
393
+ : "";
394
+ const signerKey = typeof hashOptions.signerKey === "string"
395
+ ? hashOptions.signerKey
396
+ : "";
315
397
  // Warn if critical parameters are missing
316
- if (!hashOptions.salt || !hashOptions.saltSeparator || !hashOptions.signerKey) {
398
+ if (!hashOptions.salt ||
399
+ !hashOptions.saltSeparator ||
400
+ !hashOptions.signerKey) {
317
401
  console.log(chalk.yellow(`User ${user.$id}: Missing critical Scrypt Modified parameters in hashOptions`));
318
402
  }
319
403
  await tryAwaitWithRetry(async () => remoteUsers.createScryptModifiedUser(user.$id, user.email, hashedPassword, modSalt, saltSeparator, signerKey, user.name));
320
404
  break;
321
- case 'md5':
405
+ case "md5":
322
406
  await tryAwaitWithRetry(async () => remoteUsers.createMD5User(user.$id, user.email, hashedPassword, user.name));
323
407
  break;
324
- case 'sha':
325
- case 'sha1':
326
- case 'sha256':
327
- case 'sha512':
408
+ case "sha":
409
+ case "sha1":
410
+ case "sha256":
411
+ case "sha512":
328
412
  // SHA variants - determine version from hash type
329
413
  const getPasswordHashVersion = (hash) => {
330
414
  switch (hash.toLowerCase()) {
331
- case 'sha1': return 'sha1';
332
- case 'sha256': return 'sha256';
333
- case 'sha512': return 'sha512';
334
- default: return 'sha256'; // Default to SHA256
415
+ case "sha1":
416
+ return "sha1";
417
+ case "sha256":
418
+ return "sha256";
419
+ case "sha512":
420
+ return "sha512";
421
+ default:
422
+ return "sha256"; // Default to SHA256
335
423
  }
336
424
  };
337
425
  await tryAwaitWithRetry(async () => remoteUsers.createSHAUser(user.$id, user.email, hashedPassword, getPasswordHashVersion(hashType), user.name));
338
426
  break;
339
- case 'phpass':
427
+ case "phpass":
340
428
  await tryAwaitWithRetry(async () => remoteUsers.createPHPassUser(user.$id, user.email, hashedPassword, user.name));
341
429
  break;
342
430
  default:
@@ -361,18 +449,24 @@ export const transferUsersLocalToRemote = async (localUsers, endpoint, projectId
361
449
  console.log(chalk.yellow(`User ${user.$id} created with temporary password - password reset required`));
362
450
  }
363
451
  }
364
- // Update phone, labels, and other attributes
452
+ // Update phone, labels, and other attributes for newly created users
365
453
  if (phone) {
366
454
  await tryAwaitWithRetry(async () => remoteUsers.updatePhone(user.$id, phone));
367
455
  }
368
456
  if (user.labels && user.labels.length > 0) {
369
457
  await tryAwaitWithRetry(async () => remoteUsers.updateLabels(user.$id, user.labels));
370
458
  }
371
- // Update user preferences and status
459
+ // Update user preferences and status for newly created users
372
460
  await tryAwaitWithRetry(async () => remoteUsers.updatePrefs(user.$id, user.prefs));
373
- if (!user.emailVerification) {
461
+ if (user.emailVerification) {
462
+ await tryAwaitWithRetry(async () => remoteUsers.updateEmailVerification(user.$id, true));
463
+ }
464
+ else {
374
465
  await tryAwaitWithRetry(async () => remoteUsers.updateEmailVerification(user.$id, false));
375
466
  }
467
+ if (user.phoneVerification) {
468
+ await tryAwaitWithRetry(async () => remoteUsers.updatePhoneVerification(user.$id, true));
469
+ }
376
470
  if (user.status === false) {
377
471
  await tryAwaitWithRetry(async () => remoteUsers.updateStatus(user.$id, false));
378
472
  }
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.2.16",
4
+ "version": "1.2.18",
5
5
  "main": "src/main.ts",
6
6
  "type": "module",
7
7
  "repository": {
@@ -573,14 +573,28 @@ export class ComprehensiveTransfer {
573
573
  { prefix: "Transfer" }
574
574
  );
575
575
 
576
- const indexesSuccess =
577
- await this.createCollectionIndexesWithStatusCheck(
576
+ let indexesSuccess = true;
577
+ // Check if indexes need to be created ahead of time
578
+ if (
579
+ collection.indexes.some(
580
+ (index) =>
581
+ !targetCollection.indexes.some(
582
+ (ti) =>
583
+ ti.key === index.key ||
584
+ ti.attributes.sort().join(",") ===
585
+ index.attributes.sort().join(",")
586
+ )
587
+ ) ||
588
+ collection.indexes.length !== targetCollection.indexes.length
589
+ ) {
590
+ indexesSuccess = await this.createCollectionIndexesWithStatusCheck(
578
591
  dbId,
579
592
  this.targetDatabases,
580
593
  targetCollection.$id,
581
594
  targetCollection,
582
595
  collection.indexes as any
583
596
  );
597
+ }
584
598
 
585
599
  if (!indexesSuccess) {
586
600
  MessageFormatter.error(
@@ -1442,9 +1456,9 @@ export class ComprehensiveTransfer {
1442
1456
  let totalUpdated = 0;
1443
1457
 
1444
1458
  // Check if bulk operations are supported
1445
- const supportsBulk =
1446
- this.options.sourceEndpoint.includes("cloud.appwrite.io") ||
1447
- this.options.targetEndpoint.includes("cloud.appwrite.io");
1459
+ const bulkEnabled = false;
1460
+ // Temporarily disable to see if it fixes my permissions issues
1461
+ const supportsBulk = bulkEnabled ? this.options.targetEndpoint.includes("cloud.appwrite.io") : false;
1448
1462
 
1449
1463
  if (supportsBulk) {
1450
1464
  MessageFormatter.info(`Using bulk operations for enhanced performance`, {
@@ -45,7 +45,10 @@ export const transferStorageLocalToLocal = async (
45
45
  fromBucketId: string,
46
46
  toBucketId: string
47
47
  ) => {
48
- MessageFormatter.info(`Transferring files from ${fromBucketId} to ${toBucketId}`, { prefix: "Transfer" });
48
+ MessageFormatter.info(
49
+ `Transferring files from ${fromBucketId} to ${toBucketId}`,
50
+ { prefix: "Transfer" }
51
+ );
49
52
  let lastFileId: string | undefined;
50
53
  let fromFiles = await tryAwaitWithRetry(
51
54
  async () => await storage.listFiles(fromBucketId, [Query.limit(100)])
@@ -59,7 +62,11 @@ export const transferStorageLocalToLocal = async (
59
62
  try {
60
63
  return await storage.getFileDownload(bucketId, fileId);
61
64
  } catch (error) {
62
- MessageFormatter.error(`Error downloading file ${fileId}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
65
+ MessageFormatter.error(
66
+ `Error downloading file ${fileId}`,
67
+ error instanceof Error ? error : new Error(String(error)),
68
+ { prefix: "Transfer" }
69
+ );
63
70
  attempts--;
64
71
  if (attempts === 0) throw error;
65
72
  }
@@ -72,14 +79,20 @@ export const transferStorageLocalToLocal = async (
72
79
  async () => await downloadFileWithRetry(file.bucketId, file.$id)
73
80
  );
74
81
  if (!fileData) {
75
- MessageFormatter.error(`Error downloading file ${file.$id}`, undefined, { prefix: "Transfer" });
82
+ MessageFormatter.error(
83
+ `Error downloading file ${file.$id}`,
84
+ undefined,
85
+ { prefix: "Transfer" }
86
+ );
76
87
  continue;
77
88
  }
78
89
  const fileToCreate = InputFile.fromBuffer(
79
90
  new Uint8Array(fileData),
80
91
  file.name
81
92
  );
82
- MessageFormatter.progress(`Creating file: ${file.name}`, { prefix: "Transfer" });
93
+ MessageFormatter.progress(`Creating file: ${file.name}`, {
94
+ prefix: "Transfer",
95
+ });
83
96
  try {
84
97
  await tryAwaitWithRetry(
85
98
  async () =>
@@ -118,7 +131,11 @@ export const transferStorageLocalToLocal = async (
118
131
  async () => await downloadFileWithRetry(file.bucketId, file.$id)
119
132
  );
120
133
  if (!fileData) {
121
- MessageFormatter.error(`Error downloading file ${file.$id}`, undefined, { prefix: "Transfer" });
134
+ MessageFormatter.error(
135
+ `Error downloading file ${file.$id}`,
136
+ undefined,
137
+ { prefix: "Transfer" }
138
+ );
122
139
  continue;
123
140
  }
124
141
  const fileToCreate = InputFile.fromBuffer(
@@ -137,7 +154,10 @@ export const transferStorageLocalToLocal = async (
137
154
  );
138
155
  } catch (error: any) {
139
156
  // File already exists, so we can skip it
140
- MessageFormatter.warning(`File ${file.$id} already exists, skipping...`, { prefix: "Transfer" });
157
+ MessageFormatter.warning(
158
+ `File ${file.$id} already exists, skipping...`,
159
+ { prefix: "Transfer" }
160
+ );
141
161
  continue;
142
162
  }
143
163
  numberOfFiles++;
@@ -208,7 +228,9 @@ export const transferStorageLocalToRemote = async (
208
228
  );
209
229
  } catch (error: any) {
210
230
  // File already exists, so we can skip it
211
- MessageFormatter.warning(`File ${file.$id} already exists, skipping...`, { prefix: "Transfer" });
231
+ MessageFormatter.warning(`File ${file.$id} already exists, skipping...`, {
232
+ prefix: "Transfer",
233
+ });
212
234
  continue;
213
235
  }
214
236
  numberOfFiles++;
@@ -308,22 +330,37 @@ export const transferDatabaseLocalToLocal = async (
308
330
  }
309
331
 
310
332
  // Handle attributes with enhanced status checking
311
- console.log(chalk.blue(`Creating attributes for collection ${collection.name} with enhanced monitoring...`));
312
-
313
- const allAttributes = collection.attributes.map(attr => parseAttribute(attr as any));
314
- const attributeSuccess = await createUpdateCollectionAttributesWithStatusCheck(
315
- localDb,
316
- targetDbId,
317
- targetCollection,
318
- allAttributes
333
+ console.log(
334
+ chalk.blue(
335
+ `Creating attributes for collection ${collection.name} with enhanced monitoring...`
336
+ )
319
337
  );
320
-
338
+
339
+ const allAttributes = collection.attributes.map((attr) =>
340
+ parseAttribute(attr as any)
341
+ );
342
+ const attributeSuccess =
343
+ await createUpdateCollectionAttributesWithStatusCheck(
344
+ localDb,
345
+ targetDbId,
346
+ targetCollection,
347
+ allAttributes
348
+ );
349
+
321
350
  if (!attributeSuccess) {
322
- console.log(chalk.red(`❌ Failed to create all attributes for collection ${collection.name}, skipping to next collection`));
351
+ console.log(
352
+ chalk.red(
353
+ `❌ Failed to create all attributes for collection ${collection.name}, skipping to next collection`
354
+ )
355
+ );
323
356
  continue;
324
357
  }
325
-
326
- console.log(chalk.green(`✅ All attributes created successfully for collection ${collection.name}`));
358
+
359
+ console.log(
360
+ chalk.green(
361
+ `✅ All attributes created successfully for collection ${collection.name}`
362
+ )
363
+ );
327
364
 
328
365
  // Handle indexes
329
366
  const existingIndexes = await tryAwaitWithRetry(
@@ -361,7 +398,9 @@ export const transferDatabaseLocalToLocal = async (
361
398
  }
362
399
 
363
400
  // Transfer documents
364
- const { transferDocumentsBetweenDbsLocalToLocal } = await import("../collections/methods.js");
401
+ const { transferDocumentsBetweenDbsLocalToLocal } = await import(
402
+ "../collections/methods.js"
403
+ );
365
404
  await transferDocumentsBetweenDbsLocalToLocal(
366
405
  localDb,
367
406
  fromDbId,
@@ -456,27 +495,46 @@ export const transferDatabaseLocalToRemote = async (
456
495
  }
457
496
 
458
497
  // Handle attributes with enhanced status checking
459
- console.log(chalk.blue(`Creating attributes for collection ${collection.name} with enhanced monitoring...`));
460
-
461
- const attributesToCreate = collection.attributes.map(attr => parseAttribute(attr as any));
462
-
463
- const attributesSuccess = await createUpdateCollectionAttributesWithStatusCheck(
464
- remoteDb,
465
- toDbId,
466
- targetCollection,
467
- attributesToCreate
498
+ console.log(
499
+ chalk.blue(
500
+ `Creating attributes for collection ${collection.name} with enhanced monitoring...`
501
+ )
502
+ );
503
+
504
+ const attributesToCreate = collection.attributes.map((attr) =>
505
+ parseAttribute(attr as any)
468
506
  );
469
-
507
+
508
+ const attributesSuccess =
509
+ await createUpdateCollectionAttributesWithStatusCheck(
510
+ remoteDb,
511
+ toDbId,
512
+ targetCollection,
513
+ attributesToCreate
514
+ );
515
+
470
516
  if (!attributesSuccess) {
471
- console.log(chalk.red(`Failed to create some attributes for collection ${collection.name}`));
517
+ console.log(
518
+ chalk.red(
519
+ `Failed to create some attributes for collection ${collection.name}`
520
+ )
521
+ );
472
522
  // Continue with the transfer even if some attributes failed
473
523
  } else {
474
- console.log(chalk.green(`All attributes created successfully for collection ${collection.name}`));
524
+ console.log(
525
+ chalk.green(
526
+ `All attributes created successfully for collection ${collection.name}`
527
+ )
528
+ );
475
529
  }
476
530
 
477
531
  // Handle indexes with enhanced status checking
478
- console.log(chalk.blue(`Creating indexes for collection ${collection.name} with enhanced monitoring...`));
479
-
532
+ console.log(
533
+ chalk.blue(
534
+ `Creating indexes for collection ${collection.name} with enhanced monitoring...`
535
+ )
536
+ );
537
+
480
538
  const indexesSuccess = await createOrUpdateIndexesWithStatusCheck(
481
539
  toDbId,
482
540
  remoteDb,
@@ -484,16 +542,26 @@ export const transferDatabaseLocalToRemote = async (
484
542
  targetCollection,
485
543
  collection.indexes as any
486
544
  );
487
-
545
+
488
546
  if (!indexesSuccess) {
489
- console.log(chalk.red(`Failed to create some indexes for collection ${collection.name}`));
547
+ console.log(
548
+ chalk.red(
549
+ `Failed to create some indexes for collection ${collection.name}`
550
+ )
551
+ );
490
552
  // Continue with the transfer even if some indexes failed
491
553
  } else {
492
- console.log(chalk.green(`All indexes created successfully for collection ${collection.name}`));
554
+ console.log(
555
+ chalk.green(
556
+ `All indexes created successfully for collection ${collection.name}`
557
+ )
558
+ );
493
559
  }
494
560
 
495
561
  // Transfer documents
496
- const { transferDocumentsBetweenDbsLocalToRemote } = await import("../collections/methods.js");
562
+ const { transferDocumentsBetweenDbsLocalToRemote } = await import(
563
+ "../collections/methods.js"
564
+ );
497
565
  await transferDocumentsBetweenDbsLocalToRemote(
498
566
  localDb,
499
567
  endpoint,
@@ -544,12 +612,102 @@ export const transferUsersLocalToRemote = async (
544
612
  for (const user of usersList.users) {
545
613
  try {
546
614
  // Check if user already exists in remote
615
+ let remoteUser: Models.User<Models.Preferences> | undefined;
547
616
  try {
548
- await tryAwaitWithRetry(async () => remoteUsers.get(user.$id));
549
- console.log(
550
- chalk.yellow(`User ${user.$id} already exists, skipping...`)
617
+ remoteUser = await tryAwaitWithRetry(async () =>
618
+ remoteUsers.get(user.$id)
551
619
  );
552
- continue;
620
+
621
+ // If user exists, update only the differences
622
+ if (remoteUser) {
623
+ console.log(chalk.blue(`User ${user.$id} exists, checking for updates...`));
624
+ let hasUpdates = false;
625
+
626
+ // Update name if different
627
+ if (remoteUser.name !== user.name) {
628
+ await tryAwaitWithRetry(async () =>
629
+ remoteUsers.updateName(user.$id, user.name)
630
+ );
631
+ console.log(chalk.green(`Updated name for user ${user.$id}`));
632
+ hasUpdates = true;
633
+ }
634
+
635
+ // Update email if different
636
+ if (remoteUser.email !== user.email) {
637
+ await tryAwaitWithRetry(async () =>
638
+ remoteUsers.updateEmail(user.$id, user.email)
639
+ );
640
+ console.log(chalk.green(`Updated email for user ${user.$id}`));
641
+ hasUpdates = true;
642
+ }
643
+
644
+ // Update phone if different
645
+ const normalizedLocalPhone = user.phone
646
+ ? converterFunctions.convertPhoneStringToUSInternational(user.phone)
647
+ : undefined;
648
+ if (remoteUser.phone !== normalizedLocalPhone) {
649
+ if (normalizedLocalPhone) {
650
+ await tryAwaitWithRetry(async () =>
651
+ remoteUsers.updatePhone(user.$id, normalizedLocalPhone)
652
+ );
653
+ }
654
+ console.log(chalk.green(`Updated phone for user ${user.$id}`));
655
+ hasUpdates = true;
656
+ }
657
+
658
+ // Update preferences if different
659
+ if (JSON.stringify(remoteUser.prefs) !== JSON.stringify(user.prefs)) {
660
+ await tryAwaitWithRetry(async () =>
661
+ remoteUsers.updatePrefs(user.$id, user.prefs)
662
+ );
663
+ console.log(chalk.green(`Updated preferences for user ${user.$id}`));
664
+ hasUpdates = true;
665
+ }
666
+
667
+ // Update labels if different
668
+ if (JSON.stringify(remoteUser.labels) !== JSON.stringify(user.labels)) {
669
+ await tryAwaitWithRetry(async () =>
670
+ remoteUsers.updateLabels(user.$id, user.labels)
671
+ );
672
+ console.log(chalk.green(`Updated labels for user ${user.$id}`));
673
+ hasUpdates = true;
674
+ }
675
+
676
+ // Update email verification if different
677
+ if (remoteUser.emailVerification !== user.emailVerification) {
678
+ await tryAwaitWithRetry(async () =>
679
+ remoteUsers.updateEmailVerification(user.$id, user.emailVerification)
680
+ );
681
+ console.log(chalk.green(`Updated email verification for user ${user.$id}`));
682
+ hasUpdates = true;
683
+ }
684
+
685
+ // Update phone verification if different
686
+ if (remoteUser.phoneVerification !== user.phoneVerification) {
687
+ await tryAwaitWithRetry(async () =>
688
+ remoteUsers.updatePhoneVerification(user.$id, user.phoneVerification)
689
+ );
690
+ console.log(chalk.green(`Updated phone verification for user ${user.$id}`));
691
+ hasUpdates = true;
692
+ }
693
+
694
+ // Update status if different
695
+ if (remoteUser.status !== user.status) {
696
+ await tryAwaitWithRetry(async () =>
697
+ remoteUsers.updateStatus(user.$id, user.status)
698
+ );
699
+ console.log(chalk.green(`Updated status for user ${user.$id}`));
700
+ hasUpdates = true;
701
+ }
702
+
703
+ if (!hasUpdates) {
704
+ console.log(chalk.yellow(`User ${user.$id} is already up to date, skipping...`));
705
+ } else {
706
+ totalTransferred++;
707
+ console.log(chalk.green(`Updated user ${user.$id}`));
708
+ }
709
+ continue;
710
+ }
553
711
  } catch (error: any) {
554
712
  // User doesn't exist, proceed with creation
555
713
  }
@@ -564,10 +722,10 @@ export const transferUsersLocalToRemote = async (
564
722
  const hashType = user.hash.toLowerCase();
565
723
  const hashedPassword = user.password; // This is already hashed
566
724
  const hashOptions = (user.hashOptions as Record<string, any>) || {};
567
-
725
+
568
726
  try {
569
727
  switch (hashType) {
570
- case 'argon2':
728
+ case "argon2":
571
729
  await tryAwaitWithRetry(async () =>
572
730
  remoteUsers.createArgon2User(
573
731
  user.$id,
@@ -577,8 +735,8 @@ export const transferUsersLocalToRemote = async (
577
735
  )
578
736
  );
579
737
  break;
580
-
581
- case 'bcrypt':
738
+
739
+ case "bcrypt":
582
740
  await tryAwaitWithRetry(async () =>
583
741
  remoteUsers.createBcryptUser(
584
742
  user.$id,
@@ -588,20 +746,40 @@ export const transferUsersLocalToRemote = async (
588
746
  )
589
747
  );
590
748
  break;
591
-
592
- case 'scrypt':
749
+
750
+ case "scrypt":
593
751
  // Scrypt requires additional parameters from hashOptions
594
- const salt = typeof hashOptions.salt === 'string' ? hashOptions.salt : '';
595
- const costCpu = typeof hashOptions.costCpu === 'number' ? hashOptions.costCpu : 32768;
596
- const costMemory = typeof hashOptions.costMemory === 'number' ? hashOptions.costMemory : 14;
597
- const costParallel = typeof hashOptions.costParallel === 'number' ? hashOptions.costParallel : 1;
598
- const length = typeof hashOptions.length === 'number' ? hashOptions.length : 64;
599
-
752
+ const salt =
753
+ typeof hashOptions.salt === "string" ? hashOptions.salt : "";
754
+ const costCpu =
755
+ typeof hashOptions.costCpu === "number"
756
+ ? hashOptions.costCpu
757
+ : 32768;
758
+ const costMemory =
759
+ typeof hashOptions.costMemory === "number"
760
+ ? hashOptions.costMemory
761
+ : 14;
762
+ const costParallel =
763
+ typeof hashOptions.costParallel === "number"
764
+ ? hashOptions.costParallel
765
+ : 1;
766
+ const length =
767
+ typeof hashOptions.length === "number"
768
+ ? hashOptions.length
769
+ : 64;
770
+
600
771
  // Warn if using default values due to missing hash options
601
- if (!hashOptions.salt || typeof hashOptions.costCpu !== 'number') {
602
- console.log(chalk.yellow(`User ${user.$id}: Using default Scrypt parameters due to missing hashOptions`));
772
+ if (
773
+ !hashOptions.salt ||
774
+ typeof hashOptions.costCpu !== "number"
775
+ ) {
776
+ console.log(
777
+ chalk.yellow(
778
+ `User ${user.$id}: Using default Scrypt parameters due to missing hashOptions`
779
+ )
780
+ );
603
781
  }
604
-
782
+
605
783
  await tryAwaitWithRetry(async () =>
606
784
  remoteUsers.createScryptUser(
607
785
  user.$id,
@@ -616,18 +794,33 @@ export const transferUsersLocalToRemote = async (
616
794
  )
617
795
  );
618
796
  break;
619
-
620
- case 'scryptmodified':
797
+
798
+ case "scryptmodified":
621
799
  // Scrypt Modified (Firebase) requires salt, separator, and signer key
622
- const modSalt = typeof hashOptions.salt === 'string' ? hashOptions.salt : '';
623
- const saltSeparator = typeof hashOptions.saltSeparator === 'string' ? hashOptions.saltSeparator : '';
624
- const signerKey = typeof hashOptions.signerKey === 'string' ? hashOptions.signerKey : '';
625
-
800
+ const modSalt =
801
+ typeof hashOptions.salt === "string" ? hashOptions.salt : "";
802
+ const saltSeparator =
803
+ typeof hashOptions.saltSeparator === "string"
804
+ ? hashOptions.saltSeparator
805
+ : "";
806
+ const signerKey =
807
+ typeof hashOptions.signerKey === "string"
808
+ ? hashOptions.signerKey
809
+ : "";
810
+
626
811
  // Warn if critical parameters are missing
627
- if (!hashOptions.salt || !hashOptions.saltSeparator || !hashOptions.signerKey) {
628
- console.log(chalk.yellow(`User ${user.$id}: Missing critical Scrypt Modified parameters in hashOptions`));
812
+ if (
813
+ !hashOptions.salt ||
814
+ !hashOptions.saltSeparator ||
815
+ !hashOptions.signerKey
816
+ ) {
817
+ console.log(
818
+ chalk.yellow(
819
+ `User ${user.$id}: Missing critical Scrypt Modified parameters in hashOptions`
820
+ )
821
+ );
629
822
  }
630
-
823
+
631
824
  await tryAwaitWithRetry(async () =>
632
825
  remoteUsers.createScryptModifiedUser(
633
826
  user.$id,
@@ -640,8 +833,8 @@ export const transferUsersLocalToRemote = async (
640
833
  )
641
834
  );
642
835
  break;
643
-
644
- case 'md5':
836
+
837
+ case "md5":
645
838
  await tryAwaitWithRetry(async () =>
646
839
  remoteUsers.createMD5User(
647
840
  user.$id,
@@ -651,21 +844,25 @@ export const transferUsersLocalToRemote = async (
651
844
  )
652
845
  );
653
846
  break;
654
-
655
- case 'sha':
656
- case 'sha1':
657
- case 'sha256':
658
- case 'sha512':
847
+
848
+ case "sha":
849
+ case "sha1":
850
+ case "sha256":
851
+ case "sha512":
659
852
  // SHA variants - determine version from hash type
660
853
  const getPasswordHashVersion = (hash: string) => {
661
854
  switch (hash.toLowerCase()) {
662
- case 'sha1': return 'sha1' as any;
663
- case 'sha256': return 'sha256' as any;
664
- case 'sha512': return 'sha512' as any;
665
- default: return 'sha256' as any; // Default to SHA256
855
+ case "sha1":
856
+ return "sha1" as any;
857
+ case "sha256":
858
+ return "sha256" as any;
859
+ case "sha512":
860
+ return "sha512" as any;
861
+ default:
862
+ return "sha256" as any; // Default to SHA256
666
863
  }
667
864
  };
668
-
865
+
669
866
  await tryAwaitWithRetry(async () =>
670
867
  remoteUsers.createSHAUser(
671
868
  user.$id,
@@ -676,8 +873,8 @@ export const transferUsersLocalToRemote = async (
676
873
  )
677
874
  );
678
875
  break;
679
-
680
- case 'phpass':
876
+
877
+ case "phpass":
681
878
  await tryAwaitWithRetry(async () =>
682
879
  remoteUsers.createPHPassUser(
683
880
  user.$id,
@@ -687,9 +884,13 @@ export const transferUsersLocalToRemote = async (
687
884
  )
688
885
  );
689
886
  break;
690
-
887
+
691
888
  default:
692
- console.log(chalk.yellow(`Unknown hash type '${hashType}' for user ${user.$id}, falling back to Argon2`));
889
+ console.log(
890
+ chalk.yellow(
891
+ `Unknown hash type '${hashType}' for user ${user.$id}, falling back to Argon2`
892
+ )
893
+ );
693
894
  await tryAwaitWithRetry(async () =>
694
895
  remoteUsers.createArgon2User(
695
896
  user.$id,
@@ -700,12 +901,19 @@ export const transferUsersLocalToRemote = async (
700
901
  );
701
902
  break;
702
903
  }
703
-
704
- console.log(chalk.green(`User ${user.$id} created with preserved ${hashType} password`));
705
-
904
+
905
+ console.log(
906
+ chalk.green(
907
+ `User ${user.$id} created with preserved ${hashType} password`
908
+ )
909
+ );
706
910
  } catch (error) {
707
- console.log(chalk.yellow(`Failed to create user ${user.$id} with ${hashType} hash, trying with temporary password`));
708
-
911
+ console.log(
912
+ chalk.yellow(
913
+ `Failed to create user ${user.$id} with ${hashType} hash, trying with temporary password`
914
+ )
915
+ );
916
+
709
917
  // Fallback to creating user with temporary password
710
918
  await tryAwaitWithRetry(async () =>
711
919
  remoteUsers.create(
@@ -716,14 +924,17 @@ export const transferUsersLocalToRemote = async (
716
924
  user.name
717
925
  )
718
926
  );
719
-
720
- console.log(chalk.yellow(`User ${user.$id} created with temporary password - password reset required`));
927
+
928
+ console.log(
929
+ chalk.yellow(
930
+ `User ${user.$id} created with temporary password - password reset required`
931
+ )
932
+ );
721
933
  }
722
-
723
934
  } else {
724
935
  // No hash or password - create with temporary password
725
936
  const tempPassword = user.password || `changeMe${user.email}`;
726
-
937
+
727
938
  await tryAwaitWithRetry(async () =>
728
939
  remoteUsers.create(
729
940
  user.$id,
@@ -733,36 +944,50 @@ export const transferUsersLocalToRemote = async (
733
944
  user.name
734
945
  )
735
946
  );
736
-
947
+
737
948
  if (!user.password) {
738
- console.log(chalk.yellow(`User ${user.$id} created with temporary password - password reset required`));
949
+ console.log(
950
+ chalk.yellow(
951
+ `User ${user.$id} created with temporary password - password reset required`
952
+ )
953
+ );
739
954
  }
740
955
  }
741
-
742
- // Update phone, labels, and other attributes
956
+
957
+ // Update phone, labels, and other attributes for newly created users
743
958
  if (phone) {
744
959
  await tryAwaitWithRetry(async () =>
745
960
  remoteUsers.updatePhone(user.$id, phone)
746
961
  );
747
962
  }
748
-
963
+
749
964
  if (user.labels && user.labels.length > 0) {
750
965
  await tryAwaitWithRetry(async () =>
751
966
  remoteUsers.updateLabels(user.$id, user.labels)
752
967
  );
753
968
  }
754
969
 
755
- // Update user preferences and status
970
+ // Update user preferences and status for newly created users
756
971
  await tryAwaitWithRetry(async () =>
757
972
  remoteUsers.updatePrefs(user.$id, user.prefs)
758
973
  );
759
974
 
760
- if (!user.emailVerification) {
975
+ if (user.emailVerification) {
976
+ await tryAwaitWithRetry(async () =>
977
+ remoteUsers.updateEmailVerification(user.$id, true)
978
+ );
979
+ } else {
761
980
  await tryAwaitWithRetry(async () =>
762
981
  remoteUsers.updateEmailVerification(user.$id, false)
763
982
  );
764
983
  }
765
984
 
985
+ if (user.phoneVerification) {
986
+ await tryAwaitWithRetry(async () =>
987
+ remoteUsers.updatePhoneVerification(user.$id, true)
988
+ );
989
+ }
990
+
766
991
  if (user.status === false) {
767
992
  await tryAwaitWithRetry(async () =>
768
993
  remoteUsers.updateStatus(user.$id, false)