appwrite-utils-cli 1.9.7 → 1.12.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 (376) hide show
  1. package/README.md +1004 -1004
  2. package/dist/adapters/index.d.ts +7 -8
  3. package/dist/adapters/index.js +7 -9
  4. package/dist/backups/operations/bucketBackup.js +2 -2
  5. package/dist/backups/operations/collectionBackup.d.ts +1 -1
  6. package/dist/backups/operations/collectionBackup.js +3 -3
  7. package/dist/backups/operations/comprehensiveBackup.d.ts +1 -1
  8. package/dist/backups/operations/comprehensiveBackup.js +2 -2
  9. package/dist/backups/tracking/centralizedTracking.d.ts +1 -1
  10. package/dist/backups/tracking/centralizedTracking.js +2 -2
  11. package/dist/cli/commands/configCommands.js +51 -7
  12. package/dist/cli/commands/databaseCommands.js +156 -104
  13. package/dist/cli/commands/functionCommands.js +3 -3
  14. package/dist/cli/commands/importFileCommands.d.ts +7 -0
  15. package/dist/cli/commands/importFileCommands.js +674 -0
  16. package/dist/cli/commands/schemaCommands.js +3 -3
  17. package/dist/cli/commands/storageCommands.js +2 -3
  18. package/dist/cli/commands/transferCommands.js +3 -5
  19. package/dist/collections/{attributes.d.ts → columns.d.ts} +1 -1
  20. package/dist/collections/{attributes.js → columns.js} +15 -9
  21. package/dist/collections/indexes.js +1 -3
  22. package/dist/collections/methods.d.ts +1 -1
  23. package/dist/collections/methods.js +38 -20
  24. package/dist/collections/tableOperations.d.ts +1 -0
  25. package/dist/collections/tableOperations.js +30 -11
  26. package/dist/collections/transferOperations.d.ts +1 -1
  27. package/dist/collections/transferOperations.js +3 -4
  28. package/dist/collections/wipeOperations.d.ts +4 -3
  29. package/dist/collections/wipeOperations.js +112 -39
  30. package/dist/databases/methods.js +2 -2
  31. package/dist/databases/setup.js +2 -2
  32. package/dist/examples/yamlTerminologyExample.js +2 -2
  33. package/dist/functions/deployments.d.ts +1 -1
  34. package/dist/functions/deployments.js +6 -6
  35. package/dist/functions/fnConfigDiscovery.js +2 -2
  36. package/dist/functions/methods.js +2 -2
  37. package/dist/functions/templates/count-docs-in-collection/README.md +53 -53
  38. package/dist/functions/templates/count-docs-in-collection/src/main.ts +159 -159
  39. package/dist/functions/templates/count-docs-in-collection/src/request.ts +8 -8
  40. package/dist/functions/templates/hono-typescript/README.md +285 -285
  41. package/dist/functions/templates/hono-typescript/src/adapters/request.ts +73 -73
  42. package/dist/functions/templates/hono-typescript/src/adapters/response.ts +105 -105
  43. package/dist/functions/templates/hono-typescript/src/app.ts +179 -179
  44. package/dist/functions/templates/hono-typescript/src/context.ts +102 -102
  45. package/{src/functions/templates/hono-typescript/src/index.ts → dist/functions/templates/hono-typescript/src/main.ts} +53 -53
  46. package/dist/functions/templates/hono-typescript/src/middleware/appwrite.ts +118 -118
  47. package/dist/functions/templates/typescript-node/README.md +31 -31
  48. package/dist/functions/templates/typescript-node/src/context.ts +102 -102
  49. package/dist/functions/templates/typescript-node/src/{index.ts → main.ts} +29 -29
  50. package/dist/functions/templates/uv/README.md +30 -30
  51. package/dist/functions/templates/uv/pyproject.toml +29 -29
  52. package/dist/functions/templates/uv/src/context.py +124 -124
  53. package/dist/functions/templates/uv/src/{index.py → main.py} +45 -45
  54. package/dist/init.js +1 -1
  55. package/dist/interactiveCLI.d.ts +6 -1
  56. package/dist/interactiveCLI.js +79 -25
  57. package/dist/main.js +125 -180
  58. package/dist/migrations/afterImportActions.js +2 -3
  59. package/dist/migrations/appwriteToX.d.ts +1 -1
  60. package/dist/migrations/appwriteToX.js +10 -8
  61. package/dist/migrations/comprehensiveTransfer.js +3 -5
  62. package/dist/migrations/dataLoader.d.ts +4 -2
  63. package/dist/migrations/dataLoader.js +42 -20
  64. package/dist/migrations/importController.d.ts +2 -0
  65. package/dist/migrations/importController.js +39 -24
  66. package/dist/migrations/importDataActions.js +3 -3
  67. package/dist/migrations/relationships.js +1 -2
  68. package/dist/migrations/services/DataTransformationService.js +2 -2
  69. package/dist/migrations/services/FileHandlerService.js +1 -1
  70. package/dist/migrations/services/ImportOrchestrator.d.ts +2 -0
  71. package/dist/migrations/services/ImportOrchestrator.js +15 -7
  72. package/dist/migrations/services/RateLimitManager.js +1 -1
  73. package/dist/migrations/services/RelationshipResolver.js +1 -1
  74. package/dist/migrations/services/UserMappingService.js +1 -1
  75. package/dist/migrations/services/ValidationService.js +1 -1
  76. package/dist/migrations/transfer.d.ts +8 -4
  77. package/dist/migrations/transfer.js +108 -55
  78. package/dist/migrations/yaml/YamlImportConfigLoader.js +52 -52
  79. package/dist/migrations/yaml/YamlImportIntegration.js +2 -2
  80. package/dist/migrations/yaml/generateImportSchemas.js +174 -174
  81. package/dist/setupCommands.d.ts +1 -1
  82. package/dist/setupCommands.js +5 -6
  83. package/dist/setupController.js +1 -1
  84. package/dist/shared/backupTracking.d.ts +1 -1
  85. package/dist/shared/backupTracking.js +2 -2
  86. package/dist/shared/confirmationDialogs.js +1 -1
  87. package/dist/shared/migrationHelpers.d.ts +1 -1
  88. package/dist/shared/migrationHelpers.js +4 -4
  89. package/dist/shared/operationQueue.d.ts +1 -1
  90. package/dist/shared/operationQueue.js +3 -4
  91. package/dist/shared/operationsTable.d.ts +1 -1
  92. package/dist/shared/operationsTable.js +35 -34
  93. package/dist/shared/operationsTableSchema.d.ts +3 -3
  94. package/dist/shared/operationsTableSchema.js +2 -2
  95. package/dist/shared/progressManager.js +1 -1
  96. package/dist/shared/selectionDialogs.d.ts +6 -0
  97. package/dist/shared/selectionDialogs.js +56 -12
  98. package/dist/storage/methods.d.ts +12 -0
  99. package/dist/storage/methods.js +92 -89
  100. package/dist/storage/schemas.d.ts +2 -2
  101. package/dist/tables/indexManager.d.ts +1 -1
  102. package/dist/tables/indexManager.js +2 -2
  103. package/dist/types.d.ts +2 -2
  104. package/dist/types.js +1 -1
  105. package/dist/users/methods.js +2 -3
  106. package/dist/utils/configMigration.js +1 -1
  107. package/dist/utils/index.d.ts +1 -1
  108. package/dist/utils/index.js +1 -1
  109. package/dist/utils/loadConfigs.d.ts +2 -2
  110. package/dist/utils/loadConfigs.js +6 -7
  111. package/dist/utils/setupFiles.js +139 -141
  112. package/dist/utilsController.d.ts +16 -9
  113. package/dist/utilsController.js +93 -68
  114. package/package.json +9 -3
  115. package/.appwrite/.yaml_schemas/appwrite-config.schema.json +0 -380
  116. package/.appwrite/.yaml_schemas/collection.schema.json +0 -255
  117. package/.appwrite/collections/Categories.yaml +0 -182
  118. package/.appwrite/collections/ExampleCollection.yaml +0 -36
  119. package/.appwrite/collections/Posts.yaml +0 -227
  120. package/.appwrite/collections/Users.yaml +0 -149
  121. package/.appwrite/config.yaml +0 -109
  122. package/.appwrite/import/README.md +0 -148
  123. package/.appwrite/import/categories-import.yaml +0 -129
  124. package/.appwrite/import/posts-import.yaml +0 -208
  125. package/.appwrite/import/users-import.yaml +0 -130
  126. package/.appwrite/importData/categories.json +0 -194
  127. package/.appwrite/importData/posts.json +0 -270
  128. package/.appwrite/importData/users.json +0 -220
  129. package/.appwrite/schemas/categories.json +0 -128
  130. package/.appwrite/schemas/exampleCollection.json +0 -52
  131. package/.appwrite/schemas/posts.json +0 -173
  132. package/.appwrite/schemas/users.json +0 -125
  133. package/CHANGELOG.md +0 -35
  134. package/CONFIG_TODO.md +0 -1189
  135. package/SELECTION_DIALOGS.md +0 -146
  136. package/SERVICE_IMPLEMENTATION_REPORT.md +0 -462
  137. package/dist/adapters/AdapterFactory.d.ts +0 -94
  138. package/dist/adapters/AdapterFactory.js +0 -420
  139. package/dist/adapters/DatabaseAdapter.d.ts +0 -243
  140. package/dist/adapters/DatabaseAdapter.js +0 -50
  141. package/dist/adapters/LegacyAdapter.d.ts +0 -50
  142. package/dist/adapters/LegacyAdapter.js +0 -615
  143. package/dist/adapters/TablesDBAdapter.d.ts +0 -45
  144. package/dist/adapters/TablesDBAdapter.js +0 -611
  145. package/dist/config/ConfigManager.d.ts +0 -450
  146. package/dist/config/ConfigManager.js +0 -650
  147. package/dist/config/configMigration.d.ts +0 -87
  148. package/dist/config/configMigration.js +0 -390
  149. package/dist/config/configValidation.d.ts +0 -66
  150. package/dist/config/configValidation.js +0 -358
  151. package/dist/config/index.d.ts +0 -8
  152. package/dist/config/index.js +0 -7
  153. package/dist/config/services/ConfigDiscoveryService.d.ts +0 -122
  154. package/dist/config/services/ConfigDiscoveryService.js +0 -322
  155. package/dist/config/services/ConfigLoaderService.d.ts +0 -129
  156. package/dist/config/services/ConfigLoaderService.js +0 -535
  157. package/dist/config/services/ConfigMergeService.d.ts +0 -208
  158. package/dist/config/services/ConfigMergeService.js +0 -308
  159. package/dist/config/services/ConfigValidationService.d.ts +0 -214
  160. package/dist/config/services/ConfigValidationService.js +0 -310
  161. package/dist/config/services/SessionAuthService.d.ts +0 -225
  162. package/dist/config/services/SessionAuthService.js +0 -456
  163. package/dist/config/services/__tests__/ConfigMergeService.test.d.ts +0 -1
  164. package/dist/config/services/__tests__/ConfigMergeService.test.js +0 -271
  165. package/dist/config/services/index.d.ts +0 -13
  166. package/dist/config/services/index.js +0 -10
  167. package/dist/config/yamlConfig.d.ts +0 -722
  168. package/dist/config/yamlConfig.js +0 -702
  169. package/dist/functions/pathResolution.d.ts +0 -37
  170. package/dist/functions/pathResolution.js +0 -185
  171. package/dist/functions/templates/count-docs-in-collection/package.json +0 -25
  172. package/dist/functions/templates/count-docs-in-collection/tsconfig.json +0 -28
  173. package/dist/functions/templates/hono-typescript/package.json +0 -26
  174. package/dist/functions/templates/hono-typescript/src/index.ts +0 -54
  175. package/dist/functions/templates/hono-typescript/tsconfig.json +0 -20
  176. package/dist/functions/templates/typescript-node/package.json +0 -25
  177. package/dist/functions/templates/typescript-node/tsconfig.json +0 -28
  178. package/dist/shared/attributeMapper.d.ts +0 -20
  179. package/dist/shared/attributeMapper.js +0 -203
  180. package/dist/shared/errorUtils.d.ts +0 -54
  181. package/dist/shared/errorUtils.js +0 -95
  182. package/dist/shared/functionManager.d.ts +0 -48
  183. package/dist/shared/functionManager.js +0 -348
  184. package/dist/shared/jsonSchemaGenerator.d.ts +0 -50
  185. package/dist/shared/jsonSchemaGenerator.js +0 -290
  186. package/dist/shared/logging.d.ts +0 -61
  187. package/dist/shared/logging.js +0 -116
  188. package/dist/shared/messageFormatter.d.ts +0 -39
  189. package/dist/shared/messageFormatter.js +0 -162
  190. package/dist/shared/pydanticModelGenerator.d.ts +0 -17
  191. package/dist/shared/pydanticModelGenerator.js +0 -615
  192. package/dist/shared/schemaGenerator.d.ts +0 -40
  193. package/dist/shared/schemaGenerator.js +0 -556
  194. package/dist/utils/ClientFactory.d.ts +0 -87
  195. package/dist/utils/ClientFactory.js +0 -212
  196. package/dist/utils/configDiscovery.d.ts +0 -78
  197. package/dist/utils/configDiscovery.js +0 -472
  198. package/dist/utils/constantsGenerator.d.ts +0 -31
  199. package/dist/utils/constantsGenerator.js +0 -321
  200. package/dist/utils/dataConverters.d.ts +0 -46
  201. package/dist/utils/dataConverters.js +0 -139
  202. package/dist/utils/directoryUtils.d.ts +0 -22
  203. package/dist/utils/directoryUtils.js +0 -59
  204. package/dist/utils/getClientFromConfig.d.ts +0 -39
  205. package/dist/utils/getClientFromConfig.js +0 -199
  206. package/dist/utils/helperFunctions.d.ts +0 -63
  207. package/dist/utils/helperFunctions.js +0 -156
  208. package/dist/utils/pathResolvers.d.ts +0 -53
  209. package/dist/utils/pathResolvers.js +0 -72
  210. package/dist/utils/projectConfig.d.ts +0 -122
  211. package/dist/utils/projectConfig.js +0 -206
  212. package/dist/utils/retryFailedPromises.d.ts +0 -2
  213. package/dist/utils/retryFailedPromises.js +0 -23
  214. package/dist/utils/sessionAuth.d.ts +0 -48
  215. package/dist/utils/sessionAuth.js +0 -164
  216. package/dist/utils/typeGuards.d.ts +0 -35
  217. package/dist/utils/typeGuards.js +0 -57
  218. package/dist/utils/validationRules.d.ts +0 -43
  219. package/dist/utils/validationRules.js +0 -42
  220. package/dist/utils/versionDetection.d.ts +0 -58
  221. package/dist/utils/versionDetection.js +0 -251
  222. package/dist/utils/yamlConverter.d.ts +0 -100
  223. package/dist/utils/yamlConverter.js +0 -428
  224. package/dist/utils/yamlLoader.d.ts +0 -70
  225. package/dist/utils/yamlLoader.js +0 -267
  226. package/scripts/copy-templates.ts +0 -23
  227. package/src/adapters/AdapterFactory.ts +0 -529
  228. package/src/adapters/DatabaseAdapter.ts +0 -319
  229. package/src/adapters/LegacyAdapter.ts +0 -844
  230. package/src/adapters/TablesDBAdapter.ts +0 -823
  231. package/src/adapters/index.ts +0 -37
  232. package/src/backups/operations/bucketBackup.ts +0 -277
  233. package/src/backups/operations/collectionBackup.ts +0 -310
  234. package/src/backups/operations/comprehensiveBackup.ts +0 -342
  235. package/src/backups/schemas/bucketManifest.ts +0 -78
  236. package/src/backups/schemas/comprehensiveManifest.ts +0 -76
  237. package/src/backups/tracking/centralizedTracking.ts +0 -352
  238. package/src/cli/commands/configCommands.ts +0 -201
  239. package/src/cli/commands/databaseCommands.ts +0 -879
  240. package/src/cli/commands/functionCommands.ts +0 -418
  241. package/src/cli/commands/schemaCommands.ts +0 -200
  242. package/src/cli/commands/storageCommands.ts +0 -152
  243. package/src/cli/commands/transferCommands.ts +0 -457
  244. package/src/collections/attributes.ts +0 -2020
  245. package/src/collections/attributes.ts.backup +0 -1555
  246. package/src/collections/indexes.ts +0 -352
  247. package/src/collections/methods.ts +0 -700
  248. package/src/collections/tableOperations.ts +0 -521
  249. package/src/collections/transferOperations.ts +0 -590
  250. package/src/collections/wipeOperations.ts +0 -346
  251. package/src/config/ConfigManager.ts +0 -849
  252. package/src/config/README.md +0 -274
  253. package/src/config/configMigration.ts +0 -575
  254. package/src/config/configValidation.ts +0 -445
  255. package/src/config/index.ts +0 -10
  256. package/src/config/services/ConfigDiscoveryService.ts +0 -410
  257. package/src/config/services/ConfigLoaderService.ts +0 -732
  258. package/src/config/services/ConfigMergeService.ts +0 -388
  259. package/src/config/services/ConfigValidationService.ts +0 -394
  260. package/src/config/services/SessionAuthService.ts +0 -565
  261. package/src/config/services/__tests__/ConfigMergeService.test.ts +0 -351
  262. package/src/config/services/index.ts +0 -29
  263. package/src/config/yamlConfig.ts +0 -761
  264. package/src/databases/methods.ts +0 -49
  265. package/src/databases/setup.ts +0 -77
  266. package/src/examples/yamlTerminologyExample.ts +0 -346
  267. package/src/functions/deployments.ts +0 -220
  268. package/src/functions/fnConfigDiscovery.ts +0 -103
  269. package/src/functions/methods.ts +0 -284
  270. package/src/functions/pathResolution.ts +0 -227
  271. package/src/functions/templates/count-docs-in-collection/README.md +0 -54
  272. package/src/functions/templates/count-docs-in-collection/package.json +0 -25
  273. package/src/functions/templates/count-docs-in-collection/src/main.ts +0 -159
  274. package/src/functions/templates/count-docs-in-collection/src/request.ts +0 -9
  275. package/src/functions/templates/count-docs-in-collection/tsconfig.json +0 -28
  276. package/src/functions/templates/hono-typescript/README.md +0 -286
  277. package/src/functions/templates/hono-typescript/package.json +0 -26
  278. package/src/functions/templates/hono-typescript/src/adapters/request.ts +0 -74
  279. package/src/functions/templates/hono-typescript/src/adapters/response.ts +0 -106
  280. package/src/functions/templates/hono-typescript/src/app.ts +0 -180
  281. package/src/functions/templates/hono-typescript/src/context.ts +0 -103
  282. package/src/functions/templates/hono-typescript/src/middleware/appwrite.ts +0 -119
  283. package/src/functions/templates/hono-typescript/tsconfig.json +0 -20
  284. package/src/functions/templates/typescript-node/README.md +0 -32
  285. package/src/functions/templates/typescript-node/package.json +0 -25
  286. package/src/functions/templates/typescript-node/src/context.ts +0 -103
  287. package/src/functions/templates/typescript-node/src/index.ts +0 -29
  288. package/src/functions/templates/typescript-node/tsconfig.json +0 -28
  289. package/src/functions/templates/uv/README.md +0 -31
  290. package/src/functions/templates/uv/pyproject.toml +0 -30
  291. package/src/functions/templates/uv/src/__init__.py +0 -0
  292. package/src/functions/templates/uv/src/context.py +0 -125
  293. package/src/functions/templates/uv/src/index.py +0 -46
  294. package/src/init.ts +0 -62
  295. package/src/interactiveCLI.ts +0 -1136
  296. package/src/main.ts +0 -1670
  297. package/src/migrations/afterImportActions.ts +0 -580
  298. package/src/migrations/appwriteToX.ts +0 -664
  299. package/src/migrations/comprehensiveTransfer.ts +0 -2285
  300. package/src/migrations/dataLoader.ts +0 -1702
  301. package/src/migrations/importController.ts +0 -428
  302. package/src/migrations/importDataActions.ts +0 -315
  303. package/src/migrations/relationships.ts +0 -334
  304. package/src/migrations/services/DataTransformationService.ts +0 -196
  305. package/src/migrations/services/FileHandlerService.ts +0 -311
  306. package/src/migrations/services/ImportOrchestrator.ts +0 -666
  307. package/src/migrations/services/RateLimitManager.ts +0 -363
  308. package/src/migrations/services/RelationshipResolver.ts +0 -461
  309. package/src/migrations/services/UserMappingService.ts +0 -345
  310. package/src/migrations/services/ValidationService.ts +0 -349
  311. package/src/migrations/transfer.ts +0 -1068
  312. package/src/migrations/yaml/YamlImportConfigLoader.ts +0 -439
  313. package/src/migrations/yaml/YamlImportIntegration.ts +0 -446
  314. package/src/migrations/yaml/generateImportSchemas.ts +0 -1354
  315. package/src/schemas/authUser.ts +0 -23
  316. package/src/setup.ts +0 -8
  317. package/src/setupCommands.ts +0 -603
  318. package/src/setupController.ts +0 -43
  319. package/src/shared/attributeMapper.ts +0 -229
  320. package/src/shared/backupMetadataSchema.ts +0 -93
  321. package/src/shared/backupTracking.ts +0 -211
  322. package/src/shared/confirmationDialogs.ts +0 -327
  323. package/src/shared/errorUtils.ts +0 -110
  324. package/src/shared/functionManager.ts +0 -537
  325. package/src/shared/jsonSchemaGenerator.ts +0 -383
  326. package/src/shared/logging.ts +0 -149
  327. package/src/shared/messageFormatter.ts +0 -208
  328. package/src/shared/migrationHelpers.ts +0 -232
  329. package/src/shared/operationLogger.ts +0 -20
  330. package/src/shared/operationQueue.ts +0 -377
  331. package/src/shared/operationsTable.ts +0 -338
  332. package/src/shared/operationsTableSchema.ts +0 -60
  333. package/src/shared/progressManager.ts +0 -278
  334. package/src/shared/pydanticModelGenerator.ts +0 -618
  335. package/src/shared/relationshipExtractor.ts +0 -214
  336. package/src/shared/schemaGenerator.ts +0 -644
  337. package/src/shared/selectionDialogs.ts +0 -749
  338. package/src/storage/backupCompression.ts +0 -88
  339. package/src/storage/methods.ts +0 -698
  340. package/src/storage/schemas.ts +0 -205
  341. package/src/tables/indexManager.ts +0 -409
  342. package/src/types/node-appwrite-tablesdb.d.ts +0 -44
  343. package/src/types.ts +0 -9
  344. package/src/users/methods.ts +0 -359
  345. package/src/utils/ClientFactory.ts +0 -240
  346. package/src/utils/configDiscovery.ts +0 -557
  347. package/src/utils/configMigration.ts +0 -348
  348. package/src/utils/constantsGenerator.ts +0 -369
  349. package/src/utils/dataConverters.ts +0 -159
  350. package/src/utils/directoryUtils.ts +0 -61
  351. package/src/utils/getClientFromConfig.ts +0 -257
  352. package/src/utils/helperFunctions.ts +0 -228
  353. package/src/utils/index.ts +0 -2
  354. package/src/utils/loadConfigs.ts +0 -449
  355. package/src/utils/pathResolvers.ts +0 -81
  356. package/src/utils/projectConfig.ts +0 -340
  357. package/src/utils/retryFailedPromises.ts +0 -29
  358. package/src/utils/sessionAuth.ts +0 -230
  359. package/src/utils/setupFiles.ts +0 -1238
  360. package/src/utils/typeGuards.ts +0 -65
  361. package/src/utils/validationRules.ts +0 -88
  362. package/src/utils/versionDetection.ts +0 -292
  363. package/src/utils/yamlConverter.ts +0 -542
  364. package/src/utils/yamlLoader.ts +0 -371
  365. package/src/utilsController.ts +0 -1213
  366. package/tests/README.md +0 -497
  367. package/tests/adapters/AdapterFactory.test.ts +0 -277
  368. package/tests/integration/syncOperations.test.ts +0 -463
  369. package/tests/jest.config.js +0 -25
  370. package/tests/migration/configMigration.test.ts +0 -546
  371. package/tests/setup.ts +0 -62
  372. package/tests/testUtils.ts +0 -340
  373. package/tests/utils/loadConfigs.test.ts +0 -350
  374. package/tests/validation/configValidation.test.ts +0 -412
  375. package/tmp-sync-test/.appwrite/collections/TestCollection.yaml +0 -7
  376. package/tsconfig.json +0 -44
@@ -1,2020 +0,0 @@
1
- import { Query, type Databases, type Models } from "node-appwrite";
2
- import {
3
- attributeSchema,
4
- parseAttribute,
5
- type Attribute,
6
- } from "appwrite-utils";
7
- import {
8
- nameToIdMapping,
9
- enqueueOperation,
10
- markAttributeProcessed,
11
- isAttributeProcessed,
12
- } from "../shared/operationQueue.js";
13
- import {
14
- delay,
15
- tryAwaitWithRetry,
16
- calculateExponentialBackoff,
17
- } from "../utils/helperFunctions.js";
18
- import chalk from "chalk";
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";
24
-
25
- // Extreme values that Appwrite may return, which should be treated as undefined
26
- const EXTREME_MIN_INTEGER = -9223372036854776000;
27
- const EXTREME_MAX_INTEGER = 9223372036854776000;
28
- const EXTREME_MIN_FLOAT = -1.7976931348623157e308;
29
- const EXTREME_MAX_FLOAT = 1.7976931348623157e308;
30
-
31
- /**
32
- * Type guard to check if an attribute has min/max properties
33
- */
34
- const hasMinMaxProperties = (
35
- attribute: Attribute
36
- ): attribute is Attribute & { min?: number; max?: number } => {
37
- return (
38
- attribute.type === "integer" ||
39
- attribute.type === "double" ||
40
- attribute.type === "float"
41
- );
42
- };
43
-
44
- /**
45
- * Normalizes min/max values for integer and float attributes using Decimal.js for precision
46
- * Validates that min < max and handles extreme database values
47
- */
48
- const normalizeMinMaxValues = (
49
- attribute: Attribute
50
- ): { min?: number; max?: number } => {
51
- if (!hasMinMaxProperties(attribute)) {
52
- logger.debug(
53
- `Attribute '${attribute.key}' does not have min/max properties`,
54
- {
55
- type: attribute.type,
56
- operation: "normalizeMinMaxValues",
57
- }
58
- );
59
- return {};
60
- }
61
-
62
- const { type, min, max } = attribute;
63
- let normalizedMin = min;
64
- let normalizedMax = max;
65
-
66
- logger.debug(`Normalizing min/max values for attribute '${attribute.key}'`, {
67
- type,
68
- originalMin: min,
69
- originalMax: max,
70
- operation: "normalizeMinMaxValues",
71
- });
72
-
73
- // Handle min value - only filter out extreme database values
74
- if (normalizedMin !== undefined && normalizedMin !== null) {
75
- const minValue = Number(normalizedMin);
76
- const originalMin = normalizedMin;
77
-
78
- // Check if it's an extreme database value (but don't filter out large numbers)
79
- if (type === 'integer') {
80
- if (minValue === EXTREME_MIN_INTEGER) {
81
- logger.debug(`Min value normalized to undefined for attribute '${attribute.key}'`, {
82
- type,
83
- originalValue: originalMin,
84
- numericValue: minValue,
85
- reason: 'extreme_database_value',
86
- extremeValue: EXTREME_MIN_INTEGER,
87
- operation: 'normalizeMinMaxValues'
88
- });
89
- normalizedMin = undefined;
90
- }
91
- } else { // float/double
92
- if (minValue === EXTREME_MIN_FLOAT) {
93
- logger.debug(`Min value normalized to undefined for attribute '${attribute.key}'`, {
94
- type,
95
- originalValue: originalMin,
96
- numericValue: minValue,
97
- reason: 'extreme_database_value',
98
- extremeValue: EXTREME_MIN_FLOAT,
99
- operation: 'normalizeMinMaxValues'
100
- });
101
- normalizedMin = undefined;
102
- }
103
- }
104
- }
105
-
106
- // Handle max value - only filter out extreme database values
107
- if (normalizedMax !== undefined && normalizedMax !== null) {
108
- const maxValue = Number(normalizedMax);
109
- const originalMax = normalizedMax;
110
-
111
- // Check if it's an extreme database value (but don't filter out large numbers)
112
- if (type === 'integer') {
113
- if (maxValue === EXTREME_MAX_INTEGER) {
114
- logger.debug(`Max value normalized to undefined for attribute '${attribute.key}'`, {
115
- type,
116
- originalValue: originalMax,
117
- numericValue: maxValue,
118
- reason: 'extreme_database_value',
119
- extremeValue: EXTREME_MAX_INTEGER,
120
- operation: 'normalizeMinMaxValues'
121
- });
122
- normalizedMax = undefined;
123
- }
124
- } else { // float/double
125
- if (maxValue === EXTREME_MAX_FLOAT) {
126
- logger.debug(`Max value normalized to undefined for attribute '${attribute.key}'`, {
127
- type,
128
- originalValue: originalMax,
129
- numericValue: maxValue,
130
- reason: 'extreme_database_value',
131
- extremeValue: EXTREME_MAX_FLOAT,
132
- operation: 'normalizeMinMaxValues'
133
- });
134
- normalizedMax = undefined;
135
- }
136
- }
137
- }
138
-
139
- // Validate that min < max using multiple comparison methods for reliability
140
- if (normalizedMin !== undefined && normalizedMax !== undefined &&
141
- normalizedMin !== null && normalizedMax !== null) {
142
-
143
- logger.debug(`Validating min/max values for attribute '${attribute.key}'`, {
144
- type,
145
- normalizedMin,
146
- normalizedMax,
147
- normalizedMinType: typeof normalizedMin,
148
- normalizedMaxType: typeof normalizedMax,
149
- operation: 'normalizeMinMaxValues'
150
- });
151
-
152
- // Use multiple validation approaches to ensure reliability
153
- let needsSwap = false;
154
- let comparisonMethod = '';
155
-
156
- try {
157
- // Method 1: Direct number comparison (most reliable for normal numbers)
158
- const minNum = Number(normalizedMin);
159
- const maxNum = Number(normalizedMax);
160
-
161
- if (!isNaN(minNum) && !isNaN(maxNum)) {
162
- needsSwap = minNum >= maxNum;
163
- comparisonMethod = 'direct_number_comparison';
164
- logger.debug(`Direct number comparison: ${minNum} >= ${maxNum} = ${needsSwap}`, {
165
- operation: 'normalizeMinMaxValues'
166
- });
167
- }
168
-
169
- // Method 2: Fallback to string comparison for very large numbers
170
- if (!needsSwap && (isNaN(minNum) || isNaN(maxNum) || Math.abs(minNum) > Number.MAX_SAFE_INTEGER || Math.abs(maxNum) > Number.MAX_SAFE_INTEGER)) {
171
- const minStr = normalizedMin.toString();
172
- const maxStr = normalizedMax.toString();
173
-
174
- // Simple string length and lexicographical comparison for very large numbers
175
- if (minStr.length !== maxStr.length) {
176
- needsSwap = minStr.length > maxStr.length;
177
- } else {
178
- needsSwap = minStr >= maxStr;
179
- }
180
- comparisonMethod = 'string_comparison_fallback';
181
- logger.debug(`String comparison fallback: '${minStr}' >= '${maxStr}' = ${needsSwap}`, {
182
- operation: 'normalizeMinMaxValues'
183
- });
184
- }
185
-
186
- // Method 3: Final validation using Decimal.js as last resort
187
- if (!needsSwap && (typeof normalizedMin === 'string' || typeof normalizedMax === 'string')) {
188
- try {
189
- const minDecimal = new Decimal(normalizedMin.toString());
190
- const maxDecimal = new Decimal(normalizedMax.toString());
191
- needsSwap = minDecimal.greaterThanOrEqualTo(maxDecimal);
192
- comparisonMethod = 'decimal_js_fallback';
193
- logger.debug(`Decimal.js fallback: ${normalizedMin} >= ${normalizedMax} = ${needsSwap}`, {
194
- operation: 'normalizeMinMaxValues'
195
- });
196
- } catch (decimalError) {
197
- logger.warn(`Decimal.js comparison failed for attribute '${attribute.key}': ${decimalError instanceof Error ? decimalError.message : String(decimalError)}`, {
198
- operation: 'normalizeMinMaxValues'
199
- });
200
- }
201
- }
202
-
203
- // Log final validation result
204
- if (needsSwap) {
205
- logger.error(`Invalid min/max values detected for attribute '${attribute.key}': min (${normalizedMin}) must be less than max (${normalizedMax})`, {
206
- type,
207
- min: normalizedMin,
208
- max: normalizedMax,
209
- comparisonMethod,
210
- operation: 'normalizeMinMaxValues'
211
- });
212
-
213
- // Swap values to ensure min < max (graceful handling)
214
- logger.warn(`Swapping min/max values for attribute '${attribute.key}' to fix validation`, {
215
- type,
216
- originalMin: normalizedMin,
217
- originalMax: normalizedMax,
218
- newMin: normalizedMax,
219
- newMax: normalizedMin,
220
- comparisonMethod,
221
- operation: 'normalizeMinMaxValues'
222
- });
223
-
224
- const temp = normalizedMin;
225
- normalizedMin = normalizedMax;
226
- normalizedMax = temp;
227
- } else {
228
- logger.debug(`Min/max validation passed for attribute '${attribute.key}'`, {
229
- type,
230
- min: normalizedMin,
231
- max: normalizedMax,
232
- comparisonMethod,
233
- operation: 'normalizeMinMaxValues'
234
- });
235
- }
236
-
237
- } catch (error) {
238
- logger.error(`Critical error during min/max validation for attribute '${attribute.key}'`, {
239
- type,
240
- min: normalizedMin,
241
- max: normalizedMax,
242
- error: error instanceof Error ? error.message : String(error),
243
- operation: 'normalizeMinMaxValues'
244
- });
245
-
246
- // If all comparison methods fail, set both to undefined to avoid API errors
247
- normalizedMin = undefined;
248
- normalizedMax = undefined;
249
- }
250
- }
251
-
252
- const result = { min: normalizedMin, max: normalizedMax };
253
- logger.debug(
254
- `Min/max normalization complete for attribute '${attribute.key}'`,
255
- {
256
- type,
257
- result,
258
- operation: "normalizeMinMaxValues",
259
- }
260
- );
261
-
262
- return result;
263
- };
264
-
265
- /**
266
- * Normalizes an attribute for comparison by handling extreme database values
267
- * This is used when comparing database attributes with config attributes
268
- */
269
- const normalizeAttributeForComparison = (attribute: Attribute): Attribute => {
270
- const normalized: any = { ...attribute };
271
-
272
- // Ignore defaults on required attributes to prevent false positives
273
- if (normalized.required === true && "xdefault" in normalized) {
274
- delete normalized.xdefault;
275
- }
276
-
277
- // Normalize min/max for numeric types
278
- if (hasMinMaxProperties(attribute)) {
279
- const { min, max } = normalizeMinMaxValues(attribute);
280
- normalized.min = min;
281
- normalized.max = max;
282
- }
283
-
284
- // Remove xdefault if null/undefined to ensure consistent comparison
285
- // Appwrite sets xdefault: null for required attributes, but config files omit it
286
- if (
287
- "xdefault" in normalized &&
288
- (normalized.xdefault === null || normalized.xdefault === undefined)
289
- ) {
290
- delete normalized.xdefault;
291
- }
292
-
293
- return normalized;
294
- };
295
-
296
- /**
297
- * Helper function to create an attribute using either the adapter or legacy API
298
- */
299
- const createAttributeViaAdapter = async (
300
- db: Databases | DatabaseAdapter,
301
- dbId: string,
302
- collectionId: string,
303
- attribute: Attribute
304
- ): Promise<void> => {
305
- const startTime = Date.now();
306
- const adapterType = isDatabaseAdapter(db) ? "adapter" : "legacy";
307
-
308
- logger.info(`Creating attribute '${attribute.key}' via ${adapterType}`, {
309
- type: attribute.type,
310
- dbId,
311
- collectionId,
312
- adapterType,
313
- operation: "createAttributeViaAdapter",
314
- });
315
-
316
- if (isDatabaseAdapter(db)) {
317
- // Use the adapter's unified createAttribute method
318
- const params: CreateAttributeParams = {
319
- databaseId: dbId,
320
- tableId: collectionId,
321
- key: attribute.key,
322
- type: attribute.type,
323
- required: attribute.required || false,
324
- array: attribute.array || false,
325
- ...((attribute as any).size && { size: (attribute as any).size }),
326
- ...((attribute as any).xdefault !== undefined &&
327
- !attribute.required && { default: (attribute as any).xdefault }),
328
- ...((attribute as any).encrypt && {
329
- encrypt: (attribute as any).encrypt,
330
- }),
331
- ...((attribute as any).min !== undefined && {
332
- min: (attribute as any).min,
333
- }),
334
- ...((attribute as any).max !== undefined && {
335
- max: (attribute as any).max,
336
- }),
337
- ...((attribute as any).elements && {
338
- elements: (attribute as any).elements,
339
- }),
340
- ...((attribute as any).relatedCollection && {
341
- relatedCollection: (attribute as any).relatedCollection,
342
- }),
343
- ...((attribute as any).relationType && {
344
- relationType: (attribute as any).relationType,
345
- }),
346
- ...((attribute as any).twoWay !== undefined && {
347
- twoWay: (attribute as any).twoWay,
348
- }),
349
- ...((attribute as any).onDelete && {
350
- onDelete: (attribute as any).onDelete,
351
- }),
352
- ...((attribute as any).twoWayKey && {
353
- twoWayKey: (attribute as any).twoWayKey,
354
- }),
355
- };
356
-
357
- logger.debug(`Adapter create parameters for '${attribute.key}'`, {
358
- params,
359
- operation: "createAttributeViaAdapter",
360
- });
361
-
362
- await db.createAttribute(params);
363
-
364
- const duration = Date.now() - startTime;
365
- logger.info(
366
- `Successfully created attribute '${attribute.key}' via adapter`,
367
- {
368
- duration,
369
- operation: "createAttributeViaAdapter",
370
- }
371
- );
372
- } else {
373
- // Use legacy type-specific methods
374
- logger.debug(`Using legacy creation for attribute '${attribute.key}'`, {
375
- operation: "createAttributeViaAdapter",
376
- });
377
- await createLegacyAttribute(db, dbId, collectionId, attribute);
378
-
379
- const duration = Date.now() - startTime;
380
- logger.info(
381
- `Successfully created attribute '${attribute.key}' via legacy`,
382
- {
383
- duration,
384
- operation: "createAttributeViaAdapter",
385
- }
386
- );
387
- }
388
- };
389
-
390
- /**
391
- * Helper function to update an attribute using either the adapter or legacy API
392
- */
393
- const updateAttributeViaAdapter = async (
394
- db: Databases | DatabaseAdapter,
395
- dbId: string,
396
- collectionId: string,
397
- attribute: Attribute
398
- ): Promise<void> => {
399
- if (isDatabaseAdapter(db)) {
400
- // Use the adapter's unified updateAttribute method
401
- const params: UpdateAttributeParams = {
402
- databaseId: dbId,
403
- tableId: collectionId,
404
- key: attribute.key,
405
- type: attribute.type,
406
- required: attribute.required || false,
407
- array: attribute.array || false,
408
- size: (attribute as any).size,
409
- min: (attribute as any).min,
410
- max: (attribute as any).max,
411
- encrypt: (attribute as any).encrypt,
412
- elements: (attribute as any).elements,
413
- relatedCollection: (attribute as any).relatedCollection,
414
- relationType: (attribute as any).relationType,
415
- twoWay: (attribute as any).twoWay,
416
- twoWayKey: (attribute as any).twoWayKey,
417
- onDelete: (attribute as any).onDelete
418
- };
419
- if (!attribute.required && (attribute as any).xdefault !== undefined) {
420
- params.default = (attribute as any).xdefault;
421
- }
422
- await db.updateAttribute(params);
423
- } else {
424
- // Use legacy type-specific methods
425
- await updateLegacyAttribute(db, dbId, collectionId, attribute);
426
- }
427
- };
428
-
429
- /**
430
- * Legacy attribute creation using type-specific methods
431
- */
432
- const createLegacyAttribute = async (
433
- db: Databases,
434
- dbId: string,
435
- collectionId: string,
436
- attribute: Attribute
437
- ): Promise<void> => {
438
- const startTime = Date.now();
439
- const { min: normalizedMin, max: normalizedMax } =
440
- normalizeMinMaxValues(attribute);
441
-
442
- logger.info(`Creating legacy attribute '${attribute.key}'`, {
443
- type: attribute.type,
444
- dbId,
445
- collectionId,
446
- normalizedMin,
447
- normalizedMax,
448
- operation: "createLegacyAttribute",
449
- });
450
-
451
- switch (attribute.type) {
452
- case "string":
453
- const stringParams = {
454
- size: (attribute as any).size || 255,
455
- required: attribute.required || false,
456
- defaultValue:
457
- (attribute as any).xdefault !== undefined && !attribute.required
458
- ? (attribute as any).xdefault
459
- : undefined,
460
- array: attribute.array || false,
461
- encrypt: (attribute as any).encrypt,
462
- };
463
- logger.debug(`Creating string attribute '${attribute.key}'`, {
464
- ...stringParams,
465
- operation: "createLegacyAttribute",
466
- });
467
- await db.createStringAttribute(
468
- dbId,
469
- collectionId,
470
- attribute.key,
471
- stringParams.size,
472
- stringParams.required,
473
- stringParams.defaultValue,
474
- stringParams.array,
475
- stringParams.encrypt
476
- );
477
- break;
478
- case "integer":
479
- const integerParams = {
480
- required: attribute.required || false,
481
- min:
482
- normalizedMin !== undefined
483
- ? parseInt(String(normalizedMin))
484
- : undefined,
485
- max:
486
- normalizedMax !== undefined
487
- ? parseInt(String(normalizedMax))
488
- : undefined,
489
- defaultValue:
490
- (attribute as any).xdefault !== undefined && !attribute.required
491
- ? (attribute as any).xdefault
492
- : undefined,
493
- array: attribute.array || false,
494
- };
495
- logger.debug(`Creating integer attribute '${attribute.key}'`, {
496
- ...integerParams,
497
- operation: "createLegacyAttribute",
498
- });
499
- await db.createIntegerAttribute(
500
- dbId,
501
- collectionId,
502
- attribute.key,
503
- integerParams.required,
504
- integerParams.min,
505
- integerParams.max,
506
- integerParams.defaultValue,
507
- integerParams.array
508
- );
509
- break;
510
- case "double":
511
- case "float":
512
- await db.createFloatAttribute(
513
- dbId,
514
- collectionId,
515
- attribute.key,
516
- attribute.required || false,
517
- normalizedMin !== undefined ? Number(normalizedMin) : undefined,
518
- normalizedMax !== undefined ? Number(normalizedMax) : undefined,
519
- (attribute as any).xdefault !== undefined && !attribute.required
520
- ? (attribute as any).xdefault
521
- : undefined,
522
- attribute.array || false
523
- );
524
- break;
525
- case "boolean":
526
- await db.createBooleanAttribute(
527
- dbId,
528
- collectionId,
529
- attribute.key,
530
- attribute.required || false,
531
- (attribute as any).xdefault !== undefined && !attribute.required
532
- ? (attribute as any).xdefault
533
- : undefined,
534
- attribute.array || false
535
- );
536
- break;
537
- case "datetime":
538
- await db.createDatetimeAttribute(
539
- dbId,
540
- collectionId,
541
- attribute.key,
542
- attribute.required || false,
543
- (attribute as any).xdefault !== undefined && !attribute.required
544
- ? (attribute as any).xdefault
545
- : undefined,
546
- attribute.array || false
547
- );
548
- break;
549
- case "email":
550
- await db.createEmailAttribute(
551
- dbId,
552
- collectionId,
553
- attribute.key,
554
- attribute.required || false,
555
- (attribute as any).xdefault !== undefined && !attribute.required
556
- ? (attribute as any).xdefault
557
- : undefined,
558
- attribute.array || false
559
- );
560
- break;
561
- case "ip":
562
- await db.createIpAttribute(
563
- dbId,
564
- collectionId,
565
- attribute.key,
566
- attribute.required || false,
567
- (attribute as any).xdefault !== undefined && !attribute.required
568
- ? (attribute as any).xdefault
569
- : undefined,
570
- attribute.array || false
571
- );
572
- break;
573
- case "url":
574
- await db.createUrlAttribute(
575
- dbId,
576
- collectionId,
577
- attribute.key,
578
- attribute.required || false,
579
- (attribute as any).xdefault !== undefined && !attribute.required
580
- ? (attribute as any).xdefault
581
- : undefined,
582
- attribute.array || false
583
- );
584
- break;
585
- case "enum":
586
- await db.createEnumAttribute(
587
- dbId,
588
- collectionId,
589
- attribute.key,
590
- (attribute as any).elements || [],
591
- attribute.required || false,
592
- (attribute as any).xdefault !== undefined && !attribute.required
593
- ? (attribute as any).xdefault
594
- : undefined,
595
- attribute.array || false
596
- );
597
- break;
598
- case "relationship":
599
- await db.createRelationshipAttribute(
600
- dbId,
601
- collectionId,
602
- (attribute as any).relatedCollection!,
603
- (attribute as any).relationType!,
604
- (attribute as any).twoWay,
605
- attribute.key,
606
- (attribute as any).twoWayKey,
607
- (attribute as any).onDelete
608
- );
609
- break;
610
- default:
611
- const error = new Error(
612
- `Unsupported attribute type: ${(attribute as any).type}`
613
- );
614
- logger.error(
615
- `Unsupported attribute type for '${(attribute as any).key}'`,
616
- {
617
- type: (attribute as any).type,
618
- supportedTypes: [
619
- "string",
620
- "integer",
621
- "double",
622
- "float",
623
- "boolean",
624
- "datetime",
625
- "email",
626
- "ip",
627
- "url",
628
- "enum",
629
- "relationship",
630
- ],
631
- operation: "createLegacyAttribute",
632
- }
633
- );
634
- throw error;
635
- }
636
-
637
- const duration = Date.now() - startTime;
638
- logger.info(`Successfully created legacy attribute '${attribute.key}'`, {
639
- type: attribute.type,
640
- duration,
641
- operation: "createLegacyAttribute",
642
- });
643
- };
644
-
645
- /**
646
- * Legacy attribute update using type-specific methods
647
- */
648
- const updateLegacyAttribute = async (
649
- db: Databases,
650
- dbId: string,
651
- collectionId: string,
652
- attribute: Attribute
653
- ): Promise<void> => {
654
- const { min: normalizedMin, max: normalizedMax } =
655
- normalizeMinMaxValues(attribute);
656
-
657
-
658
-
659
- switch (attribute.type) {
660
- case "string":
661
- await db.updateStringAttribute(
662
- dbId,
663
- collectionId,
664
- attribute.key,
665
- attribute.required || false,
666
- !attribute.required && (attribute as any).xdefault !== undefined
667
- ? (attribute as any).xdefault
668
- : null,
669
- attribute.size
670
- );
671
- break;
672
- case "integer":
673
- await db.updateIntegerAttribute(
674
- dbId,
675
- collectionId,
676
- attribute.key,
677
- attribute.required || false,
678
- !attribute.required && (attribute as any).xdefault !== undefined
679
- ? (attribute as any).xdefault
680
- : null,
681
- normalizedMin !== undefined
682
- ? parseInt(String(normalizedMin))
683
- : undefined,
684
- normalizedMax !== undefined
685
- ? parseInt(String(normalizedMax))
686
- : undefined
687
- );
688
- break;
689
- case "double":
690
- case "float":
691
- const minParam = normalizedMin !== undefined ? Number(normalizedMin) : undefined;
692
- const maxParam = normalizedMax !== undefined ? Number(normalizedMax) : undefined;
693
-
694
-
695
-
696
- await db.updateFloatAttribute(
697
- dbId,
698
- collectionId,
699
- attribute.key,
700
- attribute.required || false,
701
- minParam,
702
- maxParam,
703
- !attribute.required && (attribute as any).xdefault !== undefined
704
- ? (attribute as any).xdefault
705
- : null
706
- );
707
- break;
708
- case "boolean":
709
- await db.updateBooleanAttribute(
710
- dbId,
711
- collectionId,
712
- attribute.key,
713
- attribute.required || false,
714
- !attribute.required && (attribute as any).xdefault !== undefined
715
- ? (attribute as any).xdefault
716
- : null
717
- );
718
- break;
719
- case "datetime":
720
- await db.updateDatetimeAttribute(
721
- dbId,
722
- collectionId,
723
- attribute.key,
724
- attribute.required || false,
725
- !attribute.required && (attribute as any).xdefault !== undefined
726
- ? (attribute as any).xdefault
727
- : null
728
- );
729
- break;
730
- case "email":
731
- await db.updateEmailAttribute(
732
- dbId,
733
- collectionId,
734
- attribute.key,
735
- attribute.required || false,
736
- !attribute.required && (attribute as any).xdefault !== undefined
737
- ? (attribute as any).xdefault
738
- : null
739
- );
740
- break;
741
- case "ip":
742
- await db.updateIpAttribute(
743
- dbId,
744
- collectionId,
745
- attribute.key,
746
- attribute.required || false,
747
- !attribute.required && (attribute as any).xdefault !== undefined
748
- ? (attribute as any).xdefault
749
- : null
750
- );
751
- break;
752
- case "url":
753
- await db.updateUrlAttribute(
754
- dbId,
755
- collectionId,
756
- attribute.key,
757
- attribute.required || false,
758
- !attribute.required && (attribute as any).xdefault !== undefined
759
- ? (attribute as any).xdefault
760
- : null
761
- );
762
- break;
763
- case "enum":
764
- await db.updateEnumAttribute(
765
- dbId,
766
- collectionId,
767
- attribute.key,
768
- (attribute as any).elements || [],
769
- attribute.required || false,
770
- !attribute.required && (attribute as any).xdefault !== undefined
771
- ? (attribute as any).xdefault
772
- : null
773
- );
774
- break;
775
- case "relationship":
776
- await db.updateRelationshipAttribute(
777
- dbId,
778
- collectionId,
779
- attribute.key,
780
- (attribute as any).onDelete
781
- );
782
- break;
783
- default:
784
- throw new Error(
785
- `Unsupported attribute type for update: ${(attribute as any).type}`
786
- );
787
- }
788
- };
789
-
790
- // Interface for attribute with status (fixing the type issue)
791
- interface AttributeWithStatus {
792
- key: string;
793
- type: string;
794
- status: "available" | "processing" | "deleting" | "stuck" | "failed";
795
- error: string;
796
- required: boolean;
797
- array?: boolean;
798
- $createdAt: string;
799
- $updatedAt: string;
800
- [key: string]: any; // For type-specific fields
801
- }
802
-
803
- /**
804
- * Wait for attribute to become available, with retry logic for stuck attributes and exponential backoff
805
- */
806
- const waitForAttributeAvailable = async (
807
- db: Databases | DatabaseAdapter,
808
- dbId: string,
809
- collectionId: string,
810
- attributeKey: string,
811
- maxWaitTime: number = 60000, // 1 minute
812
- retryCount: number = 0,
813
- maxRetries: number = 5
814
- ): Promise<boolean> => {
815
- const startTime = Date.now();
816
- let checkInterval = 2000; // Start with 2 seconds
817
-
818
- logger.info(`Waiting for attribute '${attributeKey}' to become available`, {
819
- dbId,
820
- collectionId,
821
- maxWaitTime,
822
- retryCount,
823
- maxRetries,
824
- operation: "waitForAttributeAvailable",
825
- });
826
-
827
- // Calculate exponential backoff: 2s, 4s, 8s, 16s, 30s (capped at 30s)
828
- if (retryCount > 0) {
829
- const exponentialDelay = calculateExponentialBackoff(retryCount);
830
- await delay(exponentialDelay);
831
- }
832
-
833
- while (Date.now() - startTime < maxWaitTime) {
834
- try {
835
- const collection = isDatabaseAdapter(db)
836
- ? (await db.getTable({ databaseId: dbId, tableId: collectionId })).data
837
- : await db.getCollection(dbId, collectionId);
838
- const attribute = (collection.attributes as any[]).find(
839
- (attr: AttributeWithStatus) => attr.key === attributeKey
840
- ) as AttributeWithStatus | undefined;
841
-
842
- if (!attribute) {
843
- MessageFormatter.error(`Attribute '${attributeKey}' not found`);
844
- return false;
845
- }
846
-
847
- const statusInfo = {
848
- attributeKey,
849
- status: attribute.status,
850
- error: attribute.error,
851
- dbId,
852
- collectionId,
853
- waitTime: Date.now() - startTime,
854
- operation: "waitForAttributeAvailable",
855
- };
856
-
857
- switch (attribute.status) {
858
- case "available":
859
- logger.info(
860
- `Attribute '${attributeKey}' became available`,
861
- statusInfo
862
- );
863
- return true;
864
-
865
- case "failed":
866
- logger.error(`Attribute '${attributeKey}' failed`, statusInfo);
867
- return false;
868
-
869
- case "stuck":
870
- logger.warn(`Attribute '${attributeKey}' is stuck`, statusInfo);
871
- return false;
872
-
873
- case "processing":
874
- // Continue waiting
875
- logger.debug(
876
- `Attribute '${attributeKey}' still processing`,
877
- statusInfo
878
- );
879
- break;
880
-
881
- case "deleting":
882
- MessageFormatter.info(
883
- chalk.yellow(`Attribute '${attributeKey}' is being deleted`)
884
- );
885
- logger.warn(
886
- `Attribute '${attributeKey}' is being deleted`,
887
- statusInfo
888
- );
889
- break;
890
-
891
- default:
892
- MessageFormatter.info(
893
- chalk.yellow(
894
- `Unknown status '${attribute.status}' for attribute '${attributeKey}'`
895
- )
896
- );
897
- logger.warn(
898
- `Unknown status for attribute '${attributeKey}'`,
899
- statusInfo
900
- );
901
- break;
902
- }
903
-
904
- await delay(checkInterval);
905
- } catch (error) {
906
- const errorMessage =
907
- error instanceof Error ? error.message : String(error);
908
- MessageFormatter.error(
909
- `Error checking attribute status: ${errorMessage}`
910
- );
911
-
912
- logger.error("Error checking attribute status", {
913
- attributeKey,
914
- dbId,
915
- collectionId,
916
- error: errorMessage,
917
- waitTime: Date.now() - startTime,
918
- operation: "waitForAttributeAvailable",
919
- });
920
-
921
- return false;
922
- }
923
- }
924
-
925
- // Timeout reached
926
- MessageFormatter.info(
927
- chalk.yellow(
928
- `⏰ Timeout waiting for attribute '${attributeKey}' (${maxWaitTime}ms)`
929
- )
930
- );
931
-
932
- // If we have retries left and this isn't the last retry, try recreating
933
- if (retryCount < maxRetries) {
934
- MessageFormatter.info(
935
- chalk.yellow(
936
- `🔄 Retrying attribute creation (attempt ${
937
- retryCount + 1
938
- }/${maxRetries})`
939
- )
940
- );
941
- return false; // Signal that we need to retry
942
- }
943
-
944
- return false;
945
- };
946
-
947
- /**
948
- * Wait for all attributes in a collection to become available
949
- */
950
- const waitForAllAttributesAvailable = async (
951
- db: Databases | DatabaseAdapter,
952
- dbId: string,
953
- collectionId: string,
954
- attributeKeys: string[],
955
- maxWaitTime: number = 60000
956
- ): Promise<string[]> => {
957
- MessageFormatter.info(
958
- chalk.blue(
959
- `Waiting for ${attributeKeys.length} attributes to become available...`
960
- )
961
- );
962
-
963
- const failedAttributes: string[] = [];
964
-
965
- for (const attributeKey of attributeKeys) {
966
- const success = await waitForAttributeAvailable(
967
- db,
968
- dbId,
969
- collectionId,
970
- attributeKey,
971
- maxWaitTime
972
- );
973
- if (!success) {
974
- failedAttributes.push(attributeKey);
975
- }
976
- }
977
-
978
- return failedAttributes;
979
- };
980
-
981
- /**
982
- * Delete collection and recreate with retry logic
983
- */
984
- const deleteAndRecreateCollection = async (
985
- db: Databases | DatabaseAdapter,
986
- dbId: string,
987
- collection: Models.Collection,
988
- retryCount: number
989
- ): Promise<Models.Collection | null> => {
990
- try {
991
- MessageFormatter.info(
992
- chalk.yellow(
993
- `🗑️ Deleting collection '${collection.name}' for retry ${retryCount}`
994
- )
995
- );
996
-
997
- // Delete the collection
998
- if (isDatabaseAdapter(db)) {
999
- await db.deleteTable({ databaseId: dbId, tableId: collection.$id });
1000
- } else {
1001
- await db.deleteCollection(dbId, collection.$id);
1002
- }
1003
- MessageFormatter.warning(`Deleted collection '${collection.name}'`);
1004
-
1005
- // Wait a bit before recreating
1006
- await delay(2000);
1007
-
1008
- // Recreate the collection
1009
- MessageFormatter.info(`🔄 Recreating collection '${collection.name}'`);
1010
- const newCollection = isDatabaseAdapter(db)
1011
- ? (
1012
- await db.createTable({
1013
- databaseId: dbId,
1014
- id: collection.$id,
1015
- name: collection.name,
1016
- permissions: collection.$permissions,
1017
- documentSecurity: collection.documentSecurity,
1018
- enabled: collection.enabled,
1019
- })
1020
- ).data
1021
- : await db.createCollection(
1022
- dbId,
1023
- collection.$id,
1024
- collection.name,
1025
- collection.$permissions,
1026
- collection.documentSecurity,
1027
- collection.enabled
1028
- );
1029
-
1030
- MessageFormatter.success(`✅ Recreated collection '${collection.name}'`);
1031
- return newCollection;
1032
- } catch (error) {
1033
- MessageFormatter.info(
1034
- chalk.red(
1035
- `Failed to delete/recreate collection '${collection.name}': ${error}`
1036
- )
1037
- );
1038
- return null;
1039
- }
1040
- };
1041
-
1042
- /**
1043
- * Get the fields that should be compared for a specific attribute type
1044
- * Only returns fields that are valid for the given type to avoid false positives
1045
- */
1046
- const getComparableFields = (type: string): string[] => {
1047
- const baseFields = ["key", "type", "array", "required", "xdefault"];
1048
-
1049
- switch (type) {
1050
- case "string":
1051
- return [...baseFields, "size", "encrypt"];
1052
-
1053
- case "integer":
1054
- case "double":
1055
- case "float":
1056
- return [...baseFields, "min", "max"];
1057
-
1058
- case "enum":
1059
- return [...baseFields, "elements"];
1060
-
1061
- case "relationship":
1062
- return [
1063
- ...baseFields,
1064
- "relationType",
1065
- "twoWay",
1066
- "twoWayKey",
1067
- "onDelete",
1068
- "relatedCollection",
1069
- ];
1070
-
1071
- case "boolean":
1072
- case "datetime":
1073
- case "email":
1074
- case "ip":
1075
- case "url":
1076
- return baseFields;
1077
-
1078
- default:
1079
- // Fallback to all fields for unknown types
1080
- return [
1081
- "key",
1082
- "type",
1083
- "array",
1084
- "encrypt",
1085
- "required",
1086
- "size",
1087
- "min",
1088
- "max",
1089
- "xdefault",
1090
- "elements",
1091
- "relationType",
1092
- "twoWay",
1093
- "twoWayKey",
1094
- "onDelete",
1095
- "relatedCollection",
1096
- ];
1097
- }
1098
- };
1099
-
1100
- const attributesSame = (
1101
- databaseAttribute: Attribute,
1102
- configAttribute: Attribute
1103
- ): boolean => {
1104
- // Normalize both attributes for comparison (handle extreme database values)
1105
- const normalizedDbAttr = normalizeAttributeForComparison(databaseAttribute);
1106
- const normalizedConfigAttr = normalizeAttributeForComparison(configAttribute);
1107
-
1108
- // Use type-specific field list to avoid false positives from irrelevant fields
1109
- const attributesToCheck = getComparableFields(normalizedConfigAttr.type);
1110
- const fieldsToCheck = attributesToCheck.filter((attr) => {
1111
- if (attr !== "xdefault") {
1112
- return true;
1113
- }
1114
- const dbRequired = Boolean((normalizedDbAttr as any).required);
1115
- const configRequired = Boolean((normalizedConfigAttr as any).required);
1116
- return !(dbRequired || configRequired);
1117
- });
1118
-
1119
- const differences: string[] = [];
1120
-
1121
- const result = fieldsToCheck.every((attr) => {
1122
- // Check if both objects have the attribute
1123
- const dbHasAttr = attr in normalizedDbAttr;
1124
- const configHasAttr = attr in normalizedConfigAttr;
1125
-
1126
- // If both have the attribute, compare values
1127
- if (dbHasAttr && configHasAttr) {
1128
- const dbValue = normalizedDbAttr[attr as keyof typeof normalizedDbAttr];
1129
- const configValue =
1130
- normalizedConfigAttr[attr as keyof typeof normalizedConfigAttr];
1131
-
1132
- // Consider undefined and null as equivalent
1133
- if (
1134
- (dbValue === undefined || dbValue === null) &&
1135
- (configValue === undefined || configValue === null)
1136
- ) {
1137
- return true;
1138
- }
1139
-
1140
- // Normalize booleans: treat undefined and false as equivalent
1141
- if (typeof dbValue === "boolean" || typeof configValue === "boolean") {
1142
- const boolMatch = Boolean(dbValue) === Boolean(configValue);
1143
- if (!boolMatch) {
1144
- differences.push(`${attr}: db=${dbValue} config=${configValue}`);
1145
- }
1146
- return boolMatch;
1147
- }
1148
- // For numeric comparisons, compare numbers if both are numeric-like
1149
- if (
1150
- (typeof dbValue === "number" ||
1151
- (typeof dbValue === "string" &&
1152
- dbValue !== "" &&
1153
- !isNaN(Number(dbValue)))) &&
1154
- (typeof configValue === "number" ||
1155
- (typeof configValue === "string" &&
1156
- configValue !== "" &&
1157
- !isNaN(Number(configValue))))
1158
- ) {
1159
- const numMatch = Number(dbValue) === Number(configValue);
1160
- if (!numMatch) {
1161
- differences.push(`${attr}: db=${dbValue} config=${configValue}`);
1162
- }
1163
- return numMatch;
1164
- }
1165
-
1166
- // For array comparisons (e.g., enum elements), use order-independent equality
1167
- if (Array.isArray(dbValue) && Array.isArray(configValue)) {
1168
- const arrayMatch =
1169
- dbValue.length === configValue.length &&
1170
- dbValue.every((val) => configValue.includes(val));
1171
- if (!arrayMatch) {
1172
- differences.push(
1173
- `${attr}: db=${JSON.stringify(dbValue)} config=${JSON.stringify(
1174
- configValue
1175
- )}`
1176
- );
1177
- }
1178
- return arrayMatch;
1179
- }
1180
-
1181
- const match = dbValue === configValue;
1182
- if (!match) {
1183
- differences.push(
1184
- `${attr}: db=${JSON.stringify(dbValue)} config=${JSON.stringify(
1185
- configValue
1186
- )}`
1187
- );
1188
- }
1189
- return match;
1190
- }
1191
-
1192
- // If neither has the attribute, consider it the same
1193
- if (!dbHasAttr && !configHasAttr) {
1194
- return true;
1195
- }
1196
-
1197
- // If one has the attribute and the other doesn't, check if it's undefined or null
1198
- if (dbHasAttr && !configHasAttr) {
1199
- const dbValue = normalizedDbAttr[attr as keyof typeof normalizedDbAttr];
1200
- // Consider default-false booleans as equal to missing in config
1201
- if (typeof dbValue === "boolean") {
1202
- const match = dbValue === false; // missing in config equals false in db
1203
- if (!match) {
1204
- differences.push(`${attr}: db=${dbValue} config=<missing>`);
1205
- }
1206
- return match;
1207
- }
1208
- const match = dbValue === undefined || dbValue === null;
1209
- if (!match) {
1210
- differences.push(
1211
- `${attr}: db=${JSON.stringify(dbValue)} config=<missing>`
1212
- );
1213
- }
1214
- return match;
1215
- }
1216
-
1217
- if (!dbHasAttr && configHasAttr) {
1218
- const configValue =
1219
- normalizedConfigAttr[attr as keyof typeof normalizedConfigAttr];
1220
- // Consider default-false booleans as equal to missing in db
1221
- if (typeof configValue === "boolean") {
1222
- const match = configValue === false; // missing in db equals false in config
1223
- if (!match) {
1224
- differences.push(`${attr}: db=<missing> config=${configValue}`);
1225
- }
1226
- return match;
1227
- }
1228
- const match = configValue === undefined || configValue === null;
1229
- if (!match) {
1230
- differences.push(
1231
- `${attr}: db=<missing> config=${JSON.stringify(configValue)}`
1232
- );
1233
- }
1234
- return match;
1235
- }
1236
-
1237
- // If we reach here, the attributes are different
1238
- differences.push(`${attr}: unexpected comparison state`);
1239
- return false;
1240
- });
1241
-
1242
- if (!result && differences.length > 0) {
1243
- logger.debug(
1244
- `Attribute mismatch detected for '${normalizedConfigAttr.key}'`,
1245
- {
1246
- differences,
1247
- dbAttribute: normalizedDbAttr,
1248
- configAttribute: normalizedConfigAttr,
1249
- operation: "attributesSame",
1250
- }
1251
- );
1252
- }
1253
-
1254
- return result;
1255
- };
1256
-
1257
- /**
1258
- * Enhanced attribute creation with proper status monitoring and retry logic
1259
- */
1260
- export const createOrUpdateAttributeWithStatusCheck = async (
1261
- db: Databases | DatabaseAdapter,
1262
- dbId: string,
1263
- collection: Models.Collection,
1264
- attribute: Attribute,
1265
- retryCount: number = 0,
1266
- maxRetries: number = 5
1267
- ): Promise<boolean> => {
1268
- try {
1269
- // First, try to create/update the attribute using existing logic
1270
- const result = await createOrUpdateAttribute(
1271
- db,
1272
- dbId,
1273
- collection,
1274
- attribute
1275
- );
1276
-
1277
- // If the attribute was queued (relationship dependency unresolved),
1278
- // skip status polling and retry logic — the queue will handle it later.
1279
- if (result === "queued") {
1280
- MessageFormatter.info(
1281
- chalk.yellow(
1282
- `⏭️ Deferred relationship attribute '${attribute.key}' — queued for later once dependencies are available`
1283
- )
1284
- );
1285
- return true;
1286
- }
1287
-
1288
- // If collection creation failed, return false to indicate failure
1289
- if (result === "error") {
1290
- MessageFormatter.error(
1291
- `Failed to create collection for attribute '${attribute.key}'`
1292
- );
1293
- return false;
1294
- }
1295
-
1296
- // Now wait for the attribute to become available
1297
- const success = await waitForAttributeAvailable(
1298
- db,
1299
- dbId,
1300
- collection.$id,
1301
- attribute.key,
1302
- 60000, // 1 minute timeout
1303
- retryCount,
1304
- maxRetries
1305
- );
1306
-
1307
- if (success) {
1308
- return true;
1309
- }
1310
-
1311
- // If not successful and we have retries left, delete specific attribute and try again
1312
- if (retryCount < maxRetries) {
1313
- MessageFormatter.info(
1314
- chalk.yellow(
1315
- `Attribute '${attribute.key}' failed/stuck, deleting and retrying...`
1316
- )
1317
- );
1318
-
1319
- // Try to delete the specific stuck attribute instead of the entire collection
1320
- try {
1321
- if (isDatabaseAdapter(db)) {
1322
- await db.deleteAttribute({
1323
- databaseId: dbId,
1324
- tableId: collection.$id,
1325
- key: attribute.key,
1326
- });
1327
- } else {
1328
- await db.deleteAttribute(dbId, collection.$id, attribute.key);
1329
- }
1330
- MessageFormatter.info(
1331
- chalk.yellow(
1332
- `Deleted stuck attribute '${attribute.key}', will retry creation`
1333
- )
1334
- );
1335
-
1336
- // Wait a bit before retry
1337
- await delay(3000);
1338
-
1339
- // Get fresh collection data
1340
- const freshCollection = isDatabaseAdapter(db)
1341
- ? (await db.getTable({ databaseId: dbId, tableId: collection.$id }))
1342
- .data
1343
- : await db.getCollection(dbId, collection.$id);
1344
-
1345
- // Retry with the same collection (attribute should be gone now)
1346
- return await createOrUpdateAttributeWithStatusCheck(
1347
- db,
1348
- dbId,
1349
- freshCollection,
1350
- attribute,
1351
- retryCount + 1,
1352
- maxRetries
1353
- );
1354
- } catch (deleteError) {
1355
- MessageFormatter.info(
1356
- chalk.red(
1357
- `Failed to delete stuck attribute '${attribute.key}': ${deleteError}`
1358
- )
1359
- );
1360
-
1361
- // If attribute deletion fails, only then try collection recreation as last resort
1362
- if (retryCount >= maxRetries - 1) {
1363
- MessageFormatter.info(
1364
- chalk.yellow(
1365
- `Last resort: Recreating collection for attribute '${attribute.key}'`
1366
- )
1367
- );
1368
-
1369
- // Get fresh collection data
1370
- const freshCollection = isDatabaseAdapter(db)
1371
- ? (await db.getTable({ databaseId: dbId, tableId: collection.$id }))
1372
- .data
1373
- : await db.getCollection(dbId, collection.$id);
1374
-
1375
- // Delete and recreate collection
1376
- const newCollection = await deleteAndRecreateCollection(
1377
- db,
1378
- dbId,
1379
- freshCollection,
1380
- retryCount + 1
1381
- );
1382
-
1383
- if (newCollection) {
1384
- // Retry with the new collection
1385
- return await createOrUpdateAttributeWithStatusCheck(
1386
- db,
1387
- dbId,
1388
- newCollection,
1389
- attribute,
1390
- retryCount + 1,
1391
- maxRetries
1392
- );
1393
- }
1394
- } else {
1395
- // Continue to next retry without collection recreation
1396
- return await createOrUpdateAttributeWithStatusCheck(
1397
- db,
1398
- dbId,
1399
- collection,
1400
- attribute,
1401
- retryCount + 1,
1402
- maxRetries
1403
- );
1404
- }
1405
- }
1406
- }
1407
-
1408
- MessageFormatter.info(
1409
- chalk.red(
1410
- `❌ Failed to create attribute '${attribute.key}' after ${
1411
- maxRetries + 1
1412
- } attempts`
1413
- )
1414
- );
1415
- return false;
1416
- } catch (error) {
1417
- MessageFormatter.info(
1418
- chalk.red(`Error creating attribute '${attribute.key}': ${error}`)
1419
- );
1420
-
1421
- if (retryCount < maxRetries) {
1422
- MessageFormatter.info(
1423
- chalk.yellow(`Retrying attribute '${attribute.key}' due to error...`)
1424
- );
1425
-
1426
- // Wait a bit before retry
1427
- await delay(2000);
1428
-
1429
- return await createOrUpdateAttributeWithStatusCheck(
1430
- db,
1431
- dbId,
1432
- collection,
1433
- attribute,
1434
- retryCount + 1,
1435
- maxRetries
1436
- );
1437
- }
1438
-
1439
- return false;
1440
- }
1441
- };
1442
-
1443
- export const createOrUpdateAttribute = async (
1444
- db: Databases | DatabaseAdapter,
1445
- dbId: string,
1446
- collection: Models.Collection,
1447
- attribute: Attribute
1448
- ): Promise<"queued" | "processed" | "error"> => {
1449
- let action = "create";
1450
- let foundAttribute: Attribute | undefined;
1451
- const updateEnabled = true;
1452
- let finalAttribute: any = attribute;
1453
- try {
1454
- const collectionAttr = collection.attributes.find(
1455
- (attr: any) => attr.key === attribute.key
1456
- ) as unknown as any;
1457
- foundAttribute = parseAttribute(collectionAttr);
1458
- } catch (error) {
1459
- foundAttribute = undefined;
1460
- }
1461
-
1462
- // If attribute exists but type changed, delete it so we can recreate with new type
1463
- if (
1464
- foundAttribute &&
1465
- foundAttribute.type !== attribute.type
1466
- ) {
1467
- MessageFormatter.info(
1468
- chalk.yellow(
1469
- `Attribute '${attribute.key}' type changed from '${foundAttribute.type}' to '${attribute.type}'. Recreating attribute.`
1470
- )
1471
- );
1472
- try {
1473
- if (isDatabaseAdapter(db)) {
1474
- await db.deleteAttribute({
1475
- databaseId: dbId,
1476
- tableId: collection.$id,
1477
- key: attribute.key
1478
- });
1479
- } else {
1480
- await db.deleteAttribute(dbId, collection.$id, attribute.key);
1481
- }
1482
- // Remove from local collection metadata so downstream logic treats it as new
1483
- collection.attributes = collection.attributes.filter(
1484
- (attr: any) => attr.key !== attribute.key
1485
- );
1486
- foundAttribute = undefined;
1487
- } catch (deleteError) {
1488
- MessageFormatter.error(
1489
- `Failed to delete attribute '${attribute.key}' before recreation: ${deleteError}`
1490
- );
1491
- return "error";
1492
- }
1493
- }
1494
-
1495
- if (
1496
- foundAttribute &&
1497
- attributesSame(foundAttribute, attribute) &&
1498
- updateEnabled
1499
- ) {
1500
- // No need to do anything, they are the same
1501
- return "processed";
1502
- } else if (
1503
- foundAttribute &&
1504
- !attributesSame(foundAttribute, attribute) &&
1505
- updateEnabled
1506
- ) {
1507
- // MessageFormatter.info(
1508
- // `Updating attribute with same key ${attribute.key} but different values`
1509
- // );
1510
-
1511
- finalAttribute = {
1512
- ...foundAttribute,
1513
- ...attribute,
1514
- };
1515
- action = "update";
1516
- } else if (
1517
- !updateEnabled &&
1518
- foundAttribute &&
1519
- !attributesSame(foundAttribute, attribute)
1520
- ) {
1521
- if (isDatabaseAdapter(db)) {
1522
- await db.deleteAttribute({
1523
- databaseId: dbId,
1524
- tableId: collection.$id,
1525
- key: attribute.key,
1526
- });
1527
- } else {
1528
- await db.deleteAttribute(dbId, collection.$id, attribute.key);
1529
- }
1530
- MessageFormatter.info(
1531
- `Deleted attribute: ${attribute.key} to recreate it because they diff (update disabled temporarily)`
1532
- );
1533
- return "processed";
1534
- }
1535
-
1536
- // Relationship attribute logic with adjustments
1537
- let collectionFoundViaRelatedCollection: Models.Collection | undefined;
1538
- let relatedCollectionId: string | undefined;
1539
- if (
1540
- finalAttribute.type === "relationship" &&
1541
- finalAttribute.relatedCollection
1542
- ) {
1543
- // First try treating relatedCollection as an ID directly
1544
- try {
1545
- const byIdCollection = isDatabaseAdapter(db)
1546
- ? (
1547
- await db.getTable({
1548
- databaseId: dbId,
1549
- tableId: finalAttribute.relatedCollection,
1550
- })
1551
- ).data
1552
- : await db.getCollection(dbId, finalAttribute.relatedCollection);
1553
- collectionFoundViaRelatedCollection = byIdCollection;
1554
- relatedCollectionId = byIdCollection.$id;
1555
- // Cache by name for subsequent lookups
1556
- nameToIdMapping.set(byIdCollection.name, byIdCollection.$id);
1557
- } catch (_) {
1558
- // Not an ID or not found — fall back to name-based resolution below
1559
- }
1560
-
1561
- if (
1562
- !collectionFoundViaRelatedCollection &&
1563
- nameToIdMapping.has(finalAttribute.relatedCollection)
1564
- ) {
1565
- relatedCollectionId = nameToIdMapping.get(
1566
- finalAttribute.relatedCollection
1567
- );
1568
- try {
1569
- collectionFoundViaRelatedCollection = isDatabaseAdapter(db)
1570
- ? (
1571
- await db.getTable({
1572
- databaseId: dbId,
1573
- tableId: relatedCollectionId!,
1574
- })
1575
- ).data
1576
- : await db.getCollection(dbId, relatedCollectionId!);
1577
- } catch (e) {
1578
- // MessageFormatter.info(
1579
- // `Collection not found: ${finalAttribute.relatedCollection} when nameToIdMapping was set`
1580
- // );
1581
- collectionFoundViaRelatedCollection = undefined;
1582
- }
1583
- } else if (!collectionFoundViaRelatedCollection) {
1584
- const collectionsPulled = isDatabaseAdapter(db)
1585
- ? await db.listTables({
1586
- databaseId: dbId,
1587
- queries: [Query.equal("name", finalAttribute.relatedCollection)],
1588
- })
1589
- : await db.listCollections(dbId, [
1590
- Query.equal("name", finalAttribute.relatedCollection),
1591
- ]);
1592
- if (collectionsPulled.total && collectionsPulled.total > 0) {
1593
- collectionFoundViaRelatedCollection = isDatabaseAdapter(db)
1594
- ? (collectionsPulled as any).tables?.[0]
1595
- : (collectionsPulled as any).collections?.[0];
1596
- relatedCollectionId = collectionFoundViaRelatedCollection?.$id;
1597
- if (relatedCollectionId) {
1598
- nameToIdMapping.set(
1599
- finalAttribute.relatedCollection,
1600
- relatedCollectionId
1601
- );
1602
- }
1603
- }
1604
- }
1605
- // ONLY queue relationship attributes that have actual unresolved dependencies
1606
- if (!(relatedCollectionId && collectionFoundViaRelatedCollection)) {
1607
- MessageFormatter.info(
1608
- chalk.yellow(
1609
- `⏳ Queueing relationship attribute '${finalAttribute.key}' - related collection '${finalAttribute.relatedCollection}' not found yet`
1610
- )
1611
- );
1612
- enqueueOperation({
1613
- type: "attribute",
1614
- collectionId: collection.$id,
1615
- collection: collection,
1616
- attribute,
1617
- dependencies: [finalAttribute.relatedCollection],
1618
- });
1619
- return "queued";
1620
- }
1621
- }
1622
- finalAttribute = parseAttribute(finalAttribute);
1623
-
1624
- // Ensure collection/table exists - create it if it doesn't
1625
- try {
1626
- await (isDatabaseAdapter(db)
1627
- ? db.getTable({ databaseId: dbId, tableId: collection.$id })
1628
- : db.getCollection(dbId, collection.$id));
1629
- } catch (error) {
1630
- // Collection doesn't exist - create it
1631
- if (
1632
- (error as any).code === 404 ||
1633
- (error instanceof Error &&
1634
- (error.message.includes("collection_not_found") ||
1635
- error.message.includes(
1636
- "Collection with the requested ID could not be found"
1637
- )))
1638
- ) {
1639
- MessageFormatter.info(
1640
- `Collection '${collection.name}' doesn't exist, creating it first...`
1641
- );
1642
-
1643
- try {
1644
- if (isDatabaseAdapter(db)) {
1645
- await db.createTable({
1646
- databaseId: dbId,
1647
- id: collection.$id,
1648
- name: collection.name,
1649
- permissions: collection.$permissions || [],
1650
- documentSecurity: collection.documentSecurity ?? false,
1651
- enabled: collection.enabled ?? true,
1652
- });
1653
- } else {
1654
- await db.createCollection(
1655
- dbId,
1656
- collection.$id,
1657
- collection.name,
1658
- collection.$permissions || [],
1659
- collection.documentSecurity ?? false,
1660
- collection.enabled ?? true
1661
- );
1662
- }
1663
-
1664
- MessageFormatter.success(`Created collection '${collection.name}'`);
1665
- await delay(500); // Wait for collection to be ready
1666
- } catch (createError) {
1667
- MessageFormatter.error(
1668
- `Failed to create collection '${collection.name}'`,
1669
- createError instanceof Error
1670
- ? createError
1671
- : new Error(String(createError))
1672
- );
1673
- return "error";
1674
- }
1675
- } else {
1676
- // Other error - re-throw
1677
- throw error;
1678
- }
1679
- }
1680
-
1681
- // Use adapter-based attribute creation/update
1682
- if (action === "create") {
1683
- await tryAwaitWithRetry(
1684
- async () =>
1685
- await createAttributeViaAdapter(
1686
- db,
1687
- dbId,
1688
- collection.$id,
1689
- finalAttribute
1690
- )
1691
- );
1692
- } else {
1693
- console.log(`Updating attribute '${finalAttribute.key}'...`);
1694
- if (finalAttribute.type === "double" || finalAttribute.type === "integer") {
1695
- console.log("finalAttribute:", finalAttribute);
1696
- }
1697
- await tryAwaitWithRetry(
1698
- async () =>
1699
- await updateAttributeViaAdapter(
1700
- db,
1701
- dbId,
1702
- collection.$id,
1703
- finalAttribute
1704
- )
1705
- );
1706
- }
1707
- return "processed";
1708
- };
1709
-
1710
- /**
1711
- * Enhanced collection attribute creation with proper status monitoring
1712
- */
1713
- export const createUpdateCollectionAttributesWithStatusCheck = async (
1714
- db: Databases | DatabaseAdapter,
1715
- dbId: string,
1716
- collection: Models.Collection,
1717
- attributes: Attribute[]
1718
- ): Promise<boolean> => {
1719
- const existingAttributes: Attribute[] =
1720
- collection.attributes.map((attr) => parseAttribute(attr as any)) || [];
1721
-
1722
- const attributesToRemove = existingAttributes.filter(
1723
- (attr) => !attributes.some((a) => a.key === attr.key)
1724
- );
1725
- const indexesToRemove = collection.indexes.filter((index) =>
1726
- attributesToRemove.some((attr) => index.attributes.includes(attr.key))
1727
- );
1728
-
1729
- // Handle attribute removal first
1730
- if (attributesToRemove.length > 0) {
1731
- if (indexesToRemove.length > 0) {
1732
- MessageFormatter.info(
1733
- chalk.red(
1734
- `Removing indexes as they rely on an attribute that is being removed: ${indexesToRemove
1735
- .map((index) => index.key)
1736
- .join(", ")}`
1737
- )
1738
- );
1739
- for (const index of indexesToRemove) {
1740
- await tryAwaitWithRetry(async () => {
1741
- if (isDatabaseAdapter(db)) {
1742
- await db.deleteIndex({
1743
- databaseId: dbId,
1744
- tableId: collection.$id,
1745
- key: index.key,
1746
- });
1747
- } else {
1748
- await db.deleteIndex(dbId, collection.$id, index.key);
1749
- }
1750
- });
1751
- await delay(500); // Longer delay for deletions
1752
- }
1753
- }
1754
- for (const attr of attributesToRemove) {
1755
- MessageFormatter.info(
1756
- chalk.red(
1757
- `Removing attribute: ${attr.key} as it is no longer in the collection`
1758
- )
1759
- );
1760
- await tryAwaitWithRetry(async () => {
1761
- if (isDatabaseAdapter(db)) {
1762
- await db.deleteAttribute({
1763
- databaseId: dbId,
1764
- tableId: collection.$id,
1765
- key: attr.key,
1766
- });
1767
- } else {
1768
- await db.deleteAttribute(dbId, collection.$id, attr.key);
1769
- }
1770
- });
1771
- await delay(500); // Longer delay for deletions
1772
- }
1773
- }
1774
-
1775
- // First, get fresh collection data and determine which attributes actually need processing
1776
- let currentCollection = collection;
1777
- try {
1778
- currentCollection = isDatabaseAdapter(db)
1779
- ? (await db.getTable({ databaseId: dbId, tableId: collection.$id })).data
1780
- : await db.getCollection(dbId, collection.$id);
1781
- } catch (error) {
1782
- MessageFormatter.info(
1783
- chalk.yellow(`Warning: Could not refresh collection data: ${error}`)
1784
- );
1785
- }
1786
-
1787
- const existingAttributesMap = new Map<string, Attribute>();
1788
- try {
1789
- const parsedAttributes = currentCollection.attributes.map((attr) =>
1790
- parseAttribute(attr as any)
1791
- );
1792
- parsedAttributes.forEach((attr) =>
1793
- existingAttributesMap.set(attr.key, attr)
1794
- );
1795
- } catch (error) {
1796
- MessageFormatter.info(
1797
- chalk.yellow(`Warning: Could not parse existing attributes: ${error}`)
1798
- );
1799
- }
1800
-
1801
- // Filter to only attributes that need processing (new, changed, or not yet processed)
1802
- const attributesToProcess = attributes.filter((attribute) => {
1803
- // Skip if already processed in this session
1804
- if (isAttributeProcessed(dbId, currentCollection.$id, attribute.key)) {
1805
- return false;
1806
- }
1807
-
1808
- const existing = existingAttributesMap.get(attribute.key);
1809
- if (!existing) {
1810
- MessageFormatter.info(`➕ ${attribute.key}`);
1811
- return true;
1812
- }
1813
-
1814
- const needsUpdate = !attributesSame(existing, parseAttribute(attribute));
1815
- if (needsUpdate) {
1816
- MessageFormatter.info(`🔄 ${attribute.key}`);
1817
- } else {
1818
- MessageFormatter.info(chalk.gray(`✅ ${attribute.key}`));
1819
- }
1820
- return needsUpdate;
1821
- });
1822
-
1823
- if (attributesToProcess.length === 0) {
1824
- return true;
1825
- }
1826
-
1827
- let remainingAttributes = [...attributesToProcess];
1828
- let overallRetryCount = 0;
1829
- const maxOverallRetries = 3;
1830
-
1831
- while (
1832
- remainingAttributes.length > 0 &&
1833
- overallRetryCount < maxOverallRetries
1834
- ) {
1835
- const attributesToProcessThisRound = [...remainingAttributes];
1836
- remainingAttributes = []; // Reset for next iteration
1837
-
1838
- for (const attribute of attributesToProcessThisRound) {
1839
- const success = await createOrUpdateAttributeWithStatusCheck(
1840
- db,
1841
- dbId,
1842
- currentCollection,
1843
- attribute
1844
- );
1845
-
1846
- if (success) {
1847
- // Mark this specific attribute as processed
1848
- markAttributeProcessed(dbId, currentCollection.$id, attribute.key);
1849
-
1850
- // Get updated collection data for next iteration
1851
- try {
1852
- currentCollection = isDatabaseAdapter(db)
1853
- ? ((
1854
- await db.getTable({ databaseId: dbId, tableId: collection.$id })
1855
- ).data as Models.Collection)
1856
- : await db.getCollection({
1857
- databaseId: dbId,
1858
- collectionId: collection.$id,
1859
- });
1860
- } catch (error) {
1861
- MessageFormatter.info(
1862
- chalk.yellow(`Warning: Could not refresh collection data: ${error}`)
1863
- );
1864
- }
1865
-
1866
- // Add delay between successful attributes
1867
- await delay(1000);
1868
- } else {
1869
- MessageFormatter.info(chalk.red(`❌ ${attribute.key}`));
1870
- remainingAttributes.push(attribute); // Add back to retry list
1871
- }
1872
- }
1873
-
1874
- if (remainingAttributes.length === 0) {
1875
- return true;
1876
- }
1877
-
1878
- overallRetryCount++;
1879
-
1880
- if (overallRetryCount < maxOverallRetries) {
1881
- MessageFormatter.info(
1882
- chalk.yellow(
1883
- `⏳ Retrying ${remainingAttributes.length} failed attributes...`
1884
- )
1885
- );
1886
- await delay(5000);
1887
-
1888
- // Refresh collection data before retry
1889
- try {
1890
- currentCollection = isDatabaseAdapter(db)
1891
- ? ((await db.getTable({ databaseId: dbId, tableId: collection.$id }))
1892
- .data as Models.Collection)
1893
- : await db.getCollection(dbId, collection.$id);
1894
- } catch (error) {
1895
- // Silently continue if refresh fails
1896
- }
1897
- }
1898
- }
1899
-
1900
- // If we get here, some attributes still failed after all retries
1901
- if (attributesToProcess.length > 0) {
1902
- MessageFormatter.info(
1903
- chalk.red(
1904
- `\n❌ Failed to create ${
1905
- attributesToProcess.length
1906
- } attributes after ${maxOverallRetries} attempts: ${attributesToProcess
1907
- .map((a) => a.key)
1908
- .join(", ")}`
1909
- )
1910
- );
1911
- MessageFormatter.info(
1912
- chalk.red(
1913
- `This may indicate a fundamental issue with the attribute definitions or Appwrite instance`
1914
- )
1915
- );
1916
- return false;
1917
- }
1918
-
1919
- MessageFormatter.info(
1920
- chalk.green(
1921
- `\n✅ Successfully created all ${attributes.length} attributes for collection: ${collection.name}`
1922
- )
1923
- );
1924
- return true;
1925
- };
1926
-
1927
- export const createUpdateCollectionAttributes = async (
1928
- db: Databases | DatabaseAdapter,
1929
- dbId: string,
1930
- collection: Models.Collection,
1931
- attributes: Attribute[]
1932
- ): Promise<void> => {
1933
- MessageFormatter.info(
1934
- chalk.green(
1935
- `Creating/Updating attributes for collection: ${collection.name}`
1936
- )
1937
- );
1938
-
1939
- const existingAttributes: Attribute[] =
1940
- collection.attributes.map((attr) => parseAttribute(attr as any)) || [];
1941
-
1942
- const attributesToRemove = existingAttributes.filter(
1943
- (attr) => !attributes.some((a) => a.key === attr.key)
1944
- );
1945
- const indexesToRemove = collection.indexes.filter((index) =>
1946
- attributesToRemove.some((attr) => index.attributes.includes(attr.key))
1947
- );
1948
-
1949
- if (attributesToRemove.length > 0) {
1950
- if (indexesToRemove.length > 0) {
1951
- MessageFormatter.info(
1952
- chalk.red(
1953
- `Removing indexes as they rely on an attribute that is being removed: ${indexesToRemove
1954
- .map((index) => index.key)
1955
- .join(", ")}`
1956
- )
1957
- );
1958
- for (const index of indexesToRemove) {
1959
- await tryAwaitWithRetry(async () => {
1960
- if (isDatabaseAdapter(db)) {
1961
- await db.deleteIndex({
1962
- databaseId: dbId,
1963
- tableId: collection.$id,
1964
- key: index.key,
1965
- });
1966
- } else {
1967
- await db.deleteIndex(dbId, collection.$id, index.key);
1968
- }
1969
- });
1970
- await delay(100);
1971
- }
1972
- }
1973
- for (const attr of attributesToRemove) {
1974
- MessageFormatter.info(
1975
- chalk.red(
1976
- `Removing attribute: ${attr.key} as it is no longer in the collection`
1977
- )
1978
- );
1979
- await tryAwaitWithRetry(async () => {
1980
- if (isDatabaseAdapter(db)) {
1981
- await db.deleteAttribute({
1982
- databaseId: dbId,
1983
- tableId: collection.$id,
1984
- key: attr.key,
1985
- });
1986
- } else {
1987
- await db.deleteAttribute(dbId, collection.$id, attr.key);
1988
- }
1989
- });
1990
- await delay(50);
1991
- }
1992
- }
1993
-
1994
- const batchSize = 3;
1995
- for (let i = 0; i < attributes.length; i += batchSize) {
1996
- const batch = attributes.slice(i, i + batchSize);
1997
- const attributePromises = batch.map((attribute) =>
1998
- tryAwaitWithRetry(
1999
- async () =>
2000
- await createOrUpdateAttribute(db, dbId, collection, attribute)
2001
- )
2002
- );
2003
-
2004
- const results = await Promise.allSettled(attributePromises);
2005
- results.forEach((result) => {
2006
- if (result.status === "rejected") {
2007
- MessageFormatter.error(
2008
- "An attribute promise was rejected:",
2009
- result.reason
2010
- );
2011
- }
2012
- });
2013
-
2014
- // Add delay after each batch
2015
- await delay(200);
2016
- }
2017
- MessageFormatter.info(
2018
- `Finished creating/updating attributes for collection: ${collection.name}`
2019
- );
2020
- };