appwrite-utils-cli 1.0.9 → 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 +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/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/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
|
+
// 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
|
{
|
@@ -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.0
|
4
|
+
"version": "1.1.0",
|
5
5
|
"main": "src/main.ts",
|
6
6
|
"type": "module",
|
7
7
|
"repository": {
|
@@ -8,6 +8,165 @@ import { nameToIdMapping, enqueueOperation } from "../shared/operationQueue.js";
|
|
8
8
|
import { delay, tryAwaitWithRetry } from "../utils/helperFunctions.js";
|
9
9
|
import chalk from "chalk";
|
10
10
|
|
11
|
+
// Interface for attribute with status (fixing the type issue)
|
12
|
+
interface AttributeWithStatus {
|
13
|
+
key: string;
|
14
|
+
type: string;
|
15
|
+
status: 'available' | 'processing' | 'deleting' | 'stuck' | 'failed';
|
16
|
+
error: string;
|
17
|
+
required: boolean;
|
18
|
+
array?: boolean;
|
19
|
+
$createdAt: string;
|
20
|
+
$updatedAt: string;
|
21
|
+
[key: string]: any; // For type-specific fields
|
22
|
+
}
|
23
|
+
|
24
|
+
/**
|
25
|
+
* Wait for attribute to become available, with retry logic for stuck attributes and exponential backoff
|
26
|
+
*/
|
27
|
+
const waitForAttributeAvailable = async (
|
28
|
+
db: Databases,
|
29
|
+
dbId: string,
|
30
|
+
collectionId: string,
|
31
|
+
attributeKey: string,
|
32
|
+
maxWaitTime: number = 60000, // 1 minute
|
33
|
+
retryCount: number = 0,
|
34
|
+
maxRetries: number = 5
|
35
|
+
): Promise<boolean> => {
|
36
|
+
const startTime = Date.now();
|
37
|
+
let checkInterval = 2000; // Start with 2 seconds
|
38
|
+
|
39
|
+
// Calculate exponential backoff: 2s, 4s, 8s, 16s, 30s (capped at 30s)
|
40
|
+
if (retryCount > 0) {
|
41
|
+
const exponentialDelay = Math.min(2000 * Math.pow(2, retryCount), 30000);
|
42
|
+
console.log(chalk.blue(`Waiting for attribute '${attributeKey}' to become available (retry ${retryCount}, backoff: ${exponentialDelay}ms)...`));
|
43
|
+
await delay(exponentialDelay);
|
44
|
+
} else {
|
45
|
+
console.log(chalk.blue(`Waiting for attribute '${attributeKey}' to become available...`));
|
46
|
+
}
|
47
|
+
|
48
|
+
while (Date.now() - startTime < maxWaitTime) {
|
49
|
+
try {
|
50
|
+
const collection = await db.getCollection(dbId, collectionId);
|
51
|
+
const attribute = (collection.attributes as any[]).find(
|
52
|
+
(attr: AttributeWithStatus) => attr.key === attributeKey
|
53
|
+
) as AttributeWithStatus | undefined;
|
54
|
+
|
55
|
+
if (!attribute) {
|
56
|
+
console.log(chalk.red(`Attribute '${attributeKey}' not found`));
|
57
|
+
return false;
|
58
|
+
}
|
59
|
+
|
60
|
+
console.log(chalk.gray(`Attribute '${attributeKey}' status: ${attribute.status}`));
|
61
|
+
|
62
|
+
switch (attribute.status) {
|
63
|
+
case 'available':
|
64
|
+
console.log(chalk.green(`✅ Attribute '${attributeKey}' is now available`));
|
65
|
+
return true;
|
66
|
+
|
67
|
+
case 'failed':
|
68
|
+
console.log(chalk.red(`❌ Attribute '${attributeKey}' failed: ${attribute.error}`));
|
69
|
+
return false;
|
70
|
+
|
71
|
+
case 'stuck':
|
72
|
+
console.log(chalk.yellow(`⚠️ Attribute '${attributeKey}' is stuck, will retry...`));
|
73
|
+
return false;
|
74
|
+
|
75
|
+
case 'processing':
|
76
|
+
// Continue waiting
|
77
|
+
break;
|
78
|
+
|
79
|
+
case 'deleting':
|
80
|
+
console.log(chalk.yellow(`Attribute '${attributeKey}' is being deleted`));
|
81
|
+
break;
|
82
|
+
|
83
|
+
default:
|
84
|
+
console.log(chalk.yellow(`Unknown status '${attribute.status}' for attribute '${attributeKey}'`));
|
85
|
+
break;
|
86
|
+
}
|
87
|
+
|
88
|
+
await delay(checkInterval);
|
89
|
+
} catch (error) {
|
90
|
+
console.log(chalk.red(`Error checking attribute status: ${error}`));
|
91
|
+
return false;
|
92
|
+
}
|
93
|
+
}
|
94
|
+
|
95
|
+
// Timeout reached
|
96
|
+
console.log(chalk.yellow(`⏰ Timeout waiting for attribute '${attributeKey}' (${maxWaitTime}ms)`));
|
97
|
+
|
98
|
+
// If we have retries left and this isn't the last retry, try recreating
|
99
|
+
if (retryCount < maxRetries) {
|
100
|
+
console.log(chalk.yellow(`🔄 Retrying attribute creation (attempt ${retryCount + 1}/${maxRetries})`));
|
101
|
+
return false; // Signal that we need to retry
|
102
|
+
}
|
103
|
+
|
104
|
+
return false;
|
105
|
+
};
|
106
|
+
|
107
|
+
/**
|
108
|
+
* Wait for all attributes in a collection to become available
|
109
|
+
*/
|
110
|
+
const waitForAllAttributesAvailable = async (
|
111
|
+
db: Databases,
|
112
|
+
dbId: string,
|
113
|
+
collectionId: string,
|
114
|
+
attributeKeys: string[],
|
115
|
+
maxWaitTime: number = 60000
|
116
|
+
): Promise<string[]> => {
|
117
|
+
console.log(chalk.blue(`Waiting for ${attributeKeys.length} attributes to become available...`));
|
118
|
+
|
119
|
+
const failedAttributes: string[] = [];
|
120
|
+
|
121
|
+
for (const attributeKey of attributeKeys) {
|
122
|
+
const success = await waitForAttributeAvailable(db, dbId, collectionId, attributeKey, maxWaitTime);
|
123
|
+
if (!success) {
|
124
|
+
failedAttributes.push(attributeKey);
|
125
|
+
}
|
126
|
+
}
|
127
|
+
|
128
|
+
return failedAttributes;
|
129
|
+
};
|
130
|
+
|
131
|
+
/**
|
132
|
+
* Delete collection and recreate with retry logic
|
133
|
+
*/
|
134
|
+
const deleteAndRecreateCollection = async (
|
135
|
+
db: Databases,
|
136
|
+
dbId: string,
|
137
|
+
collection: Models.Collection,
|
138
|
+
retryCount: number
|
139
|
+
): Promise<Models.Collection | null> => {
|
140
|
+
try {
|
141
|
+
console.log(chalk.yellow(`🗑️ Deleting collection '${collection.name}' for retry ${retryCount}`));
|
142
|
+
|
143
|
+
// Delete the collection
|
144
|
+
await db.deleteCollection(dbId, collection.$id);
|
145
|
+
console.log(chalk.yellow(`Deleted collection '${collection.name}'`));
|
146
|
+
|
147
|
+
// Wait a bit before recreating
|
148
|
+
await delay(2000);
|
149
|
+
|
150
|
+
// Recreate the collection
|
151
|
+
console.log(chalk.blue(`🔄 Recreating collection '${collection.name}'`));
|
152
|
+
const newCollection = await db.createCollection(
|
153
|
+
dbId,
|
154
|
+
collection.$id,
|
155
|
+
collection.name,
|
156
|
+
collection.$permissions,
|
157
|
+
collection.documentSecurity,
|
158
|
+
collection.enabled
|
159
|
+
);
|
160
|
+
|
161
|
+
console.log(chalk.green(`✅ Recreated collection '${collection.name}'`));
|
162
|
+
return newCollection;
|
163
|
+
|
164
|
+
} catch (error) {
|
165
|
+
console.log(chalk.red(`Failed to delete/recreate collection '${collection.name}': ${error}`));
|
166
|
+
return null;
|
167
|
+
}
|
168
|
+
};
|
169
|
+
|
11
170
|
const attributesSame = (
|
12
171
|
databaseAttribute: Attribute,
|
13
172
|
configAttribute: Attribute
|
@@ -72,6 +231,87 @@ const attributesSame = (
|
|
72
231
|
});
|
73
232
|
};
|
74
233
|
|
234
|
+
/**
|
235
|
+
* Enhanced attribute creation with proper status monitoring and retry logic
|
236
|
+
*/
|
237
|
+
export const createOrUpdateAttributeWithStatusCheck = async (
|
238
|
+
db: Databases,
|
239
|
+
dbId: string,
|
240
|
+
collection: Models.Collection,
|
241
|
+
attribute: Attribute,
|
242
|
+
retryCount: number = 0,
|
243
|
+
maxRetries: number = 5
|
244
|
+
): Promise<boolean> => {
|
245
|
+
console.log(chalk.blue(`Creating/updating attribute '${attribute.key}' (attempt ${retryCount + 1}/${maxRetries + 1})`));
|
246
|
+
|
247
|
+
try {
|
248
|
+
// First, try to create/update the attribute using existing logic
|
249
|
+
await createOrUpdateAttribute(db, dbId, collection, attribute);
|
250
|
+
|
251
|
+
// Now wait for the attribute to become available
|
252
|
+
const success = await waitForAttributeAvailable(
|
253
|
+
db,
|
254
|
+
dbId,
|
255
|
+
collection.$id,
|
256
|
+
attribute.key,
|
257
|
+
60000, // 1 minute timeout
|
258
|
+
retryCount,
|
259
|
+
maxRetries
|
260
|
+
);
|
261
|
+
|
262
|
+
if (success) {
|
263
|
+
return true;
|
264
|
+
}
|
265
|
+
|
266
|
+
// If not successful and we have retries left, delete collection and try again
|
267
|
+
if (retryCount < maxRetries) {
|
268
|
+
console.log(chalk.yellow(`Attribute '${attribute.key}' failed/stuck, retrying...`));
|
269
|
+
|
270
|
+
// Get fresh collection data
|
271
|
+
const freshCollection = await db.getCollection(dbId, collection.$id);
|
272
|
+
|
273
|
+
// Delete and recreate collection
|
274
|
+
const newCollection = await deleteAndRecreateCollection(db, dbId, freshCollection, retryCount + 1);
|
275
|
+
|
276
|
+
if (newCollection) {
|
277
|
+
// Retry with the new collection
|
278
|
+
return await createOrUpdateAttributeWithStatusCheck(
|
279
|
+
db,
|
280
|
+
dbId,
|
281
|
+
newCollection,
|
282
|
+
attribute,
|
283
|
+
retryCount + 1,
|
284
|
+
maxRetries
|
285
|
+
);
|
286
|
+
}
|
287
|
+
}
|
288
|
+
|
289
|
+
console.log(chalk.red(`❌ Failed to create attribute '${attribute.key}' after ${maxRetries + 1} attempts`));
|
290
|
+
return false;
|
291
|
+
|
292
|
+
} catch (error) {
|
293
|
+
console.log(chalk.red(`Error creating attribute '${attribute.key}': ${error}`));
|
294
|
+
|
295
|
+
if (retryCount < maxRetries) {
|
296
|
+
console.log(chalk.yellow(`Retrying attribute '${attribute.key}' due to error...`));
|
297
|
+
|
298
|
+
// Wait a bit before retry
|
299
|
+
await delay(2000);
|
300
|
+
|
301
|
+
return await createOrUpdateAttributeWithStatusCheck(
|
302
|
+
db,
|
303
|
+
dbId,
|
304
|
+
collection,
|
305
|
+
attribute,
|
306
|
+
retryCount + 1,
|
307
|
+
maxRetries
|
308
|
+
);
|
309
|
+
}
|
310
|
+
|
311
|
+
return false;
|
312
|
+
}
|
313
|
+
};
|
314
|
+
|
75
315
|
export const createOrUpdateAttribute = async (
|
76
316
|
db: Databases,
|
77
317
|
dbId: string,
|
@@ -514,6 +754,105 @@ export const createOrUpdateAttribute = async (
|
|
514
754
|
}
|
515
755
|
};
|
516
756
|
|
757
|
+
/**
|
758
|
+
* Enhanced collection attribute creation with proper status monitoring
|
759
|
+
*/
|
760
|
+
export const createUpdateCollectionAttributesWithStatusCheck = async (
|
761
|
+
db: Databases,
|
762
|
+
dbId: string,
|
763
|
+
collection: Models.Collection,
|
764
|
+
attributes: Attribute[]
|
765
|
+
): Promise<boolean> => {
|
766
|
+
console.log(
|
767
|
+
chalk.green(
|
768
|
+
`Creating/Updating attributes for collection: ${collection.name} with status monitoring`
|
769
|
+
)
|
770
|
+
);
|
771
|
+
|
772
|
+
const existingAttributes: Attribute[] =
|
773
|
+
// @ts-expect-error
|
774
|
+
collection.attributes.map((attr) => parseAttribute(attr)) || [];
|
775
|
+
|
776
|
+
const attributesToRemove = existingAttributes.filter(
|
777
|
+
(attr) => !attributes.some((a) => a.key === attr.key)
|
778
|
+
);
|
779
|
+
const indexesToRemove = collection.indexes.filter((index) =>
|
780
|
+
attributesToRemove.some((attr) => index.attributes.includes(attr.key))
|
781
|
+
);
|
782
|
+
|
783
|
+
// Handle attribute removal first
|
784
|
+
if (attributesToRemove.length > 0) {
|
785
|
+
if (indexesToRemove.length > 0) {
|
786
|
+
console.log(
|
787
|
+
chalk.red(
|
788
|
+
`Removing indexes as they rely on an attribute that is being removed: ${indexesToRemove
|
789
|
+
.map((index) => index.key)
|
790
|
+
.join(", ")}`
|
791
|
+
)
|
792
|
+
);
|
793
|
+
for (const index of indexesToRemove) {
|
794
|
+
await tryAwaitWithRetry(
|
795
|
+
async () => await db.deleteIndex(dbId, collection.$id, index.key)
|
796
|
+
);
|
797
|
+
await delay(500); // Longer delay for deletions
|
798
|
+
}
|
799
|
+
}
|
800
|
+
for (const attr of attributesToRemove) {
|
801
|
+
console.log(
|
802
|
+
chalk.red(
|
803
|
+
`Removing attribute: ${attr.key} as it is no longer in the collection`
|
804
|
+
)
|
805
|
+
);
|
806
|
+
await tryAwaitWithRetry(
|
807
|
+
async () => await db.deleteAttribute(dbId, collection.$id, attr.key)
|
808
|
+
);
|
809
|
+
await delay(500); // Longer delay for deletions
|
810
|
+
}
|
811
|
+
}
|
812
|
+
|
813
|
+
// Create attributes ONE BY ONE with proper status checking
|
814
|
+
console.log(chalk.blue(`Creating ${attributes.length} attributes sequentially with status monitoring...`));
|
815
|
+
|
816
|
+
let currentCollection = collection;
|
817
|
+
const failedAttributes: string[] = [];
|
818
|
+
|
819
|
+
for (const attribute of attributes) {
|
820
|
+
console.log(chalk.blue(`\n--- Processing attribute: ${attribute.key} ---`));
|
821
|
+
|
822
|
+
const success = await createOrUpdateAttributeWithStatusCheck(
|
823
|
+
db,
|
824
|
+
dbId,
|
825
|
+
currentCollection,
|
826
|
+
attribute
|
827
|
+
);
|
828
|
+
|
829
|
+
if (success) {
|
830
|
+
console.log(chalk.green(`✅ Successfully created attribute: ${attribute.key}`));
|
831
|
+
|
832
|
+
// Get updated collection data for next iteration
|
833
|
+
try {
|
834
|
+
currentCollection = await db.getCollection(dbId, collection.$id);
|
835
|
+
} catch (error) {
|
836
|
+
console.log(chalk.yellow(`Warning: Could not refresh collection data: ${error}`));
|
837
|
+
}
|
838
|
+
|
839
|
+
// Add delay between successful attributes
|
840
|
+
await delay(1000);
|
841
|
+
} else {
|
842
|
+
console.log(chalk.red(`❌ Failed to create attribute: ${attribute.key}`));
|
843
|
+
failedAttributes.push(attribute.key);
|
844
|
+
}
|
845
|
+
}
|
846
|
+
|
847
|
+
if (failedAttributes.length > 0) {
|
848
|
+
console.log(chalk.red(`\n❌ Failed to create ${failedAttributes.length} attributes: ${failedAttributes.join(', ')}`));
|
849
|
+
return false;
|
850
|
+
}
|
851
|
+
|
852
|
+
console.log(chalk.green(`\n✅ Successfully created all ${attributes.length} attributes for collection: ${collection.name}`));
|
853
|
+
return true;
|
854
|
+
};
|
855
|
+
|
517
856
|
export const createUpdateCollectionAttributes = async (
|
518
857
|
db: Databases,
|
519
858
|
dbId: string,
|