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.
- package/README.md +102 -0
- package/dist/collections/attributes.d.ts +8 -0
- package/dist/collections/attributes.js +195 -0
- package/dist/collections/indexes.d.ts +8 -0
- package/dist/collections/indexes.js +150 -0
- package/dist/collections/methods.js +105 -53
- package/dist/interactiveCLI.js +143 -48
- package/dist/migrations/transfer.js +111 -53
- package/package.json +1 -1
- package/src/collections/attributes.ts +339 -0
- package/src/collections/indexes.ts +264 -0
- package/src/collections/methods.ts +175 -87
- package/src/interactiveCLI.ts +146 -48
- package/src/migrations/transfer.ts +228 -121
@@ -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
|
391
|
-
|
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
|
-
|
396
|
-
|
397
|
-
|
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
|
-
|
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
|
};
|
package/dist/interactiveCLI.js
CHANGED
@@ -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
|
-
//
|
1520
|
-
const
|
1521
|
-
|
1522
|
-
|
1523
|
-
|
1524
|
-
|
1525
|
-
|
1526
|
-
|
1527
|
-
{
|
1528
|
-
|
1529
|
-
|
1530
|
-
|
1531
|
-
|
1532
|
-
|
1533
|
-
|
1534
|
-
|
1535
|
-
|
1536
|
-
|
1537
|
-
|
1538
|
-
|
1539
|
-
|
1540
|
-
|
1541
|
-
|
1542
|
-
|
1543
|
-
|
1544
|
-
|
1545
|
-
|
1546
|
-
|
1547
|
-
|
1548
|
-
|
1549
|
-
|
1550
|
-
|
1551
|
-
|
1552
|
-
|
1553
|
-
|
1554
|
-
|
1555
|
-
|
1556
|
-
|
1557
|
-
|
1558
|
-
|
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
|
-
//
|
1715
|
+
// Password preservation information
|
1624
1716
|
if (transferOptions.transferTypes.includes("users") && !transferOptions.dryRun) {
|
1625
|
-
MessageFormatter.
|
1626
|
-
MessageFormatter.
|
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
|
1632
|
-
default:
|
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("
|
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
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
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
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
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
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
}
|
251
|
-
|
252
|
-
|
253
|
-
|
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
|
-
|
295
|
-
|
296
|
-
|
297
|
-
)
|
298
|
-
|
299
|
-
|
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
|
-
|
302
|
-
|
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
|
-
|
307
|
-
user.password
|
308
|
-
user.name));
|
309
|
-
if (user.
|
310
|
-
|
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
|
4
|
+
"version": "1.1.0",
|
5
5
|
"main": "src/main.ts",
|
6
6
|
"type": "module",
|
7
7
|
"repository": {
|