appwrite-utils-cli 1.7.9 → 1.8.2
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/CHANGELOG.md +14 -199
- package/README.md +87 -30
- package/dist/adapters/AdapterFactory.js +5 -25
- package/dist/adapters/DatabaseAdapter.d.ts +17 -2
- package/dist/adapters/LegacyAdapter.d.ts +2 -1
- package/dist/adapters/LegacyAdapter.js +212 -16
- package/dist/adapters/TablesDBAdapter.d.ts +2 -12
- package/dist/adapters/TablesDBAdapter.js +261 -57
- package/dist/cli/commands/databaseCommands.js +4 -3
- package/dist/cli/commands/functionCommands.js +17 -8
- package/dist/collections/attributes.js +447 -125
- package/dist/collections/methods.js +197 -186
- package/dist/collections/tableOperations.d.ts +86 -0
- package/dist/collections/tableOperations.js +434 -0
- package/dist/collections/transferOperations.d.ts +3 -2
- package/dist/collections/transferOperations.js +93 -12
- package/dist/config/yamlConfig.d.ts +221 -88
- package/dist/examples/yamlTerminologyExample.d.ts +1 -1
- package/dist/examples/yamlTerminologyExample.js +6 -3
- package/dist/functions/fnConfigDiscovery.d.ts +3 -0
- package/dist/functions/fnConfigDiscovery.js +108 -0
- package/dist/interactiveCLI.js +18 -15
- package/dist/main.js +211 -73
- package/dist/migrations/appwriteToX.d.ts +88 -23
- package/dist/migrations/comprehensiveTransfer.d.ts +2 -0
- package/dist/migrations/comprehensiveTransfer.js +83 -6
- package/dist/migrations/dataLoader.d.ts +227 -69
- package/dist/migrations/dataLoader.js +3 -3
- package/dist/migrations/importController.js +3 -3
- package/dist/migrations/relationships.d.ts +8 -2
- package/dist/migrations/services/ImportOrchestrator.js +3 -3
- package/dist/migrations/transfer.js +159 -37
- package/dist/shared/attributeMapper.d.ts +20 -0
- package/dist/shared/attributeMapper.js +203 -0
- package/dist/shared/selectionDialogs.js +8 -4
- package/dist/storage/schemas.d.ts +354 -92
- package/dist/utils/configDiscovery.js +4 -3
- package/dist/utils/versionDetection.d.ts +0 -4
- package/dist/utils/versionDetection.js +41 -173
- package/dist/utils/yamlConverter.js +89 -16
- package/dist/utils/yamlLoader.d.ts +1 -1
- package/dist/utils/yamlLoader.js +6 -2
- package/dist/utilsController.js +56 -19
- package/package.json +4 -4
- package/src/adapters/AdapterFactory.ts +119 -143
- package/src/adapters/DatabaseAdapter.ts +18 -3
- package/src/adapters/LegacyAdapter.ts +236 -105
- package/src/adapters/TablesDBAdapter.ts +773 -643
- package/src/cli/commands/databaseCommands.ts +13 -12
- package/src/cli/commands/functionCommands.ts +23 -14
- package/src/collections/attributes.ts +2054 -1611
- package/src/collections/methods.ts +208 -293
- package/src/collections/tableOperations.ts +506 -0
- package/src/collections/transferOperations.ts +218 -144
- package/src/examples/yamlTerminologyExample.ts +10 -5
- package/src/functions/fnConfigDiscovery.ts +103 -0
- package/src/interactiveCLI.ts +25 -20
- package/src/main.ts +549 -194
- package/src/migrations/comprehensiveTransfer.ts +126 -50
- package/src/migrations/dataLoader.ts +3 -3
- package/src/migrations/importController.ts +3 -3
- package/src/migrations/services/ImportOrchestrator.ts +3 -3
- package/src/migrations/transfer.ts +148 -131
- package/src/shared/attributeMapper.ts +229 -0
- package/src/shared/selectionDialogs.ts +29 -25
- package/src/utils/configDiscovery.ts +9 -3
- package/src/utils/versionDetection.ts +74 -228
- package/src/utils/yamlConverter.ts +94 -17
- package/src/utils/yamlLoader.ts +11 -4
- package/src/utilsController.ts +80 -30
|
@@ -18,8 +18,8 @@ import {
|
|
|
18
18
|
import { InputFile } from "node-appwrite/file";
|
|
19
19
|
import { MessageFormatter } from "../shared/messageFormatter.js";
|
|
20
20
|
import { processQueue, queuedOperations } from "../shared/operationQueue.js";
|
|
21
|
-
import { ProgressManager } from "../shared/progressManager.js";
|
|
22
|
-
import { getClient } from "../utils/getClientFromConfig.js";
|
|
21
|
+
import { ProgressManager } from "../shared/progressManager.js";
|
|
22
|
+
import { getClient } from "../utils/getClientFromConfig.js";
|
|
23
23
|
import {
|
|
24
24
|
transferDatabaseLocalToLocal,
|
|
25
25
|
transferDatabaseLocalToRemote,
|
|
@@ -35,7 +35,10 @@ import {
|
|
|
35
35
|
import pLimit from "p-limit";
|
|
36
36
|
import chalk from "chalk";
|
|
37
37
|
import { join } from "node:path";
|
|
38
|
-
import fs from "node:fs";
|
|
38
|
+
import fs from "node:fs";
|
|
39
|
+
import type { DatabaseAdapter } from "../adapters/DatabaseAdapter.js";
|
|
40
|
+
import { getAdapter } from "../utils/getClientFromConfig.js";
|
|
41
|
+
import { mapToCreateAttributeParams } from "../shared/attributeMapper.js";
|
|
39
42
|
|
|
40
43
|
export interface ComprehensiveTransferOptions {
|
|
41
44
|
sourceEndpoint: string;
|
|
@@ -78,10 +81,12 @@ export class ComprehensiveTransfer {
|
|
|
78
81
|
private limit: ReturnType<typeof pLimit>;
|
|
79
82
|
private userLimit: ReturnType<typeof pLimit>;
|
|
80
83
|
private fileLimit: ReturnType<typeof pLimit>;
|
|
81
|
-
private results: TransferResults;
|
|
82
|
-
private startTime: number;
|
|
83
|
-
private tempDir: string;
|
|
84
|
-
private cachedMaxFileSize?: number; // Cache successful maximumFileSize for subsequent buckets
|
|
84
|
+
private results: TransferResults;
|
|
85
|
+
private startTime: number;
|
|
86
|
+
private tempDir: string;
|
|
87
|
+
private cachedMaxFileSize?: number; // Cache successful maximumFileSize for subsequent buckets
|
|
88
|
+
private sourceAdapter?: DatabaseAdapter;
|
|
89
|
+
private targetAdapter?: DatabaseAdapter;
|
|
85
90
|
|
|
86
91
|
constructor(private options: ComprehensiveTransferOptions) {
|
|
87
92
|
this.sourceClient = getClient(
|
|
@@ -126,11 +131,27 @@ export class ComprehensiveTransfer {
|
|
|
126
131
|
this.tempDir = join(process.cwd(), ".appwrite-transfer-temp");
|
|
127
132
|
}
|
|
128
133
|
|
|
129
|
-
async execute(): Promise<TransferResults> {
|
|
130
|
-
try {
|
|
131
|
-
MessageFormatter.info("Starting comprehensive transfer", {
|
|
132
|
-
prefix: "Transfer",
|
|
133
|
-
});
|
|
134
|
+
async execute(): Promise<TransferResults> {
|
|
135
|
+
try {
|
|
136
|
+
MessageFormatter.info("Starting comprehensive transfer", {
|
|
137
|
+
prefix: "Transfer",
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Initialize adapters for unified API (TablesDB or legacy via adapter)
|
|
141
|
+
const source = await getAdapter(
|
|
142
|
+
this.options.sourceEndpoint,
|
|
143
|
+
this.options.sourceProject,
|
|
144
|
+
this.options.sourceKey,
|
|
145
|
+
'auto'
|
|
146
|
+
);
|
|
147
|
+
const target = await getAdapter(
|
|
148
|
+
this.options.targetEndpoint,
|
|
149
|
+
this.options.targetProject,
|
|
150
|
+
this.options.targetKey,
|
|
151
|
+
'auto'
|
|
152
|
+
);
|
|
153
|
+
this.sourceAdapter = source.adapter;
|
|
154
|
+
this.targetAdapter = target.adapter;
|
|
134
155
|
|
|
135
156
|
if (this.options.dryRun) {
|
|
136
157
|
MessageFormatter.info("DRY RUN MODE - No actual changes will be made", {
|
|
@@ -1405,48 +1426,103 @@ export class ComprehensiveTransfer {
|
|
|
1405
1426
|
/**
|
|
1406
1427
|
* Helper method to create collection attributes with status checking
|
|
1407
1428
|
*/
|
|
1408
|
-
private async createCollectionAttributesWithStatusCheck(
|
|
1409
|
-
databases: Databases,
|
|
1410
|
-
dbId: string,
|
|
1411
|
-
collection: Models.Collection,
|
|
1412
|
-
attributes: any[]
|
|
1413
|
-
): Promise<boolean> {
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1429
|
+
private async createCollectionAttributesWithStatusCheck(
|
|
1430
|
+
databases: Databases,
|
|
1431
|
+
dbId: string,
|
|
1432
|
+
collection: Models.Collection,
|
|
1433
|
+
attributes: any[]
|
|
1434
|
+
): Promise<boolean> {
|
|
1435
|
+
if (!this.targetAdapter) {
|
|
1436
|
+
throw new Error('Target adapter not initialized');
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
try {
|
|
1440
|
+
// Create non-relationship attributes first
|
|
1441
|
+
const nonRel = (attributes || []).filter((a: any) => a.type !== 'relationship');
|
|
1442
|
+
for (const attr of nonRel) {
|
|
1443
|
+
const params = mapToCreateAttributeParams(attr as any, { databaseId: dbId, tableId: collection.$id });
|
|
1444
|
+
await this.targetAdapter.createAttribute(params);
|
|
1445
|
+
// Small delay between creations
|
|
1446
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// Wait for attributes to become available
|
|
1450
|
+
for (const attr of nonRel) {
|
|
1451
|
+
const maxWait = 60000; // 60s
|
|
1452
|
+
const start = Date.now();
|
|
1453
|
+
let lastStatus = '';
|
|
1454
|
+
while (Date.now() - start < maxWait) {
|
|
1455
|
+
try {
|
|
1456
|
+
const tableRes = await this.targetAdapter.getTable({ databaseId: dbId, tableId: collection.$id });
|
|
1457
|
+
const cols = (tableRes as any).attributes || (tableRes as any).columns || [];
|
|
1458
|
+
const col = cols.find((c: any) => c.key === attr.key);
|
|
1459
|
+
if (col) {
|
|
1460
|
+
if (col.status === 'available') break;
|
|
1461
|
+
if (col.status === 'failed' || col.status === 'stuck') {
|
|
1462
|
+
throw new Error(col.error || `Attribute ${attr.key} failed`);
|
|
1463
|
+
}
|
|
1464
|
+
lastStatus = col.status;
|
|
1465
|
+
}
|
|
1466
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
1467
|
+
} catch {
|
|
1468
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
if (Date.now() - start >= maxWait) {
|
|
1472
|
+
MessageFormatter.warning(
|
|
1473
|
+
`Attribute ${attr.key} did not become available within 60s (last status: ${lastStatus})`,
|
|
1474
|
+
{ prefix: 'Attributes' }
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
// Create relationship attributes
|
|
1480
|
+
const rels = (attributes || []).filter((a: any) => a.type === 'relationship');
|
|
1481
|
+
for (const attr of rels) {
|
|
1482
|
+
const params = mapToCreateAttributeParams(attr as any, { databaseId: dbId, tableId: collection.$id });
|
|
1483
|
+
await this.targetAdapter.createAttribute(params);
|
|
1484
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
return true;
|
|
1488
|
+
} catch (e) {
|
|
1489
|
+
MessageFormatter.error('Failed creating attributes via adapter', e instanceof Error ? e : new Error(String(e)), { prefix: 'Attributes' });
|
|
1490
|
+
return false;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1426
1493
|
|
|
1427
1494
|
/**
|
|
1428
1495
|
* Helper method to create collection indexes with status checking
|
|
1429
1496
|
*/
|
|
1430
|
-
private async createCollectionIndexesWithStatusCheck(
|
|
1431
|
-
dbId: string,
|
|
1432
|
-
databases: Databases,
|
|
1433
|
-
collectionId: string,
|
|
1434
|
-
collection: Models.Collection,
|
|
1435
|
-
indexes: any[]
|
|
1436
|
-
): Promise<boolean> {
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1497
|
+
private async createCollectionIndexesWithStatusCheck(
|
|
1498
|
+
dbId: string,
|
|
1499
|
+
databases: Databases,
|
|
1500
|
+
collectionId: string,
|
|
1501
|
+
collection: Models.Collection,
|
|
1502
|
+
indexes: any[]
|
|
1503
|
+
): Promise<boolean> {
|
|
1504
|
+
if (!this.targetAdapter) {
|
|
1505
|
+
throw new Error('Target adapter not initialized');
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
try {
|
|
1509
|
+
for (const idx of indexes || []) {
|
|
1510
|
+
await this.targetAdapter.createIndex({
|
|
1511
|
+
databaseId: dbId,
|
|
1512
|
+
tableId: collectionId,
|
|
1513
|
+
key: idx.key,
|
|
1514
|
+
type: idx.type,
|
|
1515
|
+
attributes: idx.attributes,
|
|
1516
|
+
orders: idx.orders || []
|
|
1517
|
+
});
|
|
1518
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
1519
|
+
}
|
|
1520
|
+
return true;
|
|
1521
|
+
} catch (e) {
|
|
1522
|
+
MessageFormatter.error('Failed creating indexes via adapter', e instanceof Error ? e : new Error(String(e)), { prefix: 'Indexes' });
|
|
1523
|
+
return false;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1450
1526
|
|
|
1451
1527
|
/**
|
|
1452
1528
|
* Helper method to transfer documents between databases using bulk operations with content and permission-based filtering
|
|
@@ -315,7 +315,7 @@ export class DataLoader {
|
|
|
315
315
|
collection.$id = collectionExists.$id;
|
|
316
316
|
this.config.collections[index] = collectionConfig;
|
|
317
317
|
// Find or create an import operation for the collection
|
|
318
|
-
const adapter = new LegacyAdapter(this.database);
|
|
318
|
+
const adapter = new LegacyAdapter(this.database.client);
|
|
319
319
|
const collectionImportOperation = await findOrCreateOperation(
|
|
320
320
|
adapter,
|
|
321
321
|
dbId,
|
|
@@ -957,7 +957,7 @@ export class DataLoader {
|
|
|
957
957
|
this.oldIdToNewIdPerCollectionMap
|
|
958
958
|
.set(this.getCollectionKey(collection.name), oldIdToNewIdMap)
|
|
959
959
|
.get(this.getCollectionKey(collection.name));
|
|
960
|
-
const adapter = new LegacyAdapter(this.database);
|
|
960
|
+
const adapter = new LegacyAdapter(this.database.client);
|
|
961
961
|
if (!operationId) {
|
|
962
962
|
const collectionImportOperation = await findOrCreateOperation(
|
|
963
963
|
adapter,
|
|
@@ -1188,7 +1188,7 @@ export class DataLoader {
|
|
|
1188
1188
|
let operationId = this.collectionImportOperations.get(
|
|
1189
1189
|
this.getCollectionKey(collection.name)
|
|
1190
1190
|
);
|
|
1191
|
-
const adapter = new LegacyAdapter(this.database);
|
|
1191
|
+
const adapter = new LegacyAdapter(this.database.client);
|
|
1192
1192
|
if (!operationId) {
|
|
1193
1193
|
const collectionImportOperation = await findOrCreateOperation(
|
|
1194
1194
|
adapter,
|
|
@@ -289,7 +289,7 @@ export class ImportController {
|
|
|
289
289
|
"currentOperations",
|
|
290
290
|
importOperationId
|
|
291
291
|
);
|
|
292
|
-
const adapter = new LegacyAdapter(this.database);
|
|
292
|
+
const adapter = new LegacyAdapter(this.database.client);
|
|
293
293
|
await updateOperation(adapter, db.$id, importOperation.$id, {
|
|
294
294
|
status: "in_progress",
|
|
295
295
|
});
|
|
@@ -349,7 +349,7 @@ export class ImportController {
|
|
|
349
349
|
await Promise.all(batchPromises);
|
|
350
350
|
MessageFormatter.success(`Completed batch ${i + 1} of ${dataSplit.length}`, { prefix: "Import" });
|
|
351
351
|
if (importOperation) {
|
|
352
|
-
const adapter = new LegacyAdapter(this.database);
|
|
352
|
+
const adapter = new LegacyAdapter(this.database.client);
|
|
353
353
|
await updateOperation(adapter, db.$id, importOperation.$id, {
|
|
354
354
|
progress: processedItems,
|
|
355
355
|
});
|
|
@@ -357,7 +357,7 @@ export class ImportController {
|
|
|
357
357
|
}
|
|
358
358
|
// After all batches are processed, update the operation status to completed
|
|
359
359
|
if (importOperation) {
|
|
360
|
-
const adapter = new LegacyAdapter(this.database);
|
|
360
|
+
const adapter = new LegacyAdapter(this.database.client);
|
|
361
361
|
await updateOperation(adapter, db.$id, importOperation.$id, {
|
|
362
362
|
status: "completed",
|
|
363
363
|
});
|
|
@@ -203,7 +203,7 @@ export class ImportOrchestrator {
|
|
|
203
203
|
this.config.collections[index] = collectionConfig;
|
|
204
204
|
|
|
205
205
|
// Find or create an import operation for the collection
|
|
206
|
-
const adapter = new LegacyAdapter(this.database);
|
|
206
|
+
const adapter = new LegacyAdapter(this.database.client);
|
|
207
207
|
const collectionImportOperation = await findOrCreateOperation(
|
|
208
208
|
adapter,
|
|
209
209
|
dbId,
|
|
@@ -501,7 +501,7 @@ export class ImportOrchestrator {
|
|
|
501
501
|
logger.info(`Importing collection: ${collection.name} (${collectionData.data.length} items)`);
|
|
502
502
|
|
|
503
503
|
const operationId = this.collectionImportOperations.get(this.getCollectionKey(collection.name));
|
|
504
|
-
const adapter = new LegacyAdapter(this.database);
|
|
504
|
+
const adapter = new LegacyAdapter(this.database.client);
|
|
505
505
|
if (operationId) {
|
|
506
506
|
await updateOperation(adapter, db.$id, operationId, { status: "in_progress" });
|
|
507
507
|
}
|
|
@@ -638,7 +638,7 @@ export class ImportOrchestrator {
|
|
|
638
638
|
const operationId = this.collectionImportOperations.get(this.getCollectionKey(collection.name));
|
|
639
639
|
if (operationId) {
|
|
640
640
|
const updateData = total ? { status, total } : { status };
|
|
641
|
-
const adapter = new LegacyAdapter(this.database);
|
|
641
|
+
const adapter = new LegacyAdapter(this.database.client);
|
|
642
642
|
await updateOperation(adapter, db.$id, operationId, updateData);
|
|
643
643
|
}
|
|
644
644
|
}
|
|
@@ -10,22 +10,17 @@ import {
|
|
|
10
10
|
} from "node-appwrite";
|
|
11
11
|
import { InputFile } from "node-appwrite/file";
|
|
12
12
|
import { getAppwriteClient } from "../utils/helperFunctions.js";
|
|
13
|
-
|
|
14
|
-
createOrUpdateAttribute,
|
|
15
|
-
createUpdateCollectionAttributes,
|
|
16
|
-
createUpdateCollectionAttributesWithStatusCheck,
|
|
17
|
-
} from "../collections/attributes.js";
|
|
13
|
+
// Legacy attribute helpers retained only for local-to-local flows if needed
|
|
18
14
|
import { parseAttribute } from "appwrite-utils";
|
|
19
15
|
import chalk from "chalk";
|
|
20
16
|
import { fetchAllCollections } from "../collections/methods.js";
|
|
21
|
-
import { MessageFormatter } from "../shared/messageFormatter.js";
|
|
17
|
+
import { MessageFormatter } from "../shared/messageFormatter.js";
|
|
18
|
+
import { LegacyAdapter } from "../adapters/LegacyAdapter.js";
|
|
22
19
|
import { ProgressManager } from "../shared/progressManager.js";
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
} from "../collections/indexes.js";
|
|
28
|
-
import { getClient } from "../utils/getClientFromConfig.js";
|
|
20
|
+
import { getClient, getAdapter } from "../utils/getClientFromConfig.js";
|
|
21
|
+
import { diffTableColumns } from "../collections/tableOperations.js";
|
|
22
|
+
import { mapToCreateAttributeParams } from "../shared/attributeMapper.js";
|
|
23
|
+
import type { DatabaseAdapter } from "../adapters/DatabaseAdapter.js";
|
|
29
24
|
|
|
30
25
|
export interface TransferOptions {
|
|
31
26
|
fromDb: Models.Database | undefined;
|
|
@@ -331,75 +326,83 @@ export const transferDatabaseLocalToLocal = async (
|
|
|
331
326
|
);
|
|
332
327
|
}
|
|
333
328
|
|
|
334
|
-
//
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
);
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
)
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
329
|
+
// Create attributes via local adapter (wrap the existing client)
|
|
330
|
+
const localAdapter: DatabaseAdapter = new LegacyAdapter((localDb as any).client);
|
|
331
|
+
MessageFormatter.info(`Creating attributes for ${collection.name} via adapter...`, { prefix: 'Transfer' });
|
|
332
|
+
const uniformAttrs = collection.attributes.map((attr) => parseAttribute(attr as any));
|
|
333
|
+
const nonRel = uniformAttrs.filter((a: any) => a.type !== 'relationship');
|
|
334
|
+
for (const attr of nonRel) {
|
|
335
|
+
const params = mapToCreateAttributeParams(attr as any, { databaseId: targetDbId, tableId: targetCollection.$id });
|
|
336
|
+
await localAdapter.createAttribute(params);
|
|
337
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Wait for attributes to become available
|
|
341
|
+
for (const attr of nonRel) {
|
|
342
|
+
const maxWait = 60000; const start = Date.now();
|
|
343
|
+
let lastStatus = '';
|
|
344
|
+
while (Date.now() - start < maxWait) {
|
|
345
|
+
try {
|
|
346
|
+
const tableRes = await localAdapter.getTable({ databaseId: targetDbId, tableId: targetCollection.$id });
|
|
347
|
+
const attrs = (tableRes as any).attributes || (tableRes as any).columns || [];
|
|
348
|
+
const found = attrs.find((a: any) => a.key === attr.key);
|
|
349
|
+
if (found) {
|
|
350
|
+
if (found.status === 'available') break;
|
|
351
|
+
if (found.status === 'failed' || found.status === 'stuck') {
|
|
352
|
+
throw new Error(found.error || `Attribute ${attr.key} failed`);
|
|
353
|
+
}
|
|
354
|
+
lastStatus = found.status;
|
|
355
|
+
}
|
|
356
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
357
|
+
} catch {
|
|
358
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (Date.now() - start >= maxWait) {
|
|
362
|
+
MessageFormatter.warning(`Attribute ${attr.key} did not become available within 60s (last: ${lastStatus})`, { prefix: 'Transfer' });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Relationship attributes
|
|
367
|
+
const rels = uniformAttrs.filter((a: any) => a.type === 'relationship');
|
|
368
|
+
for (const attr of rels) {
|
|
369
|
+
const params = mapToCreateAttributeParams(attr as any, { databaseId: targetDbId, tableId: targetCollection.$id });
|
|
370
|
+
await localAdapter.createAttribute(params);
|
|
371
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Handle indexes via adapter (create or update)
|
|
375
|
+
for (const idx of collection.indexes) {
|
|
376
|
+
try {
|
|
377
|
+
await localAdapter.createIndex({
|
|
378
|
+
databaseId: targetDbId,
|
|
379
|
+
tableId: targetCollection.$id,
|
|
380
|
+
key: (idx as any).key,
|
|
381
|
+
type: (idx as any).type,
|
|
382
|
+
attributes: (idx as any).attributes,
|
|
383
|
+
orders: (idx as any).orders || []
|
|
384
|
+
});
|
|
385
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
386
|
+
MessageFormatter.success(`Index ${(idx as any).key} created`, { prefix: 'Transfer' });
|
|
387
|
+
} catch (e) {
|
|
388
|
+
// Try update path by deleting and recreating if necessary
|
|
389
|
+
try {
|
|
390
|
+
await localAdapter.deleteIndex({ databaseId: targetDbId, tableId: targetCollection.$id, key: (idx as any).key });
|
|
391
|
+
await localAdapter.createIndex({
|
|
392
|
+
databaseId: targetDbId,
|
|
393
|
+
tableId: targetCollection.$id,
|
|
394
|
+
key: (idx as any).key,
|
|
395
|
+
type: (idx as any).type,
|
|
396
|
+
attributes: (idx as any).attributes,
|
|
397
|
+
orders: (idx as any).orders || []
|
|
398
|
+
});
|
|
399
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
400
|
+
MessageFormatter.info(`Index ${(idx as any).key} recreated`, { prefix: 'Transfer' });
|
|
401
|
+
} catch (e2) {
|
|
402
|
+
MessageFormatter.error(`Failed to ensure index ${(idx as any).key}`, e2 instanceof Error ? e2 : new Error(String(e2)), { prefix: 'Transfer' });
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
403
406
|
|
|
404
407
|
// Transfer documents
|
|
405
408
|
const { transferDocumentsBetweenDbsLocalToLocal } = await import(
|
|
@@ -500,36 +503,54 @@ export const transferDatabaseLocalToRemote = async (
|
|
|
500
503
|
);
|
|
501
504
|
}
|
|
502
505
|
|
|
503
|
-
//
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
);
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
506
|
+
// Create/Update attributes via adapter (prefer adapter for remote)
|
|
507
|
+
const { adapter: remoteAdapter } = await getAdapter(endpoint, projectId, apiKey, 'auto');
|
|
508
|
+
MessageFormatter.info(`Creating attributes for ${collection.name} via adapter...`, { prefix: 'Transfer' });
|
|
509
|
+
const uniformAttrs = collection.attributes.map((attr) => parseAttribute(attr as any));
|
|
510
|
+
const nonRel = uniformAttrs.filter((a: any) => a.type !== 'relationship');
|
|
511
|
+
if (nonRel.length > 0) {
|
|
512
|
+
const tableInfo = await (remoteAdapter as DatabaseAdapter).getTable({ databaseId: toDbId, tableId: collection.$id });
|
|
513
|
+
const existingCols: any[] = (tableInfo as any).columns || (tableInfo as any).attributes || [];
|
|
514
|
+
const { toCreate, toUpdate } = diffTableColumns(existingCols, nonRel as any);
|
|
515
|
+
for (const a of toUpdate) { const p = mapToCreateAttributeParams(a as any, { databaseId: toDbId, tableId: collection.$id }); await (remoteAdapter as DatabaseAdapter).updateAttribute(p as any); await new Promise((r)=>setTimeout(r,150)); }
|
|
516
|
+
for (const a of toCreate) { const p = mapToCreateAttributeParams(a as any, { databaseId: toDbId, tableId: collection.$id }); await (remoteAdapter as DatabaseAdapter).createAttribute(p); await new Promise((r)=>setTimeout(r,150)); }
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Wait for non-relationship attributes to become available
|
|
520
|
+
for (const attr of nonRel) {
|
|
521
|
+
const maxWait = 60000; const start = Date.now();
|
|
522
|
+
let lastStatus = '';
|
|
523
|
+
while (Date.now() - start < maxWait) {
|
|
524
|
+
try {
|
|
525
|
+
const tableRes = await (remoteAdapter as DatabaseAdapter).getTable({ databaseId: toDbId, tableId: collection.$id });
|
|
526
|
+
const attrs = (tableRes as any).attributes || (tableRes as any).columns || [];
|
|
527
|
+
const found = attrs.find((a: any) => a.key === attr.key);
|
|
528
|
+
if (found) {
|
|
529
|
+
if (found.status === 'available') break;
|
|
530
|
+
if (found.status === 'failed' || found.status === 'stuck') {
|
|
531
|
+
throw new Error(found.error || `Attribute ${attr.key} failed`);
|
|
532
|
+
}
|
|
533
|
+
lastStatus = found.status;
|
|
534
|
+
}
|
|
535
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
536
|
+
} catch {
|
|
537
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (Date.now() - start >= maxWait) {
|
|
541
|
+
MessageFormatter.warning(`Attribute ${attr.key} did not become available within 60s (last: ${lastStatus})`, { prefix: 'Transfer' });
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Relationship attributes
|
|
546
|
+
const rels = uniformAttrs.filter((a: any) => a.type === 'relationship');
|
|
547
|
+
if (rels.length > 0) {
|
|
548
|
+
const tableInfo2 = await (remoteAdapter as DatabaseAdapter).getTable({ databaseId: toDbId, tableId: collection.$id });
|
|
549
|
+
const existingCols2: any[] = (tableInfo2 as any).columns || (tableInfo2 as any).attributes || [];
|
|
550
|
+
const { toCreate: rCreate, toUpdate: rUpdate } = diffTableColumns(existingCols2, rels as any);
|
|
551
|
+
for (const a of rUpdate) { const p = mapToCreateAttributeParams(a as any, { databaseId: toDbId, tableId: collection.$id }); await (remoteAdapter as DatabaseAdapter).updateAttribute(p as any); await new Promise((r)=>setTimeout(r,150)); }
|
|
552
|
+
for (const a of rCreate) { const p = mapToCreateAttributeParams(a as any, { databaseId: toDbId, tableId: collection.$id }); await (remoteAdapter as DatabaseAdapter).createAttribute(p); await new Promise((r)=>setTimeout(r,150)); }
|
|
553
|
+
}
|
|
533
554
|
|
|
534
555
|
// Handle indexes with enhanced status checking
|
|
535
556
|
MessageFormatter.info(
|
|
@@ -537,26 +558,22 @@ export const transferDatabaseLocalToRemote = async (
|
|
|
537
558
|
{ prefix: "Transfer" }
|
|
538
559
|
);
|
|
539
560
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
)
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
`All indexes created successfully for collection ${collection.name}`,
|
|
557
|
-
{ prefix: "Transfer" }
|
|
558
|
-
);
|
|
559
|
-
}
|
|
561
|
+
// Create indexes via adapter
|
|
562
|
+
for (const idx of (collection.indexes as any[]) || []) {
|
|
563
|
+
try {
|
|
564
|
+
await (remoteAdapter as DatabaseAdapter).createIndex({
|
|
565
|
+
databaseId: toDbId,
|
|
566
|
+
tableId: collection.$id,
|
|
567
|
+
key: idx.key,
|
|
568
|
+
type: idx.type,
|
|
569
|
+
attributes: idx.attributes,
|
|
570
|
+
orders: idx.orders || []
|
|
571
|
+
});
|
|
572
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
573
|
+
} catch (e) {
|
|
574
|
+
MessageFormatter.error(`Failed to create index ${idx.key}`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Transfer' });
|
|
575
|
+
}
|
|
576
|
+
}
|
|
560
577
|
|
|
561
578
|
// Transfer documents
|
|
562
579
|
const { transferDocumentsBetweenDbsLocalToRemote } = await import(
|