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.
Files changed (70) hide show
  1. package/CHANGELOG.md +14 -199
  2. package/README.md +87 -30
  3. package/dist/adapters/AdapterFactory.js +5 -25
  4. package/dist/adapters/DatabaseAdapter.d.ts +17 -2
  5. package/dist/adapters/LegacyAdapter.d.ts +2 -1
  6. package/dist/adapters/LegacyAdapter.js +212 -16
  7. package/dist/adapters/TablesDBAdapter.d.ts +2 -12
  8. package/dist/adapters/TablesDBAdapter.js +261 -57
  9. package/dist/cli/commands/databaseCommands.js +4 -3
  10. package/dist/cli/commands/functionCommands.js +17 -8
  11. package/dist/collections/attributes.js +447 -125
  12. package/dist/collections/methods.js +197 -186
  13. package/dist/collections/tableOperations.d.ts +86 -0
  14. package/dist/collections/tableOperations.js +434 -0
  15. package/dist/collections/transferOperations.d.ts +3 -2
  16. package/dist/collections/transferOperations.js +93 -12
  17. package/dist/config/yamlConfig.d.ts +221 -88
  18. package/dist/examples/yamlTerminologyExample.d.ts +1 -1
  19. package/dist/examples/yamlTerminologyExample.js +6 -3
  20. package/dist/functions/fnConfigDiscovery.d.ts +3 -0
  21. package/dist/functions/fnConfigDiscovery.js +108 -0
  22. package/dist/interactiveCLI.js +18 -15
  23. package/dist/main.js +211 -73
  24. package/dist/migrations/appwriteToX.d.ts +88 -23
  25. package/dist/migrations/comprehensiveTransfer.d.ts +2 -0
  26. package/dist/migrations/comprehensiveTransfer.js +83 -6
  27. package/dist/migrations/dataLoader.d.ts +227 -69
  28. package/dist/migrations/dataLoader.js +3 -3
  29. package/dist/migrations/importController.js +3 -3
  30. package/dist/migrations/relationships.d.ts +8 -2
  31. package/dist/migrations/services/ImportOrchestrator.js +3 -3
  32. package/dist/migrations/transfer.js +159 -37
  33. package/dist/shared/attributeMapper.d.ts +20 -0
  34. package/dist/shared/attributeMapper.js +203 -0
  35. package/dist/shared/selectionDialogs.js +8 -4
  36. package/dist/storage/schemas.d.ts +354 -92
  37. package/dist/utils/configDiscovery.js +4 -3
  38. package/dist/utils/versionDetection.d.ts +0 -4
  39. package/dist/utils/versionDetection.js +41 -173
  40. package/dist/utils/yamlConverter.js +89 -16
  41. package/dist/utils/yamlLoader.d.ts +1 -1
  42. package/dist/utils/yamlLoader.js +6 -2
  43. package/dist/utilsController.js +56 -19
  44. package/package.json +4 -4
  45. package/src/adapters/AdapterFactory.ts +119 -143
  46. package/src/adapters/DatabaseAdapter.ts +18 -3
  47. package/src/adapters/LegacyAdapter.ts +236 -105
  48. package/src/adapters/TablesDBAdapter.ts +773 -643
  49. package/src/cli/commands/databaseCommands.ts +13 -12
  50. package/src/cli/commands/functionCommands.ts +23 -14
  51. package/src/collections/attributes.ts +2054 -1611
  52. package/src/collections/methods.ts +208 -293
  53. package/src/collections/tableOperations.ts +506 -0
  54. package/src/collections/transferOperations.ts +218 -144
  55. package/src/examples/yamlTerminologyExample.ts +10 -5
  56. package/src/functions/fnConfigDiscovery.ts +103 -0
  57. package/src/interactiveCLI.ts +25 -20
  58. package/src/main.ts +549 -194
  59. package/src/migrations/comprehensiveTransfer.ts +126 -50
  60. package/src/migrations/dataLoader.ts +3 -3
  61. package/src/migrations/importController.ts +3 -3
  62. package/src/migrations/services/ImportOrchestrator.ts +3 -3
  63. package/src/migrations/transfer.ts +148 -131
  64. package/src/shared/attributeMapper.ts +229 -0
  65. package/src/shared/selectionDialogs.ts +29 -25
  66. package/src/utils/configDiscovery.ts +9 -3
  67. package/src/utils/versionDetection.ts +74 -228
  68. package/src/utils/yamlConverter.ts +94 -17
  69. package/src/utils/yamlLoader.ts +11 -4
  70. 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
- // Import the enhanced attribute creation function
1415
- const { createUpdateCollectionAttributesWithStatusCheck } = await import(
1416
- "../collections/attributes.js"
1417
- );
1418
-
1419
- return await createUpdateCollectionAttributesWithStatusCheck(
1420
- databases,
1421
- dbId,
1422
- collection,
1423
- attributes
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
- // Import the enhanced index creation function
1438
- const { createOrUpdateIndexesWithStatusCheck } = await import(
1439
- "../collections/indexes.js"
1440
- );
1441
-
1442
- return await createOrUpdateIndexesWithStatusCheck(
1443
- dbId,
1444
- databases,
1445
- collectionId,
1446
- collection,
1447
- indexes
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
- import {
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
- createOrUpdateIndex,
25
- createOrUpdateIndexes,
26
- createOrUpdateIndexesWithStatusCheck,
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
- // Handle attributes with enhanced status checking
335
- MessageFormatter.info(
336
- `Creating attributes for collection ${collection.name} with enhanced monitoring...`,
337
- { prefix: "Transfer" }
338
- );
339
-
340
- const allAttributes = collection.attributes.map((attr) =>
341
- parseAttribute(attr as any)
342
- );
343
- const attributeSuccess =
344
- await createUpdateCollectionAttributesWithStatusCheck(
345
- localDb,
346
- targetDbId,
347
- targetCollection,
348
- allAttributes
349
- );
350
-
351
- if (!attributeSuccess) {
352
- MessageFormatter.error(
353
- `Failed to create all attributes for collection ${collection.name}, skipping to next collection`,
354
- undefined,
355
- { prefix: "Transfer" }
356
- );
357
- continue;
358
- }
359
-
360
- MessageFormatter.success(
361
- `All attributes created successfully for collection ${collection.name}`,
362
- { prefix: "Transfer" }
363
- );
364
-
365
- // Handle indexes
366
- const existingIndexes = await tryAwaitWithRetry(
367
- async () => await localDb.listIndexes(targetDbId, targetCollection.$id)
368
- );
369
-
370
- for (const index of collection.indexes) {
371
- const existingIndex = existingIndexes.indexes.find(
372
- (idx) => idx.key === index.key
373
- );
374
-
375
- if (!existingIndex) {
376
- await tryAwaitWithRetry(async () =>
377
- createOrUpdateIndex(
378
- targetDbId,
379
- localDb,
380
- targetCollection.$id,
381
- index as any
382
- )
383
- );
384
- MessageFormatter.success(
385
- `Index ${index.key} created`,
386
- { prefix: "Transfer" }
387
- );
388
- } else {
389
- MessageFormatter.info(
390
- `Index ${index.key} exists, checking for updates...`,
391
- { prefix: "Transfer" }
392
- );
393
- await tryAwaitWithRetry(async () =>
394
- createOrUpdateIndex(
395
- targetDbId,
396
- localDb,
397
- targetCollection.$id,
398
- index as any
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
- // Handle attributes with enhanced status checking
504
- MessageFormatter.info(
505
- `Creating attributes for collection ${collection.name} with enhanced monitoring...`,
506
- { prefix: "Transfer" }
507
- );
508
-
509
- const attributesToCreate = collection.attributes.map((attr) =>
510
- parseAttribute(attr as any)
511
- );
512
-
513
- const attributesSuccess =
514
- await createUpdateCollectionAttributesWithStatusCheck(
515
- remoteDb,
516
- toDbId,
517
- targetCollection,
518
- attributesToCreate
519
- );
520
-
521
- if (!attributesSuccess) {
522
- MessageFormatter.warning(
523
- `Failed to create some attributes for collection ${collection.name}`,
524
- { prefix: "Transfer" }
525
- );
526
- // Continue with the transfer even if some attributes failed
527
- } else {
528
- MessageFormatter.success(
529
- `All attributes created successfully for collection ${collection.name}`,
530
- { prefix: "Transfer" }
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
- const indexesSuccess = await createOrUpdateIndexesWithStatusCheck(
541
- toDbId,
542
- remoteDb,
543
- targetCollection.$id,
544
- targetCollection,
545
- collection.indexes as any
546
- );
547
-
548
- if (!indexesSuccess) {
549
- MessageFormatter.warning(
550
- `Failed to create some indexes for collection ${collection.name}`,
551
- { prefix: "Transfer" }
552
- );
553
- // Continue with the transfer even if some indexes failed
554
- } else {
555
- MessageFormatter.success(
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(