@njdamstra/appwrite-utils-cli 1.8.9 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (284) 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 +2 -35
  25. package/dist/collections/indexes.js +1 -3
  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 +55 -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 +63 -9
  44. package/dist/main.js +130 -177
  45. package/dist/migrations/afterImportActions.js +2 -3
  46. package/dist/migrations/appwriteToX.d.ts +1 -1
  47. package/dist/migrations/appwriteToX.js +9 -7
  48. package/dist/migrations/comprehensiveTransfer.js +3 -5
  49. package/dist/migrations/dataLoader.js +2 -5
  50. package/dist/migrations/importController.js +3 -4
  51. package/dist/migrations/importDataActions.js +3 -3
  52. package/dist/migrations/relationships.js +1 -2
  53. package/dist/migrations/services/DataTransformationService.js +2 -2
  54. package/dist/migrations/services/FileHandlerService.js +1 -1
  55. package/dist/migrations/services/ImportOrchestrator.js +4 -4
  56. package/dist/migrations/services/RateLimitManager.js +1 -1
  57. package/dist/migrations/services/RelationshipResolver.js +1 -1
  58. package/dist/migrations/services/UserMappingService.js +1 -1
  59. package/dist/migrations/services/ValidationService.js +1 -1
  60. package/dist/migrations/transfer.d.ts +8 -4
  61. package/dist/migrations/transfer.js +106 -55
  62. package/dist/migrations/yaml/YamlImportConfigLoader.js +1 -1
  63. package/dist/migrations/yaml/YamlImportIntegration.js +2 -2
  64. package/dist/migrations/yaml/generateImportSchemas.js +1 -1
  65. package/dist/setupCommands.d.ts +1 -1
  66. package/dist/setupCommands.js +5 -6
  67. package/dist/setupController.js +1 -1
  68. package/dist/shared/backupTracking.d.ts +1 -1
  69. package/dist/shared/backupTracking.js +2 -2
  70. package/dist/shared/confirmationDialogs.js +1 -1
  71. package/dist/shared/migrationHelpers.d.ts +1 -1
  72. package/dist/shared/migrationHelpers.js +3 -3
  73. package/dist/shared/operationQueue.d.ts +1 -1
  74. package/dist/shared/operationQueue.js +2 -3
  75. package/dist/shared/operationsTable.d.ts +1 -1
  76. package/dist/shared/operationsTable.js +2 -2
  77. package/dist/shared/progressManager.js +1 -1
  78. package/dist/shared/selectionDialogs.js +9 -8
  79. package/dist/storage/methods.js +4 -4
  80. package/dist/storage/schemas.d.ts +2 -2
  81. package/dist/tables/indexManager.d.ts +65 -0
  82. package/dist/tables/indexManager.js +294 -0
  83. package/dist/types.d.ts +2 -2
  84. package/dist/types.js +1 -1
  85. package/dist/users/methods.js +2 -3
  86. package/dist/utils/configMigration.js +1 -1
  87. package/dist/utils/index.d.ts +1 -1
  88. package/dist/utils/index.js +1 -1
  89. package/dist/utils/loadConfigs.d.ts +2 -2
  90. package/dist/utils/loadConfigs.js +6 -7
  91. package/dist/utils/setupFiles.js +5 -7
  92. package/dist/utilsController.d.ts +15 -8
  93. package/dist/utilsController.js +57 -28
  94. package/package.json +7 -3
  95. package/src/adapters/index.ts +8 -34
  96. package/src/backups/operations/bucketBackup.ts +2 -2
  97. package/src/backups/operations/collectionBackup.ts +4 -4
  98. package/src/backups/operations/comprehensiveBackup.ts +3 -3
  99. package/src/backups/tracking/centralizedTracking.ts +3 -3
  100. package/src/cli/commands/configCommands.ts +72 -8
  101. package/src/cli/commands/databaseCommands.ts +161 -9
  102. package/src/cli/commands/functionCommands.ts +4 -3
  103. package/src/cli/commands/importFileCommands.ts +815 -0
  104. package/src/cli/commands/schemaCommands.ts +3 -3
  105. package/src/cli/commands/storageCommands.ts +2 -3
  106. package/src/cli/commands/transferCommands.ts +3 -6
  107. package/src/collections/attributes.ts +3 -39
  108. package/src/collections/indexes.ts +2 -4
  109. package/src/collections/methods.ts +115 -150
  110. package/src/collections/tableOperations.ts +57 -21
  111. package/src/collections/transferOperations.ts +4 -5
  112. package/src/collections/wipeOperations.ts +154 -51
  113. package/src/databases/methods.ts +2 -2
  114. package/src/databases/setup.ts +2 -2
  115. package/src/examples/yamlTerminologyExample.ts +2 -2
  116. package/src/functions/deployments.ts +6 -5
  117. package/src/functions/fnConfigDiscovery.ts +2 -2
  118. package/src/functions/methods.ts +17 -4
  119. package/src/init.ts +1 -1
  120. package/src/interactiveCLI.ts +75 -10
  121. package/src/main.ts +143 -287
  122. package/src/migrations/afterImportActions.ts +2 -3
  123. package/src/migrations/appwriteToX.ts +12 -8
  124. package/src/migrations/comprehensiveTransfer.ts +6 -6
  125. package/src/migrations/dataLoader.ts +2 -5
  126. package/src/migrations/importController.ts +3 -4
  127. package/src/migrations/importDataActions.ts +3 -3
  128. package/src/migrations/relationships.ts +1 -2
  129. package/src/migrations/services/DataTransformationService.ts +2 -2
  130. package/src/migrations/services/FileHandlerService.ts +1 -1
  131. package/src/migrations/services/ImportOrchestrator.ts +4 -4
  132. package/src/migrations/services/RateLimitManager.ts +1 -1
  133. package/src/migrations/services/RelationshipResolver.ts +1 -1
  134. package/src/migrations/services/UserMappingService.ts +1 -1
  135. package/src/migrations/services/ValidationService.ts +1 -1
  136. package/src/migrations/transfer.ts +126 -83
  137. package/src/migrations/yaml/YamlImportConfigLoader.ts +1 -1
  138. package/src/migrations/yaml/YamlImportIntegration.ts +2 -2
  139. package/src/migrations/yaml/generateImportSchemas.ts +1 -1
  140. package/src/setupCommands.ts +5 -6
  141. package/src/setupController.ts +1 -1
  142. package/src/shared/backupTracking.ts +3 -3
  143. package/src/shared/confirmationDialogs.ts +1 -1
  144. package/src/shared/migrationHelpers.ts +4 -4
  145. package/src/shared/operationQueue.ts +3 -4
  146. package/src/shared/operationsTable.ts +3 -3
  147. package/src/shared/progressManager.ts +1 -1
  148. package/src/shared/selectionDialogs.ts +9 -8
  149. package/src/storage/methods.ts +4 -4
  150. package/src/tables/indexManager.ts +409 -0
  151. package/src/types.ts +2 -2
  152. package/src/users/methods.ts +2 -3
  153. package/src/utils/configMigration.ts +1 -1
  154. package/src/utils/index.ts +1 -1
  155. package/src/utils/loadConfigs.ts +15 -7
  156. package/src/utils/setupFiles.ts +5 -7
  157. package/src/utilsController.ts +86 -32
  158. package/dist/adapters/AdapterFactory.d.ts +0 -94
  159. package/dist/adapters/AdapterFactory.js +0 -405
  160. package/dist/adapters/DatabaseAdapter.d.ts +0 -233
  161. package/dist/adapters/DatabaseAdapter.js +0 -50
  162. package/dist/adapters/LegacyAdapter.d.ts +0 -50
  163. package/dist/adapters/LegacyAdapter.js +0 -612
  164. package/dist/adapters/TablesDBAdapter.d.ts +0 -45
  165. package/dist/adapters/TablesDBAdapter.js +0 -571
  166. package/dist/config/ConfigManager.d.ts +0 -445
  167. package/dist/config/ConfigManager.js +0 -625
  168. package/dist/config/configMigration.d.ts +0 -87
  169. package/dist/config/configMigration.js +0 -390
  170. package/dist/config/configValidation.d.ts +0 -66
  171. package/dist/config/configValidation.js +0 -358
  172. package/dist/config/index.d.ts +0 -8
  173. package/dist/config/index.js +0 -7
  174. package/dist/config/services/ConfigDiscoveryService.d.ts +0 -126
  175. package/dist/config/services/ConfigDiscoveryService.js +0 -374
  176. package/dist/config/services/ConfigLoaderService.d.ts +0 -129
  177. package/dist/config/services/ConfigLoaderService.js +0 -540
  178. package/dist/config/services/ConfigMergeService.d.ts +0 -208
  179. package/dist/config/services/ConfigMergeService.js +0 -308
  180. package/dist/config/services/ConfigValidationService.d.ts +0 -214
  181. package/dist/config/services/ConfigValidationService.js +0 -310
  182. package/dist/config/services/SessionAuthService.d.ts +0 -225
  183. package/dist/config/services/SessionAuthService.js +0 -456
  184. package/dist/config/services/__tests__/ConfigMergeService.test.d.ts +0 -1
  185. package/dist/config/services/__tests__/ConfigMergeService.test.js +0 -271
  186. package/dist/config/services/index.d.ts +0 -13
  187. package/dist/config/services/index.js +0 -10
  188. package/dist/config/yamlConfig.d.ts +0 -722
  189. package/dist/config/yamlConfig.js +0 -702
  190. package/dist/functions/pathResolution.d.ts +0 -37
  191. package/dist/functions/pathResolution.js +0 -185
  192. package/dist/shared/attributeMapper.d.ts +0 -20
  193. package/dist/shared/attributeMapper.js +0 -203
  194. package/dist/shared/errorUtils.d.ts +0 -54
  195. package/dist/shared/errorUtils.js +0 -95
  196. package/dist/shared/functionManager.d.ts +0 -48
  197. package/dist/shared/functionManager.js +0 -336
  198. package/dist/shared/indexManager.d.ts +0 -24
  199. package/dist/shared/indexManager.js +0 -151
  200. package/dist/shared/jsonSchemaGenerator.d.ts +0 -50
  201. package/dist/shared/jsonSchemaGenerator.js +0 -290
  202. package/dist/shared/logging.d.ts +0 -61
  203. package/dist/shared/logging.js +0 -116
  204. package/dist/shared/messageFormatter.d.ts +0 -39
  205. package/dist/shared/messageFormatter.js +0 -162
  206. package/dist/shared/pydanticModelGenerator.d.ts +0 -17
  207. package/dist/shared/pydanticModelGenerator.js +0 -615
  208. package/dist/shared/schemaGenerator.d.ts +0 -40
  209. package/dist/shared/schemaGenerator.js +0 -556
  210. package/dist/utils/ClientFactory.d.ts +0 -87
  211. package/dist/utils/ClientFactory.js +0 -212
  212. package/dist/utils/configDiscovery.d.ts +0 -78
  213. package/dist/utils/configDiscovery.js +0 -472
  214. package/dist/utils/constantsGenerator.d.ts +0 -31
  215. package/dist/utils/constantsGenerator.js +0 -321
  216. package/dist/utils/dataConverters.d.ts +0 -46
  217. package/dist/utils/dataConverters.js +0 -139
  218. package/dist/utils/directoryUtils.d.ts +0 -22
  219. package/dist/utils/directoryUtils.js +0 -59
  220. package/dist/utils/getClientFromConfig.d.ts +0 -39
  221. package/dist/utils/getClientFromConfig.js +0 -199
  222. package/dist/utils/helperFunctions.d.ts +0 -63
  223. package/dist/utils/helperFunctions.js +0 -156
  224. package/dist/utils/pathResolvers.d.ts +0 -53
  225. package/dist/utils/pathResolvers.js +0 -72
  226. package/dist/utils/projectConfig.d.ts +0 -119
  227. package/dist/utils/projectConfig.js +0 -171
  228. package/dist/utils/retryFailedPromises.d.ts +0 -2
  229. package/dist/utils/retryFailedPromises.js +0 -23
  230. package/dist/utils/sessionAuth.d.ts +0 -48
  231. package/dist/utils/sessionAuth.js +0 -164
  232. package/dist/utils/typeGuards.d.ts +0 -35
  233. package/dist/utils/typeGuards.js +0 -57
  234. package/dist/utils/validationRules.d.ts +0 -43
  235. package/dist/utils/validationRules.js +0 -42
  236. package/dist/utils/versionDetection.d.ts +0 -58
  237. package/dist/utils/versionDetection.js +0 -251
  238. package/dist/utils/yamlConverter.d.ts +0 -100
  239. package/dist/utils/yamlConverter.js +0 -428
  240. package/dist/utils/yamlLoader.d.ts +0 -70
  241. package/dist/utils/yamlLoader.js +0 -267
  242. package/src/adapters/AdapterFactory.ts +0 -510
  243. package/src/adapters/DatabaseAdapter.ts +0 -306
  244. package/src/adapters/LegacyAdapter.ts +0 -841
  245. package/src/adapters/TablesDBAdapter.ts +0 -773
  246. package/src/config/ConfigManager.ts +0 -808
  247. package/src/config/README.md +0 -274
  248. package/src/config/configMigration.ts +0 -575
  249. package/src/config/configValidation.ts +0 -445
  250. package/src/config/index.ts +0 -10
  251. package/src/config/services/ConfigDiscoveryService.ts +0 -463
  252. package/src/config/services/ConfigLoaderService.ts +0 -740
  253. package/src/config/services/ConfigMergeService.ts +0 -388
  254. package/src/config/services/ConfigValidationService.ts +0 -394
  255. package/src/config/services/SessionAuthService.ts +0 -565
  256. package/src/config/services/__tests__/ConfigMergeService.test.ts +0 -351
  257. package/src/config/services/index.ts +0 -29
  258. package/src/config/yamlConfig.ts +0 -761
  259. package/src/functions/pathResolution.ts +0 -227
  260. package/src/shared/attributeMapper.ts +0 -229
  261. package/src/shared/errorUtils.ts +0 -110
  262. package/src/shared/functionManager.ts +0 -525
  263. package/src/shared/indexManager.ts +0 -254
  264. package/src/shared/jsonSchemaGenerator.ts +0 -383
  265. package/src/shared/logging.ts +0 -149
  266. package/src/shared/messageFormatter.ts +0 -208
  267. package/src/shared/pydanticModelGenerator.ts +0 -618
  268. package/src/shared/schemaGenerator.ts +0 -644
  269. package/src/utils/ClientFactory.ts +0 -240
  270. package/src/utils/configDiscovery.ts +0 -557
  271. package/src/utils/constantsGenerator.ts +0 -369
  272. package/src/utils/dataConverters.ts +0 -159
  273. package/src/utils/directoryUtils.ts +0 -61
  274. package/src/utils/getClientFromConfig.ts +0 -257
  275. package/src/utils/helperFunctions.ts +0 -228
  276. package/src/utils/pathResolvers.ts +0 -81
  277. package/src/utils/projectConfig.ts +0 -299
  278. package/src/utils/retryFailedPromises.ts +0 -29
  279. package/src/utils/sessionAuth.ts +0 -230
  280. package/src/utils/typeGuards.ts +0 -65
  281. package/src/utils/validationRules.ts +0 -88
  282. package/src/utils/versionDetection.ts +0 -292
  283. package/src/utils/yamlConverter.ts +0 -542
  284. package/src/utils/yamlLoader.ts +0 -371
@@ -1,8 +1,8 @@
1
1
  import inquirer from "inquirer";
2
2
  import path from "path";
3
3
  import chalk from "chalk";
4
- import { MessageFormatter } from "../../shared/messageFormatter.js";
5
- import { SchemaGenerator } from "../../shared/schemaGenerator.js";
4
+ import { MessageFormatter } from '@njdamstra/appwrite-utils-helpers';
5
+ import { SchemaGenerator } from '@njdamstra/appwrite-utils-helpers';
6
6
  import { setupDirsFiles } from "../../utils/setupFiles.js";
7
7
  import { fetchAllDatabases } from "../../databases/methods.js";
8
8
  import type { InteractiveCLI } from "../../interactiveCLI.js";
@@ -126,7 +126,7 @@ export const schemaCommands = {
126
126
  ]);
127
127
 
128
128
  try {
129
- const { ConstantsGenerator } = await import("../../utils/constantsGenerator.js");
129
+ const { ConstantsGenerator } = await import("@njdamstra/appwrite-utils-helpers");
130
130
  const generator = new ConstantsGenerator((cli as any).controller.config);
131
131
 
132
132
  const include = {
@@ -2,10 +2,9 @@ import inquirer from "inquirer";
2
2
  import chalk from "chalk";
3
3
  import { Storage, Permission, Role, Compression, type Models } from "node-appwrite";
4
4
  import type { InteractiveCLI } from "../../interactiveCLI.js";
5
- import { MessageFormatter } from "../../shared/messageFormatter.js";
5
+ import { MessageFormatter } from '@njdamstra/appwrite-utils-helpers';
6
6
  import { listBuckets, createBucket as createBucketApi, deleteBucket as deleteBucketApi } from "../../storage/methods.js";
7
- import { writeYamlConfig } from "../../config/yamlConfig.js";
8
- import { ConfigManager } from "../../config/ConfigManager.js";
7
+ import { writeYamlConfig, ConfigManager } from "@njdamstra/appwrite-utils-helpers";
9
8
 
10
9
  export const storageCommands = {
11
10
  async createBucket(cli: InteractiveCLI): Promise<void> {
@@ -1,9 +1,9 @@
1
1
  import inquirer from "inquirer";
2
2
  import { Databases, Storage } from "node-appwrite";
3
- import { MessageFormatter } from "../../shared/messageFormatter.js";
3
+ import { MessageFormatter } from '@njdamstra/appwrite-utils-helpers';
4
4
  import { fetchAllDatabases } from "../../databases/methods.js";
5
5
  import { listBuckets } from "../../storage/methods.js";
6
- import { getClient } from "../../utils/getClientFromConfig.js";
6
+ import { getClient } from "@njdamstra/appwrite-utils-helpers";
7
7
  import { ComprehensiveTransfer, type ComprehensiveTransferOptions } from "../../migrations/comprehensiveTransfer.js";
8
8
  import type { TransferOptions } from "../../migrations/transfer.js";
9
9
  import type { InteractiveCLI } from "../../interactiveCLI.js";
@@ -145,10 +145,7 @@ export const transferCommands = {
145
145
  fromDb,
146
146
  targetDb,
147
147
  isRemote,
148
- collections:
149
- selectedCollections.length > 0
150
- ? selectedCollections.map((c: any) => c.$id)
151
- : undefined,
148
+ collections: selectedCollections.map((c: any) => c.$id),
152
149
  sourceBucket,
153
150
  targetBucket,
154
151
  };
@@ -14,13 +14,11 @@ import {
14
14
  delay,
15
15
  tryAwaitWithRetry,
16
16
  calculateExponentialBackoff,
17
- } from "../utils/helperFunctions.js";
17
+ } from "@njdamstra/appwrite-utils-helpers";
18
18
  import chalk from "chalk";
19
19
  import { Decimal } from "decimal.js";
20
- import type { DatabaseAdapter, CreateAttributeParams, UpdateAttributeParams, DeleteAttributeParams } from "../adapters/DatabaseAdapter.js";
21
- import { logger } from "../shared/logging.js";
22
- import { MessageFormatter } from "../shared/messageFormatter.js";
23
- import { isDatabaseAdapter } from "../utils/typeGuards.js";
20
+ import type { DatabaseAdapter, CreateAttributeParams, UpdateAttributeParams, DeleteAttributeParams } from "@njdamstra/appwrite-utils-helpers";
21
+ import { logger, MessageFormatter, isDatabaseAdapter } from "@njdamstra/appwrite-utils-helpers";
24
22
 
25
23
  // Extreme values that Appwrite may return, which should be treated as undefined
26
24
  const EXTREME_MIN_INTEGER = -9223372036854776000;
@@ -651,13 +649,6 @@ const updateLegacyAttribute = async (
651
649
  collectionId: string,
652
650
  attribute: Attribute
653
651
  ): Promise<void> => {
654
- console.log(`DEBUG updateLegacyAttribute before normalizeMinMaxValues:`, {
655
- key: attribute.key,
656
- type: attribute.type,
657
- min: (attribute as any).min,
658
- max: (attribute as any).max
659
- });
660
-
661
652
  const { min: normalizedMin, max: normalizedMax } =
662
653
  normalizeMinMaxValues(attribute);
663
654
 
@@ -1515,37 +1506,10 @@ export const createOrUpdateAttribute = async (
1515
1506
  // `Updating attribute with same key ${attribute.key} but different values`
1516
1507
  // );
1517
1508
 
1518
- // DEBUG: Log before object merge to detect corruption
1519
- if ((attribute.key === 'conversationType' || attribute.key === 'messageStreakCount')) {
1520
- console.log(`[DEBUG] MERGE - key="${attribute.key}"`, {
1521
- found: {
1522
- elements: (foundAttribute as any)?.elements,
1523
- min: (foundAttribute as any)?.min,
1524
- max: (foundAttribute as any)?.max
1525
- },
1526
- desired: {
1527
- elements: (attribute as any)?.elements,
1528
- min: (attribute as any)?.min,
1529
- max: (attribute as any)?.max
1530
- }
1531
- });
1532
- }
1533
-
1534
1509
  finalAttribute = {
1535
1510
  ...foundAttribute,
1536
1511
  ...attribute,
1537
1512
  };
1538
-
1539
- // DEBUG: Log after object merge to detect corruption
1540
- if ((finalAttribute.key === 'conversationType' || finalAttribute.key === 'messageStreakCount')) {
1541
- console.log(`[DEBUG] AFTER_MERGE - key="${finalAttribute.key}"`, {
1542
- merged: {
1543
- elements: finalAttribute?.elements,
1544
- min: (finalAttribute as any)?.min,
1545
- max: (finalAttribute as any)?.max
1546
- }
1547
- });
1548
- }
1549
1513
  action = "update";
1550
1514
  } else if (
1551
1515
  !updateEnabled &&
@@ -1,9 +1,7 @@
1
1
  import { indexSchema, type Index } from "@njdamstra/appwrite-utils";
2
2
  import { Databases, IndexType, Query, type Models } from "node-appwrite";
3
- import type { DatabaseAdapter } from "../adapters/DatabaseAdapter.js";
4
- import { delay, tryAwaitWithRetry, calculateExponentialBackoff } from "../utils/helperFunctions.js";
5
- import { isLegacyDatabases } from "../utils/typeGuards.js";
6
- import { MessageFormatter } from "../shared/messageFormatter.js";
3
+ import type { DatabaseAdapter } from "@njdamstra/appwrite-utils-helpers";
4
+ import { delay, tryAwaitWithRetry, calculateExponentialBackoff, isLegacyDatabases, MessageFormatter } from "@njdamstra/appwrite-utils-helpers";
7
5
 
8
6
  // System attributes that are always available for indexing in Appwrite
9
7
  const SYSTEM_ATTRIBUTES = ['$id', '$createdAt', '$updatedAt', '$permissions'];
@@ -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
 
@@ -143,10 +143,13 @@ export function normalizeAttributeToComparable(attr: Attribute): ComparableColum
143
143
  }
144
144
 
145
145
  export function normalizeColumnToComparable(col: any): ComparableColumn {
146
- // Detect enum surfaced as string+elements from server and normalize to enum for comparison
146
+ // Detect enum surfaced as string+elements or string+format:enum from server and normalize to enum for comparison
147
147
  let t = String((col?.type ?? col?.columnType ?? '')).toLowerCase();
148
148
  const hasElements = Array.isArray(col?.elements) && (col.elements as any[]).length > 0;
149
- if (t === 'string' && hasElements) t = 'enum';
149
+ const hasEnumFormat = (col?.format === 'enum');
150
+ if (t === 'string' && (hasElements || hasEnumFormat)) {
151
+ t = 'enum';
152
+ }
150
153
  const base: ComparableColumn = {
151
154
  key: col?.key,
152
155
  type: t,
@@ -227,21 +230,29 @@ export function isIndexEqualToIndex(a: any, b: any): boolean {
227
230
  if (String(a.type).toLowerCase() !== String(b.type).toLowerCase()) return false;
228
231
 
229
232
  // 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() : [];
233
+ // Support TablesDB which returns 'columns' instead of 'attributes'
234
+ const attrsAraw = Array.isArray(a.attributes)
235
+ ? a.attributes
236
+ : (Array.isArray((a as any).columns) ? (a as any).columns : []);
237
+ const attrsA = [...attrsAraw].sort();
238
+ const attrsB = Array.isArray(b.attributes)
239
+ ? [...b.attributes].sort()
240
+ : (Array.isArray((b as any).columns) ? [...(b as any).columns].sort() : []);
232
241
  if (attrsA.length !== attrsB.length) return false;
233
242
  for (let i = 0; i < attrsA.length; i++) if (attrsA[i] !== attrsB[i]) return false;
234
243
 
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();
244
+ // Orders are only considered if CONFIG (b) has orders defined
245
+ // This prevents false positives when Appwrite returns orders but user didn't specify them
246
+ const hasConfigOrders = Array.isArray(b.orders) && b.orders.length > 0;
247
+ if (hasConfigOrders) {
248
+ // Some APIs may expose 'directions' instead of 'orders'
249
+ const ordersA = Array.isArray(a.orders)
250
+ ? [...a.orders].sort()
251
+ : (Array.isArray((a as any).directions) ? [...(a as any).directions].sort() : []);
240
252
  const ordersB = [...b.orders].sort();
241
253
  if (ordersA.length !== ordersB.length) return false;
242
254
  for (let i = 0; i < ordersA.length; i++) if (ordersA[i] !== ordersB[i]) return false;
243
255
  }
244
- // If only one side has orders, treat as equal (orders unspecified by user)
245
256
  return true;
246
257
  }
247
258
 
@@ -255,6 +266,8 @@ function compareColumnProperties(
255
266
  ): ColumnPropertyChange[] {
256
267
  const changes: ColumnPropertyChange[] = [];
257
268
  const t = String(columnType || (newAttribute as any).type || '').toLowerCase();
269
+ const key = newAttribute?.key || 'unknown';
270
+
258
271
  const mutableProps = (MUTABLE_PROPERTIES as any)[t] || [];
259
272
  const immutableProps = (IMMUTABLE_PROPERTIES as any)[t] || [];
260
273
 
@@ -274,7 +287,9 @@ function compareColumnProperties(
274
287
  let newValue = getNewVal(prop);
275
288
  // Special-case: enum elements empty/missing should not trigger updates
276
289
  if (t === 'enum' && prop === 'elements') {
277
- if (!Array.isArray(newValue) || newValue.length === 0) newValue = oldValue;
290
+ if (!Array.isArray(newValue) || newValue.length === 0) {
291
+ newValue = oldValue;
292
+ }
278
293
  }
279
294
  if (Array.isArray(oldValue) && Array.isArray(newValue)) {
280
295
  if (oldValue.length !== newValue.length || oldValue.some((v: any, i: number) => v !== newValue[i])) {
@@ -300,11 +315,11 @@ function compareColumnProperties(
300
315
  // Type change requires recreate (normalize string+elements to enum on old side)
301
316
  const oldTypeRaw = String(oldColumn?.type || oldColumn?.columnType || '').toLowerCase();
302
317
  const oldHasElements = Array.isArray(oldColumn?.elements) && (oldColumn.elements as any[]).length > 0;
303
- const oldType = oldTypeRaw === 'string' && oldHasElements ? 'enum' : oldTypeRaw;
318
+ const oldHasEnumFormat = (oldColumn?.format === 'enum');
319
+ const oldType = oldTypeRaw === 'string' && (oldHasElements || oldHasEnumFormat) ? 'enum' : oldTypeRaw;
304
320
  if (oldType && t && oldType !== t && TYPE_CHANGE_REQUIRES_RECREATE.includes(oldType)) {
305
321
  changes.push({ property: 'type', oldValue: oldType, newValue: t, requiresRecreate: true });
306
322
  }
307
-
308
323
  return changes;
309
324
  }
310
325
 
@@ -348,32 +363,53 @@ function analyzeColumnChanges(
348
363
  /**
349
364
  * Enhanced version of columns diff with detailed change analysis
350
365
  * Order: desired first, then existing (matches internal usage here)
366
+ * Handles case-insensitive key matches as renames (recreates)
351
367
  */
352
368
  export function diffColumnsDetailed(
353
369
  desiredAttributes: Attribute[],
354
370
  existingColumns: any[]
355
371
  ): ColumnOperationPlan {
372
+ // Exact key lookup (case-sensitive)
356
373
  const byKey = new Map((existingColumns || []).map((col: any) => [col?.key, col] as const));
374
+ // Case-insensitive key lookup for detecting renames
375
+ const byKeyLower = new Map((existingColumns || []).map((col: any) => [col?.key?.toLowerCase(), col] as const));
357
376
 
358
377
  const toCreate: Attribute[] = [];
359
378
  const toUpdate: Array<{ attribute: Attribute; changes: ColumnPropertyChange[] }> = [];
360
379
  const toRecreate: Array<{ oldAttribute: any; newAttribute: Attribute }> = [];
361
380
  const unchanged: string[] = [];
381
+ const handledExistingKeys = new Set<string>(); // Track which existing columns we've handled
362
382
 
363
383
  for (const attr of desiredAttributes || []) {
364
384
  const key = (attr as any)?.key;
365
- const existing = key ? byKey.get(key) : undefined;
366
- if (!existing) {
367
- toCreate.push(attr);
385
+ if (!key) continue;
386
+
387
+ // First try exact match
388
+ const exactMatch = byKey.get(key);
389
+ if (exactMatch) {
390
+ handledExistingKeys.add(key);
391
+ const analysis = analyzeColumnChanges(exactMatch, attr);
392
+ if (!analysis.hasChanges) unchanged.push(analysis.columnKey);
393
+ else if (analysis.requiresRecreate) toRecreate.push({ oldAttribute: exactMatch, newAttribute: attr });
394
+ else toUpdate.push({ attribute: attr, changes: analysis.changes });
368
395
  continue;
369
396
  }
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 });
397
+
398
+ // Check for case-insensitive match (rename scenario like oAuthAccounts -> oauthAccounts)
399
+ const caseInsensitiveMatch = byKeyLower.get(key.toLowerCase());
400
+ if (caseInsensitiveMatch && caseInsensitiveMatch.key !== key) {
401
+ // This is a rename - treat as recreate (delete old, create new)
402
+ handledExistingKeys.add(caseInsensitiveMatch.key);
403
+ toRecreate.push({ oldAttribute: caseInsensitiveMatch, newAttribute: attr });
404
+ continue;
405
+ }
406
+
407
+ // No match - it's a new attribute
408
+ toCreate.push(attr);
374
409
  }
375
410
 
376
411
  // Note: we keep toDelete empty for now (conservative behavior)
412
+ // Deletions are handled separately in methods.ts
377
413
  return { toCreate, toUpdate, toRecreate, toDelete: [], unchanged };
378
414
  }
379
415