@njdamstra/appwrite-utils-cli 1.8.9 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (285) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/CONFIG_TODO.md +1189 -0
  3. package/SELECTION_DIALOGS.md +146 -0
  4. package/SERVICE_IMPLEMENTATION_REPORT.md +462 -0
  5. package/dist/adapters/index.d.ts +7 -8
  6. package/dist/adapters/index.js +7 -9
  7. package/dist/backups/operations/bucketBackup.js +2 -2
  8. package/dist/backups/operations/collectionBackup.d.ts +1 -1
  9. package/dist/backups/operations/collectionBackup.js +3 -3
  10. package/dist/backups/operations/comprehensiveBackup.d.ts +1 -1
  11. package/dist/backups/operations/comprehensiveBackup.js +2 -2
  12. package/dist/backups/tracking/centralizedTracking.d.ts +1 -1
  13. package/dist/backups/tracking/centralizedTracking.js +2 -2
  14. package/dist/cli/commands/configCommands.js +51 -7
  15. package/dist/cli/commands/databaseCommands.d.ts +1 -0
  16. package/dist/cli/commands/databaseCommands.js +119 -9
  17. package/dist/cli/commands/functionCommands.js +3 -3
  18. package/dist/cli/commands/importFileCommands.d.ts +7 -0
  19. package/dist/cli/commands/importFileCommands.js +674 -0
  20. package/dist/cli/commands/schemaCommands.js +3 -3
  21. package/dist/cli/commands/storageCommands.js +2 -3
  22. package/dist/cli/commands/transferCommands.js +3 -5
  23. package/dist/collections/attributes.d.ts +1 -1
  24. package/dist/collections/attributes.js +85 -35
  25. package/dist/collections/indexes.js +2 -4
  26. package/dist/collections/methods.d.ts +1 -1
  27. package/dist/collections/methods.js +111 -192
  28. package/dist/collections/tableOperations.d.ts +1 -0
  29. package/dist/collections/tableOperations.js +90 -23
  30. package/dist/collections/transferOperations.d.ts +1 -1
  31. package/dist/collections/transferOperations.js +3 -4
  32. package/dist/collections/wipeOperations.d.ts +4 -3
  33. package/dist/collections/wipeOperations.js +112 -39
  34. package/dist/databases/methods.js +2 -2
  35. package/dist/databases/setup.js +2 -2
  36. package/dist/examples/yamlTerminologyExample.js +2 -2
  37. package/dist/functions/deployments.d.ts +1 -1
  38. package/dist/functions/deployments.js +5 -5
  39. package/dist/functions/fnConfigDiscovery.js +2 -2
  40. package/dist/functions/methods.js +16 -4
  41. package/dist/init.js +1 -1
  42. package/dist/interactiveCLI.d.ts +6 -1
  43. package/dist/interactiveCLI.js +64 -10
  44. package/dist/main.js +130 -177
  45. package/dist/migrations/afterImportActions.js +2 -3
  46. package/dist/migrations/appwriteToX.d.ts +97 -1
  47. package/dist/migrations/appwriteToX.js +9 -7
  48. package/dist/migrations/comprehensiveTransfer.js +3 -5
  49. package/dist/migrations/dataLoader.d.ts +194 -2
  50. package/dist/migrations/dataLoader.js +2 -5
  51. package/dist/migrations/importController.js +3 -4
  52. package/dist/migrations/importDataActions.js +3 -3
  53. package/dist/migrations/relationships.js +1 -2
  54. package/dist/migrations/services/DataTransformationService.js +2 -2
  55. package/dist/migrations/services/FileHandlerService.js +1 -1
  56. package/dist/migrations/services/ImportOrchestrator.js +4 -4
  57. package/dist/migrations/services/RateLimitManager.js +1 -1
  58. package/dist/migrations/services/RelationshipResolver.js +1 -1
  59. package/dist/migrations/services/UserMappingService.js +1 -1
  60. package/dist/migrations/services/ValidationService.js +1 -1
  61. package/dist/migrations/transfer.d.ts +8 -4
  62. package/dist/migrations/transfer.js +106 -55
  63. package/dist/migrations/yaml/YamlImportConfigLoader.js +1 -1
  64. package/dist/migrations/yaml/YamlImportIntegration.js +2 -2
  65. package/dist/migrations/yaml/generateImportSchemas.js +1 -1
  66. package/dist/setupCommands.d.ts +1 -1
  67. package/dist/setupCommands.js +5 -6
  68. package/dist/setupController.js +1 -1
  69. package/dist/shared/backupTracking.d.ts +1 -1
  70. package/dist/shared/backupTracking.js +2 -2
  71. package/dist/shared/confirmationDialogs.js +1 -1
  72. package/dist/shared/migrationHelpers.d.ts +1 -1
  73. package/dist/shared/migrationHelpers.js +3 -3
  74. package/dist/shared/operationQueue.d.ts +1 -1
  75. package/dist/shared/operationQueue.js +2 -3
  76. package/dist/shared/operationsTable.d.ts +1 -1
  77. package/dist/shared/operationsTable.js +2 -2
  78. package/dist/shared/progressManager.js +1 -1
  79. package/dist/shared/selectionDialogs.js +9 -8
  80. package/dist/storage/methods.js +4 -4
  81. package/dist/storage/schemas.d.ts +386 -2
  82. package/dist/tables/indexManager.d.ts +65 -0
  83. package/dist/tables/indexManager.js +294 -0
  84. package/dist/types.d.ts +2 -2
  85. package/dist/types.js +1 -1
  86. package/dist/users/methods.js +2 -3
  87. package/dist/utils/configMigration.js +1 -1
  88. package/dist/utils/index.d.ts +1 -1
  89. package/dist/utils/index.js +1 -1
  90. package/dist/utils/loadConfigs.d.ts +2 -2
  91. package/dist/utils/loadConfigs.js +6 -7
  92. package/dist/utils/setupFiles.js +5 -7
  93. package/dist/utilsController.d.ts +15 -8
  94. package/dist/utilsController.js +57 -28
  95. package/package.json +8 -4
  96. package/src/adapters/index.ts +8 -34
  97. package/src/backups/operations/bucketBackup.ts +2 -2
  98. package/src/backups/operations/collectionBackup.ts +4 -4
  99. package/src/backups/operations/comprehensiveBackup.ts +3 -3
  100. package/src/backups/tracking/centralizedTracking.ts +3 -3
  101. package/src/cli/commands/configCommands.ts +72 -8
  102. package/src/cli/commands/databaseCommands.ts +161 -9
  103. package/src/cli/commands/functionCommands.ts +4 -3
  104. package/src/cli/commands/importFileCommands.ts +815 -0
  105. package/src/cli/commands/schemaCommands.ts +3 -3
  106. package/src/cli/commands/storageCommands.ts +2 -3
  107. package/src/cli/commands/transferCommands.ts +3 -6
  108. package/src/collections/attributes.ts +155 -39
  109. package/src/collections/indexes.ts +5 -7
  110. package/src/collections/methods.ts +115 -150
  111. package/src/collections/tableOperations.ts +92 -21
  112. package/src/collections/transferOperations.ts +4 -5
  113. package/src/collections/wipeOperations.ts +154 -51
  114. package/src/databases/methods.ts +2 -2
  115. package/src/databases/setup.ts +2 -2
  116. package/src/examples/yamlTerminologyExample.ts +2 -2
  117. package/src/functions/deployments.ts +6 -5
  118. package/src/functions/fnConfigDiscovery.ts +2 -2
  119. package/src/functions/methods.ts +19 -6
  120. package/src/init.ts +1 -1
  121. package/src/interactiveCLI.ts +78 -13
  122. package/src/main.ts +143 -287
  123. package/src/migrations/afterImportActions.ts +2 -3
  124. package/src/migrations/appwriteToX.ts +12 -8
  125. package/src/migrations/comprehensiveTransfer.ts +6 -6
  126. package/src/migrations/dataLoader.ts +2 -5
  127. package/src/migrations/importController.ts +3 -4
  128. package/src/migrations/importDataActions.ts +3 -3
  129. package/src/migrations/relationships.ts +1 -2
  130. package/src/migrations/services/DataTransformationService.ts +2 -2
  131. package/src/migrations/services/FileHandlerService.ts +1 -1
  132. package/src/migrations/services/ImportOrchestrator.ts +4 -4
  133. package/src/migrations/services/RateLimitManager.ts +1 -1
  134. package/src/migrations/services/RelationshipResolver.ts +1 -1
  135. package/src/migrations/services/UserMappingService.ts +1 -1
  136. package/src/migrations/services/ValidationService.ts +1 -1
  137. package/src/migrations/transfer.ts +126 -83
  138. package/src/migrations/yaml/YamlImportConfigLoader.ts +1 -1
  139. package/src/migrations/yaml/YamlImportIntegration.ts +2 -2
  140. package/src/migrations/yaml/generateImportSchemas.ts +1 -1
  141. package/src/setupCommands.ts +5 -6
  142. package/src/setupController.ts +1 -1
  143. package/src/shared/backupTracking.ts +3 -3
  144. package/src/shared/confirmationDialogs.ts +1 -1
  145. package/src/shared/migrationHelpers.ts +4 -4
  146. package/src/shared/operationQueue.ts +3 -4
  147. package/src/shared/operationsTable.ts +3 -3
  148. package/src/shared/progressManager.ts +1 -1
  149. package/src/shared/selectionDialogs.ts +9 -8
  150. package/src/storage/methods.ts +4 -4
  151. package/src/tables/indexManager.ts +409 -0
  152. package/src/types.ts +2 -2
  153. package/src/users/methods.ts +2 -3
  154. package/src/utils/configMigration.ts +1 -1
  155. package/src/utils/index.ts +1 -1
  156. package/src/utils/loadConfigs.ts +15 -7
  157. package/src/utils/setupFiles.ts +5 -7
  158. package/src/utilsController.ts +86 -32
  159. package/dist/adapters/AdapterFactory.d.ts +0 -94
  160. package/dist/adapters/AdapterFactory.js +0 -405
  161. package/dist/adapters/DatabaseAdapter.d.ts +0 -233
  162. package/dist/adapters/DatabaseAdapter.js +0 -50
  163. package/dist/adapters/LegacyAdapter.d.ts +0 -50
  164. package/dist/adapters/LegacyAdapter.js +0 -612
  165. package/dist/adapters/TablesDBAdapter.d.ts +0 -45
  166. package/dist/adapters/TablesDBAdapter.js +0 -571
  167. package/dist/config/ConfigManager.d.ts +0 -445
  168. package/dist/config/ConfigManager.js +0 -625
  169. package/dist/config/configMigration.d.ts +0 -87
  170. package/dist/config/configMigration.js +0 -390
  171. package/dist/config/configValidation.d.ts +0 -66
  172. package/dist/config/configValidation.js +0 -358
  173. package/dist/config/index.d.ts +0 -8
  174. package/dist/config/index.js +0 -7
  175. package/dist/config/services/ConfigDiscoveryService.d.ts +0 -126
  176. package/dist/config/services/ConfigDiscoveryService.js +0 -374
  177. package/dist/config/services/ConfigLoaderService.d.ts +0 -129
  178. package/dist/config/services/ConfigLoaderService.js +0 -540
  179. package/dist/config/services/ConfigMergeService.d.ts +0 -208
  180. package/dist/config/services/ConfigMergeService.js +0 -308
  181. package/dist/config/services/ConfigValidationService.d.ts +0 -214
  182. package/dist/config/services/ConfigValidationService.js +0 -310
  183. package/dist/config/services/SessionAuthService.d.ts +0 -225
  184. package/dist/config/services/SessionAuthService.js +0 -456
  185. package/dist/config/services/__tests__/ConfigMergeService.test.d.ts +0 -1
  186. package/dist/config/services/__tests__/ConfigMergeService.test.js +0 -271
  187. package/dist/config/services/index.d.ts +0 -13
  188. package/dist/config/services/index.js +0 -10
  189. package/dist/config/yamlConfig.d.ts +0 -722
  190. package/dist/config/yamlConfig.js +0 -702
  191. package/dist/functions/pathResolution.d.ts +0 -37
  192. package/dist/functions/pathResolution.js +0 -185
  193. package/dist/shared/attributeMapper.d.ts +0 -20
  194. package/dist/shared/attributeMapper.js +0 -203
  195. package/dist/shared/errorUtils.d.ts +0 -54
  196. package/dist/shared/errorUtils.js +0 -95
  197. package/dist/shared/functionManager.d.ts +0 -48
  198. package/dist/shared/functionManager.js +0 -336
  199. package/dist/shared/indexManager.d.ts +0 -24
  200. package/dist/shared/indexManager.js +0 -151
  201. package/dist/shared/jsonSchemaGenerator.d.ts +0 -50
  202. package/dist/shared/jsonSchemaGenerator.js +0 -290
  203. package/dist/shared/logging.d.ts +0 -61
  204. package/dist/shared/logging.js +0 -116
  205. package/dist/shared/messageFormatter.d.ts +0 -39
  206. package/dist/shared/messageFormatter.js +0 -162
  207. package/dist/shared/pydanticModelGenerator.d.ts +0 -17
  208. package/dist/shared/pydanticModelGenerator.js +0 -615
  209. package/dist/shared/schemaGenerator.d.ts +0 -40
  210. package/dist/shared/schemaGenerator.js +0 -556
  211. package/dist/utils/ClientFactory.d.ts +0 -87
  212. package/dist/utils/ClientFactory.js +0 -212
  213. package/dist/utils/configDiscovery.d.ts +0 -78
  214. package/dist/utils/configDiscovery.js +0 -472
  215. package/dist/utils/constantsGenerator.d.ts +0 -31
  216. package/dist/utils/constantsGenerator.js +0 -321
  217. package/dist/utils/dataConverters.d.ts +0 -46
  218. package/dist/utils/dataConverters.js +0 -139
  219. package/dist/utils/directoryUtils.d.ts +0 -22
  220. package/dist/utils/directoryUtils.js +0 -59
  221. package/dist/utils/getClientFromConfig.d.ts +0 -39
  222. package/dist/utils/getClientFromConfig.js +0 -199
  223. package/dist/utils/helperFunctions.d.ts +0 -63
  224. package/dist/utils/helperFunctions.js +0 -156
  225. package/dist/utils/pathResolvers.d.ts +0 -53
  226. package/dist/utils/pathResolvers.js +0 -72
  227. package/dist/utils/projectConfig.d.ts +0 -119
  228. package/dist/utils/projectConfig.js +0 -171
  229. package/dist/utils/retryFailedPromises.d.ts +0 -2
  230. package/dist/utils/retryFailedPromises.js +0 -23
  231. package/dist/utils/sessionAuth.d.ts +0 -48
  232. package/dist/utils/sessionAuth.js +0 -164
  233. package/dist/utils/typeGuards.d.ts +0 -35
  234. package/dist/utils/typeGuards.js +0 -57
  235. package/dist/utils/validationRules.d.ts +0 -43
  236. package/dist/utils/validationRules.js +0 -42
  237. package/dist/utils/versionDetection.d.ts +0 -58
  238. package/dist/utils/versionDetection.js +0 -251
  239. package/dist/utils/yamlConverter.d.ts +0 -100
  240. package/dist/utils/yamlConverter.js +0 -428
  241. package/dist/utils/yamlLoader.d.ts +0 -70
  242. package/dist/utils/yamlLoader.js +0 -267
  243. package/src/adapters/AdapterFactory.ts +0 -510
  244. package/src/adapters/DatabaseAdapter.ts +0 -306
  245. package/src/adapters/LegacyAdapter.ts +0 -841
  246. package/src/adapters/TablesDBAdapter.ts +0 -773
  247. package/src/config/ConfigManager.ts +0 -808
  248. package/src/config/README.md +0 -274
  249. package/src/config/configMigration.ts +0 -575
  250. package/src/config/configValidation.ts +0 -445
  251. package/src/config/index.ts +0 -10
  252. package/src/config/services/ConfigDiscoveryService.ts +0 -463
  253. package/src/config/services/ConfigLoaderService.ts +0 -740
  254. package/src/config/services/ConfigMergeService.ts +0 -388
  255. package/src/config/services/ConfigValidationService.ts +0 -394
  256. package/src/config/services/SessionAuthService.ts +0 -565
  257. package/src/config/services/__tests__/ConfigMergeService.test.ts +0 -351
  258. package/src/config/services/index.ts +0 -29
  259. package/src/config/yamlConfig.ts +0 -761
  260. package/src/functions/pathResolution.ts +0 -227
  261. package/src/shared/attributeMapper.ts +0 -229
  262. package/src/shared/errorUtils.ts +0 -110
  263. package/src/shared/functionManager.ts +0 -525
  264. package/src/shared/indexManager.ts +0 -254
  265. package/src/shared/jsonSchemaGenerator.ts +0 -383
  266. package/src/shared/logging.ts +0 -149
  267. package/src/shared/messageFormatter.ts +0 -208
  268. package/src/shared/pydanticModelGenerator.ts +0 -618
  269. package/src/shared/schemaGenerator.ts +0 -644
  270. package/src/utils/ClientFactory.ts +0 -240
  271. package/src/utils/configDiscovery.ts +0 -557
  272. package/src/utils/constantsGenerator.ts +0 -369
  273. package/src/utils/dataConverters.ts +0 -159
  274. package/src/utils/directoryUtils.ts +0 -61
  275. package/src/utils/getClientFromConfig.ts +0 -257
  276. package/src/utils/helperFunctions.ts +0 -228
  277. package/src/utils/pathResolvers.ts +0 -81
  278. package/src/utils/projectConfig.ts +0 -299
  279. package/src/utils/retryFailedPromises.ts +0 -29
  280. package/src/utils/sessionAuth.ts +0 -230
  281. package/src/utils/typeGuards.ts +0 -65
  282. package/src/utils/validationRules.ts +0 -88
  283. package/src/utils/versionDetection.ts +0 -292
  284. package/src/utils/yamlConverter.ts +0 -542
  285. package/src/utils/yamlLoader.ts +0 -371
@@ -6,19 +6,19 @@ import {
6
6
  type Models,
7
7
  } from "node-appwrite";
8
8
  import type { AppwriteConfig, CollectionCreate, Indexes, Attribute } from "@njdamstra/appwrite-utils";
9
- import type { DatabaseAdapter } from "../adapters/DatabaseAdapter.js";
10
- import { getAdapterFromConfig } from "../utils/getClientFromConfig.js";
9
+ import type { DatabaseAdapter } from "@njdamstra/appwrite-utils-helpers";
10
+ import { getAdapterFromConfig } from "@njdamstra/appwrite-utils-helpers";
11
11
  import {
12
12
  nameToIdMapping,
13
13
  processQueue,
14
14
  queuedOperations,
15
15
  clearProcessingState,
16
16
  isCollectionProcessed,
17
- markCollectionProcessed
17
+ markCollectionProcessed,
18
+ enqueueOperation
18
19
  } from "../shared/operationQueue.js";
19
- import { logger } from "../shared/logging.js";
20
+ import { logger, SchemaGenerator } from "@njdamstra/appwrite-utils-helpers";
20
21
  // Legacy attribute/index helpers removed in favor of unified adapter path
21
- import { SchemaGenerator } from "../shared/schemaGenerator.js";
22
22
  import {
23
23
  isNull,
24
24
  isUndefined,
@@ -26,11 +26,11 @@ import {
26
26
  isPlainObject,
27
27
  isString,
28
28
  } from "es-toolkit";
29
- import { delay, tryAwaitWithRetry } from "../utils/helperFunctions.js";
30
- import { MessageFormatter } from "../shared/messageFormatter.js";
31
- import { isLegacyDatabases } from "../utils/typeGuards.js";
32
- import { mapToCreateAttributeParams, mapToUpdateAttributeParams } from "../shared/attributeMapper.js";
29
+ import { delay, tryAwaitWithRetry } from "@njdamstra/appwrite-utils-helpers";
30
+ import { MessageFormatter, mapToCreateAttributeParams, mapToUpdateAttributeParams } from "@njdamstra/appwrite-utils-helpers";
31
+ import { isLegacyDatabases } from "@njdamstra/appwrite-utils-helpers";
33
32
  import { diffTableColumns, isIndexEqualToIndex, diffColumnsDetailed, executeColumnOperations } from "./tableOperations.js";
33
+ import { createOrUpdateIndexesViaAdapter, deleteObsoleteIndexesViaAdapter } from "../tables/indexManager.js";
34
34
 
35
35
  // Re-export wipe operations
36
36
  export {
@@ -375,45 +375,105 @@ export const createOrUpdateCollectionsViaAdapter = async (
375
375
  );
376
376
  }
377
377
 
378
- // Relationship attributes — resolve relatedCollection to ID, then diff and create/update
379
- const rels = (attributes || []).filter((a: Attribute) => a.type === 'relationship') as any[];
380
- if (rels.length > 0) {
381
- for (const attr of rels) {
378
+ // Relationship attributes — resolve relatedCollection to ID, then diff and create/update with recreate support
379
+ const relsAll = (attributes || []).filter((a: Attribute) => a.type === 'relationship') as any[];
380
+ if (relsAll.length > 0) {
381
+ const relsResolved: any[] = [];
382
+ const relsDeferred: any[] = [];
383
+
384
+ // Resolve related collections (names -> IDs) using cache or lookup.
385
+ // If not resolvable yet (target table created later in the same push), queue for later.
386
+ for (const attr of relsAll) {
382
387
  const relNameOrId = attr.relatedCollection as string | undefined;
383
388
  if (!relNameOrId) continue;
384
389
  let relId = nameToIdMapping.get(relNameOrId) || relNameOrId;
385
- if (!nameToIdMapping.has(relNameOrId)) {
390
+ let resolved = false;
391
+ if (nameToIdMapping.has(relNameOrId)) {
392
+ resolved = true;
393
+ } else {
394
+ // Try resolve by name
386
395
  try {
387
396
  const relList = await adapter.listTables({ databaseId, queries: [Query.equal('name', relNameOrId)] });
388
397
  const relItems: any[] = (relList as any).tables || [];
389
398
  if (relItems[0]?.$id) {
390
399
  relId = relItems[0].$id;
391
400
  nameToIdMapping.set(relNameOrId, relId);
401
+ resolved = true;
392
402
  }
393
403
  } catch {}
404
+
405
+ // If the relNameOrId looks like an ID but isn't resolved yet, attempt a direct get
406
+ if (!resolved && relNameOrId && relNameOrId.length >= 10) {
407
+ try {
408
+ const probe = await adapter.getTable({ databaseId, tableId: relNameOrId });
409
+ if ((probe as any).data?.$id) {
410
+ nameToIdMapping.set(relNameOrId, relNameOrId);
411
+ relId = relNameOrId;
412
+ resolved = true;
413
+ }
414
+ } catch {}
415
+ }
416
+ }
417
+
418
+ if (resolved && relId && typeof relId === 'string') {
419
+ attr.relatedCollection = relId;
420
+ relsResolved.push(attr);
421
+ } else {
422
+ // Defer until related table exists; queue a surgical operation
423
+ enqueueOperation({
424
+ type: 'attribute',
425
+ collectionId: tableId,
426
+ attribute: attr,
427
+ dependencies: [relNameOrId]
428
+ });
429
+ relsDeferred.push(attr);
394
430
  }
395
- if (relId && typeof relId === 'string') attr.relatedCollection = relId;
396
431
  }
432
+
433
+ // Compute a detailed plan for immediately resolvable relationships
397
434
  const tableInfo2 = await adapter.getTable({ databaseId, tableId });
398
435
  const existingCols2: any[] = (tableInfo2 as any).data?.columns || (tableInfo2 as any).data?.attributes || [];
399
- const { toCreate: relCreate, toUpdate: relUpdate, unchanged: relUnchanged } = diffTableColumns(existingCols2, rels as any);
436
+ const relPlan = diffColumnsDetailed(relsResolved as any, existingCols2);
400
437
 
401
- // Relationship plan with icons
438
+ // Relationship plan with icons (includes recreates)
402
439
  {
403
440
  const parts: string[] = [];
404
- if (relCreate.length) parts.push(`➕ ${relCreate.length} (${relCreate.map((a:any)=>a.key).join(', ')})`);
405
- if (relUpdate.length) parts.push(`🔧 ${relUpdate.length} (${relUpdate.map((a:any)=>a.key).join(', ')})`);
406
- if (relUnchanged.length) parts.push(`⏭️ ${relUnchanged.length}`);
441
+ if (relPlan.toCreate.length) parts.push(`➕ ${relPlan.toCreate.length} (${relPlan.toCreate.map((a:any)=>a.key).join(', ')})`);
442
+ if (relPlan.toUpdate.length) parts.push(`🔧 ${relPlan.toUpdate.length} (${relPlan.toUpdate.map((u:any)=>u.attribute?.key ?? u.key).join(', ')})`);
443
+ if (relPlan.toRecreate.length) parts.push(`♻️ ${relPlan.toRecreate.length} (${relPlan.toRecreate.map((r:any)=>r.newAttribute?.key ?? r?.key).join(', ')})`);
444
+ if (relPlan.unchanged.length) parts.push(`⏭️ ${relPlan.unchanged.length}`);
407
445
  MessageFormatter.info(`Plan → ${parts.join(' | ') || 'no changes'}`, { prefix: 'Relationships' });
408
446
  }
409
- for (const attr of relUpdate) { try { await updateAttr(tableId, attr as Attribute); } catch (e) { MessageFormatter.error(`Failed to update relationship ${(attr as any).key}`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Attributes' }); } }
410
- for (const attr of relCreate) { try { await createAttr(tableId, attr as Attribute); } catch (e) { MessageFormatter.error(`Failed to create relationship ${(attr as any).key}`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Attributes' }); } }
447
+
448
+ // Execute plan using the same operation executor to properly handle deletes/recreates
449
+ const relResults = await executeColumnOperations(adapter, databaseId, tableId, relPlan);
450
+ if (relResults.success.length > 0) {
451
+ const totalRelationships = relPlan.toCreate.length + relPlan.toUpdate.length + relPlan.toRecreate.length + relPlan.unchanged.length;
452
+ const activeRelationships = relPlan.toCreate.length + relPlan.toUpdate.length + relPlan.toRecreate.length;
453
+
454
+ if (relResults.success.length !== activeRelationships) {
455
+ // Show both counts when they differ (usually due to recreations)
456
+ MessageFormatter.success(`Processed ${relResults.success.length} operations for ${activeRelationships} relationship${activeRelationships === 1 ? '' : 's'}`, { prefix: 'Relationships' });
457
+ } else {
458
+ MessageFormatter.success(`Processed ${relResults.success.length} relationship${relResults.success.length === 1 ? '' : 's'}`, { prefix: 'Relationships' });
459
+ }
460
+ }
461
+ if (relResults.errors.length > 0) {
462
+ MessageFormatter.error(`${relResults.errors.length} relationship operations failed:`, undefined, { prefix: 'Relationships' });
463
+ for (const err of relResults.errors) {
464
+ MessageFormatter.error(` ${err.column}: ${err.error}`, undefined, { prefix: 'Relationships' });
465
+ }
466
+ }
467
+
468
+ if (relsDeferred.length > 0) {
469
+ MessageFormatter.info(`Deferred ${relsDeferred.length} relationship(s) until related tables become available`, { prefix: 'Relationships' });
470
+ }
411
471
  }
412
472
 
413
473
  // Wait for all attributes to become available before creating indexes
414
474
  const allAttrKeys = [
415
475
  ...nonRel.map((a: any) => a.key),
416
- ...rels.filter((a: any) => a.relatedCollection).map((a: any) => a.key)
476
+ ...relsAll.filter((a: any) => a.relatedCollection).map((a: any) => a.key)
417
477
  ];
418
478
 
419
479
  if (allAttrKeys.length > 0) {
@@ -456,144 +516,38 @@ export const createOrUpdateCollectionsViaAdapter = async (
456
516
  }
457
517
  }
458
518
 
459
- // Prefer local config indexes, but fall back to collection's own indexes if no local config exists (TablesDB path)
519
+ // Index management: create/update indexes using clean adapter-based system
460
520
  const localTableConfig = config.collections?.find(
461
521
  c => c.name === collectionData.name || c.$id === collectionData.$id
462
522
  );
463
523
  const idxs = (localTableConfig?.indexes ?? indexes ?? []) as any[];
464
- // Compare with existing indexes and create/update accordingly with status checks
465
- try {
466
- const existingIdxRes = await adapter.listIndexes({ databaseId, tableId });
467
- const existingIdx: any[] = (existingIdxRes as any).data || (existingIdxRes as any).indexes || [];
468
- MessageFormatter.debug(`Existing index keys: ${existingIdx.map((i:any)=>i.key).join(', ')}`, undefined, { prefix: 'Indexes' });
469
- // Show a concise plan with icons before executing
470
- const idxPlanPlus: string[] = [];
471
- const idxPlanPlusMinus: string[] = [];
472
- const idxPlanSkip: string[] = [];
473
- for (const idx of idxs) {
474
- const found = existingIdx.find((i: any) => i.key === idx.key);
475
- if (found) {
476
- if (isIndexEqualToIndex(found, idx)) idxPlanSkip.push(idx.key);
477
- else idxPlanPlusMinus.push(idx.key);
478
- } else idxPlanPlus.push(idx.key);
479
- }
480
- const planParts: string[] = [];
481
- if (idxPlanPlus.length) planParts.push(`➕ ${idxPlanPlus.length} (${idxPlanPlus.join(', ')})`);
482
- if (idxPlanPlusMinus.length) planParts.push(`🔧 ${idxPlanPlusMinus.length} (${idxPlanPlusMinus.join(', ')})`);
483
- if (idxPlanSkip.length) planParts.push(`⏭️ ${idxPlanSkip.length}`);
484
- MessageFormatter.info(`Plan → ${planParts.join(' | ') || 'no changes'}`, { prefix: 'Indexes' });
485
- const created: string[] = [];
486
- const updated: string[] = [];
487
- const skipped: string[] = [];
488
- for (const idx of idxs) {
489
- const found = existingIdx.find((i: any) => i.key === idx.key);
490
- if (found) {
491
- if (isIndexEqualToIndex(found, idx)) {
492
- MessageFormatter.info(`Index ${idx.key} unchanged`, { prefix: 'Indexes' });
493
- skipped.push(idx.key);
494
- } else {
495
- try { await adapter.deleteIndex({ databaseId, tableId, key: idx.key }); await delay(100); } catch {}
496
- try {
497
- await adapter.createIndex({ databaseId, tableId, key: idx.key, type: idx.type, attributes: idx.attributes, orders: idx.orders || [] });
498
- updated.push(idx.key);
499
- } catch (e: any) {
500
- const msg = (e?.message || '').toString().toLowerCase();
501
- if (msg.includes('already exists')) {
502
- MessageFormatter.info(`Index ${idx.key} already exists after delete attempt, skipping`, { prefix: 'Indexes' });
503
- skipped.push(idx.key);
504
- } else {
505
- throw e;
506
- }
507
- }
508
- }
509
- } else {
510
- try {
511
- await adapter.createIndex({ databaseId, tableId, key: idx.key, type: idx.type, attributes: idx.attributes, orders: idx.orders || [] });
512
- created.push(idx.key);
513
- } catch (e: any) {
514
- const msg = (e?.message || '').toString().toLowerCase();
515
- if (msg.includes('already exists')) {
516
- MessageFormatter.info(`Index ${idx.key} already exists (create), skipping`, { prefix: 'Indexes' });
517
- skipped.push(idx.key);
518
- } else {
519
- throw e;
520
- }
521
- }
522
- }
523
- // Wait for index availability
524
- const maxWait = 60000; const start = Date.now(); let lastStatus = '';
525
- while (Date.now() - start < maxWait) {
526
- try {
527
- const li = await adapter.listIndexes({ databaseId, tableId });
528
- const list: any[] = (li as any).data || (li as any).indexes || [];
529
- const cur = list.find((i: any) => i.key === idx.key);
530
- if (cur) {
531
- if (cur.status === 'available') break;
532
- if (cur.status === 'failed' || cur.status === 'stuck') { throw new Error(cur.error || `Index ${idx.key} failed`); }
533
- lastStatus = cur.status;
534
- }
535
- await delay(2000);
536
- } catch { await delay(2000); }
537
- }
538
- await delay(150);
539
- }
540
- MessageFormatter.info(`Summary → ➕ ${created.length} | 🔧 ${updated.length} | ⏭️ ${skipped.length}` , { prefix: 'Indexes' });
541
- } catch (e) {
542
- MessageFormatter.error(`Failed to list/create indexes`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Indexes' });
543
- }
544
524
 
545
- // Deletions for indexes: remove remote indexes not declared in YAML/config
546
- try {
547
- const desiredIndexKeys = new Set((indexes || []).map((i: any) => i.key));
548
- const idxRes = await adapter.listIndexes({ databaseId, tableId });
549
- const existingIdx: any[] = (idxRes as any).data || (idxRes as any).indexes || [];
550
- const extraIdx = existingIdx
551
- .filter((i: any) => i?.key && !desiredIndexKeys.has(i.key))
552
- .map((i: any) => i.key as string);
553
- if (extraIdx.length > 0) {
554
- MessageFormatter.info(`Plan → 🗑️ ${extraIdx.length} indexes (${extraIdx.join(', ')})`, { prefix: 'Indexes' });
555
- const deleted: string[] = [];
556
- const errors: Array<{ key: string; error: string }> = [];
557
- for (const key of extraIdx) {
558
- try {
559
- await adapter.deleteIndex({ databaseId, tableId, key });
560
- // Optionally wait for index to disappear
561
- const start = Date.now();
562
- const maxWait = 30000;
563
- while (Date.now() - start < maxWait) {
564
- try {
565
- const li = await adapter.listIndexes({ databaseId, tableId });
566
- const list: any[] = (li as any).data || (li as any).indexes || [];
567
- if (!list.find((ix: any) => ix.key === key)) break;
568
- } catch {}
569
- await delay(1000);
570
- }
571
- deleted.push(key);
572
- } catch (e: any) {
573
- errors.push({ key, error: e?.message || String(e) });
574
- }
575
- }
576
- if (deleted.length) {
577
- MessageFormatter.success(`Deleted ${deleted.length} indexes: ${deleted.join(', ')}`, { prefix: 'Indexes' });
578
- }
579
- if (errors.length) {
580
- MessageFormatter.error(`${errors.length} index deletions failed`, undefined, { prefix: 'Indexes' });
581
- errors.forEach(er => MessageFormatter.error(` ${er.key}: ${er.error}`, undefined, { prefix: 'Indexes' }));
582
- }
583
- } else {
584
- MessageFormatter.info(`Plan → 🗑️ 0 indexes`, { prefix: 'Indexes' });
585
- }
586
- } catch (e) {
587
- MessageFormatter.warning(`Could not evaluate index deletions: ${(e as Error)?.message || e}`, { prefix: 'Indexes' });
588
- }
525
+ // Create/update indexes with proper planning and execution
526
+ await createOrUpdateIndexesViaAdapter(adapter, databaseId, tableId, idxs, indexes);
527
+
528
+ // Handle obsolete index deletions
529
+ const desiredIndexKeys: Set<string> = new Set((indexes || []).map((i: any) => i.key as string));
530
+ await deleteObsoleteIndexesViaAdapter(adapter, databaseId, tableId, desiredIndexKeys);
589
531
 
590
532
  // Deletions: remove columns/attributes that are present remotely but not in desired config
591
533
  try {
592
534
  const desiredKeys = new Set((attributes || []).map((a: any) => a.key));
535
+ // Also track case-insensitive keys to avoid double-deletion of renames (handled as recreates)
536
+ const desiredKeysLower = new Set((attributes || []).map((a: any) => a.key?.toLowerCase()));
593
537
  const tableInfo3 = await adapter.getTable({ databaseId, tableId });
594
538
  const existingCols3: any[] = (tableInfo3 as any).data?.columns || (tableInfo3 as any).data?.attributes || [];
595
539
  const toDelete = existingCols3
596
- .filter((col: any) => col?.key && !desiredKeys.has(col.key))
540
+ .filter((col: any) => {
541
+ if (!col?.key) return false;
542
+ // Exact match - keep it
543
+ if (desiredKeys.has(col.key)) return false;
544
+ // Case-insensitive match (rename scenario) - already handled as recreate, don't delete again
545
+ if (desiredKeysLower.has(col.key?.toLowerCase())) return false;
546
+ // Don't delete child-side relationship attributes - they're auto-managed by Appwrite
547
+ // for two-way relationships and deleting them would break the parent relationship
548
+ if (col.type === 'relationship' && col.side === 'child') return false;
549
+ return true;
550
+ })
597
551
  .map((col: any) => col.key as string);
598
552
 
599
553
  if (toDelete.length > 0) {
@@ -607,7 +561,9 @@ export const createOrUpdateCollectionsViaAdapter = async (
607
561
  const idxRes = await adapter.listIndexes({ databaseId, tableId });
608
562
  const ilist: any[] = (idxRes as any).data || (idxRes as any).indexes || [];
609
563
  for (const idx of ilist) {
610
- const attrs: string[] = Array.isArray(idx.attributes) ? idx.attributes : [];
564
+ const attrs: string[] = Array.isArray(idx.attributes)
565
+ ? idx.attributes
566
+ : (Array.isArray((idx as any).columns) ? (idx as any).columns : []);
611
567
  if (attrs.includes(key)) {
612
568
  MessageFormatter.info(`🗑️ Deleting index '${idx.key}' referencing '${key}'`, { prefix: 'Indexes' });
613
569
  await adapter.deleteIndex({ databaseId, tableId, key: idx.key });
@@ -692,6 +648,15 @@ export const createOrUpdateCollectionsViaAdapter = async (
692
648
  }
693
649
  }
694
650
  }
651
+
652
+ // Process any remaining queued operations to complete relationship sync
653
+ try {
654
+ MessageFormatter.info(`🔄 Processing final operation queue for database ${databaseId}`, { prefix: "Tables" });
655
+ await processQueue(adapter, databaseId);
656
+ MessageFormatter.info(`✅ Operation queue processing completed`, { prefix: "Tables" });
657
+ } catch (error) {
658
+ MessageFormatter.error(`Failed to process operation queue`, error instanceof Error ? error : new Error(String(error)), { prefix: 'Tables' });
659
+ }
695
660
  };
696
661
 
697
662
  export const generateMockData = async (
@@ -1,5 +1,5 @@
1
1
  import type { Attribute } from "@njdamstra/appwrite-utils";
2
- import { mapToCreateAttributeParams, mapToUpdateAttributeParams } from "../shared/attributeMapper.js";
2
+ import { mapToCreateAttributeParams, mapToUpdateAttributeParams } from "@njdamstra/appwrite-utils-helpers";
3
3
  import { Decimal } from "decimal.js";
4
4
  const EXTREME_BOUND = new Decimal('1e12');
5
5
 
@@ -32,6 +32,10 @@ interface ColumnOperationPlan {
32
32
  // Property configuration for different column types
33
33
  const MUTABLE_PROPERTIES = {
34
34
  string: ["required", "default", "size", "array"],
35
+ varchar: ["required", "default", "size", "array"],
36
+ text: ["required", "default", "array"],
37
+ mediumtext: ["required", "default", "array"],
38
+ longtext: ["required", "default", "array"],
35
39
  integer: ["required", "default", "min", "max", "array"],
36
40
  float: ["required", "default", "min", "max", "array"],
37
41
  double: ["required", "default", "min", "max", "array"],
@@ -41,11 +45,18 @@ const MUTABLE_PROPERTIES = {
41
45
  ip: ["required", "default", "array"],
42
46
  url: ["required", "default", "array"],
43
47
  enum: ["required", "default", "elements", "array"],
48
+ point: ["required", "default"],
49
+ line: ["required", "default"],
50
+ polygon: ["required", "default"],
44
51
  relationship: ["required", "default"],
45
52
  } as const;
46
53
 
47
54
  const IMMUTABLE_PROPERTIES = {
48
55
  string: ["encrypt", "key"],
56
+ varchar: ["encrypt", "key"],
57
+ text: ["encrypt", "key"],
58
+ mediumtext: ["encrypt", "key"],
59
+ longtext: ["encrypt", "key"],
49
60
  integer: ["encrypt", "key"],
50
61
  float: ["encrypt", "key"],
51
62
  double: ["encrypt", "key"],
@@ -55,11 +66,18 @@ const IMMUTABLE_PROPERTIES = {
55
66
  ip: ["key"],
56
67
  url: ["key"],
57
68
  enum: ["key"],
69
+ point: ["key"],
70
+ line: ["key"],
71
+ polygon: ["key"],
58
72
  relationship: ["key", "relatedCollection", "relationType", "twoWay", "twoWayKey", "onDelete"],
59
73
  } as const;
60
74
 
61
75
  const TYPE_CHANGE_REQUIRES_RECREATE = [
62
76
  "string",
77
+ "varchar",
78
+ "text",
79
+ "mediumtext",
80
+ "longtext",
63
81
  "integer",
64
82
  "float",
65
83
  "double",
@@ -69,6 +87,9 @@ const TYPE_CHANGE_REQUIRES_RECREATE = [
69
87
  "ip",
70
88
  "url",
71
89
  "enum",
90
+ "point",
91
+ "line",
92
+ "polygon",
72
93
  "relationship",
73
94
  ];
74
95
 
@@ -117,6 +138,13 @@ export function normalizeAttributeToComparable(attr: Attribute): ComparableColum
117
138
  base.size = (attr as any).size ?? 255;
118
139
  base.encrypt = !!((attr as any).encrypt);
119
140
  }
141
+ if (t === 'varchar') {
142
+ base.size = (attr as any).size ?? 255;
143
+ base.encrypt = !!((attr as any).encrypt);
144
+ }
145
+ if (t === 'text' || t === 'mediumtext' || t === 'longtext') {
146
+ base.encrypt = !!((attr as any).encrypt);
147
+ }
120
148
  if (t === 'integer' || t === 'float' || t === 'double') {
121
149
  const min = toNumber((attr as any).min);
122
150
  const max = toNumber((attr as any).max);
@@ -143,10 +171,13 @@ export function normalizeAttributeToComparable(attr: Attribute): ComparableColum
143
171
  }
144
172
 
145
173
  export function normalizeColumnToComparable(col: any): ComparableColumn {
146
- // Detect enum surfaced as string+elements from server and normalize to enum for comparison
174
+ // Detect enum surfaced as string+elements or string+format:enum from server and normalize to enum for comparison
147
175
  let t = String((col?.type ?? col?.columnType ?? '')).toLowerCase();
148
176
  const hasElements = Array.isArray(col?.elements) && (col.elements as any[]).length > 0;
149
- if (t === 'string' && hasElements) t = 'enum';
177
+ const hasEnumFormat = (col?.format === 'enum');
178
+ if (t === 'string' && (hasElements || hasEnumFormat)) {
179
+ t = 'enum';
180
+ }
150
181
  const base: ComparableColumn = {
151
182
  key: col?.key,
152
183
  type: t,
@@ -159,6 +190,13 @@ export function normalizeColumnToComparable(col: any): ComparableColumn {
159
190
  base.size = typeof col?.size === 'number' ? col.size : undefined;
160
191
  base.encrypt = !!col?.encrypt;
161
192
  }
193
+ if (t === 'varchar') {
194
+ base.size = typeof col?.size === 'number' ? col.size : undefined;
195
+ base.encrypt = !!col?.encrypt;
196
+ }
197
+ if (t === 'text' || t === 'mediumtext' || t === 'longtext') {
198
+ base.encrypt = !!col?.encrypt;
199
+ }
162
200
  if (t === 'integer' || t === 'float' || t === 'double') {
163
201
  // Preserve raw min/max without forcing extremes; compare with Decimal in shallowEqual
164
202
  const rawMin = (col as any)?.min;
@@ -227,21 +265,29 @@ export function isIndexEqualToIndex(a: any, b: any): boolean {
227
265
  if (String(a.type).toLowerCase() !== String(b.type).toLowerCase()) return false;
228
266
 
229
267
  // Compare attributes as sets (order-insensitive)
230
- const attrsA = Array.isArray(a.attributes) ? [...a.attributes].sort() : [];
231
- const attrsB = Array.isArray(b.attributes) ? [...b.attributes].sort() : [];
268
+ // Support TablesDB which returns 'columns' instead of 'attributes'
269
+ const attrsAraw = Array.isArray(a.attributes)
270
+ ? a.attributes
271
+ : (Array.isArray((a as any).columns) ? (a as any).columns : []);
272
+ const attrsA = [...attrsAraw].sort();
273
+ const attrsB = Array.isArray(b.attributes)
274
+ ? [...b.attributes].sort()
275
+ : (Array.isArray((b as any).columns) ? [...(b as any).columns].sort() : []);
232
276
  if (attrsA.length !== attrsB.length) return false;
233
277
  for (let i = 0; i < attrsA.length; i++) if (attrsA[i] !== attrsB[i]) return false;
234
278
 
235
- // Orders are only considered if BOTH have orders defined
236
- const hasOrdersA = Array.isArray(a.orders) && a.orders.length > 0;
237
- const hasOrdersB = Array.isArray(b.orders) && b.orders.length > 0;
238
- if (hasOrdersA && hasOrdersB) {
239
- const ordersA = [...a.orders].sort();
279
+ // Orders are only considered if CONFIG (b) has orders defined
280
+ // This prevents false positives when Appwrite returns orders but user didn't specify them
281
+ const hasConfigOrders = Array.isArray(b.orders) && b.orders.length > 0;
282
+ if (hasConfigOrders) {
283
+ // Some APIs may expose 'directions' instead of 'orders'
284
+ const ordersA = Array.isArray(a.orders)
285
+ ? [...a.orders].sort()
286
+ : (Array.isArray((a as any).directions) ? [...(a as any).directions].sort() : []);
240
287
  const ordersB = [...b.orders].sort();
241
288
  if (ordersA.length !== ordersB.length) return false;
242
289
  for (let i = 0; i < ordersA.length; i++) if (ordersA[i] !== ordersB[i]) return false;
243
290
  }
244
- // If only one side has orders, treat as equal (orders unspecified by user)
245
291
  return true;
246
292
  }
247
293
 
@@ -255,6 +301,8 @@ function compareColumnProperties(
255
301
  ): ColumnPropertyChange[] {
256
302
  const changes: ColumnPropertyChange[] = [];
257
303
  const t = String(columnType || (newAttribute as any).type || '').toLowerCase();
304
+ const key = newAttribute?.key || 'unknown';
305
+
258
306
  const mutableProps = (MUTABLE_PROPERTIES as any)[t] || [];
259
307
  const immutableProps = (IMMUTABLE_PROPERTIES as any)[t] || [];
260
308
 
@@ -274,7 +322,9 @@ function compareColumnProperties(
274
322
  let newValue = getNewVal(prop);
275
323
  // Special-case: enum elements empty/missing should not trigger updates
276
324
  if (t === 'enum' && prop === 'elements') {
277
- if (!Array.isArray(newValue) || newValue.length === 0) newValue = oldValue;
325
+ if (!Array.isArray(newValue) || newValue.length === 0) {
326
+ newValue = oldValue;
327
+ }
278
328
  }
279
329
  if (Array.isArray(oldValue) && Array.isArray(newValue)) {
280
330
  if (oldValue.length !== newValue.length || oldValue.some((v: any, i: number) => v !== newValue[i])) {
@@ -300,11 +350,11 @@ function compareColumnProperties(
300
350
  // Type change requires recreate (normalize string+elements to enum on old side)
301
351
  const oldTypeRaw = String(oldColumn?.type || oldColumn?.columnType || '').toLowerCase();
302
352
  const oldHasElements = Array.isArray(oldColumn?.elements) && (oldColumn.elements as any[]).length > 0;
303
- const oldType = oldTypeRaw === 'string' && oldHasElements ? 'enum' : oldTypeRaw;
353
+ const oldHasEnumFormat = (oldColumn?.format === 'enum');
354
+ const oldType = oldTypeRaw === 'string' && (oldHasElements || oldHasEnumFormat) ? 'enum' : oldTypeRaw;
304
355
  if (oldType && t && oldType !== t && TYPE_CHANGE_REQUIRES_RECREATE.includes(oldType)) {
305
356
  changes.push({ property: 'type', oldValue: oldType, newValue: t, requiresRecreate: true });
306
357
  }
307
-
308
358
  return changes;
309
359
  }
310
360
 
@@ -348,32 +398,53 @@ function analyzeColumnChanges(
348
398
  /**
349
399
  * Enhanced version of columns diff with detailed change analysis
350
400
  * Order: desired first, then existing (matches internal usage here)
401
+ * Handles case-insensitive key matches as renames (recreates)
351
402
  */
352
403
  export function diffColumnsDetailed(
353
404
  desiredAttributes: Attribute[],
354
405
  existingColumns: any[]
355
406
  ): ColumnOperationPlan {
407
+ // Exact key lookup (case-sensitive)
356
408
  const byKey = new Map((existingColumns || []).map((col: any) => [col?.key, col] as const));
409
+ // Case-insensitive key lookup for detecting renames
410
+ const byKeyLower = new Map((existingColumns || []).map((col: any) => [col?.key?.toLowerCase(), col] as const));
357
411
 
358
412
  const toCreate: Attribute[] = [];
359
413
  const toUpdate: Array<{ attribute: Attribute; changes: ColumnPropertyChange[] }> = [];
360
414
  const toRecreate: Array<{ oldAttribute: any; newAttribute: Attribute }> = [];
361
415
  const unchanged: string[] = [];
416
+ const handledExistingKeys = new Set<string>(); // Track which existing columns we've handled
362
417
 
363
418
  for (const attr of desiredAttributes || []) {
364
419
  const key = (attr as any)?.key;
365
- const existing = key ? byKey.get(key) : undefined;
366
- if (!existing) {
367
- toCreate.push(attr);
420
+ if (!key) continue;
421
+
422
+ // First try exact match
423
+ const exactMatch = byKey.get(key);
424
+ if (exactMatch) {
425
+ handledExistingKeys.add(key);
426
+ const analysis = analyzeColumnChanges(exactMatch, attr);
427
+ if (!analysis.hasChanges) unchanged.push(analysis.columnKey);
428
+ else if (analysis.requiresRecreate) toRecreate.push({ oldAttribute: exactMatch, newAttribute: attr });
429
+ else toUpdate.push({ attribute: attr, changes: analysis.changes });
368
430
  continue;
369
431
  }
370
- const analysis = analyzeColumnChanges(existing, attr);
371
- if (!analysis.hasChanges) unchanged.push(analysis.columnKey);
372
- else if (analysis.requiresRecreate) toRecreate.push({ oldAttribute: existing, newAttribute: attr });
373
- else toUpdate.push({ attribute: attr, changes: analysis.changes });
432
+
433
+ // Check for case-insensitive match (rename scenario like oAuthAccounts -> oauthAccounts)
434
+ const caseInsensitiveMatch = byKeyLower.get(key.toLowerCase());
435
+ if (caseInsensitiveMatch && caseInsensitiveMatch.key !== key) {
436
+ // This is a rename - treat as recreate (delete old, create new)
437
+ handledExistingKeys.add(caseInsensitiveMatch.key);
438
+ toRecreate.push({ oldAttribute: caseInsensitiveMatch, newAttribute: attr });
439
+ continue;
440
+ }
441
+
442
+ // No match - it's a new attribute
443
+ toCreate.push(attr);
374
444
  }
375
445
 
376
446
  // Note: we keep toDelete empty for now (conservative behavior)
447
+ // Deletions are handled separately in methods.ts
377
448
  return { toCreate, toUpdate, toRecreate, toDelete: [], unchanged };
378
449
  }
379
450
 
@@ -4,12 +4,11 @@ import {
4
4
  ID,
5
5
  Query,
6
6
  } from "node-appwrite";
7
- import { tryAwaitWithRetry, delay, calculateExponentialBackoff } from "../utils/helperFunctions.js";
8
- import { MessageFormatter } from "../shared/messageFormatter.js";
7
+ import { tryAwaitWithRetry, delay, calculateExponentialBackoff, MessageFormatter } from "@njdamstra/appwrite-utils-helpers";
9
8
  import { chunk } from "es-toolkit";
10
- import type { DatabaseAdapter } from "../adapters/DatabaseAdapter.js";
11
- import { isLegacyDatabases } from "../utils/typeGuards.js";
12
- import { getAdapter } from "../utils/getClientFromConfig.js";
9
+ import type { DatabaseAdapter } from "@njdamstra/appwrite-utils-helpers";
10
+ import { isLegacyDatabases } from "@njdamstra/appwrite-utils-helpers";
11
+ import { getAdapter } from "@njdamstra/appwrite-utils-helpers";
13
12
 
14
13
  /**
15
14
  * Transfers all documents from one collection to another in a different database