appwrite-utils-cli 1.0.8 → 1.1.0

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.
@@ -7,6 +7,7 @@ import { isNull, isUndefined, isNil, isPlainObject, isString, isJSONValue, chunk
7
7
  import { delay, tryAwaitWithRetry } from "../utils/helperFunctions.js";
8
8
  import { MessageFormatter } from "../shared/messageFormatter.js";
9
9
  import { ProgressManager } from "../shared/progressManager.js";
10
+ import chalk from "chalk";
10
11
  export const documentExists = async (db, dbId, targetCollectionId, toCreateObject) => {
11
12
  const collection = await db.getCollection(dbId, targetCollectionId);
12
13
  const attributes = collection.attributes;
@@ -380,69 +381,120 @@ export const transferDocumentsBetweenDbsLocalToLocal = async (db, fromDbId, toDb
380
381
  }
381
382
  MessageFormatter.success(`Transferred ${totalDocumentsTransferred} documents from database ${fromDbId} to database ${toDbId} -- collection ${fromCollId} to collection ${toCollId}`, { prefix: "Transfer" });
382
383
  };
384
+ /**
385
+ * Enhanced document transfer with fault tolerance and exponential backoff
386
+ */
387
+ const transferDocumentWithRetry = async (db, dbId, collectionId, documentId, documentData, permissions, maxRetries = 3, retryCount = 0) => {
388
+ try {
389
+ await db.createDocument(dbId, collectionId, documentId, documentData, permissions);
390
+ return true;
391
+ }
392
+ catch (error) {
393
+ // Check if document already exists
394
+ if (error.code === 409 || error.message?.includes('already exists')) {
395
+ console.log(chalk.yellow(`Document ${documentId} already exists, skipping...`));
396
+ return true;
397
+ }
398
+ if (retryCount < maxRetries) {
399
+ // Calculate exponential backoff: 1s, 2s, 4s
400
+ const exponentialDelay = Math.min(1000 * Math.pow(2, retryCount), 8000);
401
+ console.log(chalk.yellow(`Retrying document ${documentId} (attempt ${retryCount + 1}/${maxRetries}, backoff: ${exponentialDelay}ms)`));
402
+ await delay(exponentialDelay);
403
+ return await transferDocumentWithRetry(db, dbId, collectionId, documentId, documentData, permissions, maxRetries, retryCount + 1);
404
+ }
405
+ console.log(chalk.red(`Failed to transfer document ${documentId} after ${maxRetries} retries: ${error.message}`));
406
+ return false;
407
+ }
408
+ };
409
+ /**
410
+ * Enhanced batch document transfer with fault tolerance
411
+ */
412
+ const transferDocumentBatchWithRetry = async (db, dbId, collectionId, documents, batchSize = 10) => {
413
+ let successful = 0;
414
+ let failed = 0;
415
+ // Process documents in smaller batches to avoid overwhelming the server
416
+ const documentBatches = chunk(documents, batchSize);
417
+ for (const batch of documentBatches) {
418
+ console.log(chalk.blue(`Processing batch of ${batch.length} documents...`));
419
+ const batchPromises = batch.map(async (doc) => {
420
+ const toCreateObject = { ...doc };
421
+ delete toCreateObject.$databaseId;
422
+ delete toCreateObject.$collectionId;
423
+ delete toCreateObject.$createdAt;
424
+ delete toCreateObject.$updatedAt;
425
+ delete toCreateObject.$id;
426
+ delete toCreateObject.$permissions;
427
+ const result = await transferDocumentWithRetry(db, dbId, collectionId, doc.$id, toCreateObject, doc.$permissions || []);
428
+ return { docId: doc.$id, success: result };
429
+ });
430
+ const results = await Promise.allSettled(batchPromises);
431
+ results.forEach((result, index) => {
432
+ if (result.status === 'fulfilled') {
433
+ if (result.value.success) {
434
+ successful++;
435
+ }
436
+ else {
437
+ failed++;
438
+ }
439
+ }
440
+ else {
441
+ console.log(chalk.red(`Batch promise rejected for document ${batch[index].$id}: ${result.reason}`));
442
+ failed++;
443
+ }
444
+ });
445
+ // Add delay between batches to avoid rate limiting
446
+ if (documentBatches.indexOf(batch) < documentBatches.length - 1) {
447
+ await delay(500);
448
+ }
449
+ }
450
+ return { successful, failed };
451
+ };
383
452
  export const transferDocumentsBetweenDbsLocalToRemote = async (localDb, endpoint, projectId, apiKey, fromDbId, toDbId, fromCollId, toCollId) => {
453
+ console.log(chalk.blue(`Starting enhanced document transfer from ${fromCollId} to ${toCollId}...`));
384
454
  const client = new Client()
385
455
  .setEndpoint(endpoint)
386
456
  .setProject(projectId)
387
457
  .setKey(apiKey);
388
- let totalDocumentsTransferred = 0;
389
458
  const remoteDb = new Databases(client);
390
- let fromCollDocs = await tryAwaitWithRetry(async () => localDb.listDocuments(fromDbId, fromCollId, [Query.limit(50)]));
391
- if (fromCollDocs.documents.length === 0) {
459
+ let totalDocumentsProcessed = 0;
460
+ let totalSuccessful = 0;
461
+ let totalFailed = 0;
462
+ // Fetch documents in batches
463
+ let hasMoreDocuments = true;
464
+ let lastDocumentId;
465
+ while (hasMoreDocuments) {
466
+ const queries = [Query.limit(50)];
467
+ if (lastDocumentId) {
468
+ queries.push(Query.cursorAfter(lastDocumentId));
469
+ }
470
+ const fromCollDocs = await tryAwaitWithRetry(async () => localDb.listDocuments(fromDbId, fromCollId, queries));
471
+ if (fromCollDocs.documents.length === 0) {
472
+ hasMoreDocuments = false;
473
+ break;
474
+ }
475
+ console.log(chalk.blue(`Processing ${fromCollDocs.documents.length} documents...`));
476
+ const { successful, failed } = await transferDocumentBatchWithRetry(remoteDb, toDbId, toCollId, fromCollDocs.documents);
477
+ totalDocumentsProcessed += fromCollDocs.documents.length;
478
+ totalSuccessful += successful;
479
+ totalFailed += failed;
480
+ // Check if we have more documents to process
481
+ if (fromCollDocs.documents.length < 50) {
482
+ hasMoreDocuments = false;
483
+ }
484
+ else {
485
+ lastDocumentId = fromCollDocs.documents[fromCollDocs.documents.length - 1].$id;
486
+ }
487
+ console.log(chalk.gray(`Batch complete: ${successful} successful, ${failed} failed`));
488
+ }
489
+ if (totalDocumentsProcessed === 0) {
392
490
  MessageFormatter.info(`No documents found in collection ${fromCollId}`, { prefix: "Transfer" });
393
491
  return;
394
492
  }
395
- else if (fromCollDocs.documents.length < 50) {
396
- const batchedPromises = fromCollDocs.documents.map((doc) => {
397
- const toCreateObject = {
398
- ...doc,
399
- };
400
- delete toCreateObject.$databaseId;
401
- delete toCreateObject.$collectionId;
402
- delete toCreateObject.$createdAt;
403
- delete toCreateObject.$updatedAt;
404
- delete toCreateObject.$id;
405
- delete toCreateObject.$permissions;
406
- return tryAwaitWithRetry(async () => remoteDb.createDocument(toDbId, toCollId, doc.$id, toCreateObject, doc.$permissions));
407
- });
408
- await Promise.all(batchedPromises);
409
- totalDocumentsTransferred += fromCollDocs.documents.length;
493
+ const message = `Total documents processed: ${totalDocumentsProcessed}, successful: ${totalSuccessful}, failed: ${totalFailed}`;
494
+ if (totalFailed > 0) {
495
+ MessageFormatter.warning(message, { prefix: "Transfer" });
410
496
  }
411
497
  else {
412
- const batchedPromises = fromCollDocs.documents.map((doc) => {
413
- const toCreateObject = {
414
- ...doc,
415
- };
416
- delete toCreateObject.$databaseId;
417
- delete toCreateObject.$collectionId;
418
- delete toCreateObject.$createdAt;
419
- delete toCreateObject.$updatedAt;
420
- delete toCreateObject.$id;
421
- delete toCreateObject.$permissions;
422
- return tryAwaitWithRetry(async () => remoteDb.createDocument(toDbId, toCollId, doc.$id, toCreateObject, doc.$permissions));
423
- });
424
- await Promise.all(batchedPromises);
425
- totalDocumentsTransferred += fromCollDocs.documents.length;
426
- while (fromCollDocs.documents.length === 50) {
427
- fromCollDocs = await tryAwaitWithRetry(async () => localDb.listDocuments(fromDbId, fromCollId, [
428
- Query.limit(50),
429
- Query.cursorAfter(fromCollDocs.documents[fromCollDocs.documents.length - 1].$id),
430
- ]));
431
- const batchedPromises = fromCollDocs.documents.map((doc) => {
432
- const toCreateObject = {
433
- ...doc,
434
- };
435
- delete toCreateObject.$databaseId;
436
- delete toCreateObject.$collectionId;
437
- delete toCreateObject.$createdAt;
438
- delete toCreateObject.$updatedAt;
439
- delete toCreateObject.$id;
440
- delete toCreateObject.$permissions;
441
- return tryAwaitWithRetry(async () => remoteDb.createDocument(toDbId, toCollId, doc.$id, toCreateObject, doc.$permissions));
442
- });
443
- await Promise.all(batchedPromises);
444
- totalDocumentsTransferred += fromCollDocs.documents.length;
445
- }
498
+ MessageFormatter.success(message, { prefix: "Transfer" });
446
499
  }
447
- MessageFormatter.success(`Total documents transferred from database ${fromDbId} to database ${toDbId} -- collection ${fromCollId} to collection ${toCollId}: ${totalDocumentsTransferred}`, { prefix: "Transfer" });
448
500
  };
@@ -1516,48 +1516,140 @@ export class InteractiveCLI {
1516
1516
  async comprehensiveTransfer() {
1517
1517
  MessageFormatter.info("Starting comprehensive transfer configuration...", { prefix: "Transfer" });
1518
1518
  try {
1519
- // Get source configuration
1520
- const sourceConfig = await inquirer.prompt([
1521
- {
1522
- type: "input",
1523
- name: "sourceEndpoint",
1524
- message: "Enter the source Appwrite endpoint:",
1525
- validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
1526
- },
1527
- {
1528
- type: "input",
1529
- name: "sourceProject",
1530
- message: "Enter the source project ID:",
1531
- validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
1532
- },
1533
- {
1534
- type: "password",
1535
- name: "sourceKey",
1536
- message: "Enter the source API key:",
1537
- validate: (input) => input.trim() !== "" || "API key cannot be empty",
1538
- },
1539
- ]);
1540
- // Get target configuration
1541
- const targetConfig = await inquirer.prompt([
1542
- {
1543
- type: "input",
1544
- name: "targetEndpoint",
1545
- message: "Enter the target Appwrite endpoint:",
1546
- validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
1547
- },
1548
- {
1549
- type: "input",
1550
- name: "targetProject",
1551
- message: "Enter the target project ID:",
1552
- validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
1553
- },
1554
- {
1555
- type: "password",
1556
- name: "targetKey",
1557
- message: "Enter the target API key:",
1558
- validate: (input) => input.trim() !== "" || "API key cannot be empty",
1559
- },
1560
- ]);
1519
+ // Check if user has an appwrite config for easier setup
1520
+ const hasAppwriteConfig = this.controller?.config?.appwriteEndpoint &&
1521
+ this.controller?.config?.appwriteProject &&
1522
+ this.controller?.config?.appwriteKey;
1523
+ let sourceConfig;
1524
+ let targetConfig;
1525
+ if (hasAppwriteConfig) {
1526
+ // Offer to use existing config for source
1527
+ const { useConfigForSource } = await inquirer.prompt([
1528
+ {
1529
+ type: "confirm",
1530
+ name: "useConfigForSource",
1531
+ message: "Use your current appwriteConfig as the source?",
1532
+ default: true,
1533
+ },
1534
+ ]);
1535
+ if (useConfigForSource) {
1536
+ sourceConfig = {
1537
+ sourceEndpoint: this.controller.config.appwriteEndpoint,
1538
+ sourceProject: this.controller.config.appwriteProject,
1539
+ sourceKey: this.controller.config.appwriteKey,
1540
+ };
1541
+ MessageFormatter.info(`Using config source: ${sourceConfig.sourceEndpoint}`, { prefix: "Transfer" });
1542
+ }
1543
+ else {
1544
+ // Get source configuration manually
1545
+ sourceConfig = await inquirer.prompt([
1546
+ {
1547
+ type: "input",
1548
+ name: "sourceEndpoint",
1549
+ message: "Enter the source Appwrite endpoint:",
1550
+ validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
1551
+ },
1552
+ {
1553
+ type: "input",
1554
+ name: "sourceProject",
1555
+ message: "Enter the source project ID:",
1556
+ validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
1557
+ },
1558
+ {
1559
+ type: "password",
1560
+ name: "sourceKey",
1561
+ message: "Enter the source API key:",
1562
+ validate: (input) => input.trim() !== "" || "API key cannot be empty",
1563
+ },
1564
+ ]);
1565
+ }
1566
+ // Offer to use existing config for target
1567
+ const { useConfigForTarget } = await inquirer.prompt([
1568
+ {
1569
+ type: "confirm",
1570
+ name: "useConfigForTarget",
1571
+ message: "Use your current appwriteConfig as the target?",
1572
+ default: false,
1573
+ },
1574
+ ]);
1575
+ if (useConfigForTarget) {
1576
+ targetConfig = {
1577
+ targetEndpoint: this.controller.config.appwriteEndpoint,
1578
+ targetProject: this.controller.config.appwriteProject,
1579
+ targetKey: this.controller.config.appwriteKey,
1580
+ };
1581
+ MessageFormatter.info(`Using config target: ${targetConfig.targetEndpoint}`, { prefix: "Transfer" });
1582
+ }
1583
+ else {
1584
+ // Get target configuration manually
1585
+ targetConfig = await inquirer.prompt([
1586
+ {
1587
+ type: "input",
1588
+ name: "targetEndpoint",
1589
+ message: "Enter the target Appwrite endpoint:",
1590
+ validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
1591
+ },
1592
+ {
1593
+ type: "input",
1594
+ name: "targetProject",
1595
+ message: "Enter the target project ID:",
1596
+ validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
1597
+ },
1598
+ {
1599
+ type: "password",
1600
+ name: "targetKey",
1601
+ message: "Enter the target API key:",
1602
+ validate: (input) => input.trim() !== "" || "API key cannot be empty",
1603
+ },
1604
+ ]);
1605
+ }
1606
+ }
1607
+ else {
1608
+ // No appwrite config found, get both configurations manually
1609
+ MessageFormatter.info("No appwriteConfig found, please enter source and target configurations manually", { prefix: "Transfer" });
1610
+ // Get source configuration
1611
+ sourceConfig = await inquirer.prompt([
1612
+ {
1613
+ type: "input",
1614
+ name: "sourceEndpoint",
1615
+ message: "Enter the source Appwrite endpoint:",
1616
+ validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
1617
+ },
1618
+ {
1619
+ type: "input",
1620
+ name: "sourceProject",
1621
+ message: "Enter the source project ID:",
1622
+ validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
1623
+ },
1624
+ {
1625
+ type: "password",
1626
+ name: "sourceKey",
1627
+ message: "Enter the source API key:",
1628
+ validate: (input) => input.trim() !== "" || "API key cannot be empty",
1629
+ },
1630
+ ]);
1631
+ // Get target configuration
1632
+ targetConfig = await inquirer.prompt([
1633
+ {
1634
+ type: "input",
1635
+ name: "targetEndpoint",
1636
+ message: "Enter the target Appwrite endpoint:",
1637
+ validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
1638
+ },
1639
+ {
1640
+ type: "input",
1641
+ name: "targetProject",
1642
+ message: "Enter the target project ID:",
1643
+ validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
1644
+ },
1645
+ {
1646
+ type: "password",
1647
+ name: "targetKey",
1648
+ message: "Enter the target API key:",
1649
+ validate: (input) => input.trim() !== "" || "API key cannot be empty",
1650
+ },
1651
+ ]);
1652
+ }
1561
1653
  // Get transfer options
1562
1654
  const transferOptions = await inquirer.prompt([
1563
1655
  {
@@ -1620,16 +1712,18 @@ export class InteractiveCLI {
1620
1712
  MessageFormatter.info("Transfer cancelled by user", { prefix: "Transfer" });
1621
1713
  return;
1622
1714
  }
1623
- // Important password warning
1715
+ // Password preservation information
1624
1716
  if (transferOptions.transferTypes.includes("users") && !transferOptions.dryRun) {
1625
- MessageFormatter.warning("IMPORTANT: User passwords cannot be transferred due to Appwrite security limitations.", { prefix: "Transfer" });
1626
- MessageFormatter.warning("Users will need to reset their passwords after transfer.", { prefix: "Transfer" });
1717
+ MessageFormatter.info("User Password Transfer Information:", { prefix: "Transfer" });
1718
+ MessageFormatter.info("Users with hashed passwords (Argon2, Bcrypt, Scrypt, MD5, SHA, PHPass) will preserve their passwords", { prefix: "Transfer" });
1719
+ MessageFormatter.info("⚠️ Users without hash information will receive temporary passwords and need to reset", { prefix: "Transfer" });
1720
+ MessageFormatter.info("🔒 All user data (preferences, labels, verification status) will be preserved", { prefix: "Transfer" });
1627
1721
  const { continueWithUsers } = await inquirer.prompt([
1628
1722
  {
1629
1723
  type: "confirm",
1630
1724
  name: "continueWithUsers",
1631
- message: "Continue with user transfer knowing passwords will be reset?",
1632
- default: false,
1725
+ message: "Continue with user transfer?",
1726
+ default: true,
1633
1727
  },
1634
1728
  ]);
1635
1729
  if (!continueWithUsers) {
@@ -1665,7 +1759,8 @@ export class InteractiveCLI {
1665
1759
  else {
1666
1760
  MessageFormatter.success("Comprehensive transfer completed!", { prefix: "Transfer" });
1667
1761
  if (transferOptions.transferTypes.includes("users") && results.users.transferred > 0) {
1668
- MessageFormatter.info("Remember to notify users about password reset requirements", { prefix: "Transfer" });
1762
+ MessageFormatter.info("Users with preserved password hashes can log in with their original passwords", { prefix: "Transfer" });
1763
+ MessageFormatter.info("Users with temporary passwords will need to reset their passwords", { prefix: "Transfer" });
1669
1764
  }
1670
1765
  }
1671
1766
  }
@@ -2,13 +2,13 @@ import { converterFunctions, tryAwaitWithRetry } from "appwrite-utils";
2
2
  import { Client, Databases, IndexType, Query, Storage, Users, } from "node-appwrite";
3
3
  import { InputFile } from "node-appwrite/file";
4
4
  import { getAppwriteClient } from "../utils/helperFunctions.js";
5
- import { createOrUpdateAttribute, createUpdateCollectionAttributes, } from "../collections/attributes.js";
5
+ import { createOrUpdateAttribute, createUpdateCollectionAttributes, createUpdateCollectionAttributesWithStatusCheck, } from "../collections/attributes.js";
6
6
  import { parseAttribute } from "appwrite-utils";
7
7
  import chalk from "chalk";
8
8
  import { fetchAllCollections } from "../collections/methods.js";
9
9
  import { MessageFormatter } from "../shared/messageFormatter.js";
10
10
  import { ProgressManager } from "../shared/progressManager.js";
11
- import { createOrUpdateIndex, createOrUpdateIndexes, } from "../collections/indexes.js";
11
+ import { createOrUpdateIndex, createOrUpdateIndexes, createOrUpdateIndexesWithStatusCheck, } from "../collections/indexes.js";
12
12
  import { getClient } from "../utils/getClientFromConfig.js";
13
13
  export const transferStorageLocalToLocal = async (storage, fromBucketId, toBucketId) => {
14
14
  MessageFormatter.info(`Transferring files from ${fromBucketId} to ${toBucketId}`, { prefix: "Transfer" });
@@ -161,20 +161,15 @@ export const transferDatabaseLocalToLocal = async (localDb, fromDbId, targetDbId
161
161
  console.log(chalk.yellow(`Creating collection ${collection.name} in target database...`));
162
162
  targetCollection = await tryAwaitWithRetry(async () => localDb.createCollection(targetDbId, collection.$id, collection.name, collection.$permissions, collection.documentSecurity, collection.enabled));
163
163
  }
164
- // Handle attributes
165
- const existingAttributes = await tryAwaitWithRetry(async () => await localDb.listAttributes(targetDbId, targetCollection.$id));
166
- for (const attribute of collection.attributes) {
167
- const parsedAttribute = parseAttribute(attribute);
168
- const existingAttribute = existingAttributes.attributes.find((attr) => attr.key === parsedAttribute.key);
169
- if (!existingAttribute) {
170
- await tryAwaitWithRetry(async () => createOrUpdateAttribute(localDb, targetDbId, targetCollection, parsedAttribute));
171
- console.log(chalk.green(`Attribute ${parsedAttribute.key} created`));
172
- }
173
- else {
174
- console.log(chalk.blue(`Attribute ${parsedAttribute.key} exists, checking for updates...`));
175
- await tryAwaitWithRetry(async () => createOrUpdateAttribute(localDb, targetDbId, targetCollection, parsedAttribute));
176
- }
164
+ // Handle attributes with enhanced status checking
165
+ console.log(chalk.blue(`Creating attributes for collection ${collection.name} with enhanced monitoring...`));
166
+ const allAttributes = collection.attributes.map(attr => parseAttribute(attr));
167
+ const attributeSuccess = await createUpdateCollectionAttributesWithStatusCheck(localDb, targetDbId, targetCollection, allAttributes);
168
+ if (!attributeSuccess) {
169
+ console.log(chalk.red(`❌ Failed to create all attributes for collection ${collection.name}, skipping to next collection`));
170
+ continue;
177
171
  }
172
+ console.log(chalk.green(`✅ All attributes created successfully for collection ${collection.name}`));
178
173
  // Handle indexes
179
174
  const existingIndexes = await tryAwaitWithRetry(async () => await localDb.listIndexes(targetDbId, targetCollection.$id));
180
175
  for (const index of collection.indexes) {
@@ -226,32 +221,26 @@ export const transferDatabaseLocalToRemote = async (localDb, endpoint, projectId
226
221
  console.log(chalk.yellow(`Creating collection ${collection.name} in remote database...`));
227
222
  targetCollection = await tryAwaitWithRetry(async () => remoteDb.createCollection(toDbId, collection.$id, collection.name, collection.$permissions, collection.documentSecurity, collection.enabled));
228
223
  }
229
- // Handle attributes
230
- const existingAttributes = await tryAwaitWithRetry(async () => await remoteDb.listAttributes(toDbId, targetCollection.$id));
231
- for (const attribute of collection.attributes) {
232
- const parsedAttribute = parseAttribute(attribute);
233
- const existingAttribute = existingAttributes.attributes.find((attr) => attr.key === parsedAttribute.key);
234
- if (!existingAttribute) {
235
- await tryAwaitWithRetry(async () => createOrUpdateAttribute(remoteDb, toDbId, targetCollection, parsedAttribute));
236
- console.log(chalk.green(`Attribute ${parsedAttribute.key} created`));
237
- }
238
- else {
239
- console.log(chalk.blue(`Attribute ${parsedAttribute.key} exists, checking for updates...`));
240
- await tryAwaitWithRetry(async () => createOrUpdateAttribute(remoteDb, toDbId, targetCollection, parsedAttribute));
241
- }
224
+ // Handle attributes with enhanced status checking
225
+ console.log(chalk.blue(`Creating attributes for collection ${collection.name} with enhanced monitoring...`));
226
+ const attributesToCreate = collection.attributes.map(attr => parseAttribute(attr));
227
+ const attributesSuccess = await createUpdateCollectionAttributesWithStatusCheck(remoteDb, toDbId, targetCollection, attributesToCreate);
228
+ if (!attributesSuccess) {
229
+ console.log(chalk.red(`Failed to create some attributes for collection ${collection.name}`));
230
+ // Continue with the transfer even if some attributes failed
242
231
  }
243
- // Handle indexes
244
- const existingIndexes = await tryAwaitWithRetry(async () => await remoteDb.listIndexes(toDbId, targetCollection.$id));
245
- for (const index of collection.indexes) {
246
- const existingIndex = existingIndexes.indexes.find((idx) => idx.key === index.key);
247
- if (!existingIndex) {
248
- await createOrUpdateIndex(toDbId, remoteDb, targetCollection.$id, index);
249
- console.log(chalk.green(`Index ${index.key} created`));
250
- }
251
- else {
252
- console.log(chalk.blue(`Index ${index.key} exists, checking for updates...`));
253
- await createOrUpdateIndex(toDbId, remoteDb, targetCollection.$id, index);
254
- }
232
+ else {
233
+ console.log(chalk.green(`All attributes created successfully for collection ${collection.name}`));
234
+ }
235
+ // Handle indexes with enhanced status checking
236
+ console.log(chalk.blue(`Creating indexes for collection ${collection.name} with enhanced monitoring...`));
237
+ const indexesSuccess = await createOrUpdateIndexesWithStatusCheck(toDbId, remoteDb, targetCollection.$id, targetCollection, collection.indexes);
238
+ if (!indexesSuccess) {
239
+ console.log(chalk.red(`Failed to create some indexes for collection ${collection.name}`));
240
+ // Continue with the transfer even if some indexes failed
241
+ }
242
+ else {
243
+ console.log(chalk.green(`All indexes created successfully for collection ${collection.name}`));
255
244
  }
256
245
  // Transfer documents
257
246
  const { transferDocumentsBetweenDbsLocalToRemote } = await import("../collections/methods.js");
@@ -291,25 +280,94 @@ export const transferUsersLocalToRemote = async (localUsers, endpoint, projectId
291
280
  const phone = user.phone
292
281
  ? converterFunctions.convertPhoneStringToUSInternational(user.phone)
293
282
  : undefined;
294
- if (user.hash) {
295
- await tryAwaitWithRetry(async () => remoteUsers.createArgon2User(user.$id, user.email, user.password, // password - cannot transfer hashed passwords
296
- user.name // phone - optional
297
- ));
298
- if (phone) {
299
- await tryAwaitWithRetry(async () => remoteUsers.updatePhone(user.$id, phone));
283
+ // Handle user creation based on hash type
284
+ if (user.hash && user.password) {
285
+ // User has a hashed password - recreate with proper hash method
286
+ const hashType = user.hash.toLowerCase();
287
+ const hashedPassword = user.password; // This is already hashed
288
+ const hashOptions = user.hashOptions || {};
289
+ try {
290
+ switch (hashType) {
291
+ case 'argon2':
292
+ await tryAwaitWithRetry(async () => remoteUsers.createArgon2User(user.$id, user.email, hashedPassword, user.name));
293
+ break;
294
+ case 'bcrypt':
295
+ await tryAwaitWithRetry(async () => remoteUsers.createBcryptUser(user.$id, user.email, hashedPassword, user.name));
296
+ break;
297
+ case 'scrypt':
298
+ // 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;
304
+ // Warn if using default values due to missing hash options
305
+ if (!hashOptions.salt || typeof hashOptions.costCpu !== 'number') {
306
+ console.log(chalk.yellow(`User ${user.$id}: Using default Scrypt parameters due to missing hashOptions`));
307
+ }
308
+ await tryAwaitWithRetry(async () => remoteUsers.createScryptUser(user.$id, user.email, hashedPassword, salt, costCpu, costMemory, costParallel, length, user.name));
309
+ break;
310
+ case 'scryptmodified':
311
+ // 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 : '';
315
+ // Warn if critical parameters are missing
316
+ if (!hashOptions.salt || !hashOptions.saltSeparator || !hashOptions.signerKey) {
317
+ console.log(chalk.yellow(`User ${user.$id}: Missing critical Scrypt Modified parameters in hashOptions`));
318
+ }
319
+ await tryAwaitWithRetry(async () => remoteUsers.createScryptModifiedUser(user.$id, user.email, hashedPassword, modSalt, saltSeparator, signerKey, user.name));
320
+ break;
321
+ case 'md5':
322
+ await tryAwaitWithRetry(async () => remoteUsers.createMD5User(user.$id, user.email, hashedPassword, user.name));
323
+ break;
324
+ case 'sha':
325
+ case 'sha1':
326
+ case 'sha256':
327
+ case 'sha512':
328
+ // SHA variants - determine version from hash type
329
+ const getPasswordHashVersion = (hash) => {
330
+ 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
335
+ }
336
+ };
337
+ await tryAwaitWithRetry(async () => remoteUsers.createSHAUser(user.$id, user.email, hashedPassword, getPasswordHashVersion(hashType), user.name));
338
+ break;
339
+ case 'phpass':
340
+ await tryAwaitWithRetry(async () => remoteUsers.createPHPassUser(user.$id, user.email, hashedPassword, user.name));
341
+ break;
342
+ default:
343
+ console.log(chalk.yellow(`Unknown hash type '${hashType}' for user ${user.$id}, falling back to Argon2`));
344
+ await tryAwaitWithRetry(async () => remoteUsers.createArgon2User(user.$id, user.email, hashedPassword, user.name));
345
+ break;
346
+ }
347
+ console.log(chalk.green(`User ${user.$id} created with preserved ${hashType} password`));
300
348
  }
301
- if (user.labels && user.labels.length > 0) {
302
- await tryAwaitWithRetry(async () => remoteUsers.updateLabels(user.$id, user.labels));
349
+ catch (error) {
350
+ console.log(chalk.yellow(`Failed to create user ${user.$id} with ${hashType} hash, trying with temporary password`));
351
+ // Fallback to creating user with temporary password
352
+ await tryAwaitWithRetry(async () => remoteUsers.create(user.$id, user.email, phone, `changeMe${user.email}`, user.name));
353
+ console.log(chalk.yellow(`User ${user.$id} created with temporary password - password reset required`));
303
354
  }
304
355
  }
305
356
  else {
306
- await tryAwaitWithRetry(async () => remoteUsers.create(user.$id, user.email, phone, // phone - optional
307
- user.password, // password - cannot transfer hashed passwords
308
- user.name));
309
- if (user.labels && user.labels.length > 0) {
310
- await tryAwaitWithRetry(async () => remoteUsers.updateLabels(user.$id, user.labels));
357
+ // No hash or password - create with temporary password
358
+ const tempPassword = user.password || `changeMe${user.email}`;
359
+ await tryAwaitWithRetry(async () => remoteUsers.create(user.$id, user.email, phone, tempPassword, user.name));
360
+ if (!user.password) {
361
+ console.log(chalk.yellow(`User ${user.$id} created with temporary password - password reset required`));
311
362
  }
312
363
  }
364
+ // Update phone, labels, and other attributes
365
+ if (phone) {
366
+ await tryAwaitWithRetry(async () => remoteUsers.updatePhone(user.$id, phone));
367
+ }
368
+ if (user.labels && user.labels.length > 0) {
369
+ await tryAwaitWithRetry(async () => remoteUsers.updateLabels(user.$id, user.labels));
370
+ }
313
371
  // Update user preferences and status
314
372
  await tryAwaitWithRetry(async () => remoteUsers.updatePrefs(user.$id, user.prefs));
315
373
  if (!user.emailVerification) {
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.0.8",
4
+ "version": "1.1.0",
5
5
  "main": "src/main.ts",
6
6
  "type": "module",
7
7
  "repository": {