appwrite-utils-cli 1.5.2 → 1.6.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 (233) hide show
  1. package/CHANGELOG.md +199 -0
  2. package/README.md +251 -29
  3. package/dist/adapters/AdapterFactory.d.ts +10 -3
  4. package/dist/adapters/AdapterFactory.js +213 -17
  5. package/dist/adapters/TablesDBAdapter.js +60 -17
  6. package/dist/backups/operations/bucketBackup.d.ts +19 -0
  7. package/dist/backups/operations/bucketBackup.js +197 -0
  8. package/dist/backups/operations/collectionBackup.d.ts +30 -0
  9. package/dist/backups/operations/collectionBackup.js +201 -0
  10. package/dist/backups/operations/comprehensiveBackup.d.ts +25 -0
  11. package/dist/backups/operations/comprehensiveBackup.js +238 -0
  12. package/dist/backups/schemas/bucketManifest.d.ts +93 -0
  13. package/dist/backups/schemas/bucketManifest.js +33 -0
  14. package/dist/backups/schemas/comprehensiveManifest.d.ts +108 -0
  15. package/dist/backups/schemas/comprehensiveManifest.js +32 -0
  16. package/dist/backups/tracking/centralizedTracking.d.ts +34 -0
  17. package/dist/backups/tracking/centralizedTracking.js +274 -0
  18. package/dist/cli/commands/configCommands.d.ts +8 -0
  19. package/dist/cli/commands/configCommands.js +160 -0
  20. package/dist/cli/commands/databaseCommands.d.ts +13 -0
  21. package/dist/cli/commands/databaseCommands.js +478 -0
  22. package/dist/cli/commands/functionCommands.d.ts +7 -0
  23. package/dist/cli/commands/functionCommands.js +289 -0
  24. package/dist/cli/commands/schemaCommands.d.ts +7 -0
  25. package/dist/cli/commands/schemaCommands.js +134 -0
  26. package/dist/cli/commands/transferCommands.d.ts +5 -0
  27. package/dist/cli/commands/transferCommands.js +384 -0
  28. package/dist/collections/attributes.d.ts +5 -4
  29. package/dist/collections/attributes.js +539 -246
  30. package/dist/collections/indexes.js +39 -37
  31. package/dist/collections/methods.d.ts +2 -16
  32. package/dist/collections/methods.js +90 -538
  33. package/dist/collections/transferOperations.d.ts +7 -0
  34. package/dist/collections/transferOperations.js +331 -0
  35. package/dist/collections/wipeOperations.d.ts +16 -0
  36. package/dist/collections/wipeOperations.js +328 -0
  37. package/dist/config/configMigration.d.ts +87 -0
  38. package/dist/config/configMigration.js +390 -0
  39. package/dist/config/configValidation.d.ts +66 -0
  40. package/dist/config/configValidation.js +358 -0
  41. package/dist/config/yamlConfig.d.ts +455 -1
  42. package/dist/config/yamlConfig.js +145 -52
  43. package/dist/databases/methods.js +3 -2
  44. package/dist/databases/setup.d.ts +1 -2
  45. package/dist/databases/setup.js +9 -87
  46. package/dist/examples/yamlTerminologyExample.d.ts +42 -0
  47. package/dist/examples/yamlTerminologyExample.js +269 -0
  48. package/dist/functions/deployments.js +11 -10
  49. package/dist/functions/methods.d.ts +1 -1
  50. package/dist/functions/methods.js +5 -4
  51. package/dist/init.js +9 -9
  52. package/dist/interactiveCLI.d.ts +8 -17
  53. package/dist/interactiveCLI.js +181 -1172
  54. package/dist/main.js +364 -21
  55. package/dist/migrations/afterImportActions.js +22 -30
  56. package/dist/migrations/appwriteToX.js +71 -25
  57. package/dist/migrations/dataLoader.js +35 -26
  58. package/dist/migrations/importController.js +29 -30
  59. package/dist/migrations/relationships.js +13 -12
  60. package/dist/migrations/services/ImportOrchestrator.js +16 -19
  61. package/dist/migrations/transfer.js +46 -46
  62. package/dist/migrations/yaml/YamlImportConfigLoader.d.ts +3 -1
  63. package/dist/migrations/yaml/YamlImportConfigLoader.js +6 -3
  64. package/dist/migrations/yaml/YamlImportIntegration.d.ts +9 -3
  65. package/dist/migrations/yaml/YamlImportIntegration.js +22 -11
  66. package/dist/migrations/yaml/generateImportSchemas.d.ts +14 -1
  67. package/dist/migrations/yaml/generateImportSchemas.js +736 -7
  68. package/dist/schemas/authUser.d.ts +1 -1
  69. package/dist/setupController.js +3 -2
  70. package/dist/shared/backupMetadataSchema.d.ts +94 -0
  71. package/dist/shared/backupMetadataSchema.js +38 -0
  72. package/dist/shared/backupTracking.d.ts +18 -0
  73. package/dist/shared/backupTracking.js +176 -0
  74. package/dist/shared/confirmationDialogs.js +15 -15
  75. package/dist/shared/errorUtils.d.ts +54 -0
  76. package/dist/shared/errorUtils.js +95 -0
  77. package/dist/shared/functionManager.js +20 -19
  78. package/dist/shared/indexManager.js +12 -11
  79. package/dist/shared/jsonSchemaGenerator.js +10 -26
  80. package/dist/shared/logging.d.ts +51 -0
  81. package/dist/shared/logging.js +70 -0
  82. package/dist/shared/messageFormatter.d.ts +2 -0
  83. package/dist/shared/messageFormatter.js +10 -0
  84. package/dist/shared/migrationHelpers.d.ts +6 -16
  85. package/dist/shared/migrationHelpers.js +24 -21
  86. package/dist/shared/operationLogger.d.ts +8 -1
  87. package/dist/shared/operationLogger.js +11 -24
  88. package/dist/shared/operationQueue.d.ts +28 -1
  89. package/dist/shared/operationQueue.js +268 -66
  90. package/dist/shared/operationsTable.d.ts +26 -0
  91. package/dist/shared/operationsTable.js +286 -0
  92. package/dist/shared/operationsTableSchema.d.ts +48 -0
  93. package/dist/shared/operationsTableSchema.js +35 -0
  94. package/dist/shared/relationshipExtractor.d.ts +56 -0
  95. package/dist/shared/relationshipExtractor.js +138 -0
  96. package/dist/shared/schemaGenerator.d.ts +19 -1
  97. package/dist/shared/schemaGenerator.js +56 -75
  98. package/dist/storage/backupCompression.d.ts +20 -0
  99. package/dist/storage/backupCompression.js +67 -0
  100. package/dist/storage/methods.d.ts +16 -2
  101. package/dist/storage/methods.js +98 -14
  102. package/dist/users/methods.js +9 -8
  103. package/dist/utils/configDiscovery.d.ts +78 -0
  104. package/dist/utils/configDiscovery.js +430 -0
  105. package/dist/utils/directoryUtils.d.ts +22 -0
  106. package/dist/utils/directoryUtils.js +59 -0
  107. package/dist/utils/getClientFromConfig.d.ts +17 -8
  108. package/dist/utils/getClientFromConfig.js +162 -17
  109. package/dist/utils/helperFunctions.d.ts +16 -2
  110. package/dist/utils/helperFunctions.js +19 -5
  111. package/dist/utils/loadConfigs.d.ts +34 -9
  112. package/dist/utils/loadConfigs.js +236 -316
  113. package/dist/utils/pathResolvers.d.ts +53 -0
  114. package/dist/utils/pathResolvers.js +72 -0
  115. package/dist/utils/projectConfig.d.ts +119 -0
  116. package/dist/utils/projectConfig.js +171 -0
  117. package/dist/utils/retryFailedPromises.js +4 -2
  118. package/dist/utils/sessionAuth.d.ts +48 -0
  119. package/dist/utils/sessionAuth.js +164 -0
  120. package/dist/utils/sessionPreservationExample.d.ts +1666 -0
  121. package/dist/utils/sessionPreservationExample.js +101 -0
  122. package/dist/utils/setupFiles.js +301 -41
  123. package/dist/utils/typeGuards.d.ts +35 -0
  124. package/dist/utils/typeGuards.js +57 -0
  125. package/dist/utils/versionDetection.js +145 -9
  126. package/dist/utils/yamlConverter.d.ts +53 -3
  127. package/dist/utils/yamlConverter.js +232 -13
  128. package/dist/utils/yamlLoader.d.ts +70 -0
  129. package/dist/utils/yamlLoader.js +263 -0
  130. package/dist/utilsController.d.ts +36 -3
  131. package/dist/utilsController.js +186 -56
  132. package/package.json +12 -2
  133. package/src/adapters/AdapterFactory.ts +263 -35
  134. package/src/adapters/TablesDBAdapter.ts +225 -36
  135. package/src/backups/operations/bucketBackup.ts +277 -0
  136. package/src/backups/operations/collectionBackup.ts +310 -0
  137. package/src/backups/operations/comprehensiveBackup.ts +342 -0
  138. package/src/backups/schemas/bucketManifest.ts +78 -0
  139. package/src/backups/schemas/comprehensiveManifest.ts +76 -0
  140. package/src/backups/tracking/centralizedTracking.ts +352 -0
  141. package/src/cli/commands/configCommands.ts +194 -0
  142. package/src/cli/commands/databaseCommands.ts +635 -0
  143. package/src/cli/commands/functionCommands.ts +379 -0
  144. package/src/cli/commands/schemaCommands.ts +163 -0
  145. package/src/cli/commands/transferCommands.ts +457 -0
  146. package/src/collections/attributes.ts +900 -621
  147. package/src/collections/attributes.ts.backup +1555 -0
  148. package/src/collections/indexes.ts +116 -114
  149. package/src/collections/methods.ts +295 -968
  150. package/src/collections/transferOperations.ts +516 -0
  151. package/src/collections/wipeOperations.ts +501 -0
  152. package/src/config/README.md +274 -0
  153. package/src/config/configMigration.ts +575 -0
  154. package/src/config/configValidation.ts +445 -0
  155. package/src/config/yamlConfig.ts +168 -55
  156. package/src/databases/methods.ts +3 -2
  157. package/src/databases/setup.ts +11 -138
  158. package/src/examples/yamlTerminologyExample.ts +341 -0
  159. package/src/functions/deployments.ts +14 -12
  160. package/src/functions/methods.ts +11 -11
  161. package/src/functions/templates/hono-typescript/README.md +286 -0
  162. package/src/functions/templates/hono-typescript/package.json +26 -0
  163. package/src/functions/templates/hono-typescript/src/adapters/request.ts +74 -0
  164. package/src/functions/templates/hono-typescript/src/adapters/response.ts +106 -0
  165. package/src/functions/templates/hono-typescript/src/app.ts +180 -0
  166. package/src/functions/templates/hono-typescript/src/context.ts +103 -0
  167. package/src/functions/templates/hono-typescript/src/index.ts +54 -0
  168. package/src/functions/templates/hono-typescript/src/middleware/appwrite.ts +119 -0
  169. package/src/functions/templates/hono-typescript/tsconfig.json +20 -0
  170. package/src/functions/templates/typescript-node/package.json +2 -1
  171. package/src/functions/templates/typescript-node/src/context.ts +103 -0
  172. package/src/functions/templates/typescript-node/src/index.ts +18 -12
  173. package/src/functions/templates/uv/pyproject.toml +1 -0
  174. package/src/functions/templates/uv/src/context.py +125 -0
  175. package/src/functions/templates/uv/src/index.py +35 -5
  176. package/src/init.ts +9 -11
  177. package/src/interactiveCLI.ts +278 -1596
  178. package/src/main.ts +418 -24
  179. package/src/migrations/afterImportActions.ts +71 -44
  180. package/src/migrations/appwriteToX.ts +100 -34
  181. package/src/migrations/dataLoader.ts +48 -34
  182. package/src/migrations/importController.ts +44 -39
  183. package/src/migrations/relationships.ts +28 -18
  184. package/src/migrations/services/ImportOrchestrator.ts +24 -27
  185. package/src/migrations/transfer.ts +159 -121
  186. package/src/migrations/yaml/YamlImportConfigLoader.ts +11 -4
  187. package/src/migrations/yaml/YamlImportIntegration.ts +47 -20
  188. package/src/migrations/yaml/generateImportSchemas.ts +751 -12
  189. package/src/setupController.ts +3 -2
  190. package/src/shared/backupMetadataSchema.ts +93 -0
  191. package/src/shared/backupTracking.ts +211 -0
  192. package/src/shared/confirmationDialogs.ts +19 -19
  193. package/src/shared/errorUtils.ts +110 -0
  194. package/src/shared/functionManager.ts +21 -20
  195. package/src/shared/indexManager.ts +12 -11
  196. package/src/shared/jsonSchemaGenerator.ts +38 -52
  197. package/src/shared/logging.ts +75 -0
  198. package/src/shared/messageFormatter.ts +14 -1
  199. package/src/shared/migrationHelpers.ts +45 -38
  200. package/src/shared/operationLogger.ts +11 -36
  201. package/src/shared/operationQueue.ts +322 -93
  202. package/src/shared/operationsTable.ts +338 -0
  203. package/src/shared/operationsTableSchema.ts +60 -0
  204. package/src/shared/relationshipExtractor.ts +214 -0
  205. package/src/shared/schemaGenerator.ts +179 -219
  206. package/src/storage/backupCompression.ts +88 -0
  207. package/src/storage/methods.ts +131 -34
  208. package/src/users/methods.ts +11 -9
  209. package/src/utils/configDiscovery.ts +502 -0
  210. package/src/utils/directoryUtils.ts +61 -0
  211. package/src/utils/getClientFromConfig.ts +205 -22
  212. package/src/utils/helperFunctions.ts +23 -5
  213. package/src/utils/loadConfigs.ts +313 -345
  214. package/src/utils/pathResolvers.ts +81 -0
  215. package/src/utils/projectConfig.ts +299 -0
  216. package/src/utils/retryFailedPromises.ts +4 -2
  217. package/src/utils/sessionAuth.ts +230 -0
  218. package/src/utils/setupFiles.ts +322 -54
  219. package/src/utils/typeGuards.ts +65 -0
  220. package/src/utils/versionDetection.ts +218 -64
  221. package/src/utils/yamlConverter.ts +296 -13
  222. package/src/utils/yamlLoader.ts +364 -0
  223. package/src/utilsController.ts +314 -110
  224. package/tests/README.md +497 -0
  225. package/tests/adapters/AdapterFactory.test.ts +277 -0
  226. package/tests/integration/syncOperations.test.ts +463 -0
  227. package/tests/jest.config.js +25 -0
  228. package/tests/migration/configMigration.test.ts +546 -0
  229. package/tests/setup.ts +62 -0
  230. package/tests/testUtils.ts +340 -0
  231. package/tests/utils/loadConfigs.test.ts +350 -0
  232. package/tests/validation/configValidation.test.ts +412 -0
  233. package/src/utils/schemaStrings.ts +0 -517
@@ -4,9 +4,530 @@ import {
4
4
  parseAttribute,
5
5
  type Attribute,
6
6
  } from "appwrite-utils";
7
- import { nameToIdMapping, enqueueOperation } from "../shared/operationQueue.js";
8
- import { delay, tryAwaitWithRetry } from "../utils/helperFunctions.js";
7
+ import {
8
+ nameToIdMapping,
9
+ enqueueOperation,
10
+ markAttributeProcessed,
11
+ isAttributeProcessed
12
+ } from "../shared/operationQueue.js";
13
+ import { delay, tryAwaitWithRetry, calculateExponentialBackoff } from "../utils/helperFunctions.js";
9
14
  import chalk from "chalk";
15
+ import type { DatabaseAdapter, CreateAttributeParams, UpdateAttributeParams, DeleteAttributeParams } from "../adapters/DatabaseAdapter.js";
16
+ import { logger } from "../shared/logging.js";
17
+ import { MessageFormatter } from "../shared/messageFormatter.js";
18
+ import { isDatabaseAdapter } from "../utils/typeGuards.js";
19
+
20
+ // Threshold for treating min/max values as undefined (10 billion)
21
+ const MIN_MAX_THRESHOLD = 10_000_000_000;
22
+
23
+ // Extreme values that Appwrite may return, which should be treated as undefined
24
+ const EXTREME_MIN_INTEGER = -9223372036854776000;
25
+ const EXTREME_MAX_INTEGER = 9223372036854776000;
26
+ const EXTREME_MIN_FLOAT = -1.7976931348623157e+308;
27
+ const EXTREME_MAX_FLOAT = 1.7976931348623157e+308;
28
+
29
+ /**
30
+ * Type guard to check if an attribute has min/max properties
31
+ */
32
+ const hasMinMaxProperties = (attribute: Attribute): attribute is Attribute & { min?: number; max?: number } => {
33
+ return attribute.type === 'integer' || attribute.type === 'double' || attribute.type === 'float';
34
+ };
35
+
36
+ /**
37
+ * Normalizes min/max values for integer and float attributes
38
+ * Sets values to undefined if they exceed the threshold or are extreme values from database
39
+ */
40
+ const normalizeMinMaxValues = (attribute: Attribute): { min?: number; max?: number } => {
41
+ if (!hasMinMaxProperties(attribute)) {
42
+ logger.debug(`Attribute '${attribute.key}' does not have min/max properties`, {
43
+ type: attribute.type,
44
+ operation: 'normalizeMinMaxValues'
45
+ });
46
+ return {};
47
+ }
48
+
49
+ const { type, min, max } = attribute;
50
+ let normalizedMin = min;
51
+ let normalizedMax = max;
52
+
53
+ logger.debug(`Normalizing min/max values for attribute '${attribute.key}'`, {
54
+ type,
55
+ originalMin: min,
56
+ originalMax: max,
57
+ operation: 'normalizeMinMaxValues'
58
+ });
59
+
60
+ // Handle min value
61
+ if (normalizedMin !== undefined && normalizedMin !== null) {
62
+ const minValue = Number(normalizedMin);
63
+ const originalMin = normalizedMin;
64
+
65
+ // Check if it exceeds threshold or is an extreme database value
66
+ if (type === 'integer') {
67
+ if (Math.abs(minValue) >= MIN_MAX_THRESHOLD || minValue === EXTREME_MIN_INTEGER) {
68
+ logger.debug(`Min value normalized to undefined for attribute '${attribute.key}'`, {
69
+ type,
70
+ originalValue: originalMin,
71
+ numericValue: minValue,
72
+ reason: Math.abs(minValue) >= MIN_MAX_THRESHOLD ? 'exceeds_threshold' : 'extreme_database_value',
73
+ threshold: MIN_MAX_THRESHOLD,
74
+ extremeValue: EXTREME_MIN_INTEGER,
75
+ operation: 'normalizeMinMaxValues'
76
+ });
77
+ normalizedMin = undefined;
78
+ }
79
+ } else { // float/double
80
+ if (Math.abs(minValue) >= MIN_MAX_THRESHOLD || minValue === EXTREME_MIN_FLOAT) {
81
+ logger.debug(`Min value normalized to undefined for attribute '${attribute.key}'`, {
82
+ type,
83
+ originalValue: originalMin,
84
+ numericValue: minValue,
85
+ reason: Math.abs(minValue) >= MIN_MAX_THRESHOLD ? 'exceeds_threshold' : 'extreme_database_value',
86
+ threshold: MIN_MAX_THRESHOLD,
87
+ extremeValue: EXTREME_MIN_FLOAT,
88
+ operation: 'normalizeMinMaxValues'
89
+ });
90
+ normalizedMin = undefined;
91
+ }
92
+ }
93
+ }
94
+
95
+ // Handle max value
96
+ if (normalizedMax !== undefined && normalizedMax !== null) {
97
+ const maxValue = Number(normalizedMax);
98
+ const originalMax = normalizedMax;
99
+
100
+ // Check if it exceeds threshold or is an extreme database value
101
+ if (type === 'integer') {
102
+ if (Math.abs(maxValue) >= MIN_MAX_THRESHOLD || maxValue === EXTREME_MAX_INTEGER) {
103
+ logger.debug(`Max value normalized to undefined for attribute '${attribute.key}'`, {
104
+ type,
105
+ originalValue: originalMax,
106
+ numericValue: maxValue,
107
+ reason: Math.abs(maxValue) >= MIN_MAX_THRESHOLD ? 'exceeds_threshold' : 'extreme_database_value',
108
+ threshold: MIN_MAX_THRESHOLD,
109
+ extremeValue: EXTREME_MAX_INTEGER,
110
+ operation: 'normalizeMinMaxValues'
111
+ });
112
+ normalizedMax = undefined;
113
+ }
114
+ } else { // float/double
115
+ if (Math.abs(maxValue) >= MIN_MAX_THRESHOLD || maxValue === EXTREME_MAX_FLOAT) {
116
+ logger.debug(`Max value normalized to undefined for attribute '${attribute.key}'`, {
117
+ type,
118
+ originalValue: originalMax,
119
+ numericValue: maxValue,
120
+ reason: Math.abs(maxValue) >= MIN_MAX_THRESHOLD ? 'exceeds_threshold' : 'extreme_database_value',
121
+ threshold: MIN_MAX_THRESHOLD,
122
+ extremeValue: EXTREME_MAX_FLOAT,
123
+ operation: 'normalizeMinMaxValues'
124
+ });
125
+ normalizedMax = undefined;
126
+ }
127
+ }
128
+ }
129
+
130
+ const result = { min: normalizedMin, max: normalizedMax };
131
+ logger.debug(`Min/max normalization complete for attribute '${attribute.key}'`, {
132
+ type,
133
+ result,
134
+ operation: 'normalizeMinMaxValues'
135
+ });
136
+
137
+ return result;
138
+ };
139
+
140
+ /**
141
+ * Normalizes an attribute for comparison by handling extreme database values
142
+ * This is used when comparing database attributes with config attributes
143
+ */
144
+ const normalizeAttributeForComparison = (attribute: Attribute): Attribute => {
145
+ if (!hasMinMaxProperties(attribute)) {
146
+ return attribute;
147
+ }
148
+
149
+ const { min, max } = normalizeMinMaxValues(attribute);
150
+ return { ...(attribute as any), min, max };
151
+ };
152
+
153
+ /**
154
+ * Helper function to create an attribute using either the adapter or legacy API
155
+ */
156
+ const createAttributeViaAdapter = async (
157
+ db: Databases | DatabaseAdapter,
158
+ dbId: string,
159
+ collectionId: string,
160
+ attribute: Attribute
161
+ ): Promise<void> => {
162
+ const startTime = Date.now();
163
+ const adapterType = isDatabaseAdapter(db) ? 'adapter' : 'legacy';
164
+
165
+ logger.info(`Creating attribute '${attribute.key}' via ${adapterType}`, {
166
+ type: attribute.type,
167
+ dbId,
168
+ collectionId,
169
+ adapterType,
170
+ operation: 'createAttributeViaAdapter'
171
+ });
172
+
173
+ if (isDatabaseAdapter(db)) {
174
+ // Use the adapter's unified createAttribute method
175
+ const params: CreateAttributeParams = {
176
+ databaseId: dbId,
177
+ tableId: collectionId,
178
+ key: attribute.key,
179
+ type: attribute.type,
180
+ required: attribute.required || false,
181
+ array: attribute.array || false,
182
+ ...((attribute as any).size && { size: (attribute as any).size }),
183
+ ...((attribute as any).xdefault !== undefined && !attribute.required && { default: (attribute as any).xdefault }),
184
+ ...((attribute as any).encrypted && { encrypt: (attribute as any).encrypted }),
185
+ ...((attribute as any).min !== undefined && { min: (attribute as any).min }),
186
+ ...((attribute as any).max !== undefined && { max: (attribute as any).max }),
187
+ ...((attribute as any).elements && { elements: (attribute as any).elements }),
188
+ ...((attribute as any).relatedCollection && { relatedCollection: (attribute as any).relatedCollection }),
189
+ ...((attribute as any).relationType && { relationType: (attribute as any).relationType }),
190
+ ...((attribute as any).twoWay !== undefined && { twoWay: (attribute as any).twoWay }),
191
+ ...((attribute as any).onDelete && { onDelete: (attribute as any).onDelete }),
192
+ ...((attribute as any).twoWayKey && { twoWayKey: (attribute as any).twoWayKey })
193
+ };
194
+
195
+ logger.debug(`Adapter create parameters for '${attribute.key}'`, {
196
+ params,
197
+ operation: 'createAttributeViaAdapter'
198
+ });
199
+
200
+ await db.createAttribute(params);
201
+
202
+ const duration = Date.now() - startTime;
203
+ logger.info(`Successfully created attribute '${attribute.key}' via adapter`, {
204
+ duration,
205
+ operation: 'createAttributeViaAdapter'
206
+ });
207
+ } else {
208
+ // Use legacy type-specific methods
209
+ logger.debug(`Using legacy creation for attribute '${attribute.key}'`, {
210
+ operation: 'createAttributeViaAdapter'
211
+ });
212
+ await createLegacyAttribute(db, dbId, collectionId, attribute);
213
+
214
+ const duration = Date.now() - startTime;
215
+ logger.info(`Successfully created attribute '${attribute.key}' via legacy`, {
216
+ duration,
217
+ operation: 'createAttributeViaAdapter'
218
+ });
219
+ }
220
+ };
221
+
222
+ /**
223
+ * Helper function to update an attribute using either the adapter or legacy API
224
+ */
225
+ const updateAttributeViaAdapter = async (
226
+ db: Databases | DatabaseAdapter,
227
+ dbId: string,
228
+ collectionId: string,
229
+ attribute: Attribute
230
+ ): Promise<void> => {
231
+ if (isDatabaseAdapter(db)) {
232
+ // Use the adapter's unified updateAttribute method
233
+ const params: UpdateAttributeParams = {
234
+ databaseId: dbId,
235
+ tableId: collectionId,
236
+ key: attribute.key,
237
+ required: attribute.required || false,
238
+ ...((attribute as any).xdefault !== undefined && !attribute.required && { default: (attribute as any).xdefault })
239
+ };
240
+ await db.updateAttribute(params);
241
+ } else {
242
+ // Use legacy type-specific methods
243
+ await updateLegacyAttribute(db, dbId, collectionId, attribute);
244
+ }
245
+ };
246
+
247
+ /**
248
+ * Legacy attribute creation using type-specific methods
249
+ */
250
+ const createLegacyAttribute = async (
251
+ db: Databases,
252
+ dbId: string,
253
+ collectionId: string,
254
+ attribute: Attribute
255
+ ): Promise<void> => {
256
+ const startTime = Date.now();
257
+ const { min: normalizedMin, max: normalizedMax } = normalizeMinMaxValues(attribute);
258
+
259
+ logger.info(`Creating legacy attribute '${attribute.key}'`, {
260
+ type: attribute.type,
261
+ dbId,
262
+ collectionId,
263
+ normalizedMin,
264
+ normalizedMax,
265
+ operation: 'createLegacyAttribute'
266
+ });
267
+
268
+ switch (attribute.type) {
269
+ case "string":
270
+ const stringParams = {
271
+ size: (attribute as any).size || 255,
272
+ required: attribute.required || false,
273
+ defaultValue: (attribute as any).xdefault !== undefined && !attribute.required ? (attribute as any).xdefault : undefined,
274
+ array: attribute.array || false,
275
+ encrypted: (attribute as any).encrypted
276
+ };
277
+ logger.debug(`Creating string attribute '${attribute.key}'`, {
278
+ ...stringParams,
279
+ operation: 'createLegacyAttribute'
280
+ });
281
+ await db.createStringAttribute(
282
+ dbId,
283
+ collectionId,
284
+ attribute.key,
285
+ stringParams.size,
286
+ stringParams.required,
287
+ stringParams.defaultValue,
288
+ stringParams.array,
289
+ stringParams.encrypted
290
+ );
291
+ break;
292
+ case "integer":
293
+ const integerParams = {
294
+ required: attribute.required || false,
295
+ min: normalizedMin !== undefined ? parseInt(String(normalizedMin)) : undefined,
296
+ max: normalizedMax !== undefined ? parseInt(String(normalizedMax)) : undefined,
297
+ defaultValue: (attribute as any).xdefault !== undefined && !attribute.required ? (attribute as any).xdefault : undefined,
298
+ array: attribute.array || false
299
+ };
300
+ logger.debug(`Creating integer attribute '${attribute.key}'`, {
301
+ ...integerParams,
302
+ operation: 'createLegacyAttribute'
303
+ });
304
+ await db.createIntegerAttribute(
305
+ dbId,
306
+ collectionId,
307
+ attribute.key,
308
+ integerParams.required,
309
+ integerParams.min,
310
+ integerParams.max,
311
+ integerParams.defaultValue,
312
+ integerParams.array
313
+ );
314
+ break;
315
+ case "double":
316
+ case "float":
317
+ await db.createFloatAttribute(
318
+ dbId,
319
+ collectionId,
320
+ attribute.key,
321
+ attribute.required || false,
322
+ normalizedMin !== undefined ? Number(normalizedMin) : undefined,
323
+ normalizedMax !== undefined ? Number(normalizedMax) : undefined,
324
+ (attribute as any).xdefault !== undefined && !attribute.required ? (attribute as any).xdefault : undefined,
325
+ attribute.array || false
326
+ );
327
+ break;
328
+ case "boolean":
329
+ await db.createBooleanAttribute(
330
+ dbId,
331
+ collectionId,
332
+ attribute.key,
333
+ attribute.required || false,
334
+ (attribute as any).xdefault !== undefined && !attribute.required ? (attribute as any).xdefault : undefined,
335
+ attribute.array || false
336
+ );
337
+ break;
338
+ case "datetime":
339
+ await db.createDatetimeAttribute(
340
+ dbId,
341
+ collectionId,
342
+ attribute.key,
343
+ attribute.required || false,
344
+ (attribute as any).xdefault !== undefined && !attribute.required ? (attribute as any).xdefault : undefined,
345
+ attribute.array || false
346
+ );
347
+ break;
348
+ case "email":
349
+ await db.createEmailAttribute(
350
+ dbId,
351
+ collectionId,
352
+ attribute.key,
353
+ attribute.required || false,
354
+ (attribute as any).xdefault !== undefined && !attribute.required ? (attribute as any).xdefault : undefined,
355
+ attribute.array || false
356
+ );
357
+ break;
358
+ case "ip":
359
+ await db.createIpAttribute(
360
+ dbId,
361
+ collectionId,
362
+ attribute.key,
363
+ attribute.required || false,
364
+ (attribute as any).xdefault !== undefined && !attribute.required ? (attribute as any).xdefault : undefined,
365
+ attribute.array || false
366
+ );
367
+ break;
368
+ case "url":
369
+ await db.createUrlAttribute(
370
+ dbId,
371
+ collectionId,
372
+ attribute.key,
373
+ attribute.required || false,
374
+ (attribute as any).xdefault !== undefined && !attribute.required ? (attribute as any).xdefault : undefined,
375
+ attribute.array || false
376
+ );
377
+ break;
378
+ case "enum":
379
+ await db.createEnumAttribute(
380
+ dbId,
381
+ collectionId,
382
+ attribute.key,
383
+ (attribute as any).elements || [],
384
+ attribute.required || false,
385
+ (attribute as any).xdefault !== undefined && !attribute.required ? (attribute as any).xdefault : undefined,
386
+ attribute.array || false
387
+ );
388
+ break;
389
+ case "relationship":
390
+ await db.createRelationshipAttribute(
391
+ dbId,
392
+ collectionId,
393
+ (attribute as any).relatedCollection!,
394
+ (attribute as any).relationType!,
395
+ (attribute as any).twoWay,
396
+ attribute.key,
397
+ (attribute as any).twoWayKey,
398
+ (attribute as any).onDelete
399
+ );
400
+ break;
401
+ default:
402
+ const error = new Error(`Unsupported attribute type: ${(attribute as any).type}`);
403
+ logger.error(`Unsupported attribute type for '${(attribute as any).key}'`, {
404
+ type: (attribute as any).type,
405
+ supportedTypes: ['string', 'integer', 'double', 'float', 'boolean', 'datetime', 'email', 'ip', 'url', 'enum', 'relationship'],
406
+ operation: 'createLegacyAttribute'
407
+ });
408
+ throw error;
409
+ }
410
+
411
+ const duration = Date.now() - startTime;
412
+ logger.info(`Successfully created legacy attribute '${attribute.key}'`, {
413
+ type: attribute.type,
414
+ duration,
415
+ operation: 'createLegacyAttribute'
416
+ });
417
+ };
418
+
419
+ /**
420
+ * Legacy attribute update using type-specific methods
421
+ */
422
+ const updateLegacyAttribute = async (
423
+ db: Databases,
424
+ dbId: string,
425
+ collectionId: string,
426
+ attribute: Attribute
427
+ ): Promise<void> => {
428
+ const { min: normalizedMin, max: normalizedMax } = normalizeMinMaxValues(attribute);
429
+
430
+ switch (attribute.type) {
431
+ case "string":
432
+ await db.updateStringAttribute(
433
+ dbId,
434
+ collectionId,
435
+ attribute.key,
436
+ attribute.required || false,
437
+ (attribute as any).xdefault !== undefined && !attribute.required ? (attribute as any).xdefault : undefined,
438
+ attribute.size
439
+ );
440
+ break;
441
+ case "integer":
442
+ await db.updateIntegerAttribute(
443
+ dbId,
444
+ collectionId,
445
+ attribute.key,
446
+ attribute.required || false,
447
+ (attribute as any).xdefault !== undefined && !attribute.required ? (attribute as any).xdefault : undefined,
448
+ normalizedMin !== undefined ? parseInt(String(normalizedMin)) : undefined,
449
+ normalizedMax !== undefined ? parseInt(String(normalizedMax)) : undefined
450
+ );
451
+ break;
452
+ case "double":
453
+ case "float":
454
+ await db.updateFloatAttribute(
455
+ dbId,
456
+ collectionId,
457
+ attribute.key,
458
+ attribute.required || false,
459
+ normalizedMin !== undefined ? Number(normalizedMin) : undefined,
460
+ normalizedMax !== undefined ? Number(normalizedMax) : undefined,
461
+ attribute.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined
462
+ );
463
+ break;
464
+ case "boolean":
465
+ await db.updateBooleanAttribute(
466
+ dbId,
467
+ collectionId,
468
+ attribute.key,
469
+ attribute.required || false,
470
+ attribute.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined
471
+ );
472
+ break;
473
+ case "datetime":
474
+ await db.updateDatetimeAttribute(
475
+ dbId,
476
+ collectionId,
477
+ attribute.key,
478
+ attribute.required || false,
479
+ attribute.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined
480
+ );
481
+ break;
482
+ case "email":
483
+ await db.updateEmailAttribute(
484
+ dbId,
485
+ collectionId,
486
+ attribute.key,
487
+ attribute.required || false,
488
+ attribute.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined
489
+ );
490
+ break;
491
+ case "ip":
492
+ await db.updateIpAttribute(
493
+ dbId,
494
+ collectionId,
495
+ attribute.key,
496
+ attribute.required || false,
497
+ attribute.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined
498
+ );
499
+ break;
500
+ case "url":
501
+ await db.updateUrlAttribute(
502
+ dbId,
503
+ collectionId,
504
+ attribute.key,
505
+ attribute.required || false,
506
+ attribute.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined
507
+ );
508
+ break;
509
+ case "enum":
510
+ await db.updateEnumAttribute(
511
+ dbId,
512
+ collectionId,
513
+ attribute.key,
514
+ (attribute as any).elements || [],
515
+ attribute.required || false,
516
+ attribute.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined
517
+ );
518
+ break;
519
+ case "relationship":
520
+ await db.updateRelationshipAttribute(
521
+ dbId,
522
+ collectionId,
523
+ attribute.key,
524
+ (attribute as any).onDelete
525
+ );
526
+ break;
527
+ default:
528
+ throw new Error(`Unsupported attribute type for update: ${(attribute as any).type}`);
529
+ }
530
+ };
10
531
 
11
532
  // Interface for attribute with status (fixing the type issue)
12
533
  interface AttributeWithStatus {
@@ -25,7 +546,7 @@ interface AttributeWithStatus {
25
546
  * Wait for attribute to become available, with retry logic for stuck attributes and exponential backoff
26
547
  */
27
548
  const waitForAttributeAvailable = async (
28
- db: Databases,
549
+ db: Databases | DatabaseAdapter,
29
550
  dbId: string,
30
551
  collectionId: string,
31
552
  attributeKey: string,
@@ -36,17 +557,26 @@ const waitForAttributeAvailable = async (
36
557
  const startTime = Date.now();
37
558
  let checkInterval = 2000; // Start with 2 seconds
38
559
 
560
+ logger.info(`Waiting for attribute '${attributeKey}' to become available`, {
561
+ dbId,
562
+ collectionId,
563
+ maxWaitTime,
564
+ retryCount,
565
+ maxRetries,
566
+ operation: 'waitForAttributeAvailable'
567
+ });
568
+
39
569
  // Calculate exponential backoff: 2s, 4s, 8s, 16s, 30s (capped at 30s)
40
570
  if (retryCount > 0) {
41
- const exponentialDelay = Math.min(2000 * Math.pow(2, retryCount), 30000);
42
- console.log(
571
+ const exponentialDelay = calculateExponentialBackoff(retryCount);
572
+ MessageFormatter.info(
43
573
  chalk.blue(
44
574
  `Waiting for attribute '${attributeKey}' to become available (retry ${retryCount}, backoff: ${exponentialDelay}ms)...`
45
575
  )
46
576
  );
47
577
  await delay(exponentialDelay);
48
578
  } else {
49
- console.log(
579
+ MessageFormatter.info(
50
580
  chalk.blue(
51
581
  `Waiting for attribute '${attributeKey}' to become available...`
52
582
  )
@@ -55,71 +585,100 @@ const waitForAttributeAvailable = async (
55
585
 
56
586
  while (Date.now() - startTime < maxWaitTime) {
57
587
  try {
58
- const collection = await db.getCollection(dbId, collectionId);
588
+ const collection = isDatabaseAdapter(db)
589
+ ? (await db.getTable({ databaseId: dbId, tableId: collectionId })).data
590
+ : await db.getCollection(dbId, collectionId);
59
591
  const attribute = (collection.attributes as any[]).find(
60
592
  (attr: AttributeWithStatus) => attr.key === attributeKey
61
593
  ) as AttributeWithStatus | undefined;
62
594
 
63
595
  if (!attribute) {
64
- console.log(chalk.red(`Attribute '${attributeKey}' not found`));
596
+ MessageFormatter.error(`Attribute '${attributeKey}' not found`);
65
597
  return false;
66
598
  }
67
599
 
68
- console.log(
600
+ MessageFormatter.info(
69
601
  chalk.gray(`Attribute '${attributeKey}' status: ${attribute.status}`)
70
602
  );
71
603
 
604
+ const statusInfo = {
605
+ attributeKey,
606
+ status: attribute.status,
607
+ error: attribute.error,
608
+ dbId,
609
+ collectionId,
610
+ waitTime: Date.now() - startTime,
611
+ operation: 'waitForAttributeAvailable'
612
+ };
613
+
72
614
  switch (attribute.status) {
73
615
  case "available":
74
- console.log(
616
+ MessageFormatter.info(
75
617
  chalk.green(`✅ Attribute '${attributeKey}' is now available`)
76
618
  );
619
+ logger.info(`Attribute '${attributeKey}' became available`, statusInfo);
77
620
  return true;
78
621
 
79
622
  case "failed":
80
- console.log(
623
+ MessageFormatter.info(
81
624
  chalk.red(
82
625
  `❌ Attribute '${attributeKey}' failed: ${attribute.error}`
83
626
  )
84
627
  );
628
+ logger.error(`Attribute '${attributeKey}' failed`, statusInfo);
85
629
  return false;
86
630
 
87
631
  case "stuck":
88
- console.log(
632
+ MessageFormatter.info(
89
633
  chalk.yellow(
90
634
  `⚠️ Attribute '${attributeKey}' is stuck, will retry...`
91
635
  )
92
636
  );
637
+ logger.warn(`Attribute '${attributeKey}' is stuck`, statusInfo);
93
638
  return false;
94
639
 
95
640
  case "processing":
96
641
  // Continue waiting
642
+ logger.debug(`Attribute '${attributeKey}' still processing`, statusInfo);
97
643
  break;
98
644
 
99
645
  case "deleting":
100
- console.log(
646
+ MessageFormatter.info(
101
647
  chalk.yellow(`Attribute '${attributeKey}' is being deleted`)
102
648
  );
649
+ logger.warn(`Attribute '${attributeKey}' is being deleted`, statusInfo);
103
650
  break;
104
651
 
105
652
  default:
106
- console.log(
653
+ MessageFormatter.info(
107
654
  chalk.yellow(
108
655
  `Unknown status '${attribute.status}' for attribute '${attributeKey}'`
109
656
  )
110
657
  );
658
+ logger.warn(`Unknown status for attribute '${attributeKey}'`, statusInfo);
111
659
  break;
112
660
  }
113
661
 
114
662
  await delay(checkInterval);
115
663
  } catch (error) {
116
- console.log(chalk.red(`Error checking attribute status: ${error}`));
664
+ const errorMessage = error instanceof Error ? error.message : String(error);
665
+ MessageFormatter.error(`Error checking attribute status: ${errorMessage}`);
666
+
667
+ logger.error('Error checking attribute status', {
668
+ attributeKey,
669
+ dbId,
670
+ collectionId,
671
+ error: errorMessage,
672
+ waitTime: Date.now() - startTime,
673
+ operation: 'waitForAttributeAvailable'
674
+ });
675
+
117
676
  return false;
118
677
  }
119
678
  }
120
679
 
121
680
  // Timeout reached
122
- console.log(
681
+ MessageFormatter.info(
123
682
  chalk.yellow(
124
683
  `⏰ Timeout waiting for attribute '${attributeKey}' (${maxWaitTime}ms)`
125
684
  )
@@ -127,7 +686,7 @@ const waitForAttributeAvailable = async (
127
686
 
128
687
  // If we have retries left and this isn't the last retry, try recreating
129
688
  if (retryCount < maxRetries) {
130
- console.log(
689
+ MessageFormatter.info(
131
690
  chalk.yellow(
132
691
  `🔄 Retrying attribute creation (attempt ${
133
692
  retryCount + 1
@@ -144,13 +703,13 @@ const waitForAttributeAvailable = async (
144
703
  * Wait for all attributes in a collection to become available
145
704
  */
146
705
  const waitForAllAttributesAvailable = async (
147
- db: Databases,
706
+ db: Databases | DatabaseAdapter,
148
707
  dbId: string,
149
708
  collectionId: string,
150
709
  attributeKeys: string[],
151
710
  maxWaitTime: number = 60000
152
711
  ): Promise<string[]> => {
153
- console.log(
712
+ MessageFormatter.info(
154
713
  chalk.blue(
155
714
  `Waiting for ${attributeKeys.length} attributes to become available...`
156
715
  )
@@ -178,40 +737,53 @@ const waitForAllAttributesAvailable = async (
178
737
  * Delete collection and recreate with retry logic
179
738
  */
180
739
  const deleteAndRecreateCollection = async (
181
- db: Databases,
740
+ db: Databases | DatabaseAdapter,
182
741
  dbId: string,
183
742
  collection: Models.Collection,
184
743
  retryCount: number
185
744
  ): Promise<Models.Collection | null> => {
186
745
  try {
187
- console.log(
746
+ MessageFormatter.info(
188
747
  chalk.yellow(
189
748
  `🗑️ Deleting collection '${collection.name}' for retry ${retryCount}`
190
749
  )
191
750
  );
192
751
 
193
752
  // Delete the collection
194
- await db.deleteCollection(dbId, collection.$id);
195
- console.log(chalk.yellow(`Deleted collection '${collection.name}'`));
753
+ if (isDatabaseAdapter(db)) {
754
+ await db.deleteTable({ databaseId: dbId, tableId: collection.$id });
755
+ } else {
756
+ await db.deleteCollection(dbId, collection.$id);
757
+ }
758
+ MessageFormatter.warning(`Deleted collection '${collection.name}'`);
196
759
 
197
760
  // Wait a bit before recreating
198
761
  await delay(2000);
199
762
 
200
763
  // Recreate the collection
201
- console.log(chalk.blue(`🔄 Recreating collection '${collection.name}'`));
202
- const newCollection = await db.createCollection(
203
- dbId,
204
- collection.$id,
205
- collection.name,
206
- collection.$permissions,
207
- collection.documentSecurity,
208
- collection.enabled
209
- );
764
+ MessageFormatter.info(`🔄 Recreating collection '${collection.name}'`);
765
+ const newCollection = isDatabaseAdapter(db)
766
+ ? (await db.createTable({
767
+ databaseId: dbId,
768
+ id: collection.$id,
769
+ name: collection.name,
770
+ permissions: collection.$permissions,
771
+ documentSecurity: collection.documentSecurity,
772
+ enabled: collection.enabled
773
+ })).data
774
+ : await db.createCollection(
775
+ dbId,
776
+ collection.$id,
777
+ collection.name,
778
+ collection.$permissions,
779
+ collection.documentSecurity,
780
+ collection.enabled
781
+ );
210
782
 
211
- console.log(chalk.green(`✅ Recreated collection '${collection.name}'`));
783
+ MessageFormatter.success(`✅ Recreated collection '${collection.name}'`);
212
784
  return newCollection;
213
785
  } catch (error) {
214
- console.log(
786
+ MessageFormatter.info(
215
787
  chalk.red(
216
788
  `Failed to delete/recreate collection '${collection.name}': ${error}`
217
789
  )
@@ -224,33 +796,37 @@ const attributesSame = (
224
796
  databaseAttribute: Attribute,
225
797
  configAttribute: Attribute
226
798
  ): boolean => {
227
- const attributesToCheck = [
228
- "key",
229
- "type",
230
- "array",
231
- "encrypted",
232
- "required",
233
- "size",
234
- "min",
235
- "max",
236
- "xdefault",
237
- "elements",
238
- "relationType",
239
- "twoWay",
240
- "twoWayKey",
241
- "onDelete",
242
- "relatedCollection",
243
- ];
244
-
245
- return attributesToCheck.every((attr) => {
246
- // Check if both objects have the attribute
247
- const dbHasAttr = attr in databaseAttribute;
248
- const configHasAttr = attr in configAttribute;
799
+ // Normalize both attributes for comparison (handle extreme database values)
800
+ const normalizedDbAttr = normalizeAttributeForComparison(databaseAttribute);
801
+ const normalizedConfigAttr = normalizeAttributeForComparison(configAttribute);
802
+
803
+ const attributesToCheck = [
804
+ "key",
805
+ "type",
806
+ "array",
807
+ "encrypted",
808
+ "required",
809
+ "size",
810
+ "min",
811
+ "max",
812
+ "xdefault",
813
+ "elements",
814
+ "relationType",
815
+ "twoWay",
816
+ "twoWayKey",
817
+ "onDelete",
818
+ "relatedCollection",
819
+ ];
820
+
821
+ return attributesToCheck.every((attr) => {
822
+ // Check if both objects have the attribute
823
+ const dbHasAttr = attr in normalizedDbAttr;
824
+ const configHasAttr = attr in normalizedConfigAttr;
249
825
 
250
826
  // If both have the attribute, compare values
251
- if (dbHasAttr && configHasAttr) {
252
- const dbValue = databaseAttribute[attr as keyof typeof databaseAttribute];
253
- const configValue = configAttribute[attr as keyof typeof configAttribute];
827
+ if (dbHasAttr && configHasAttr) {
828
+ const dbValue = normalizedDbAttr[attr as keyof typeof normalizedDbAttr];
829
+ const configValue = normalizedConfigAttr[attr as keyof typeof normalizedConfigAttr];
254
830
 
255
831
  // Consider undefined and null as equivalent
256
832
  if (
@@ -260,19 +836,19 @@ const attributesSame = (
260
836
  return true;
261
837
  }
262
838
 
263
- // Normalize booleans: treat undefined and false as equivalent
264
- if (typeof dbValue === "boolean" || typeof configValue === "boolean") {
265
- return Boolean(dbValue) === Boolean(configValue);
266
- }
267
- // For numeric comparisons, compare numbers if both are numeric-like
268
- if (
269
- (typeof dbValue === "number" || (typeof dbValue === "string" && dbValue !== "" && !isNaN(Number(dbValue)))) &&
270
- (typeof configValue === "number" || (typeof configValue === "string" && configValue !== "" && !isNaN(Number(configValue))))
271
- ) {
272
- return Number(dbValue) === Number(configValue);
273
- }
274
- return dbValue === configValue;
275
- }
839
+ // Normalize booleans: treat undefined and false as equivalent
840
+ if (typeof dbValue === "boolean" || typeof configValue === "boolean") {
841
+ return Boolean(dbValue) === Boolean(configValue);
842
+ }
843
+ // For numeric comparisons, compare numbers if both are numeric-like
844
+ if (
845
+ (typeof dbValue === "number" || (typeof dbValue === "string" && dbValue !== "" && !isNaN(Number(dbValue)))) &&
846
+ (typeof configValue === "number" || (typeof configValue === "string" && configValue !== "" && !isNaN(Number(configValue))))
847
+ ) {
848
+ return Number(dbValue) === Number(configValue);
849
+ }
850
+ return dbValue === configValue;
851
+ }
276
852
 
277
853
  // If neither has the attribute, consider it the same
278
854
  if (!dbHasAttr && !configHasAttr) {
@@ -280,23 +856,23 @@ const attributesSame = (
280
856
  }
281
857
 
282
858
  // If one has the attribute and the other doesn't, check if it's undefined or null
283
- if (dbHasAttr && !configHasAttr) {
284
- const dbValue = databaseAttribute[attr as keyof typeof databaseAttribute];
285
- // Consider default-false booleans as equal to missing in config
286
- if (typeof dbValue === "boolean") {
287
- return dbValue === false; // missing in config equals false in db
288
- }
289
- return dbValue === undefined || dbValue === null;
290
- }
291
-
292
- if (!dbHasAttr && configHasAttr) {
293
- const configValue = configAttribute[attr as keyof typeof configAttribute];
294
- // Consider default-false booleans as equal to missing in db
295
- if (typeof configValue === "boolean") {
296
- return configValue === false; // missing in db equals false in config
297
- }
298
- return configValue === undefined || configValue === null;
299
- }
859
+ if (dbHasAttr && !configHasAttr) {
860
+ const dbValue = normalizedDbAttr[attr as keyof typeof normalizedDbAttr];
861
+ // Consider default-false booleans as equal to missing in config
862
+ if (typeof dbValue === "boolean") {
863
+ return dbValue === false; // missing in config equals false in db
864
+ }
865
+ return dbValue === undefined || dbValue === null;
866
+ }
867
+
868
+ if (!dbHasAttr && configHasAttr) {
869
+ const configValue = normalizedConfigAttr[attr as keyof typeof normalizedConfigAttr];
870
+ // Consider default-false booleans as equal to missing in db
871
+ if (typeof configValue === "boolean") {
872
+ return configValue === false; // missing in db equals false in config
873
+ }
874
+ return configValue === undefined || configValue === null;
875
+ }
300
876
 
301
877
  // If we reach here, the attributes are different
302
878
  return false;
@@ -306,15 +882,15 @@ const attributesSame = (
306
882
  /**
307
883
  * Enhanced attribute creation with proper status monitoring and retry logic
308
884
  */
309
- export const createOrUpdateAttributeWithStatusCheck = async (
310
- db: Databases,
311
- dbId: string,
312
- collection: Models.Collection,
313
- attribute: Attribute,
314
- retryCount: number = 0,
315
- maxRetries: number = 5
316
- ): Promise<boolean> => {
317
- console.log(
885
+ export const createOrUpdateAttributeWithStatusCheck = async (
886
+ db: Databases | DatabaseAdapter,
887
+ dbId: string,
888
+ collection: Models.Collection,
889
+ attribute: Attribute,
890
+ retryCount: number = 0,
891
+ maxRetries: number = 5
892
+ ): Promise<boolean> => {
893
+ MessageFormatter.info(
318
894
  chalk.blue(
319
895
  `Creating/updating attribute '${attribute.key}' (attempt ${
320
896
  retryCount + 1
@@ -322,31 +898,31 @@ export const createOrUpdateAttributeWithStatusCheck = async (
322
898
  )
323
899
  );
324
900
 
325
- try {
326
- // First, try to create/update the attribute using existing logic
327
- const result = await createOrUpdateAttribute(db, dbId, collection, attribute);
328
-
329
- // If the attribute was queued (relationship dependency unresolved),
330
- // skip status polling and retry logic — the queue will handle it later.
331
- if (result === "queued") {
332
- console.log(
333
- chalk.yellow(
334
- `⏭️ Deferred relationship attribute '${attribute.key}' — queued for later once dependencies are available`
335
- )
336
- );
337
- return true;
338
- }
339
-
340
- // Now wait for the attribute to become available
341
- const success = await waitForAttributeAvailable(
342
- db,
343
- dbId,
344
- collection.$id,
345
- attribute.key,
346
- 60000, // 1 minute timeout
347
- retryCount,
348
- maxRetries
349
- );
901
+ try {
902
+ // First, try to create/update the attribute using existing logic
903
+ const result = await createOrUpdateAttribute(db, dbId, collection, attribute);
904
+
905
+ // If the attribute was queued (relationship dependency unresolved),
906
+ // skip status polling and retry logic — the queue will handle it later.
907
+ if (result === "queued") {
908
+ MessageFormatter.info(
909
+ chalk.yellow(
910
+ `⏭️ Deferred relationship attribute '${attribute.key}' — queued for later once dependencies are available`
911
+ )
912
+ );
913
+ return true;
914
+ }
915
+
916
+ // Now wait for the attribute to become available
917
+ const success = await waitForAttributeAvailable(
918
+ db,
919
+ dbId,
920
+ collection.$id,
921
+ attribute.key,
922
+ 60000, // 1 minute timeout
923
+ retryCount,
924
+ maxRetries
925
+ );
350
926
 
351
927
  if (success) {
352
928
  return true;
@@ -354,7 +930,7 @@ export const createOrUpdateAttributeWithStatusCheck = async (
354
930
 
355
931
  // If not successful and we have retries left, delete specific attribute and try again
356
932
  if (retryCount < maxRetries) {
357
- console.log(
933
+ MessageFormatter.info(
358
934
  chalk.yellow(
359
935
  `Attribute '${attribute.key}' failed/stuck, deleting and retrying...`
360
936
  )
@@ -362,8 +938,12 @@ export const createOrUpdateAttributeWithStatusCheck = async (
362
938
 
363
939
  // Try to delete the specific stuck attribute instead of the entire collection
364
940
  try {
365
- await db.deleteAttribute(dbId, collection.$id, attribute.key);
366
- console.log(
941
+ if (isDatabaseAdapter(db)) {
942
+ await db.deleteAttribute({ databaseId: dbId, tableId: collection.$id, key: attribute.key });
943
+ } else {
944
+ await db.deleteAttribute(dbId, collection.$id, attribute.key);
945
+ }
946
+ MessageFormatter.info(
367
947
  chalk.yellow(
368
948
  `Deleted stuck attribute '${attribute.key}', will retry creation`
369
949
  )
@@ -373,7 +953,9 @@ export const createOrUpdateAttributeWithStatusCheck = async (
373
953
  await delay(3000);
374
954
 
375
955
  // Get fresh collection data
376
- const freshCollection = await db.getCollection(dbId, collection.$id);
956
+ const freshCollection = isDatabaseAdapter(db)
957
+ ? (await db.getTable({ databaseId: dbId, tableId: collection.$id })).data
958
+ : await db.getCollection(dbId, collection.$id);
377
959
 
378
960
  // Retry with the same collection (attribute should be gone now)
379
961
  return await createOrUpdateAttributeWithStatusCheck(
@@ -385,7 +967,7 @@ export const createOrUpdateAttributeWithStatusCheck = async (
385
967
  maxRetries
386
968
  );
387
969
  } catch (deleteError) {
388
- console.log(
970
+ MessageFormatter.info(
389
971
  chalk.red(
390
972
  `Failed to delete stuck attribute '${attribute.key}': ${deleteError}`
391
973
  )
@@ -393,14 +975,16 @@ export const createOrUpdateAttributeWithStatusCheck = async (
393
975
 
394
976
  // If attribute deletion fails, only then try collection recreation as last resort
395
977
  if (retryCount >= maxRetries - 1) {
396
- console.log(
978
+ MessageFormatter.info(
397
979
  chalk.yellow(
398
980
  `Last resort: Recreating collection for attribute '${attribute.key}'`
399
981
  )
400
982
  );
401
983
 
402
984
  // Get fresh collection data
403
- const freshCollection = await db.getCollection(dbId, collection.$id);
985
+ const freshCollection = isDatabaseAdapter(db)
986
+ ? (await db.getTable({ databaseId: dbId, tableId: collection.$id })).data
987
+ : await db.getCollection(dbId, collection.$id);
404
988
 
405
989
  // Delete and recreate collection
406
990
  const newCollection = await deleteAndRecreateCollection(
@@ -435,7 +1019,7 @@ export const createOrUpdateAttributeWithStatusCheck = async (
435
1019
  }
436
1020
  }
437
1021
 
438
- console.log(
1022
+ MessageFormatter.info(
439
1023
  chalk.red(
440
1024
  `❌ Failed to create attribute '${attribute.key}' after ${
441
1025
  maxRetries + 1
@@ -444,12 +1028,12 @@ export const createOrUpdateAttributeWithStatusCheck = async (
444
1028
  );
445
1029
  return false;
446
1030
  } catch (error) {
447
- console.log(
1031
+ MessageFormatter.info(
448
1032
  chalk.red(`Error creating attribute '${attribute.key}': ${error}`)
449
1033
  );
450
1034
 
451
1035
  if (retryCount < maxRetries) {
452
- console.log(
1036
+ MessageFormatter.info(
453
1037
  chalk.yellow(`Retrying attribute '${attribute.key}' due to error...`)
454
1038
  );
455
1039
 
@@ -470,12 +1054,12 @@ export const createOrUpdateAttributeWithStatusCheck = async (
470
1054
  }
471
1055
  };
472
1056
 
473
- export const createOrUpdateAttribute = async (
474
- db: Databases,
475
- dbId: string,
476
- collection: Models.Collection,
477
- attribute: Attribute
478
- ): Promise<"queued" | "processed"> => {
1057
+ export const createOrUpdateAttribute = async (
1058
+ db: Databases | DatabaseAdapter,
1059
+ dbId: string,
1060
+ collection: Models.Collection,
1061
+ attribute: Attribute
1062
+ ): Promise<"queued" | "processed"> => {
479
1063
  let action = "create";
480
1064
  let foundAttribute: Attribute | undefined;
481
1065
  const updateEnabled = true;
@@ -485,24 +1069,24 @@ export const createOrUpdateAttribute = async (
485
1069
  (attr: any) => attr.key === attribute.key
486
1070
  ) as unknown as any;
487
1071
  foundAttribute = parseAttribute(collectionAttr);
488
- // console.log(`Found attribute: ${JSON.stringify(foundAttribute)}`);
1072
+
489
1073
  } catch (error) {
490
1074
  foundAttribute = undefined;
491
1075
  }
492
1076
 
493
- if (
494
- foundAttribute &&
495
- attributesSame(foundAttribute, attribute) &&
496
- updateEnabled
497
- ) {
498
- // No need to do anything, they are the same
499
- return "processed";
1077
+ if (
1078
+ foundAttribute &&
1079
+ attributesSame(foundAttribute, attribute) &&
1080
+ updateEnabled
1081
+ ) {
1082
+ // No need to do anything, they are the same
1083
+ return "processed";
500
1084
  } else if (
501
1085
  foundAttribute &&
502
1086
  !attributesSame(foundAttribute, attribute) &&
503
1087
  updateEnabled
504
1088
  ) {
505
- // console.log(
1089
+ // MessageFormatter.info(
506
1090
  // `Updating attribute with same key ${attribute.key} but different values`
507
1091
  // );
508
1092
  finalAttribute = {
@@ -514,460 +1098,114 @@ export const createOrUpdateAttribute = async (
514
1098
  !updateEnabled &&
515
1099
  foundAttribute &&
516
1100
  !attributesSame(foundAttribute, attribute)
517
- ) {
518
- await db.deleteAttribute(dbId, collection.$id, attribute.key);
519
- console.log(
520
- `Deleted attribute: ${attribute.key} to recreate it because they diff (update disabled temporarily)`
521
- );
522
- return "processed";
523
- }
1101
+ ) {
1102
+ if (isDatabaseAdapter(db)) {
1103
+ await db.deleteAttribute({ databaseId: dbId, tableId: collection.$id, key: attribute.key });
1104
+ } else {
1105
+ await db.deleteAttribute(dbId, collection.$id, attribute.key);
1106
+ }
1107
+ MessageFormatter.info(
1108
+ `Deleted attribute: ${attribute.key} to recreate it because they diff (update disabled temporarily)`
1109
+ );
1110
+ return "processed";
1111
+ }
524
1112
 
525
- // console.log(`${action}-ing attribute: ${finalAttribute.key}`);
1113
+
526
1114
 
527
1115
  // Relationship attribute logic with adjustments
528
1116
  let collectionFoundViaRelatedCollection: Models.Collection | undefined;
529
1117
  let relatedCollectionId: string | undefined;
530
- if (
531
- finalAttribute.type === "relationship" &&
532
- finalAttribute.relatedCollection
533
- ) {
534
- // First try treating relatedCollection as an ID directly
535
- try {
536
- const byIdCollection = await db.getCollection(dbId, finalAttribute.relatedCollection);
537
- collectionFoundViaRelatedCollection = byIdCollection;
538
- relatedCollectionId = byIdCollection.$id;
539
- // Cache by name for subsequent lookups
540
- nameToIdMapping.set(byIdCollection.name, byIdCollection.$id);
541
- } catch (_) {
542
- // Not an ID or not found — fall back to name-based resolution below
543
- }
544
-
545
- if (!collectionFoundViaRelatedCollection && nameToIdMapping.has(finalAttribute.relatedCollection)) {
546
- relatedCollectionId = nameToIdMapping.get(
547
- finalAttribute.relatedCollection
548
- );
549
- try {
550
- collectionFoundViaRelatedCollection = await db.getCollection(
551
- dbId,
552
- relatedCollectionId!
553
- );
554
- } catch (e) {
555
- // console.log(
556
- // `Collection not found: ${finalAttribute.relatedCollection} when nameToIdMapping was set`
557
- // );
558
- collectionFoundViaRelatedCollection = undefined;
559
- }
560
- } else if (!collectionFoundViaRelatedCollection) {
561
- const collectionsPulled = await db.listCollections(dbId, [
562
- Query.equal("name", finalAttribute.relatedCollection),
563
- ]);
564
- if (collectionsPulled.total > 0) {
565
- collectionFoundViaRelatedCollection = collectionsPulled.collections[0];
566
- relatedCollectionId = collectionFoundViaRelatedCollection.$id;
567
- nameToIdMapping.set(
568
- finalAttribute.relatedCollection,
569
- relatedCollectionId
570
- );
571
- }
572
- }
573
- // ONLY queue relationship attributes that have actual unresolved dependencies
574
- if (!(relatedCollectionId && collectionFoundViaRelatedCollection)) {
575
- console.log(
576
- chalk.yellow(
577
- `⏳ Queueing relationship attribute '${finalAttribute.key}' - related collection '${finalAttribute.relatedCollection}' not found yet`
578
- )
579
- );
580
- enqueueOperation({
581
- type: "attribute",
582
- collectionId: collection.$id,
583
- collection: collection,
584
- attribute,
585
- dependencies: [finalAttribute.relatedCollection],
586
- });
587
- return "queued";
588
- }
589
- }
590
- finalAttribute = parseAttribute(finalAttribute);
591
- // console.log(`Final Attribute: ${JSON.stringify(finalAttribute)}`);
592
- switch (finalAttribute.type) {
593
- case "string":
594
- if (action === "create") {
595
- await tryAwaitWithRetry(
596
- async () =>
597
- await db.createStringAttribute(
598
- dbId,
599
- collection.$id,
600
- finalAttribute.key,
601
- finalAttribute.size,
602
- finalAttribute.required || false,
603
- finalAttribute.xdefault !== undefined && !finalAttribute.required
604
- ? finalAttribute.xdefault
605
- : null,
606
- finalAttribute.array || false,
607
- finalAttribute.encrypted
608
- )
609
- );
610
- } else {
611
- await tryAwaitWithRetry(
612
- async () =>
613
- await db.updateStringAttribute(
614
- dbId,
615
- collection.$id,
616
- finalAttribute.key,
617
- finalAttribute.required || false,
618
- finalAttribute.xdefault !== undefined && !finalAttribute.required
619
- ? finalAttribute.xdefault
620
- : null,
621
- finalAttribute.size
622
- )
623
- );
1118
+ if (
1119
+ finalAttribute.type === "relationship" &&
1120
+ finalAttribute.relatedCollection
1121
+ ) {
1122
+ // First try treating relatedCollection as an ID directly
1123
+ try {
1124
+ const byIdCollection = isDatabaseAdapter(db)
1125
+ ? (await db.getTable({ databaseId: dbId, tableId: finalAttribute.relatedCollection })).data
1126
+ : await db.getCollection(dbId, finalAttribute.relatedCollection);
1127
+ collectionFoundViaRelatedCollection = byIdCollection;
1128
+ relatedCollectionId = byIdCollection.$id;
1129
+ // Cache by name for subsequent lookups
1130
+ nameToIdMapping.set(byIdCollection.name, byIdCollection.$id);
1131
+ } catch (_) {
1132
+ // Not an ID or not found — fall back to name-based resolution below
1133
+ }
1134
+
1135
+ if (!collectionFoundViaRelatedCollection && nameToIdMapping.has(finalAttribute.relatedCollection)) {
1136
+ relatedCollectionId = nameToIdMapping.get(
1137
+ finalAttribute.relatedCollection
1138
+ );
1139
+ try {
1140
+ collectionFoundViaRelatedCollection = isDatabaseAdapter(db)
1141
+ ? (await db.getTable({ databaseId: dbId, tableId: relatedCollectionId! })).data
1142
+ : await db.getCollection(dbId, relatedCollectionId!);
1143
+ } catch (e) {
1144
+ // MessageFormatter.info(
1145
+ // `Collection not found: ${finalAttribute.relatedCollection} when nameToIdMapping was set`
1146
+ // );
1147
+ collectionFoundViaRelatedCollection = undefined;
624
1148
  }
625
- break;
626
- case "integer":
627
- if (action === "create") {
628
- if (
629
- finalAttribute.min &&
630
- BigInt(finalAttribute.min) === BigInt(-9223372036854776000)
631
- ) {
632
- finalAttribute.min = undefined;
633
- }
634
- if (
635
- finalAttribute.max &&
636
- BigInt(finalAttribute.max) === BigInt(9223372036854776000)
637
- ) {
638
- finalAttribute.max = undefined;
1149
+ } else if (!collectionFoundViaRelatedCollection) {
1150
+ const collectionsPulled = isDatabaseAdapter(db)
1151
+ ? await db.listTables({ databaseId: dbId, queries: [Query.equal("name", finalAttribute.relatedCollection)] })
1152
+ : await db.listCollections(dbId, [Query.equal("name", finalAttribute.relatedCollection)]);
1153
+ if (collectionsPulled.total && collectionsPulled.total > 0) {
1154
+ collectionFoundViaRelatedCollection = isDatabaseAdapter(db)
1155
+ ? (collectionsPulled as any).tables?.[0]
1156
+ : (collectionsPulled as any).collections?.[0];
1157
+ relatedCollectionId = collectionFoundViaRelatedCollection?.$id;
1158
+ if (relatedCollectionId) {
1159
+ nameToIdMapping.set(
1160
+ finalAttribute.relatedCollection,
1161
+ relatedCollectionId
1162
+ );
639
1163
  }
640
- const minValue =
641
- finalAttribute.min !== undefined && finalAttribute.min !== null
642
- ? parseInt(finalAttribute.min)
643
- : -9007199254740991;
644
- const maxValue =
645
- finalAttribute.max !== undefined && finalAttribute.max !== null
646
- ? parseInt(finalAttribute.max)
647
- : 9007199254740991;
648
- console.log(
649
- `DEBUG: Creating integer attribute '${
650
- finalAttribute.key
651
- }' with min=${minValue}, max=${maxValue}, minType=${typeof minValue}, maxType=${typeof maxValue}`
652
- );
653
- await tryAwaitWithRetry(
654
- async () =>
655
- await db.createIntegerAttribute(
656
- dbId,
657
- collection.$id,
658
- finalAttribute.key,
659
- finalAttribute.required || false,
660
- minValue,
661
- maxValue,
662
- finalAttribute.xdefault !== undefined && !finalAttribute.required
663
- ? finalAttribute.xdefault
664
- : null,
665
- finalAttribute.array || false
666
- )
667
- );
668
- } else {
669
- if (
670
- finalAttribute.min &&
671
- BigInt(finalAttribute.min) === BigInt(-9223372036854776000)
672
- ) {
673
- finalAttribute.min = undefined;
674
- }
675
- if (
676
- finalAttribute.max &&
677
- BigInt(finalAttribute.max) === BigInt(9223372036854776000)
678
- ) {
679
- finalAttribute.max = undefined;
680
- }
681
- const minValue =
682
- finalAttribute.min !== undefined && finalAttribute.min !== null
683
- ? parseInt(finalAttribute.min)
684
- : -9007199254740991;
685
- const maxValue =
686
- finalAttribute.max !== undefined && finalAttribute.max !== null
687
- ? parseInt(finalAttribute.max)
688
- : 9007199254740991;
689
- console.log(
690
- `DEBUG: Updating integer attribute '${
691
- finalAttribute.key
692
- }' with min=${minValue}, max=${maxValue}, minType=${typeof minValue}, maxType=${typeof maxValue}`
693
- );
694
- await tryAwaitWithRetry(
695
- async () =>
696
- await db.updateIntegerAttribute(
697
- dbId,
698
- collection.$id,
699
- finalAttribute.key,
700
- finalAttribute.required || false,
701
- finalAttribute.xdefault !== undefined && !finalAttribute.required
702
- ? finalAttribute.xdefault
703
- : null,
704
- minValue,
705
- maxValue
706
- )
707
- );
708
- }
709
- break;
710
- case "double":
711
- case "float": // Backward compatibility
712
- if (action === "create") {
713
- await tryAwaitWithRetry(
714
- async () =>
715
- await db.createFloatAttribute(
716
- dbId,
717
- collection.$id,
718
- finalAttribute.key,
719
- finalAttribute.required || false,
720
- finalAttribute.min,
721
- finalAttribute.max,
722
- finalAttribute.xdefault !== undefined && !finalAttribute.required
723
- ? finalAttribute.xdefault
724
- : null,
725
- finalAttribute.array || false
726
- )
727
- );
728
- } else {
729
- await tryAwaitWithRetry(
730
- async () =>
731
- await db.updateFloatAttribute(
732
- dbId,
733
- collection.$id,
734
- finalAttribute.key,
735
- finalAttribute.required || false,
736
- finalAttribute.min,
737
- finalAttribute.max,
738
- finalAttribute.xdefault !== undefined && !finalAttribute.required
739
- ? finalAttribute.xdefault
740
- : null
741
- )
742
- );
743
- }
744
- break;
745
- case "boolean":
746
- if (action === "create") {
747
- await tryAwaitWithRetry(
748
- async () =>
749
- await db.createBooleanAttribute(
750
- dbId,
751
- collection.$id,
752
- finalAttribute.key,
753
- finalAttribute.required || false,
754
- finalAttribute.xdefault !== undefined && !finalAttribute.required
755
- ? finalAttribute.xdefault
756
- : null,
757
- finalAttribute.array || false
758
- )
759
- );
760
- } else {
761
- await tryAwaitWithRetry(
762
- async () =>
763
- await db.updateBooleanAttribute(
764
- dbId,
765
- collection.$id,
766
- finalAttribute.key,
767
- finalAttribute.required || false,
768
- finalAttribute.xdefault !== undefined && !finalAttribute.required
769
- ? finalAttribute.xdefault
770
- : null
771
- )
772
- );
773
- }
774
- break;
775
- case "datetime":
776
- if (action === "create") {
777
- await tryAwaitWithRetry(
778
- async () =>
779
- await db.createDatetimeAttribute(
780
- dbId,
781
- collection.$id,
782
- finalAttribute.key,
783
- finalAttribute.required || false,
784
- finalAttribute.xdefault !== undefined && !finalAttribute.required
785
- ? finalAttribute.xdefault
786
- : null,
787
- finalAttribute.array || false
788
- )
789
- );
790
- } else {
791
- await tryAwaitWithRetry(
792
- async () =>
793
- await db.updateDatetimeAttribute(
794
- dbId,
795
- collection.$id,
796
- finalAttribute.key,
797
- finalAttribute.required || false,
798
- finalAttribute.xdefault !== undefined && !finalAttribute.required
799
- ? finalAttribute.xdefault
800
- : null
801
- )
802
- );
803
- }
804
- break;
805
- case "email":
806
- if (action === "create") {
807
- await tryAwaitWithRetry(
808
- async () =>
809
- await db.createEmailAttribute(
810
- dbId,
811
- collection.$id,
812
- finalAttribute.key,
813
- finalAttribute.required || false,
814
- finalAttribute.xdefault !== undefined && !finalAttribute.required
815
- ? finalAttribute.xdefault
816
- : null,
817
- finalAttribute.array || false
818
- )
819
- );
820
- } else {
821
- await tryAwaitWithRetry(
822
- async () =>
823
- await db.updateEmailAttribute(
824
- dbId,
825
- collection.$id,
826
- finalAttribute.key,
827
- finalAttribute.required || false,
828
- finalAttribute.xdefault !== undefined && !finalAttribute.required
829
- ? finalAttribute.xdefault
830
- : null
831
- )
832
- );
833
- }
834
- break;
835
- case "ip":
836
- if (action === "create") {
837
- await tryAwaitWithRetry(
838
- async () =>
839
- await db.createIpAttribute(
840
- dbId,
841
- collection.$id,
842
- finalAttribute.key,
843
- finalAttribute.required || false,
844
- finalAttribute.xdefault !== undefined && !finalAttribute.required
845
- ? finalAttribute.xdefault
846
- : null,
847
- finalAttribute.array || false
848
- )
849
- );
850
- } else {
851
- await tryAwaitWithRetry(
852
- async () =>
853
- await db.updateIpAttribute(
854
- dbId,
855
- collection.$id,
856
- finalAttribute.key,
857
- finalAttribute.required || false,
858
- finalAttribute.xdefault !== undefined && !finalAttribute.required
859
- ? finalAttribute.xdefault
860
- : null
861
- )
862
- );
863
- }
864
- break;
865
- case "url":
866
- if (action === "create") {
867
- await tryAwaitWithRetry(
868
- async () =>
869
- await db.createUrlAttribute(
870
- dbId,
871
- collection.$id,
872
- finalAttribute.key,
873
- finalAttribute.required || false,
874
- finalAttribute.xdefault !== undefined && !finalAttribute.required
875
- ? finalAttribute.xdefault
876
- : null,
877
- finalAttribute.array || false
878
- )
879
- );
880
- } else {
881
- await tryAwaitWithRetry(
882
- async () =>
883
- await db.updateUrlAttribute(
884
- dbId,
885
- collection.$id,
886
- finalAttribute.key,
887
- finalAttribute.required || false,
888
- finalAttribute.xdefault !== undefined && !finalAttribute.required
889
- ? finalAttribute.xdefault
890
- : null
891
- )
892
- );
893
- }
894
- break;
895
- case "enum":
896
- if (action === "create") {
897
- await tryAwaitWithRetry(
898
- async () =>
899
- await db.createEnumAttribute(
900
- dbId,
901
- collection.$id,
902
- finalAttribute.key,
903
- finalAttribute.elements,
904
- finalAttribute.required || false,
905
- finalAttribute.xdefault !== undefined && !finalAttribute.required
906
- ? finalAttribute.xdefault
907
- : null,
908
- finalAttribute.array || false
909
- )
910
- );
911
- } else {
912
- await tryAwaitWithRetry(
913
- async () =>
914
- await db.updateEnumAttribute(
915
- dbId,
916
- collection.$id,
917
- finalAttribute.key,
918
- finalAttribute.elements,
919
- finalAttribute.required || false,
920
- finalAttribute.xdefault !== undefined && !finalAttribute.required
921
- ? finalAttribute.xdefault
922
- : null
923
- )
924
- );
925
- }
926
- break;
927
- case "relationship":
928
- if (action === "create") {
929
- await tryAwaitWithRetry(
930
- async () =>
931
- await db.createRelationshipAttribute(
932
- dbId,
933
- collection.$id,
934
- relatedCollectionId!,
935
- finalAttribute.relationType,
936
- finalAttribute.twoWay,
937
- finalAttribute.key,
938
- finalAttribute.twoWayKey,
939
- finalAttribute.onDelete
940
- )
941
- );
942
- } else {
943
- await tryAwaitWithRetry(
944
- async () =>
945
- await db.updateRelationshipAttribute(
946
- dbId,
947
- collection.$id,
948
- finalAttribute.key,
949
- finalAttribute.onDelete
950
- )
951
- );
952
1164
  }
953
- break;
954
- default:
955
- console.error("Invalid attribute type");
956
- break;
957
- }
958
- return "processed";
959
- };
1165
+ }
1166
+ // ONLY queue relationship attributes that have actual unresolved dependencies
1167
+ if (!(relatedCollectionId && collectionFoundViaRelatedCollection)) {
1168
+ MessageFormatter.info(
1169
+ chalk.yellow(
1170
+ `⏳ Queueing relationship attribute '${finalAttribute.key}' - related collection '${finalAttribute.relatedCollection}' not found yet`
1171
+ )
1172
+ );
1173
+ enqueueOperation({
1174
+ type: "attribute",
1175
+ collectionId: collection.$id,
1176
+ collection: collection,
1177
+ attribute,
1178
+ dependencies: [finalAttribute.relatedCollection],
1179
+ });
1180
+ return "queued";
1181
+ }
1182
+ }
1183
+ finalAttribute = parseAttribute(finalAttribute);
1184
+
1185
+
1186
+ // Use adapter-based attribute creation/update
1187
+ if (action === "create") {
1188
+ await tryAwaitWithRetry(
1189
+ async () => await createAttributeViaAdapter(db, dbId, collection.$id, finalAttribute)
1190
+ );
1191
+ } else {
1192
+ await tryAwaitWithRetry(
1193
+ async () => await updateAttributeViaAdapter(db, dbId, collection.$id, finalAttribute)
1194
+ );
1195
+ }
1196
+ return "processed";
1197
+ };
960
1198
 
961
1199
  /**
962
1200
  * Enhanced collection attribute creation with proper status monitoring
963
1201
  */
964
1202
  export const createUpdateCollectionAttributesWithStatusCheck = async (
965
- db: Databases,
1203
+ db: Databases | DatabaseAdapter,
966
1204
  dbId: string,
967
1205
  collection: Models.Collection,
968
1206
  attributes: Attribute[]
969
1207
  ): Promise<boolean> => {
970
- console.log(
1208
+ MessageFormatter.info(
971
1209
  chalk.green(
972
1210
  `Creating/Updating attributes for collection: ${collection.name} with status monitoring`
973
1211
  )
@@ -987,7 +1225,7 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (
987
1225
  // Handle attribute removal first
988
1226
  if (attributesToRemove.length > 0) {
989
1227
  if (indexesToRemove.length > 0) {
990
- console.log(
1228
+ MessageFormatter.info(
991
1229
  chalk.red(
992
1230
  `Removing indexes as they rely on an attribute that is being removed: ${indexesToRemove
993
1231
  .map((index) => index.key)
@@ -996,26 +1234,38 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (
996
1234
  );
997
1235
  for (const index of indexesToRemove) {
998
1236
  await tryAwaitWithRetry(
999
- async () => await db.deleteIndex(dbId, collection.$id, index.key)
1237
+ async () => {
1238
+ if (isDatabaseAdapter(db)) {
1239
+ await db.deleteIndex({ databaseId: dbId, tableId: collection.$id, key: index.key });
1240
+ } else {
1241
+ await db.deleteIndex(dbId, collection.$id, index.key);
1242
+ }
1243
+ }
1000
1244
  );
1001
1245
  await delay(500); // Longer delay for deletions
1002
1246
  }
1003
1247
  }
1004
1248
  for (const attr of attributesToRemove) {
1005
- console.log(
1249
+ MessageFormatter.info(
1006
1250
  chalk.red(
1007
1251
  `Removing attribute: ${attr.key} as it is no longer in the collection`
1008
1252
  )
1009
1253
  );
1010
1254
  await tryAwaitWithRetry(
1011
- async () => await db.deleteAttribute(dbId, collection.$id, attr.key)
1255
+ async () => {
1256
+ if (isDatabaseAdapter(db)) {
1257
+ await db.deleteAttribute({ databaseId: dbId, tableId: collection.$id, key: attr.key });
1258
+ } else {
1259
+ await db.deleteAttribute(dbId, collection.$id, attr.key);
1260
+ }
1261
+ }
1012
1262
  );
1013
1263
  await delay(500); // Longer delay for deletions
1014
1264
  }
1015
1265
  }
1016
1266
 
1017
1267
  // First, get fresh collection data and determine which attributes actually need processing
1018
- console.log(
1268
+ MessageFormatter.info(
1019
1269
  chalk.blue(
1020
1270
  `Analyzing ${attributes.length} attributes to determine which need processing...`
1021
1271
  )
@@ -1023,9 +1273,11 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (
1023
1273
 
1024
1274
  let currentCollection = collection;
1025
1275
  try {
1026
- currentCollection = await db.getCollection(dbId, collection.$id);
1276
+ currentCollection = isDatabaseAdapter(db)
1277
+ ? (await db.getTable({ databaseId: dbId, tableId: collection.$id })).data
1278
+ : await db.getCollection(dbId, collection.$id);
1027
1279
  } catch (error) {
1028
- console.log(
1280
+ MessageFormatter.info(
1029
1281
  chalk.yellow(`Warning: Could not refresh collection data: ${error}`)
1030
1282
  );
1031
1283
  }
@@ -1040,24 +1292,32 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (
1040
1292
  existingAttributesMap.set(attr.key, attr)
1041
1293
  );
1042
1294
  } catch (error) {
1043
- console.log(
1295
+ MessageFormatter.info(
1044
1296
  chalk.yellow(`Warning: Could not parse existing attributes: ${error}`)
1045
1297
  );
1046
1298
  }
1047
1299
 
1048
- // Filter to only attributes that need processing (new or changed)
1300
+ // Filter to only attributes that need processing (new, changed, or not yet processed)
1049
1301
  const attributesToProcess = attributes.filter((attribute) => {
1302
+ // Skip if already processed in this session
1303
+ if (isAttributeProcessed(currentCollection.$id, attribute.key)) {
1304
+ MessageFormatter.info(
1305
+ chalk.gray(`⏭️ Attribute '${attribute.key}' already processed in this session (skipping)`)
1306
+ );
1307
+ return false;
1308
+ }
1309
+
1050
1310
  const existing = existingAttributesMap.get(attribute.key);
1051
1311
  if (!existing) {
1052
- console.log(chalk.blue(`➕ New attribute: ${attribute.key}`));
1312
+ MessageFormatter.info(`➕ New attribute: ${attribute.key}`);
1053
1313
  return true;
1054
1314
  }
1055
1315
 
1056
1316
  const needsUpdate = !attributesSame(existing, attribute);
1057
1317
  if (needsUpdate) {
1058
- console.log(chalk.blue(`🔄 Changed attribute: ${attribute.key}`));
1318
+ MessageFormatter.info(`🔄 Changed attribute: ${attribute.key}`);
1059
1319
  } else {
1060
- console.log(
1320
+ MessageFormatter.info(
1061
1321
  chalk.gray(`✅ Unchanged attribute: ${attribute.key} (skipping)`)
1062
1322
  );
1063
1323
  }
@@ -1065,7 +1325,7 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (
1065
1325
  });
1066
1326
 
1067
1327
  if (attributesToProcess.length === 0) {
1068
- console.log(
1328
+ MessageFormatter.info(
1069
1329
  chalk.green(
1070
1330
  `✅ All ${attributes.length} attributes are already up to date for collection: ${collection.name}`
1071
1331
  )
@@ -1073,7 +1333,7 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (
1073
1333
  return true;
1074
1334
  }
1075
1335
 
1076
- console.log(
1336
+ MessageFormatter.info(
1077
1337
  chalk.blue(
1078
1338
  `Creating ${attributesToProcess.length} attributes sequentially with status monitoring...`
1079
1339
  )
@@ -1090,7 +1350,7 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (
1090
1350
  const attributesToProcessThisRound = [...remainingAttributes];
1091
1351
  remainingAttributes = []; // Reset for next iteration
1092
1352
 
1093
- console.log(
1353
+ MessageFormatter.info(
1094
1354
  chalk.blue(
1095
1355
  `\n=== Attempt ${
1096
1356
  overallRetryCount + 1
@@ -1101,7 +1361,7 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (
1101
1361
  );
1102
1362
 
1103
1363
  for (const attribute of attributesToProcessThisRound) {
1104
- console.log(
1364
+ MessageFormatter.info(
1105
1365
  chalk.blue(`\n--- Processing attribute: ${attribute.key} ---`)
1106
1366
  );
1107
1367
 
@@ -1113,15 +1373,20 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (
1113
1373
  );
1114
1374
 
1115
1375
  if (success) {
1116
- console.log(
1376
+ MessageFormatter.info(
1117
1377
  chalk.green(`✅ Successfully created attribute: ${attribute.key}`)
1118
1378
  );
1119
1379
 
1380
+ // Mark this specific attribute as processed
1381
+ markAttributeProcessed(currentCollection.$id, attribute.key);
1382
+
1120
1383
  // Get updated collection data for next iteration
1121
1384
  try {
1122
- currentCollection = await db.getCollection(dbId, collection.$id);
1385
+ currentCollection = isDatabaseAdapter(db)
1386
+ ? (await db.getTable({ databaseId: dbId, tableId: collection.$id })).data as Models.Collection
1387
+ : await db.getCollection(dbId, collection.$id);
1123
1388
  } catch (error) {
1124
- console.log(
1389
+ MessageFormatter.info(
1125
1390
  chalk.yellow(`Warning: Could not refresh collection data: ${error}`)
1126
1391
  );
1127
1392
  }
@@ -1129,7 +1394,7 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (
1129
1394
  // Add delay between successful attributes
1130
1395
  await delay(1000);
1131
1396
  } else {
1132
- console.log(
1397
+ MessageFormatter.info(
1133
1398
  chalk.red(
1134
1399
  `❌ Failed to create attribute: ${attribute.key}, will retry in next round`
1135
1400
  )
@@ -1139,7 +1404,7 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (
1139
1404
  }
1140
1405
 
1141
1406
  if (remainingAttributes.length === 0) {
1142
- console.log(
1407
+ MessageFormatter.info(
1143
1408
  chalk.green(
1144
1409
  `\n✅ Successfully created all ${attributesToProcess.length} attributes for collection: ${collection.name}`
1145
1410
  )
@@ -1150,7 +1415,7 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (
1150
1415
  overallRetryCount++;
1151
1416
 
1152
1417
  if (overallRetryCount < maxOverallRetries) {
1153
- console.log(
1418
+ MessageFormatter.info(
1154
1419
  chalk.yellow(
1155
1420
  `\n⏳ Waiting 5 seconds before retrying ${attributesToProcess.length} failed attributes...`
1156
1421
  )
@@ -1159,10 +1424,12 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (
1159
1424
 
1160
1425
  // Refresh collection data before retry
1161
1426
  try {
1162
- currentCollection = await db.getCollection(dbId, collection.$id);
1163
- console.log(chalk.blue(`Refreshed collection data for retry`));
1427
+ currentCollection = isDatabaseAdapter(db)
1428
+ ? (await db.getTable({ databaseId: dbId, tableId: collection.$id })).data as Models.Collection
1429
+ : await db.getCollection(dbId, collection.$id);
1430
+ MessageFormatter.info(`Refreshed collection data for retry`);
1164
1431
  } catch (error) {
1165
- console.log(
1432
+ MessageFormatter.info(
1166
1433
  chalk.yellow(
1167
1434
  `Warning: Could not refresh collection data for retry: ${error}`
1168
1435
  )
@@ -1173,7 +1440,7 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (
1173
1440
 
1174
1441
  // If we get here, some attributes still failed after all retries
1175
1442
  if (attributesToProcess.length > 0) {
1176
- console.log(
1443
+ MessageFormatter.info(
1177
1444
  chalk.red(
1178
1445
  `\n❌ Failed to create ${
1179
1446
  attributesToProcess.length
@@ -1182,7 +1449,7 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (
1182
1449
  .join(", ")}`
1183
1450
  )
1184
1451
  );
1185
- console.log(
1452
+ MessageFormatter.info(
1186
1453
  chalk.red(
1187
1454
  `This may indicate a fundamental issue with the attribute definitions or Appwrite instance`
1188
1455
  )
@@ -1190,7 +1457,7 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (
1190
1457
  return false;
1191
1458
  }
1192
1459
 
1193
- console.log(
1460
+ MessageFormatter.info(
1194
1461
  chalk.green(
1195
1462
  `\n✅ Successfully created all ${attributes.length} attributes for collection: ${collection.name}`
1196
1463
  )
@@ -1199,12 +1466,12 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (
1199
1466
  };
1200
1467
 
1201
1468
  export const createUpdateCollectionAttributes = async (
1202
- db: Databases,
1469
+ db: Databases | DatabaseAdapter,
1203
1470
  dbId: string,
1204
1471
  collection: Models.Collection,
1205
1472
  attributes: Attribute[]
1206
1473
  ): Promise<void> => {
1207
- console.log(
1474
+ MessageFormatter.info(
1208
1475
  chalk.green(
1209
1476
  `Creating/Updating attributes for collection: ${collection.name}`
1210
1477
  )
@@ -1223,7 +1490,7 @@ export const createUpdateCollectionAttributes = async (
1223
1490
 
1224
1491
  if (attributesToRemove.length > 0) {
1225
1492
  if (indexesToRemove.length > 0) {
1226
- console.log(
1493
+ MessageFormatter.info(
1227
1494
  chalk.red(
1228
1495
  `Removing indexes as they rely on an attribute that is being removed: ${indexesToRemove
1229
1496
  .map((index) => index.key)
@@ -1232,19 +1499,31 @@ export const createUpdateCollectionAttributes = async (
1232
1499
  );
1233
1500
  for (const index of indexesToRemove) {
1234
1501
  await tryAwaitWithRetry(
1235
- async () => await db.deleteIndex(dbId, collection.$id, index.key)
1502
+ async () => {
1503
+ if (isDatabaseAdapter(db)) {
1504
+ await db.deleteIndex({ databaseId: dbId, tableId: collection.$id, key: index.key });
1505
+ } else {
1506
+ await db.deleteIndex(dbId, collection.$id, index.key);
1507
+ }
1508
+ }
1236
1509
  );
1237
1510
  await delay(100);
1238
1511
  }
1239
1512
  }
1240
1513
  for (const attr of attributesToRemove) {
1241
- console.log(
1514
+ MessageFormatter.info(
1242
1515
  chalk.red(
1243
1516
  `Removing attribute: ${attr.key} as it is no longer in the collection`
1244
1517
  )
1245
1518
  );
1246
1519
  await tryAwaitWithRetry(
1247
- async () => await db.deleteAttribute(dbId, collection.$id, attr.key)
1520
+ async () => {
1521
+ if (isDatabaseAdapter(db)) {
1522
+ await db.deleteAttribute({ databaseId: dbId, tableId: collection.$id, key: attr.key });
1523
+ } else {
1524
+ await db.deleteAttribute(dbId, collection.$id, attr.key);
1525
+ }
1526
+ }
1248
1527
  );
1249
1528
  await delay(50);
1250
1529
  }
@@ -1263,14 +1542,14 @@ export const createUpdateCollectionAttributes = async (
1263
1542
  const results = await Promise.allSettled(attributePromises);
1264
1543
  results.forEach((result) => {
1265
1544
  if (result.status === "rejected") {
1266
- console.error("An attribute promise was rejected:", result.reason);
1545
+ MessageFormatter.error("An attribute promise was rejected:", result.reason);
1267
1546
  }
1268
1547
  });
1269
1548
 
1270
1549
  // Add delay after each batch
1271
1550
  await delay(200);
1272
1551
  }
1273
- console.log(
1552
+ MessageFormatter.info(
1274
1553
  `Finished creating/updating attributes for collection: ${collection.name}`
1275
1554
  );
1276
1555
  };