appwrite-utils-cli 1.0.9 → 1.1.1
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 +48 -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 +134 -42
- package/dist/migrations/comprehensiveTransfer.d.ts +28 -0
- package/dist/migrations/comprehensiveTransfer.js +225 -7
- package/dist/migrations/transfer.js +29 -40
- 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 +137 -42
- package/src/migrations/comprehensiveTransfer.ts +348 -14
- package/src/migrations/transfer.ts +48 -99
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
|
+
// Initialize controller to optionally load config if available (supports both YAML and TypeScript configs)\n await this.initControllerIfNeeded();\n \n // 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
|
{
|
@@ -57,10 +57,38 @@ export declare class ComprehensiveTransfer {
|
|
57
57
|
execute(): Promise<TransferResults>;
|
58
58
|
private transferAllUsers;
|
59
59
|
private transferAllDatabases;
|
60
|
+
/**
|
61
|
+
* Phase 1: Create database structure (collections, attributes, indexes) without transferring documents
|
62
|
+
*/
|
63
|
+
private createDatabaseStructure;
|
64
|
+
/**
|
65
|
+
* Phase 2: Transfer documents to all collections in the database
|
66
|
+
*/
|
67
|
+
private transferDatabaseDocuments;
|
60
68
|
private transferAllBuckets;
|
61
69
|
private transferBucketFiles;
|
62
70
|
private validateAndDownloadFile;
|
63
71
|
private transferAllFunctions;
|
64
72
|
private downloadFunction;
|
73
|
+
/**
|
74
|
+
* Helper method to fetch all collections from a database
|
75
|
+
*/
|
76
|
+
private fetchAllCollections;
|
77
|
+
/**
|
78
|
+
* Helper method to parse attribute objects (simplified version of parseAttribute)
|
79
|
+
*/
|
80
|
+
private parseAttribute;
|
81
|
+
/**
|
82
|
+
* Helper method to create collection attributes with status checking
|
83
|
+
*/
|
84
|
+
private createCollectionAttributesWithStatusCheck;
|
85
|
+
/**
|
86
|
+
* Helper method to create collection indexes with status checking
|
87
|
+
*/
|
88
|
+
private createCollectionIndexesWithStatusCheck;
|
89
|
+
/**
|
90
|
+
* Helper method to transfer documents between databases
|
91
|
+
*/
|
92
|
+
private transferDocumentsBetweenDatabases;
|
65
93
|
private printSummary;
|
66
94
|
}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { converterFunctions, tryAwaitWithRetry } from "appwrite-utils";
|
1
|
+
import { converterFunctions, tryAwaitWithRetry, parseAttribute } from "appwrite-utils";
|
2
2
|
import { Client, Databases, Storage, Users, Functions, Query, } from "node-appwrite";
|
3
3
|
import { InputFile } from "node-appwrite/file";
|
4
4
|
import { MessageFormatter } from "../shared/messageFormatter.js";
|
@@ -132,7 +132,9 @@ export class ComprehensiveTransfer {
|
|
132
132
|
MessageFormatter.info(`DRY RUN: Would transfer ${sourceDatabases.databases.length} databases`, { prefix: "Transfer" });
|
133
133
|
return;
|
134
134
|
}
|
135
|
-
|
135
|
+
// Phase 1: Create all databases and collections (structure only)
|
136
|
+
MessageFormatter.info("Phase 1: Creating database structures (databases, collections, attributes, indexes)", { prefix: "Transfer" });
|
137
|
+
const structureCreationTasks = sourceDatabases.databases.map(db => this.limit(async () => {
|
136
138
|
try {
|
137
139
|
// Check if database exists in target
|
138
140
|
const existingDb = targetDatabases.databases.find(tdb => tdb.$id === db.$id);
|
@@ -141,23 +143,130 @@ export class ComprehensiveTransfer {
|
|
141
143
|
await this.targetDatabases.create(db.$id, db.name, db.enabled);
|
142
144
|
MessageFormatter.success(`Created database: ${db.name}`, { prefix: "Transfer" });
|
143
145
|
}
|
144
|
-
//
|
145
|
-
await
|
146
|
+
// Create collections, attributes, and indexes WITHOUT transferring documents
|
147
|
+
await this.createDatabaseStructure(db.$id);
|
148
|
+
MessageFormatter.success(`Database structure created: ${db.name}`, { prefix: "Transfer" });
|
149
|
+
}
|
150
|
+
catch (error) {
|
151
|
+
MessageFormatter.error(`Database structure creation failed for ${db.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
152
|
+
this.results.databases.failed++;
|
153
|
+
}
|
154
|
+
}));
|
155
|
+
await Promise.all(structureCreationTasks);
|
156
|
+
// Phase 2: Transfer all documents after all structures are created
|
157
|
+
MessageFormatter.info("Phase 2: Transferring documents to all collections", { prefix: "Transfer" });
|
158
|
+
const documentTransferTasks = sourceDatabases.databases.map(db => this.limit(async () => {
|
159
|
+
try {
|
160
|
+
// Transfer documents for this database
|
161
|
+
await this.transferDatabaseDocuments(db.$id);
|
146
162
|
this.results.databases.transferred++;
|
147
|
-
MessageFormatter.success(`Database ${db.name}
|
163
|
+
MessageFormatter.success(`Database documents transferred: ${db.name}`, { prefix: "Transfer" });
|
148
164
|
}
|
149
165
|
catch (error) {
|
150
|
-
MessageFormatter.error(`
|
166
|
+
MessageFormatter.error(`Document transfer failed for ${db.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
151
167
|
this.results.databases.failed++;
|
152
168
|
}
|
153
169
|
}));
|
154
|
-
await Promise.all(
|
170
|
+
await Promise.all(documentTransferTasks);
|
155
171
|
MessageFormatter.success("Database transfer phase completed", { prefix: "Transfer" });
|
156
172
|
}
|
157
173
|
catch (error) {
|
158
174
|
MessageFormatter.error("Database transfer phase failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
159
175
|
}
|
160
176
|
}
|
177
|
+
/**
|
178
|
+
* Phase 1: Create database structure (collections, attributes, indexes) without transferring documents
|
179
|
+
*/
|
180
|
+
async createDatabaseStructure(dbId) {
|
181
|
+
MessageFormatter.info(`Creating database structure for ${dbId}`, { prefix: "Transfer" });
|
182
|
+
try {
|
183
|
+
// Get all collections from source database
|
184
|
+
const sourceCollections = await this.fetchAllCollections(dbId, this.sourceDatabases);
|
185
|
+
MessageFormatter.info(`Found ${sourceCollections.length} collections in source database ${dbId}`, { prefix: "Transfer" });
|
186
|
+
// Process each collection
|
187
|
+
for (const collection of sourceCollections) {
|
188
|
+
MessageFormatter.info(`Processing collection: ${collection.name} (${collection.$id})`, { prefix: "Transfer" });
|
189
|
+
try {
|
190
|
+
// Create or update collection in target
|
191
|
+
let targetCollection;
|
192
|
+
const existingCollection = await tryAwaitWithRetry(async () => this.targetDatabases.listCollections(dbId, [Query.equal("$id", collection.$id)]));
|
193
|
+
if (existingCollection.collections.length > 0) {
|
194
|
+
targetCollection = existingCollection.collections[0];
|
195
|
+
MessageFormatter.info(`Collection ${collection.name} exists in target database`, { prefix: "Transfer" });
|
196
|
+
// Update collection if needed
|
197
|
+
if (targetCollection.name !== collection.name ||
|
198
|
+
JSON.stringify(targetCollection.$permissions) !== JSON.stringify(collection.$permissions) ||
|
199
|
+
targetCollection.documentSecurity !== collection.documentSecurity ||
|
200
|
+
targetCollection.enabled !== collection.enabled) {
|
201
|
+
targetCollection = await tryAwaitWithRetry(async () => this.targetDatabases.updateCollection(dbId, collection.$id, collection.name, collection.$permissions, collection.documentSecurity, collection.enabled));
|
202
|
+
MessageFormatter.success(`Collection ${collection.name} updated`, { prefix: "Transfer" });
|
203
|
+
}
|
204
|
+
}
|
205
|
+
else {
|
206
|
+
MessageFormatter.info(`Creating collection ${collection.name} in target database...`, { prefix: "Transfer" });
|
207
|
+
targetCollection = await tryAwaitWithRetry(async () => this.targetDatabases.createCollection(dbId, collection.$id, collection.name, collection.$permissions, collection.documentSecurity, collection.enabled));
|
208
|
+
MessageFormatter.success(`Collection ${collection.name} created`, { prefix: "Transfer" });
|
209
|
+
}
|
210
|
+
// Handle attributes with enhanced status checking
|
211
|
+
MessageFormatter.info(`Creating attributes for collection ${collection.name} with enhanced monitoring...`, { prefix: "Transfer" });
|
212
|
+
const attributesToCreate = collection.attributes.map(attr => parseAttribute(attr));
|
213
|
+
const attributesSuccess = await this.createCollectionAttributesWithStatusCheck(this.targetDatabases, dbId, targetCollection, attributesToCreate);
|
214
|
+
if (!attributesSuccess) {
|
215
|
+
MessageFormatter.error(`Failed to create some attributes for collection ${collection.name}`, undefined, { prefix: "Transfer" });
|
216
|
+
// Continue with the transfer even if some attributes failed
|
217
|
+
}
|
218
|
+
else {
|
219
|
+
MessageFormatter.success(`All attributes created successfully for collection ${collection.name}`, { prefix: "Transfer" });
|
220
|
+
}
|
221
|
+
// Handle indexes with enhanced status checking
|
222
|
+
MessageFormatter.info(`Creating indexes for collection ${collection.name} with enhanced monitoring...`, { prefix: "Transfer" });
|
223
|
+
const indexesSuccess = await this.createCollectionIndexesWithStatusCheck(dbId, this.targetDatabases, targetCollection.$id, targetCollection, collection.indexes);
|
224
|
+
if (!indexesSuccess) {
|
225
|
+
MessageFormatter.error(`Failed to create some indexes for collection ${collection.name}`, undefined, { prefix: "Transfer" });
|
226
|
+
// Continue with the transfer even if some indexes failed
|
227
|
+
}
|
228
|
+
else {
|
229
|
+
MessageFormatter.success(`All indexes created successfully for collection ${collection.name}`, { prefix: "Transfer" });
|
230
|
+
}
|
231
|
+
MessageFormatter.success(`Structure complete for collection ${collection.name}`, { prefix: "Transfer" });
|
232
|
+
}
|
233
|
+
catch (error) {
|
234
|
+
MessageFormatter.error(`Error processing collection ${collection.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
235
|
+
}
|
236
|
+
}
|
237
|
+
}
|
238
|
+
catch (error) {
|
239
|
+
MessageFormatter.error(`Failed to create database structure for ${dbId}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
240
|
+
throw error;
|
241
|
+
}
|
242
|
+
}
|
243
|
+
/**
|
244
|
+
* Phase 2: Transfer documents to all collections in the database
|
245
|
+
*/
|
246
|
+
async transferDatabaseDocuments(dbId) {
|
247
|
+
MessageFormatter.info(`Transferring documents for database ${dbId}`, { prefix: "Transfer" });
|
248
|
+
try {
|
249
|
+
// Get all collections from source database
|
250
|
+
const sourceCollections = await this.fetchAllCollections(dbId, this.sourceDatabases);
|
251
|
+
MessageFormatter.info(`Transferring documents for ${sourceCollections.length} collections in database ${dbId}`, { prefix: "Transfer" });
|
252
|
+
// Process each collection
|
253
|
+
for (const collection of sourceCollections) {
|
254
|
+
MessageFormatter.info(`Transferring documents for collection: ${collection.name} (${collection.$id})`, { prefix: "Transfer" });
|
255
|
+
try {
|
256
|
+
// Transfer documents
|
257
|
+
await this.transferDocumentsBetweenDatabases(this.sourceDatabases, this.targetDatabases, dbId, dbId, collection.$id, collection.$id);
|
258
|
+
MessageFormatter.success(`Documents transferred for collection ${collection.name}`, { prefix: "Transfer" });
|
259
|
+
}
|
260
|
+
catch (error) {
|
261
|
+
MessageFormatter.error(`Error transferring documents for collection ${collection.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
262
|
+
}
|
263
|
+
}
|
264
|
+
}
|
265
|
+
catch (error) {
|
266
|
+
MessageFormatter.error(`Failed to transfer documents for database ${dbId}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
267
|
+
throw error;
|
268
|
+
}
|
269
|
+
}
|
161
270
|
async transferAllBuckets() {
|
162
271
|
MessageFormatter.info("Starting bucket transfer phase", { prefix: "Transfer" });
|
163
272
|
try {
|
@@ -346,6 +455,115 @@ export class ComprehensiveTransfer {
|
|
346
455
|
return null;
|
347
456
|
}
|
348
457
|
}
|
458
|
+
/**
|
459
|
+
* Helper method to fetch all collections from a database
|
460
|
+
*/
|
461
|
+
async fetchAllCollections(dbId, databases) {
|
462
|
+
const collections = [];
|
463
|
+
let lastId;
|
464
|
+
while (true) {
|
465
|
+
const queries = [Query.limit(100)];
|
466
|
+
if (lastId) {
|
467
|
+
queries.push(Query.cursorAfter(lastId));
|
468
|
+
}
|
469
|
+
const result = await tryAwaitWithRetry(async () => databases.listCollections(dbId, queries));
|
470
|
+
if (result.collections.length === 0) {
|
471
|
+
break;
|
472
|
+
}
|
473
|
+
collections.push(...result.collections);
|
474
|
+
if (result.collections.length < 100) {
|
475
|
+
break;
|
476
|
+
}
|
477
|
+
lastId = result.collections[result.collections.length - 1].$id;
|
478
|
+
}
|
479
|
+
return collections;
|
480
|
+
}
|
481
|
+
/**
|
482
|
+
* Helper method to parse attribute objects (simplified version of parseAttribute)
|
483
|
+
*/
|
484
|
+
parseAttribute(attr) {
|
485
|
+
// This is a simplified version - in production you'd use the actual parseAttribute from appwrite-utils
|
486
|
+
return {
|
487
|
+
key: attr.key,
|
488
|
+
type: attr.type,
|
489
|
+
size: attr.size,
|
490
|
+
required: attr.required,
|
491
|
+
array: attr.array,
|
492
|
+
default: attr.default,
|
493
|
+
format: attr.format,
|
494
|
+
elements: attr.elements,
|
495
|
+
min: attr.min,
|
496
|
+
max: attr.max,
|
497
|
+
relatedCollection: attr.relatedCollection,
|
498
|
+
relationType: attr.relationType,
|
499
|
+
twoWay: attr.twoWay,
|
500
|
+
twoWayKey: attr.twoWayKey,
|
501
|
+
onDelete: attr.onDelete,
|
502
|
+
side: attr.side
|
503
|
+
};
|
504
|
+
}
|
505
|
+
/**
|
506
|
+
* Helper method to create collection attributes with status checking
|
507
|
+
*/
|
508
|
+
async createCollectionAttributesWithStatusCheck(databases, dbId, collection, attributes) {
|
509
|
+
// Import the enhanced attribute creation function
|
510
|
+
const { createUpdateCollectionAttributesWithStatusCheck } = await import("../collections/attributes.js");
|
511
|
+
return await createUpdateCollectionAttributesWithStatusCheck(databases, dbId, collection, attributes);
|
512
|
+
}
|
513
|
+
/**
|
514
|
+
* Helper method to create collection indexes with status checking
|
515
|
+
*/
|
516
|
+
async createCollectionIndexesWithStatusCheck(dbId, databases, collectionId, collection, indexes) {
|
517
|
+
// Import the enhanced index creation function
|
518
|
+
const { createOrUpdateIndexesWithStatusCheck } = await import("../collections/indexes.js");
|
519
|
+
return await createOrUpdateIndexesWithStatusCheck(dbId, databases, collectionId, collection, indexes);
|
520
|
+
}
|
521
|
+
/**
|
522
|
+
* Helper method to transfer documents between databases
|
523
|
+
*/
|
524
|
+
async transferDocumentsBetweenDatabases(sourceDb, targetDb, sourceDbId, targetDbId, sourceCollectionId, targetCollectionId) {
|
525
|
+
MessageFormatter.info(`Transferring documents from ${sourceCollectionId} to ${targetCollectionId}`, { prefix: "Transfer" });
|
526
|
+
let lastId;
|
527
|
+
let totalTransferred = 0;
|
528
|
+
while (true) {
|
529
|
+
const queries = [Query.limit(50)]; // Smaller batch size for better performance
|
530
|
+
if (lastId) {
|
531
|
+
queries.push(Query.cursorAfter(lastId));
|
532
|
+
}
|
533
|
+
const documents = await tryAwaitWithRetry(async () => sourceDb.listDocuments(sourceDbId, sourceCollectionId, queries));
|
534
|
+
if (documents.documents.length === 0) {
|
535
|
+
break;
|
536
|
+
}
|
537
|
+
// Transfer documents with rate limiting
|
538
|
+
const transferTasks = documents.documents.map(doc => this.limit(async () => {
|
539
|
+
try {
|
540
|
+
// Check if document already exists
|
541
|
+
try {
|
542
|
+
await targetDb.getDocument(targetDbId, targetCollectionId, doc.$id);
|
543
|
+
MessageFormatter.info(`Document ${doc.$id} already exists, skipping`, { prefix: "Transfer" });
|
544
|
+
return;
|
545
|
+
}
|
546
|
+
catch (error) {
|
547
|
+
// Document doesn't exist, proceed with creation
|
548
|
+
}
|
549
|
+
// Create document in target
|
550
|
+
const { $id, $createdAt, $updatedAt, $permissions, $databaseId, $collectionId, ...docData } = doc;
|
551
|
+
await tryAwaitWithRetry(async () => targetDb.createDocument(targetDbId, targetCollectionId, doc.$id, docData, doc.$permissions));
|
552
|
+
totalTransferred++;
|
553
|
+
MessageFormatter.success(`Transferred document ${doc.$id}`, { prefix: "Transfer" });
|
554
|
+
}
|
555
|
+
catch (error) {
|
556
|
+
MessageFormatter.error(`Failed to transfer document ${doc.$id}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
557
|
+
}
|
558
|
+
}));
|
559
|
+
await Promise.all(transferTasks);
|
560
|
+
if (documents.documents.length < 50) {
|
561
|
+
break;
|
562
|
+
}
|
563
|
+
lastId = documents.documents[documents.documents.length - 1].$id;
|
564
|
+
}
|
565
|
+
MessageFormatter.info(`Transferred ${totalTransferred} documents from ${sourceCollectionId} to ${targetCollectionId}`, { prefix: "Transfer" });
|
566
|
+
}
|
349
567
|
printSummary() {
|
350
568
|
const duration = Math.round((Date.now() - this.startTime) / 1000);
|
351
569
|
MessageFormatter.info("=== COMPREHENSIVE TRANSFER SUMMARY ===", { prefix: "Transfer" });
|
@@ -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");
|
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.
|
4
|
+
"version": "1.1.1",
|
5
5
|
"main": "src/main.ts",
|
6
6
|
"type": "module",
|
7
7
|
"repository": {
|