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
@@ -2,13 +2,14 @@ import { Databases, ID, Permission, Query, } from "node-appwrite";
2
2
  import { getAdapterFromConfig } from "../utils/getClientFromConfig.js";
3
3
  import { nameToIdMapping, processQueue, queuedOperations, clearProcessingState, isCollectionProcessed, markCollectionProcessed } from "../shared/operationQueue.js";
4
4
  import { logger } from "../shared/logging.js";
5
- import { createUpdateCollectionAttributesWithStatusCheck } from "./attributes.js";
6
- import { createOrUpdateIndexesWithStatusCheck } from "./indexes.js";
5
+ // Legacy attribute/index helpers removed in favor of unified adapter path
7
6
  import { SchemaGenerator } from "../shared/schemaGenerator.js";
8
7
  import { isNull, isUndefined, isNil, isPlainObject, isString, } from "es-toolkit";
9
8
  import { delay, tryAwaitWithRetry } from "../utils/helperFunctions.js";
10
9
  import { MessageFormatter } from "../shared/messageFormatter.js";
11
10
  import { isLegacyDatabases } from "../utils/typeGuards.js";
11
+ import { mapToCreateAttributeParams, mapToUpdateAttributeParams } from "../shared/attributeMapper.js";
12
+ import { diffTableColumns, isIndexEqualToIndex, diffColumnsDetailed, executeColumnOperations } from "./tableOperations.js";
12
13
  // Re-export wipe operations
13
14
  export { wipeDatabase, wipeCollection, wipeAllTables, wipeTableRows, } from "./wipeOperations.js";
14
15
  // Re-export transfer operations
@@ -122,130 +123,9 @@ export const generateSchemas = async (config, appwriteFolderPath) => {
122
123
  export const createOrUpdateCollections = async (database, databaseId, config, deletedCollections, selectedCollections = []) => {
123
124
  // Clear processing state at the start of a new operation
124
125
  clearProcessingState();
125
- // If API mode is tablesdb, route to adapter-based implementation
126
- try {
127
- const { adapter, apiMode } = await getAdapterFromConfig(config);
128
- if (apiMode === 'tablesdb') {
129
- await createOrUpdateCollectionsViaAdapter(adapter, databaseId, config, deletedCollections, selectedCollections);
130
- return;
131
- }
132
- }
133
- catch {
134
- // Fallback to legacy path below
135
- }
136
- const collectionsToProcess = selectedCollections.length > 0 ? selectedCollections : config.collections;
137
- if (!collectionsToProcess) {
138
- return;
139
- }
140
- const usedIds = new Set();
141
- MessageFormatter.info(`Processing ${collectionsToProcess.length} collections with intelligent state management`, { prefix: "Collections" });
142
- for (const collection of collectionsToProcess) {
143
- const { attributes, indexes, ...collectionData } = collection;
144
- // Check if this collection has already been processed in this session
145
- if (collectionData.$id && isCollectionProcessed(collectionData.$id)) {
146
- MessageFormatter.info(`Collection '${collectionData.name}' already processed, skipping`, { prefix: "Collections" });
147
- continue;
148
- }
149
- // Prepare permissions for the collection
150
- const permissions = [];
151
- if (collection.$permissions && collection.$permissions.length > 0) {
152
- for (const permission of collection.$permissions) {
153
- if (typeof permission === "string") {
154
- permissions.push(permission);
155
- }
156
- else {
157
- switch (permission.permission) {
158
- case "read":
159
- permissions.push(Permission.read(permission.target));
160
- break;
161
- case "create":
162
- permissions.push(Permission.create(permission.target));
163
- break;
164
- case "update":
165
- permissions.push(Permission.update(permission.target));
166
- break;
167
- case "delete":
168
- permissions.push(Permission.delete(permission.target));
169
- break;
170
- case "write":
171
- permissions.push(Permission.write(permission.target));
172
- break;
173
- default:
174
- MessageFormatter.warning(`Unknown permission: ${permission.permission}`, { prefix: "Collections" });
175
- break;
176
- }
177
- }
178
- }
179
- }
180
- // Check if the collection already exists by name
181
- let collectionsFound = await tryAwaitWithRetry(async () => await database.listCollections(databaseId, [
182
- Query.equal("name", collectionData.name),
183
- ]));
184
- let collectionToUse = collectionsFound.total > 0 ? collectionsFound.collections[0] : null;
185
- // Determine the correct ID for the collection
186
- let collectionId;
187
- if (!collectionToUse) {
188
- MessageFormatter.info(`Creating collection: ${collectionData.name}`, { prefix: "Collections" });
189
- let foundColl = deletedCollections?.find((coll) => coll.collectionName.toLowerCase().trim().replace(" ", "") ===
190
- collectionData.name.toLowerCase().trim().replace(" ", ""));
191
- if (collectionData.$id) {
192
- collectionId = collectionData.$id;
193
- }
194
- else if (foundColl && !usedIds.has(foundColl.collectionId)) {
195
- collectionId = foundColl.collectionId;
196
- }
197
- else {
198
- collectionId = ID.unique();
199
- }
200
- usedIds.add(collectionId);
201
- // Create the collection with the determined ID
202
- try {
203
- collectionToUse = await tryAwaitWithRetry(async () => await database.createCollection(databaseId, collectionId, collectionData.name, permissions, collectionData.documentSecurity ?? false, collectionData.enabled ?? true));
204
- collectionData.$id = collectionToUse.$id;
205
- nameToIdMapping.set(collectionData.name, collectionToUse.$id);
206
- }
207
- catch (error) {
208
- MessageFormatter.error(`Failed to create collection ${collectionData.name} with ID ${collectionId}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Collections" });
209
- continue;
210
- }
211
- }
212
- else {
213
- MessageFormatter.info(`Collection ${collectionData.name} exists, updating it`, { prefix: "Collections" });
214
- await tryAwaitWithRetry(async () => await database.updateCollection(databaseId, collectionToUse.$id, collectionData.name, permissions, collectionData.documentSecurity ?? false, collectionData.enabled ?? true));
215
- // Cache the existing collection ID
216
- nameToIdMapping.set(collectionData.name, collectionToUse.$id);
217
- }
218
- // Add delay after creating/updating collection
219
- await delay(250);
220
- // Update attributes and indexes for the collection
221
- MessageFormatter.progress("Creating Attributes", { prefix: "Collections" });
222
- await createUpdateCollectionAttributesWithStatusCheck(database, databaseId, collectionToUse,
223
- // @ts-expect-error
224
- attributes);
225
- // Add delay after creating attributes
226
- await delay(250);
227
- // Prefer local config indexes, but fall back to collection's own indexes if no local config exists
228
- const localCollectionConfig = config.collections?.find(c => c.name === collectionData.name || c.$id === collectionData.$id);
229
- const indexesToUse = localCollectionConfig?.indexes ?? indexes ?? [];
230
- MessageFormatter.progress("Creating Indexes", { prefix: "Collections" });
231
- await createOrUpdateIndexesWithStatusCheck(databaseId, database, collectionToUse.$id, collectionToUse, indexesToUse);
232
- // Delete indexes that exist on server but not in local config
233
- const { deleteObsoleteIndexes } = await import('../shared/indexManager.js');
234
- await deleteObsoleteIndexes(database, databaseId, collectionToUse, { indexes: indexesToUse }, { verbose: true });
235
- // Mark this collection as fully processed to prevent re-processing
236
- markCollectionProcessed(collectionToUse.$id, collectionData.name);
237
- // Add delay after creating indexes
238
- await delay(250);
239
- }
240
- // Process any remaining relationship attributes in the queue
241
- // This surgical approach only processes specific attributes, not entire collections
242
- if (queuedOperations.length > 0) {
243
- MessageFormatter.info(`🔧 Processing ${queuedOperations.length} queued relationship attributes (surgical approach)`, { prefix: "Collections" });
244
- await processQueue(database, databaseId);
245
- }
246
- else {
247
- MessageFormatter.info("✅ No queued relationship attributes to process", { prefix: "Collections" });
248
- }
126
+ // Always use adapter path (LegacyAdapter translates when pre-1.8)
127
+ const { adapter } = await getAdapterFromConfig(config);
128
+ await createOrUpdateCollectionsViaAdapter(adapter, databaseId, config, deletedCollections, selectedCollections);
249
129
  };
250
130
  // New: Adapter-based implementation for TablesDB with state management
251
131
  export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, config, deletedCollections, selectedCollections = []) => {
@@ -254,29 +134,15 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
254
134
  return;
255
135
  const usedIds = new Set();
256
136
  MessageFormatter.info(`Processing ${collectionsToProcess.length} tables via adapter with intelligent state management`, { prefix: "Tables" });
257
- // Helper: create attributes through adapter
137
+ // Helpers for attribute operations through adapter
258
138
  const createAttr = async (tableId, attr) => {
259
- const base = {
260
- databaseId,
261
- tableId,
262
- key: attr.key,
263
- type: attr.type,
264
- size: attr.size,
265
- required: !!attr.required,
266
- default: attr.xdefault,
267
- array: !!attr.array,
268
- min: attr.min,
269
- max: attr.max,
270
- elements: attr.elements,
271
- encrypt: attr.encrypted,
272
- relatedCollection: attr.relatedCollection,
273
- relationType: attr.relationType,
274
- twoWay: attr.twoWay,
275
- twoWayKey: attr.twoWayKey,
276
- onDelete: attr.onDelete,
277
- side: attr.side,
278
- };
279
- await adapter.createAttribute(base);
139
+ const params = mapToCreateAttributeParams(attr, { databaseId, tableId });
140
+ await adapter.createAttribute(params);
141
+ await delay(150);
142
+ };
143
+ const updateAttr = async (tableId, attr) => {
144
+ const params = mapToUpdateAttributeParams(attr, { databaseId, tableId });
145
+ await adapter.updateAttribute(params);
280
146
  await delay(150);
281
147
  };
282
148
  // Local queue for unresolved relationships
@@ -357,37 +223,92 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
357
223
  }
358
224
  // Add small delay after table create/update
359
225
  await delay(250);
360
- // Create attributes: non-relationship first
226
+ // Create/Update attributes: non-relationship first using enhanced planning
361
227
  const nonRel = (attributes || []).filter((a) => a.type !== 'relationship');
362
- for (const attr of nonRel) {
363
- await createAttr(tableId, attr);
228
+ if (nonRel.length > 0) {
229
+ // Fetch existing columns once
230
+ const tableInfo = await adapter.getTable({ databaseId, tableId });
231
+ const existingCols = tableInfo.data?.columns || tableInfo.data?.attributes || [];
232
+ // Plan with icons
233
+ const plan = diffColumnsDetailed(nonRel, existingCols);
234
+ const plus = plan.toCreate.map((a) => a.key);
235
+ const plusminus = plan.toUpdate.map((u) => u.attribute.key);
236
+ const minus = plan.toRecreate.map((r) => r.newAttribute.key);
237
+ const skip = plan.unchanged;
238
+ const parts = [];
239
+ if (plus.length)
240
+ parts.push(`➕ ${plus.length} (${plus.join(', ')})`);
241
+ if (plusminus.length)
242
+ parts.push(`🔧 ${plusminus.length} (${plusminus.join(', ')})`);
243
+ if (minus.length)
244
+ parts.push(`♻️ ${minus.length} (${minus.join(', ')})`);
245
+ if (skip.length)
246
+ parts.push(`⏭️ ${skip.length}`);
247
+ MessageFormatter.info(`Plan → ${parts.join(' | ') || 'no changes'}`, { prefix: 'Attributes' });
248
+ // Execute
249
+ const colResults = await executeColumnOperations(adapter, databaseId, tableId, plan);
250
+ if (colResults.success.length > 0) {
251
+ MessageFormatter.success(`Processed ${colResults.success.length} ops`, { prefix: 'Attributes' });
252
+ }
253
+ if (colResults.errors.length > 0) {
254
+ MessageFormatter.error(`${colResults.errors.length} attribute operations failed:`, undefined, { prefix: 'Attributes' });
255
+ for (const err of colResults.errors) {
256
+ MessageFormatter.error(` ${err.column}: ${err.error}`, undefined, { prefix: 'Attributes' });
257
+ }
258
+ }
259
+ MessageFormatter.info(`Summary → ➕ ${plan.toCreate.length} | 🔧 ${plan.toUpdate.length} | ♻️ ${plan.toRecreate.length} | ⏭️ ${plan.unchanged.length}`, { prefix: 'Attributes' });
364
260
  }
365
- // Relationship attributes — resolve relatedCollection to ID
261
+ // Relationship attributes — resolve relatedCollection to ID, then diff and create/update
366
262
  const rels = (attributes || []).filter((a) => a.type === 'relationship');
367
- for (const attr of rels) {
368
- const relNameOrId = attr.relatedCollection;
369
- if (!relNameOrId)
370
- continue;
371
- let relId = nameToIdMapping.get(relNameOrId) || relNameOrId;
372
- // If looks like a name (not ULID) and not in cache, try query by name
373
- if (!nameToIdMapping.has(relNameOrId)) {
374
- try {
375
- const relList = await adapter.listTables({ databaseId, queries: [Query.equal('name', relNameOrId)] });
376
- const relItems = relList.tables || [];
377
- if (relItems[0]?.$id) {
378
- relId = relItems[0].$id;
379
- nameToIdMapping.set(relNameOrId, relId);
263
+ if (rels.length > 0) {
264
+ for (const attr of rels) {
265
+ const relNameOrId = attr.relatedCollection;
266
+ if (!relNameOrId)
267
+ continue;
268
+ let relId = nameToIdMapping.get(relNameOrId) || relNameOrId;
269
+ if (!nameToIdMapping.has(relNameOrId)) {
270
+ try {
271
+ const relList = await adapter.listTables({ databaseId, queries: [Query.equal('name', relNameOrId)] });
272
+ const relItems = relList.tables || [];
273
+ if (relItems[0]?.$id) {
274
+ relId = relItems[0].$id;
275
+ nameToIdMapping.set(relNameOrId, relId);
276
+ }
380
277
  }
278
+ catch { }
381
279
  }
382
- catch { }
280
+ if (relId && typeof relId === 'string')
281
+ attr.relatedCollection = relId;
383
282
  }
384
- if (relId && typeof relId === 'string') {
385
- attr.relatedCollection = relId;
386
- await createAttr(tableId, attr);
283
+ const tableInfo2 = await adapter.getTable({ databaseId, tableId });
284
+ const existingCols2 = tableInfo2.data?.columns || tableInfo2.data?.attributes || [];
285
+ const { toCreate: relCreate, toUpdate: relUpdate, unchanged: relUnchanged } = diffTableColumns(existingCols2, rels);
286
+ // Relationship plan with icons
287
+ {
288
+ const parts = [];
289
+ if (relCreate.length)
290
+ parts.push(`➕ ${relCreate.length} (${relCreate.map((a) => a.key).join(', ')})`);
291
+ if (relUpdate.length)
292
+ parts.push(`🔧 ${relUpdate.length} (${relUpdate.map((a) => a.key).join(', ')})`);
293
+ if (relUnchanged.length)
294
+ parts.push(`⏭️ ${relUnchanged.length}`);
295
+ MessageFormatter.info(`Plan → ${parts.join(' | ') || 'no changes'}`, { prefix: 'Relationships' });
387
296
  }
388
- else {
389
- // Defer if unresolved
390
- relQueue.push({ tableId, attr: attr });
297
+ for (const attr of relUpdate) {
298
+ try {
299
+ await updateAttr(tableId, attr);
300
+ }
301
+ catch (e) {
302
+ MessageFormatter.error(`Failed to update relationship ${attr.key}`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Attributes' });
303
+ }
304
+ }
305
+ for (const attr of relCreate) {
306
+ try {
307
+ await createAttr(tableId, attr);
308
+ }
309
+ catch (e) {
310
+ MessageFormatter.error(`Failed to create relationship ${attr.key}`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Attributes' });
311
+ }
391
312
  }
392
313
  }
393
314
  // Wait for all attributes to become available before creating indexes
@@ -403,7 +324,7 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
403
324
  while (Date.now() - startTime < maxWait) {
404
325
  try {
405
326
  const tableData = await adapter.getTable({ databaseId, tableId });
406
- const attrs = tableData.attributes || [];
327
+ const attrs = tableData.data?.columns || tableData.data?.attributes || [];
407
328
  const attr = attrs.find((a) => a.key === attrKey);
408
329
  if (attr) {
409
330
  if (attr.status === 'available') {
@@ -431,21 +352,111 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
431
352
  // Prefer local config indexes, but fall back to collection's own indexes if no local config exists (TablesDB path)
432
353
  const localTableConfig = config.collections?.find(c => c.name === collectionData.name || c.$id === collectionData.$id);
433
354
  const idxs = (localTableConfig?.indexes ?? indexes ?? []);
434
- for (const idx of idxs) {
435
- try {
436
- await adapter.createIndex({
437
- databaseId,
438
- tableId,
439
- key: idx.key,
440
- type: idx.type,
441
- attributes: idx.attributes,
442
- orders: idx.orders || []
443
- });
444
- await delay(150);
355
+ // Compare with existing indexes and create/update accordingly with status checks
356
+ try {
357
+ const existingIdxRes = await adapter.listIndexes({ databaseId, tableId });
358
+ const existingIdx = existingIdxRes.data || existingIdxRes.indexes || [];
359
+ MessageFormatter.debug(`Existing index keys: ${existingIdx.map((i) => i.key).join(', ')}`, undefined, { prefix: 'Indexes' });
360
+ // Show a concise plan with icons before executing
361
+ const idxPlanPlus = [];
362
+ const idxPlanPlusMinus = [];
363
+ const idxPlanSkip = [];
364
+ for (const idx of idxs) {
365
+ const found = existingIdx.find((i) => i.key === idx.key);
366
+ if (found) {
367
+ if (isIndexEqualToIndex(found, idx))
368
+ idxPlanSkip.push(idx.key);
369
+ else
370
+ idxPlanPlusMinus.push(idx.key);
371
+ }
372
+ else
373
+ idxPlanPlus.push(idx.key);
445
374
  }
446
- catch (e) {
447
- MessageFormatter.error(`Failed to create index ${idx.key}`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Indexes' });
375
+ const planParts = [];
376
+ if (idxPlanPlus.length)
377
+ planParts.push(`➕ ${idxPlanPlus.length} (${idxPlanPlus.join(', ')})`);
378
+ if (idxPlanPlusMinus.length)
379
+ planParts.push(`🔧 ${idxPlanPlusMinus.length} (${idxPlanPlusMinus.join(', ')})`);
380
+ if (idxPlanSkip.length)
381
+ planParts.push(`⏭️ ${idxPlanSkip.length}`);
382
+ MessageFormatter.info(`Plan → ${planParts.join(' | ') || 'no changes'}`, { prefix: 'Indexes' });
383
+ const created = [];
384
+ const updated = [];
385
+ const skipped = [];
386
+ for (const idx of idxs) {
387
+ const found = existingIdx.find((i) => i.key === idx.key);
388
+ if (found) {
389
+ if (isIndexEqualToIndex(found, idx)) {
390
+ MessageFormatter.info(`Index ${idx.key} unchanged`, { prefix: 'Indexes' });
391
+ skipped.push(idx.key);
392
+ }
393
+ else {
394
+ try {
395
+ await adapter.deleteIndex({ databaseId, tableId, key: idx.key });
396
+ await delay(100);
397
+ }
398
+ catch { }
399
+ try {
400
+ await adapter.createIndex({ databaseId, tableId, key: idx.key, type: idx.type, attributes: idx.attributes, orders: idx.orders || [] });
401
+ updated.push(idx.key);
402
+ }
403
+ catch (e) {
404
+ const msg = (e?.message || '').toString().toLowerCase();
405
+ if (msg.includes('already exists')) {
406
+ MessageFormatter.info(`Index ${idx.key} already exists after delete attempt, skipping`, { prefix: 'Indexes' });
407
+ skipped.push(idx.key);
408
+ }
409
+ else {
410
+ throw e;
411
+ }
412
+ }
413
+ }
414
+ }
415
+ else {
416
+ try {
417
+ await adapter.createIndex({ databaseId, tableId, key: idx.key, type: idx.type, attributes: idx.attributes, orders: idx.orders || [] });
418
+ created.push(idx.key);
419
+ }
420
+ catch (e) {
421
+ const msg = (e?.message || '').toString().toLowerCase();
422
+ if (msg.includes('already exists')) {
423
+ MessageFormatter.info(`Index ${idx.key} already exists (create), skipping`, { prefix: 'Indexes' });
424
+ skipped.push(idx.key);
425
+ }
426
+ else {
427
+ throw e;
428
+ }
429
+ }
430
+ }
431
+ // Wait for index availability
432
+ const maxWait = 60000;
433
+ const start = Date.now();
434
+ let lastStatus = '';
435
+ while (Date.now() - start < maxWait) {
436
+ try {
437
+ const li = await adapter.listIndexes({ databaseId, tableId });
438
+ const list = li.data || li.indexes || [];
439
+ const cur = list.find((i) => i.key === idx.key);
440
+ if (cur) {
441
+ if (cur.status === 'available')
442
+ break;
443
+ if (cur.status === 'failed' || cur.status === 'stuck') {
444
+ throw new Error(cur.error || `Index ${idx.key} failed`);
445
+ }
446
+ lastStatus = cur.status;
447
+ }
448
+ await delay(2000);
449
+ }
450
+ catch {
451
+ await delay(2000);
452
+ }
453
+ }
454
+ await delay(150);
448
455
  }
456
+ MessageFormatter.info(`Summary → ➕ ${created.length} | 🔧 ${updated.length} | ⏭️ ${skipped.length}`, { prefix: 'Indexes' });
457
+ }
458
+ catch (e) {
459
+ MessageFormatter.error(`Failed to list/create indexes`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Indexes' });
449
460
  }
450
461
  // Mark this table as fully processed to prevent re-processing
451
462
  markCollectionProcessed(tableId, collectionData.name);
@@ -0,0 +1,86 @@
1
+ import type { Attribute } from "appwrite-utils";
2
+ interface ColumnPropertyChange {
3
+ property: string;
4
+ oldValue: any;
5
+ newValue: any;
6
+ requiresRecreate: boolean;
7
+ }
8
+ interface ColumnOperationPlan {
9
+ toCreate: Attribute[];
10
+ toUpdate: Array<{
11
+ attribute: Attribute;
12
+ changes: ColumnPropertyChange[];
13
+ }>;
14
+ toRecreate: Array<{
15
+ oldAttribute: any;
16
+ newAttribute: Attribute;
17
+ }>;
18
+ toDelete: Array<{
19
+ attribute: any;
20
+ }>;
21
+ unchanged: string[];
22
+ }
23
+ type ComparableColumn = {
24
+ key: string;
25
+ type: string;
26
+ required?: boolean;
27
+ array?: boolean;
28
+ default?: any;
29
+ size?: number;
30
+ min?: number;
31
+ max?: number;
32
+ elements?: string[];
33
+ encrypt?: boolean;
34
+ relatedCollection?: string;
35
+ relationType?: string;
36
+ twoWay?: boolean;
37
+ twoWayKey?: string;
38
+ onDelete?: string;
39
+ side?: string;
40
+ };
41
+ export declare function normalizeAttributeToComparable(attr: Attribute): ComparableColumn;
42
+ export declare function normalizeColumnToComparable(col: any): ComparableColumn;
43
+ export declare function isColumnEqualToColumn(a: any, b: any): boolean;
44
+ export declare function isIndexEqualToIndex(a: any, b: any): boolean;
45
+ /**
46
+ * Enhanced version of columns diff with detailed change analysis
47
+ * Order: desired first, then existing (matches internal usage here)
48
+ */
49
+ export declare function diffColumnsDetailed(desiredAttributes: Attribute[], existingColumns: any[]): ColumnOperationPlan;
50
+ /**
51
+ * Returns true if there is any difference between existing columns and desired attributes
52
+ */
53
+ export declare function areTableColumnsDiff(existingColumns: any[], desired: Attribute[]): boolean;
54
+ export declare function diffTableColumns(existingColumns: any[], desired: Attribute[]): {
55
+ toCreate: Attribute[];
56
+ toUpdate: Attribute[];
57
+ unchanged: string[];
58
+ };
59
+ /**
60
+ * Execute the column operation plan using the adapter
61
+ */
62
+ export declare function executeColumnOperations(adapter: any, databaseId: string, tableId: string, plan: ColumnOperationPlan): Promise<{
63
+ success: string[];
64
+ errors: Array<{
65
+ column: string;
66
+ error: string;
67
+ }>;
68
+ }>;
69
+ /**
70
+ * Integration function for methods.ts - processes columns using enhanced logic
71
+ */
72
+ export declare function processTableColumns(adapter: any, databaseId: string, tableId: string, desiredAttributes: Attribute[], existingColumns?: any[]): Promise<{
73
+ totalProcessed: number;
74
+ success: string[];
75
+ errors: Array<{
76
+ column: string;
77
+ error: string;
78
+ }>;
79
+ summary: {
80
+ created: number;
81
+ updated: number;
82
+ recreated: number;
83
+ unchanged: number;
84
+ };
85
+ }>;
86
+ export {};