appwrite-utils-cli 1.11.0 → 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 (250) hide show
  1. package/{src/adapters/index.ts → dist/adapters/index.d.ts} +0 -1
  2. package/dist/adapters/index.js +10 -0
  3. package/dist/backups/operations/bucketBackup.d.ts +19 -0
  4. package/dist/backups/operations/bucketBackup.js +197 -0
  5. package/dist/backups/operations/collectionBackup.d.ts +30 -0
  6. package/dist/backups/operations/collectionBackup.js +201 -0
  7. package/dist/backups/operations/comprehensiveBackup.d.ts +25 -0
  8. package/dist/backups/operations/comprehensiveBackup.js +238 -0
  9. package/dist/backups/schemas/bucketManifest.d.ts +93 -0
  10. package/dist/backups/schemas/bucketManifest.js +33 -0
  11. package/dist/backups/schemas/comprehensiveManifest.d.ts +108 -0
  12. package/dist/backups/schemas/comprehensiveManifest.js +32 -0
  13. package/dist/backups/tracking/centralizedTracking.d.ts +34 -0
  14. package/dist/backups/tracking/centralizedTracking.js +274 -0
  15. package/dist/cli/commands/configCommands.d.ts +8 -0
  16. package/dist/cli/commands/configCommands.js +210 -0
  17. package/dist/cli/commands/databaseCommands.d.ts +14 -0
  18. package/dist/cli/commands/databaseCommands.js +696 -0
  19. package/dist/cli/commands/functionCommands.d.ts +7 -0
  20. package/dist/cli/commands/functionCommands.js +330 -0
  21. package/dist/cli/commands/importFileCommands.d.ts +7 -0
  22. package/dist/cli/commands/importFileCommands.js +674 -0
  23. package/dist/cli/commands/schemaCommands.d.ts +7 -0
  24. package/dist/cli/commands/schemaCommands.js +169 -0
  25. package/dist/cli/commands/storageCommands.d.ts +5 -0
  26. package/dist/cli/commands/storageCommands.js +142 -0
  27. package/dist/cli/commands/transferCommands.d.ts +5 -0
  28. package/dist/cli/commands/transferCommands.js +382 -0
  29. package/dist/collections/columns.d.ts +13 -0
  30. package/dist/collections/columns.js +1339 -0
  31. package/dist/collections/indexes.d.ts +12 -0
  32. package/dist/collections/indexes.js +215 -0
  33. package/dist/collections/methods.d.ts +19 -0
  34. package/dist/collections/methods.js +605 -0
  35. package/dist/collections/tableOperations.d.ts +87 -0
  36. package/dist/collections/tableOperations.js +466 -0
  37. package/dist/collections/transferOperations.d.ts +8 -0
  38. package/dist/collections/transferOperations.js +411 -0
  39. package/dist/collections/wipeOperations.d.ts +17 -0
  40. package/dist/collections/wipeOperations.js +306 -0
  41. package/dist/databases/methods.d.ts +6 -0
  42. package/dist/databases/methods.js +35 -0
  43. package/dist/databases/setup.d.ts +5 -0
  44. package/dist/databases/setup.js +45 -0
  45. package/dist/examples/yamlTerminologyExample.d.ts +42 -0
  46. package/dist/examples/yamlTerminologyExample.js +272 -0
  47. package/dist/functions/deployments.d.ts +4 -0
  48. package/dist/functions/deployments.js +146 -0
  49. package/dist/functions/fnConfigDiscovery.d.ts +3 -0
  50. package/dist/functions/fnConfigDiscovery.js +108 -0
  51. package/dist/functions/methods.d.ts +16 -0
  52. package/dist/functions/methods.js +174 -0
  53. package/dist/init.d.ts +2 -0
  54. package/dist/init.js +57 -0
  55. package/dist/interactiveCLI.d.ts +36 -0
  56. package/dist/interactiveCLI.js +952 -0
  57. package/dist/main.d.ts +2 -0
  58. package/dist/main.js +1125 -0
  59. package/dist/migrations/afterImportActions.d.ts +17 -0
  60. package/dist/migrations/afterImportActions.js +305 -0
  61. package/dist/migrations/appwriteToX.d.ts +211 -0
  62. package/dist/migrations/appwriteToX.js +493 -0
  63. package/dist/migrations/comprehensiveTransfer.d.ts +147 -0
  64. package/dist/migrations/comprehensiveTransfer.js +1315 -0
  65. package/dist/migrations/dataLoader.d.ts +755 -0
  66. package/dist/migrations/dataLoader.js +1272 -0
  67. package/dist/migrations/importController.d.ts +25 -0
  68. package/dist/migrations/importController.js +283 -0
  69. package/dist/migrations/importDataActions.d.ts +50 -0
  70. package/dist/migrations/importDataActions.js +230 -0
  71. package/dist/migrations/relationships.d.ts +29 -0
  72. package/dist/migrations/relationships.js +203 -0
  73. package/dist/migrations/services/DataTransformationService.d.ts +55 -0
  74. package/dist/migrations/services/DataTransformationService.js +158 -0
  75. package/dist/migrations/services/FileHandlerService.d.ts +75 -0
  76. package/dist/migrations/services/FileHandlerService.js +236 -0
  77. package/dist/migrations/services/ImportOrchestrator.d.ts +99 -0
  78. package/dist/migrations/services/ImportOrchestrator.js +493 -0
  79. package/dist/migrations/services/RateLimitManager.d.ts +138 -0
  80. package/dist/migrations/services/RateLimitManager.js +279 -0
  81. package/dist/migrations/services/RelationshipResolver.d.ts +120 -0
  82. package/dist/migrations/services/RelationshipResolver.js +332 -0
  83. package/dist/migrations/services/UserMappingService.d.ts +109 -0
  84. package/dist/migrations/services/UserMappingService.js +277 -0
  85. package/dist/migrations/services/ValidationService.d.ts +74 -0
  86. package/dist/migrations/services/ValidationService.js +260 -0
  87. package/dist/migrations/transfer.d.ts +30 -0
  88. package/dist/migrations/transfer.js +661 -0
  89. package/dist/migrations/yaml/YamlImportConfigLoader.d.ts +131 -0
  90. package/dist/migrations/yaml/YamlImportConfigLoader.js +383 -0
  91. package/dist/migrations/yaml/YamlImportIntegration.d.ts +93 -0
  92. package/dist/migrations/yaml/YamlImportIntegration.js +341 -0
  93. package/dist/migrations/yaml/generateImportSchemas.d.ts +30 -0
  94. package/dist/migrations/yaml/generateImportSchemas.js +1327 -0
  95. package/dist/schemas/authUser.d.ts +24 -0
  96. package/dist/schemas/authUser.js +17 -0
  97. package/dist/setup.d.ts +2 -0
  98. package/{src/setup.ts → dist/setup.js} +0 -3
  99. package/dist/setupCommands.d.ts +58 -0
  100. package/dist/setupCommands.js +489 -0
  101. package/dist/setupController.d.ts +9 -0
  102. package/dist/setupController.js +34 -0
  103. package/dist/shared/backupMetadataSchema.d.ts +94 -0
  104. package/dist/shared/backupMetadataSchema.js +38 -0
  105. package/dist/shared/backupTracking.d.ts +18 -0
  106. package/dist/shared/backupTracking.js +176 -0
  107. package/dist/shared/confirmationDialogs.d.ts +75 -0
  108. package/dist/shared/confirmationDialogs.js +236 -0
  109. package/dist/shared/migrationHelpers.d.ts +61 -0
  110. package/dist/shared/migrationHelpers.js +145 -0
  111. package/{src/shared/operationLogger.ts → dist/shared/operationLogger.d.ts} +1 -11
  112. package/dist/shared/operationLogger.js +12 -0
  113. package/dist/shared/operationQueue.d.ts +40 -0
  114. package/dist/shared/operationQueue.js +310 -0
  115. package/dist/shared/operationsTable.d.ts +26 -0
  116. package/dist/shared/operationsTable.js +287 -0
  117. package/dist/shared/operationsTableSchema.d.ts +48 -0
  118. package/dist/shared/operationsTableSchema.js +35 -0
  119. package/dist/shared/progressManager.d.ts +62 -0
  120. package/dist/shared/progressManager.js +215 -0
  121. package/dist/shared/relationshipExtractor.d.ts +56 -0
  122. package/dist/shared/relationshipExtractor.js +138 -0
  123. package/dist/shared/selectionDialogs.d.ts +220 -0
  124. package/dist/shared/selectionDialogs.js +588 -0
  125. package/dist/storage/backupCompression.d.ts +20 -0
  126. package/dist/storage/backupCompression.js +67 -0
  127. package/dist/storage/methods.d.ts +44 -0
  128. package/dist/storage/methods.js +475 -0
  129. package/dist/storage/schemas.d.ts +842 -0
  130. package/dist/storage/schemas.js +175 -0
  131. package/dist/tables/indexManager.d.ts +65 -0
  132. package/dist/tables/indexManager.js +294 -0
  133. package/{src/types.ts → dist/types.d.ts} +1 -6
  134. package/dist/types.js +3 -0
  135. package/dist/users/methods.d.ts +16 -0
  136. package/dist/users/methods.js +276 -0
  137. package/dist/utils/configMigration.d.ts +1 -0
  138. package/dist/utils/configMigration.js +261 -0
  139. package/dist/utils/index.js +2 -0
  140. package/dist/utils/loadConfigs.d.ts +50 -0
  141. package/dist/utils/loadConfigs.js +357 -0
  142. package/dist/utils/setupFiles.d.ts +4 -0
  143. package/dist/utils/setupFiles.js +1190 -0
  144. package/dist/utilsController.d.ts +114 -0
  145. package/dist/utilsController.js +898 -0
  146. package/package.json +6 -3
  147. package/CHANGELOG.md +0 -35
  148. package/CONFIG_TODO.md +0 -1189
  149. package/SELECTION_DIALOGS.md +0 -146
  150. package/SERVICE_IMPLEMENTATION_REPORT.md +0 -462
  151. package/scripts/copy-templates.ts +0 -23
  152. package/src/backups/operations/bucketBackup.ts +0 -277
  153. package/src/backups/operations/collectionBackup.ts +0 -310
  154. package/src/backups/operations/comprehensiveBackup.ts +0 -342
  155. package/src/backups/schemas/bucketManifest.ts +0 -78
  156. package/src/backups/schemas/comprehensiveManifest.ts +0 -76
  157. package/src/backups/tracking/centralizedTracking.ts +0 -352
  158. package/src/cli/commands/configCommands.ts +0 -265
  159. package/src/cli/commands/databaseCommands.ts +0 -931
  160. package/src/cli/commands/functionCommands.ts +0 -419
  161. package/src/cli/commands/importFileCommands.ts +0 -815
  162. package/src/cli/commands/schemaCommands.ts +0 -200
  163. package/src/cli/commands/storageCommands.ts +0 -151
  164. package/src/cli/commands/transferCommands.ts +0 -454
  165. package/src/collections/attributes.ts.backup +0 -1555
  166. package/src/collections/columns.ts +0 -2025
  167. package/src/collections/indexes.ts +0 -350
  168. package/src/collections/methods.ts +0 -714
  169. package/src/collections/tableOperations.ts +0 -542
  170. package/src/collections/transferOperations.ts +0 -589
  171. package/src/collections/wipeOperations.ts +0 -449
  172. package/src/databases/methods.ts +0 -49
  173. package/src/databases/setup.ts +0 -77
  174. package/src/examples/yamlTerminologyExample.ts +0 -346
  175. package/src/functions/deployments.ts +0 -221
  176. package/src/functions/fnConfigDiscovery.ts +0 -103
  177. package/src/functions/methods.ts +0 -284
  178. package/src/init.ts +0 -62
  179. package/src/interactiveCLI.ts +0 -1201
  180. package/src/main.ts +0 -1517
  181. package/src/migrations/afterImportActions.ts +0 -579
  182. package/src/migrations/appwriteToX.ts +0 -668
  183. package/src/migrations/comprehensiveTransfer.ts +0 -2285
  184. package/src/migrations/dataLoader.ts +0 -1729
  185. package/src/migrations/importController.ts +0 -440
  186. package/src/migrations/importDataActions.ts +0 -315
  187. package/src/migrations/relationships.ts +0 -333
  188. package/src/migrations/services/DataTransformationService.ts +0 -196
  189. package/src/migrations/services/FileHandlerService.ts +0 -311
  190. package/src/migrations/services/ImportOrchestrator.ts +0 -675
  191. package/src/migrations/services/RateLimitManager.ts +0 -363
  192. package/src/migrations/services/RelationshipResolver.ts +0 -461
  193. package/src/migrations/services/UserMappingService.ts +0 -345
  194. package/src/migrations/services/ValidationService.ts +0 -349
  195. package/src/migrations/transfer.ts +0 -1113
  196. package/src/migrations/yaml/YamlImportConfigLoader.ts +0 -439
  197. package/src/migrations/yaml/YamlImportIntegration.ts +0 -446
  198. package/src/migrations/yaml/generateImportSchemas.ts +0 -1354
  199. package/src/schemas/authUser.ts +0 -23
  200. package/src/setupCommands.ts +0 -602
  201. package/src/setupController.ts +0 -43
  202. package/src/shared/backupMetadataSchema.ts +0 -93
  203. package/src/shared/backupTracking.ts +0 -211
  204. package/src/shared/confirmationDialogs.ts +0 -327
  205. package/src/shared/migrationHelpers.ts +0 -232
  206. package/src/shared/operationQueue.ts +0 -376
  207. package/src/shared/operationsTable.ts +0 -338
  208. package/src/shared/operationsTableSchema.ts +0 -60
  209. package/src/shared/progressManager.ts +0 -278
  210. package/src/shared/relationshipExtractor.ts +0 -214
  211. package/src/shared/selectionDialogs.ts +0 -802
  212. package/src/storage/backupCompression.ts +0 -88
  213. package/src/storage/methods.ts +0 -711
  214. package/src/storage/schemas.ts +0 -205
  215. package/src/tables/indexManager.ts +0 -409
  216. package/src/types/node-appwrite-tablesdb.d.ts +0 -44
  217. package/src/users/methods.ts +0 -358
  218. package/src/utils/configMigration.ts +0 -348
  219. package/src/utils/loadConfigs.ts +0 -457
  220. package/src/utils/setupFiles.ts +0 -1236
  221. package/src/utilsController.ts +0 -1263
  222. package/tests/README.md +0 -497
  223. package/tests/adapters/AdapterFactory.test.ts +0 -277
  224. package/tests/integration/syncOperations.test.ts +0 -463
  225. package/tests/jest.config.js +0 -25
  226. package/tests/migration/configMigration.test.ts +0 -546
  227. package/tests/setup.ts +0 -62
  228. package/tests/testUtils.ts +0 -340
  229. package/tests/utils/loadConfigs.test.ts +0 -350
  230. package/tests/validation/configValidation.test.ts +0 -412
  231. package/tsconfig.json +0 -44
  232. /package/{src → dist}/functions/templates/count-docs-in-collection/README.md +0 -0
  233. /package/{src → dist}/functions/templates/count-docs-in-collection/src/main.ts +0 -0
  234. /package/{src → dist}/functions/templates/count-docs-in-collection/src/request.ts +0 -0
  235. /package/{src → dist}/functions/templates/hono-typescript/README.md +0 -0
  236. /package/{src → dist}/functions/templates/hono-typescript/src/adapters/request.ts +0 -0
  237. /package/{src → dist}/functions/templates/hono-typescript/src/adapters/response.ts +0 -0
  238. /package/{src → dist}/functions/templates/hono-typescript/src/app.ts +0 -0
  239. /package/{src → dist}/functions/templates/hono-typescript/src/context.ts +0 -0
  240. /package/{src → dist}/functions/templates/hono-typescript/src/main.ts +0 -0
  241. /package/{src → dist}/functions/templates/hono-typescript/src/middleware/appwrite.ts +0 -0
  242. /package/{src → dist}/functions/templates/typescript-node/README.md +0 -0
  243. /package/{src → dist}/functions/templates/typescript-node/src/context.ts +0 -0
  244. /package/{src → dist}/functions/templates/typescript-node/src/main.ts +0 -0
  245. /package/{src → dist}/functions/templates/uv/README.md +0 -0
  246. /package/{src → dist}/functions/templates/uv/pyproject.toml +0 -0
  247. /package/{src → dist}/functions/templates/uv/src/__init__.py +0 -0
  248. /package/{src → dist}/functions/templates/uv/src/context.py +0 -0
  249. /package/{src → dist}/functions/templates/uv/src/main.py +0 -0
  250. /package/{src/utils/index.ts → dist/utils/index.d.ts} +0 -0
@@ -0,0 +1,605 @@
1
+ import { Databases, ID, Permission, Query, } from "node-appwrite";
2
+ import { getAdapterFromConfig } from "appwrite-utils-helpers";
3
+ import { nameToIdMapping, processQueue, queuedOperations, clearProcessingState, isCollectionProcessed, markCollectionProcessed, enqueueOperation } from "../shared/operationQueue.js";
4
+ import { logger, SchemaGenerator } from "appwrite-utils-helpers";
5
+ // Legacy attribute/index helpers removed in favor of unified adapter path
6
+ import { isNull, isUndefined, isNil, isPlainObject, isString, } from "es-toolkit";
7
+ import { delay, tryAwaitWithRetry } from "appwrite-utils-helpers";
8
+ import { MessageFormatter, mapToCreateAttributeParams, mapToUpdateAttributeParams } from "appwrite-utils-helpers";
9
+ import { isLegacyDatabases } from "appwrite-utils-helpers";
10
+ import { diffTableColumns, isIndexEqualToIndex, diffColumnsDetailed, executeColumnOperations } from "./tableOperations.js";
11
+ import { createOrUpdateIndexesViaAdapter, deleteObsoleteIndexesViaAdapter } from "../tables/indexManager.js";
12
+ // Re-export wipe operations
13
+ export { wipeDatabase, wipeCollection, wipeAllTables, wipeTableRows, } from "./wipeOperations.js";
14
+ // Re-export transfer operations
15
+ export { transferDocumentsBetweenDbsLocalToLocal, transferDocumentsBetweenDbsLocalToRemote, } from "./transferOperations.js";
16
+ export const documentExists = async (db, dbId, targetCollectionId, toCreateObject) => {
17
+ const collection = await (isLegacyDatabases(db) ?
18
+ db.getCollection(dbId, targetCollectionId) :
19
+ db.getTable({ databaseId: dbId, tableId: targetCollectionId }));
20
+ const attributes = collection.attributes;
21
+ let arrayTypeAttributes = attributes
22
+ .filter((attribute) => attribute.array === true)
23
+ .map((attribute) => attribute.key);
24
+ const isJsonString = (str) => {
25
+ try {
26
+ const json = JSON.parse(str);
27
+ return typeof json === "object" && json !== null;
28
+ }
29
+ catch (e) {
30
+ return false;
31
+ }
32
+ };
33
+ // Convert object to entries and filter
34
+ const validEntries = Object.entries(toCreateObject).filter(([key, value]) => !arrayTypeAttributes.includes(key) &&
35
+ !key.startsWith("$") &&
36
+ !isNull(value) &&
37
+ !isUndefined(value) &&
38
+ !isNil(value) &&
39
+ !isPlainObject(value) &&
40
+ !Array.isArray(value) &&
41
+ !(isString(value) && isJsonString(value)) &&
42
+ (isString(value) ? value.length < 4096 && value.length > 0 : true));
43
+ // Map and filter valid entries
44
+ const validMappedEntries = validEntries
45
+ .map(([key, value]) => [
46
+ key,
47
+ isString(value) || typeof value === "number" || typeof value === "boolean"
48
+ ? value
49
+ : null,
50
+ ])
51
+ .filter(([key, value]) => !isNull(value) && isString(key))
52
+ .slice(0, 25);
53
+ // Convert to Query parameters
54
+ const validQueryParams = validMappedEntries.map(([key, value]) => Query.equal(key, value));
55
+ // Execute the query with the validated and prepared parameters
56
+ const result = await (isLegacyDatabases(db) ?
57
+ db.listDocuments(dbId, targetCollectionId, validQueryParams) :
58
+ db.listRows({ databaseId: dbId, tableId: targetCollectionId, queries: validQueryParams }));
59
+ const items = isLegacyDatabases(db) ? result.documents : (result.rows || result.documents);
60
+ return items?.[0] || null;
61
+ };
62
+ export const checkForCollection = async (db, dbId, collection) => {
63
+ try {
64
+ const isLegacy = isLegacyDatabases(db);
65
+ const entityType = isLegacy ? "Collection" : "Table";
66
+ MessageFormatter.progress(`Checking for ${entityType.toLowerCase()} with name: ${collection.name}`, { prefix: entityType + "s" });
67
+ const response = await tryAwaitWithRetry(async () => isLegacy ?
68
+ await db.listCollections(dbId, [Query.equal("name", collection.name)]) :
69
+ await db.listTables({ databaseId: dbId, queries: [Query.equal("name", collection.name)] }));
70
+ const items = isLegacy ? response.collections : (response.tables || response.collections);
71
+ if (items && items.length > 0) {
72
+ MessageFormatter.info(`${entityType} found: ${items[0].$id}`, { prefix: entityType + "s" });
73
+ // Return remote collection for update operations (don't merge local config over it)
74
+ return items[0];
75
+ }
76
+ else {
77
+ MessageFormatter.info(`No ${entityType.toLowerCase()} found with name: ${collection.name}`, { prefix: entityType + "s" });
78
+ return null;
79
+ }
80
+ }
81
+ catch (error) {
82
+ const errorMessage = error instanceof Error ? error.message : String(error);
83
+ MessageFormatter.error(`Error checking for collection: ${collection.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Collections" });
84
+ logger.error('Collection check failed', {
85
+ collectionName: collection.name,
86
+ dbId,
87
+ error: errorMessage,
88
+ operation: 'checkForCollection'
89
+ });
90
+ return null;
91
+ }
92
+ };
93
+ // Helper function to fetch and cache collection by name
94
+ export const fetchAndCacheCollectionByName = async (db, dbId, collectionName) => {
95
+ const isLegacy = isLegacyDatabases(db);
96
+ const entityType = isLegacy ? "Collection" : "Table";
97
+ if (nameToIdMapping.has(collectionName)) {
98
+ const collectionId = nameToIdMapping.get(collectionName);
99
+ MessageFormatter.debug(`${entityType} found in cache: ${collectionId}`, undefined, { prefix: entityType + "s" });
100
+ return await tryAwaitWithRetry(async () => isLegacy ?
101
+ await db.getCollection(dbId, collectionId) :
102
+ await db.getTable({ databaseId: dbId, tableId: collectionId }));
103
+ }
104
+ else {
105
+ MessageFormatter.progress(`Fetching ${entityType.toLowerCase()} by name: ${collectionName}`, { prefix: entityType + "s" });
106
+ const collectionsPulled = await tryAwaitWithRetry(async () => isLegacy ?
107
+ await db.listCollections(dbId, [Query.equal("name", collectionName)]) :
108
+ await db.listTables({ databaseId: dbId, queries: [Query.equal("name", collectionName)] }));
109
+ const items = isLegacy ? collectionsPulled.collections : (collectionsPulled.tables || collectionsPulled.collections);
110
+ if ((collectionsPulled.total || items?.length) > 0) {
111
+ const collection = items[0];
112
+ MessageFormatter.info(`${entityType} found: ${collection.$id}`, { prefix: entityType + "s" });
113
+ nameToIdMapping.set(collectionName, collection.$id);
114
+ return collection;
115
+ }
116
+ else {
117
+ MessageFormatter.warning(`${entityType} not found by name: ${collectionName}`, { prefix: entityType + "s" });
118
+ return undefined;
119
+ }
120
+ }
121
+ };
122
+ export const generateSchemas = async (config, appwriteFolderPath) => {
123
+ const schemaGenerator = new SchemaGenerator(config, appwriteFolderPath);
124
+ await schemaGenerator.generateSchemas();
125
+ };
126
+ export const createOrUpdateCollections = async (database, databaseId, config, deletedCollections, selectedCollections = []) => {
127
+ // Clear processing state at the start of a new operation
128
+ clearProcessingState();
129
+ // Always use adapter path (LegacyAdapter translates when pre-1.8)
130
+ const { adapter } = await getAdapterFromConfig(config);
131
+ await createOrUpdateCollectionsViaAdapter(adapter, databaseId, config, deletedCollections, selectedCollections);
132
+ };
133
+ // New: Adapter-based implementation for TablesDB with state management
134
+ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, config, deletedCollections, selectedCollections = []) => {
135
+ const collectionsToProcess = selectedCollections.length > 0 ? selectedCollections : (config.collections || []);
136
+ if (!collectionsToProcess || collectionsToProcess.length === 0)
137
+ return;
138
+ const usedIds = new Set();
139
+ MessageFormatter.info(`Processing ${collectionsToProcess.length} tables via adapter with intelligent state management`, { prefix: "Tables" });
140
+ // Helpers for attribute operations through adapter
141
+ const createAttr = async (tableId, attr) => {
142
+ const params = mapToCreateAttributeParams(attr, { databaseId, tableId });
143
+ await adapter.createAttribute(params);
144
+ await delay(150);
145
+ };
146
+ const updateAttr = async (tableId, attr) => {
147
+ const params = mapToUpdateAttributeParams(attr, { databaseId, tableId });
148
+ await adapter.updateAttribute(params);
149
+ await delay(150);
150
+ };
151
+ // Local queue for unresolved relationships
152
+ const relQueue = [];
153
+ for (const collection of collectionsToProcess) {
154
+ const { attributes, indexes, ...collectionData } = collection;
155
+ // Check if this table has already been processed in this session (per database)
156
+ if (collectionData.$id && isCollectionProcessed(collectionData.$id, databaseId)) {
157
+ MessageFormatter.info(`Table '${collectionData.name}' already processed, skipping`, { prefix: "Tables" });
158
+ continue;
159
+ }
160
+ // Prepare permissions as strings (reuse Permission helper)
161
+ const permissions = [];
162
+ if (collection.$permissions && collection.$permissions.length > 0) {
163
+ for (const p of collection.$permissions) {
164
+ if (typeof p === 'string')
165
+ permissions.push(p);
166
+ else {
167
+ switch (p.permission) {
168
+ case 'read':
169
+ permissions.push(Permission.read(p.target));
170
+ break;
171
+ case 'create':
172
+ permissions.push(Permission.create(p.target));
173
+ break;
174
+ case 'update':
175
+ permissions.push(Permission.update(p.target));
176
+ break;
177
+ case 'delete':
178
+ permissions.push(Permission.delete(p.target));
179
+ break;
180
+ case 'write':
181
+ permissions.push(Permission.write(p.target));
182
+ break;
183
+ default: break;
184
+ }
185
+ }
186
+ }
187
+ }
188
+ // Find existing table — prefer lookup by ID (if provided), then by name
189
+ let table;
190
+ let tableId;
191
+ // 1) Try by explicit $id first (handles rename scenarios)
192
+ if (collectionData.$id) {
193
+ try {
194
+ const byId = await adapter.getTable({ databaseId, tableId: collectionData.$id });
195
+ table = byId.data || byId.tables?.[0];
196
+ if (table?.$id) {
197
+ MessageFormatter.info(`Found existing table by ID: ${table.$id}`, { prefix: 'Tables' });
198
+ }
199
+ }
200
+ catch {
201
+ // Not found by ID; fall back to name lookup
202
+ }
203
+ }
204
+ // 2) If not found by ID, try by name
205
+ if (!table) {
206
+ const list = await adapter.listTables({ databaseId, queries: [Query.equal('name', collectionData.name)] });
207
+ const items = list.tables || [];
208
+ table = items[0];
209
+ if (table?.$id) {
210
+ // If local has $id that differs from remote, prefer remote (IDs are immutable)
211
+ if (collectionData.$id && collectionData.$id !== table.$id) {
212
+ MessageFormatter.warning(`Config $id '${collectionData.$id}' differs from existing table ID '${table.$id}'. Using existing table.`, { prefix: 'Tables' });
213
+ }
214
+ }
215
+ }
216
+ if (!table) {
217
+ // Determine ID (prefer provided $id or re-use deleted one)
218
+ let foundColl = deletedCollections?.find((coll) => coll.collectionName.toLowerCase().trim().replace(" ", "") === collectionData.name.toLowerCase().trim().replace(" ", ""));
219
+ if (collectionData.$id)
220
+ tableId = collectionData.$id;
221
+ else if (foundColl && !usedIds.has(foundColl.collectionId))
222
+ tableId = foundColl.collectionId;
223
+ else
224
+ tableId = ID.unique();
225
+ usedIds.add(tableId);
226
+ const res = await adapter.createTable({
227
+ databaseId,
228
+ id: tableId,
229
+ name: collectionData.name,
230
+ permissions,
231
+ documentSecurity: !!collectionData.documentSecurity,
232
+ enabled: collectionData.enabled !== false
233
+ });
234
+ table = res.data || res;
235
+ nameToIdMapping.set(collectionData.name, tableId);
236
+ }
237
+ else {
238
+ tableId = table.$id;
239
+ await adapter.updateTable({
240
+ databaseId,
241
+ id: tableId,
242
+ name: collectionData.name,
243
+ permissions,
244
+ documentSecurity: !!collectionData.documentSecurity,
245
+ enabled: collectionData.enabled !== false
246
+ });
247
+ // Cache the existing table ID
248
+ nameToIdMapping.set(collectionData.name, tableId);
249
+ }
250
+ // Add small delay after table create/update
251
+ await delay(250);
252
+ // Create/Update attributes: non-relationship first using enhanced planning
253
+ const nonRel = (attributes || []).filter((a) => a.type !== 'relationship');
254
+ if (nonRel.length > 0) {
255
+ // Fetch existing columns once
256
+ const tableInfo = await adapter.getTable({ databaseId, tableId });
257
+ const existingCols = tableInfo.data?.columns || tableInfo.data?.attributes || [];
258
+ // Plan with icons
259
+ const plan = diffColumnsDetailed(nonRel, existingCols);
260
+ const plus = plan.toCreate.map((a) => a.key);
261
+ const plusminus = plan.toUpdate.map((u) => u.attribute.key);
262
+ const minus = plan.toRecreate.map((r) => r.newAttribute.key);
263
+ const skip = plan.unchanged;
264
+ // Compute deletions (remote extras not present locally)
265
+ const desiredKeysForDelete = new Set((attributes || []).map((a) => a.key));
266
+ const extraRemoteKeys = (existingCols || [])
267
+ .map((c) => c?.key)
268
+ .filter((k) => !!k && !desiredKeysForDelete.has(k));
269
+ const parts = [];
270
+ if (plus.length)
271
+ parts.push(`➕ ${plus.length} (${plus.join(', ')})`);
272
+ if (plusminus.length)
273
+ parts.push(`🔧 ${plusminus.length} (${plusminus.join(', ')})`);
274
+ if (minus.length)
275
+ parts.push(`♻️ ${minus.length} (${minus.join(', ')})`);
276
+ if (skip.length)
277
+ parts.push(`⏭️ ${skip.length}`);
278
+ parts.push(`🗑️ ${extraRemoteKeys.length}${extraRemoteKeys.length ? ` (${extraRemoteKeys.join(', ')})` : ''}`);
279
+ MessageFormatter.info(`Plan → ${parts.join(' | ') || 'no changes'}`, { prefix: 'Attributes' });
280
+ // Execute
281
+ const colResults = await executeColumnOperations(adapter, databaseId, tableId, plan);
282
+ if (colResults.success.length > 0) {
283
+ MessageFormatter.success(`Processed ${colResults.success.length} ops`, { prefix: 'Attributes' });
284
+ }
285
+ if (colResults.errors.length > 0) {
286
+ MessageFormatter.error(`${colResults.errors.length} attribute operations failed:`, undefined, { prefix: 'Attributes' });
287
+ for (const err of colResults.errors) {
288
+ MessageFormatter.error(` ${err.column}: ${err.error}`, undefined, { prefix: 'Attributes' });
289
+ }
290
+ }
291
+ MessageFormatter.info(`Summary → ➕ ${plan.toCreate.length} | 🔧 ${plan.toUpdate.length} | ♻️ ${plan.toRecreate.length} | ⏭️ ${plan.unchanged.length}`, { prefix: 'Attributes' });
292
+ }
293
+ // Relationship attributes — resolve relatedCollection to ID, then diff and create/update with recreate support
294
+ const relsAll = (attributes || []).filter((a) => a.type === 'relationship');
295
+ if (relsAll.length > 0) {
296
+ const relsResolved = [];
297
+ const relsDeferred = [];
298
+ // Resolve related collections (names -> IDs) using cache or lookup.
299
+ // If not resolvable yet (target table created later in the same push), queue for later.
300
+ for (const attr of relsAll) {
301
+ const relNameOrId = attr.relatedCollection;
302
+ if (!relNameOrId)
303
+ continue;
304
+ let relId = nameToIdMapping.get(relNameOrId) || relNameOrId;
305
+ let resolved = false;
306
+ if (nameToIdMapping.has(relNameOrId)) {
307
+ resolved = true;
308
+ }
309
+ else {
310
+ // Try resolve by name
311
+ try {
312
+ const relList = await adapter.listTables({ databaseId, queries: [Query.equal('name', relNameOrId)] });
313
+ const relItems = relList.tables || [];
314
+ if (relItems[0]?.$id) {
315
+ relId = relItems[0].$id;
316
+ nameToIdMapping.set(relNameOrId, relId);
317
+ resolved = true;
318
+ }
319
+ }
320
+ catch { }
321
+ // If the relNameOrId looks like an ID but isn't resolved yet, attempt a direct get
322
+ if (!resolved && relNameOrId && relNameOrId.length >= 10) {
323
+ try {
324
+ const probe = await adapter.getTable({ databaseId, tableId: relNameOrId });
325
+ if (probe.data?.$id) {
326
+ nameToIdMapping.set(relNameOrId, relNameOrId);
327
+ relId = relNameOrId;
328
+ resolved = true;
329
+ }
330
+ }
331
+ catch { }
332
+ }
333
+ }
334
+ if (resolved && relId && typeof relId === 'string') {
335
+ attr.relatedCollection = relId;
336
+ relsResolved.push(attr);
337
+ }
338
+ else {
339
+ // Defer until related table exists; queue a surgical operation
340
+ enqueueOperation({
341
+ type: 'attribute',
342
+ collectionId: tableId,
343
+ attribute: attr,
344
+ dependencies: [relNameOrId]
345
+ });
346
+ relsDeferred.push(attr);
347
+ }
348
+ }
349
+ // Compute a detailed plan for immediately resolvable relationships
350
+ const tableInfo2 = await adapter.getTable({ databaseId, tableId });
351
+ const existingCols2 = tableInfo2.data?.columns || tableInfo2.data?.attributes || [];
352
+ const relPlan = diffColumnsDetailed(relsResolved, existingCols2);
353
+ // Relationship plan with icons (includes recreates)
354
+ {
355
+ const parts = [];
356
+ if (relPlan.toCreate.length)
357
+ parts.push(`➕ ${relPlan.toCreate.length} (${relPlan.toCreate.map((a) => a.key).join(', ')})`);
358
+ if (relPlan.toUpdate.length)
359
+ parts.push(`🔧 ${relPlan.toUpdate.length} (${relPlan.toUpdate.map((u) => u.attribute?.key ?? u.key).join(', ')})`);
360
+ if (relPlan.toRecreate.length)
361
+ parts.push(`♻️ ${relPlan.toRecreate.length} (${relPlan.toRecreate.map((r) => r.newAttribute?.key ?? r?.key).join(', ')})`);
362
+ if (relPlan.unchanged.length)
363
+ parts.push(`⏭️ ${relPlan.unchanged.length}`);
364
+ MessageFormatter.info(`Plan → ${parts.join(' | ') || 'no changes'}`, { prefix: 'Relationships' });
365
+ }
366
+ // Execute plan using the same operation executor to properly handle deletes/recreates
367
+ const relResults = await executeColumnOperations(adapter, databaseId, tableId, relPlan);
368
+ if (relResults.success.length > 0) {
369
+ const totalRelationships = relPlan.toCreate.length + relPlan.toUpdate.length + relPlan.toRecreate.length + relPlan.unchanged.length;
370
+ const activeRelationships = relPlan.toCreate.length + relPlan.toUpdate.length + relPlan.toRecreate.length;
371
+ if (relResults.success.length !== activeRelationships) {
372
+ // Show both counts when they differ (usually due to recreations)
373
+ MessageFormatter.success(`Processed ${relResults.success.length} operations for ${activeRelationships} relationship${activeRelationships === 1 ? '' : 's'}`, { prefix: 'Relationships' });
374
+ }
375
+ else {
376
+ MessageFormatter.success(`Processed ${relResults.success.length} relationship${relResults.success.length === 1 ? '' : 's'}`, { prefix: 'Relationships' });
377
+ }
378
+ }
379
+ if (relResults.errors.length > 0) {
380
+ MessageFormatter.error(`${relResults.errors.length} relationship operations failed:`, undefined, { prefix: 'Relationships' });
381
+ for (const err of relResults.errors) {
382
+ MessageFormatter.error(` ${err.column}: ${err.error}`, undefined, { prefix: 'Relationships' });
383
+ }
384
+ }
385
+ if (relsDeferred.length > 0) {
386
+ MessageFormatter.info(`Deferred ${relsDeferred.length} relationship(s) until related tables become available`, { prefix: 'Relationships' });
387
+ }
388
+ }
389
+ // Wait for all attributes to become available before creating indexes
390
+ const allAttrKeys = [
391
+ ...nonRel.map((a) => a.key),
392
+ ...relsAll.filter((a) => a.relatedCollection).map((a) => a.key)
393
+ ];
394
+ if (allAttrKeys.length > 0) {
395
+ for (const attrKey of allAttrKeys) {
396
+ const maxWait = 60000; // 60 seconds
397
+ const startTime = Date.now();
398
+ let lastStatus = '';
399
+ while (Date.now() - startTime < maxWait) {
400
+ try {
401
+ const tableData = await adapter.getTable({ databaseId, tableId });
402
+ const attrs = tableData.data?.columns || tableData.data?.attributes || [];
403
+ const attr = attrs.find((a) => a.key === attrKey);
404
+ if (attr) {
405
+ if (attr.status === 'available') {
406
+ break; // Attribute is ready
407
+ }
408
+ if (attr.status === 'failed' || attr.status === 'stuck') {
409
+ throw new Error(`Attribute ${attrKey} failed to create: ${attr.error || 'unknown error'}`);
410
+ }
411
+ // Still processing, continue waiting
412
+ lastStatus = attr.status;
413
+ }
414
+ await delay(2000); // Check every 2 seconds
415
+ }
416
+ catch (e) {
417
+ // If we can't check status, assume it's processing and continue
418
+ await delay(2000);
419
+ }
420
+ }
421
+ // Timeout check
422
+ if (Date.now() - startTime >= maxWait) {
423
+ MessageFormatter.warning(`Attribute ${attrKey} did not become available within ${maxWait / 1000}s (last status: ${lastStatus}). Proceeding anyway.`, { prefix: 'Attributes' });
424
+ }
425
+ }
426
+ }
427
+ // Index management: create/update indexes using clean adapter-based system
428
+ const localTableConfig = config.collections?.find(c => c.name === collectionData.name || c.$id === collectionData.$id);
429
+ const idxs = (localTableConfig?.indexes ?? indexes ?? []);
430
+ // Create/update indexes with proper planning and execution
431
+ await createOrUpdateIndexesViaAdapter(adapter, databaseId, tableId, idxs, indexes);
432
+ // Handle obsolete index deletions
433
+ const desiredIndexKeys = new Set((indexes || []).map((i) => i.key));
434
+ await deleteObsoleteIndexesViaAdapter(adapter, databaseId, tableId, desiredIndexKeys);
435
+ // Deletions: remove columns/attributes that are present remotely but not in desired config
436
+ try {
437
+ const desiredKeys = new Set((attributes || []).map((a) => a.key));
438
+ // Also track case-insensitive keys to avoid double-deletion of renames (handled as recreates)
439
+ const desiredKeysLower = new Set((attributes || []).map((a) => a.key?.toLowerCase()));
440
+ const tableInfo3 = await adapter.getTable({ databaseId, tableId });
441
+ const existingCols3 = tableInfo3.data?.columns || tableInfo3.data?.attributes || [];
442
+ const toDelete = existingCols3
443
+ .filter((col) => {
444
+ if (!col?.key)
445
+ return false;
446
+ // Exact match - keep it
447
+ if (desiredKeys.has(col.key))
448
+ return false;
449
+ // Case-insensitive match (rename scenario) - already handled as recreate, don't delete again
450
+ if (desiredKeysLower.has(col.key?.toLowerCase()))
451
+ return false;
452
+ // Don't delete child-side relationship attributes - they're auto-managed by Appwrite
453
+ // for two-way relationships and deleting them would break the parent relationship
454
+ if (col.type === 'relationship' && col.side === 'child')
455
+ return false;
456
+ return true;
457
+ })
458
+ .map((col) => col.key);
459
+ if (toDelete.length > 0) {
460
+ MessageFormatter.info(`Plan → 🗑️ ${toDelete.length} (${toDelete.join(', ')})`, { prefix: 'Attributes' });
461
+ const deleted = [];
462
+ const errors = [];
463
+ for (const key of toDelete) {
464
+ try {
465
+ // Drop any indexes that reference this attribute to avoid server errors
466
+ try {
467
+ const idxRes = await adapter.listIndexes({ databaseId, tableId });
468
+ const ilist = idxRes.data || idxRes.indexes || [];
469
+ for (const idx of ilist) {
470
+ const attrs = Array.isArray(idx.attributes)
471
+ ? idx.attributes
472
+ : (Array.isArray(idx.columns) ? idx.columns : []);
473
+ if (attrs.includes(key)) {
474
+ MessageFormatter.info(`🗑️ Deleting index '${idx.key}' referencing '${key}'`, { prefix: 'Indexes' });
475
+ await adapter.deleteIndex({ databaseId, tableId, key: idx.key });
476
+ await delay(500);
477
+ }
478
+ }
479
+ }
480
+ catch { }
481
+ await adapter.deleteAttribute({ databaseId, tableId, key });
482
+ // Wait briefly for deletion to settle
483
+ const start = Date.now();
484
+ const maxWaitMs = 60000;
485
+ while (Date.now() - start < maxWaitMs) {
486
+ try {
487
+ const tinfo = await adapter.getTable({ databaseId, tableId });
488
+ const cols = tinfo.data?.columns || tinfo.data?.attributes || [];
489
+ const found = cols.find((c) => c.key === key);
490
+ if (!found)
491
+ break;
492
+ if (found.status && found.status !== 'deleting')
493
+ break;
494
+ }
495
+ catch { }
496
+ await delay(1000);
497
+ }
498
+ deleted.push(key);
499
+ }
500
+ catch (e) {
501
+ errors.push({ key, error: e?.message || String(e) });
502
+ }
503
+ }
504
+ if (deleted.length) {
505
+ MessageFormatter.success(`Deleted ${deleted.length} attributes: ${deleted.join(', ')}`, { prefix: 'Attributes' });
506
+ }
507
+ if (errors.length) {
508
+ MessageFormatter.error(`${errors.length} deletions failed`, undefined, { prefix: 'Attributes' });
509
+ errors.forEach(er => MessageFormatter.error(` ${er.key}: ${er.error}`, undefined, { prefix: 'Attributes' }));
510
+ }
511
+ }
512
+ else {
513
+ MessageFormatter.info(`Plan → 🗑️ 0`, { prefix: 'Attributes' });
514
+ }
515
+ }
516
+ catch (e) {
517
+ MessageFormatter.warning(`Could not evaluate deletions: ${e?.message || e}`, { prefix: 'Attributes' });
518
+ }
519
+ // Mark this table as fully processed for this database to prevent re-processing in the same DB only
520
+ markCollectionProcessed(tableId, collectionData.name, databaseId);
521
+ }
522
+ // Process queued relationships once mapping likely populated
523
+ if (relQueue.length > 0) {
524
+ MessageFormatter.info(`🔧 Processing ${relQueue.length} queued relationship attributes for tables`, { prefix: "Tables" });
525
+ for (const { tableId, attr } of relQueue) {
526
+ const relNameOrId = attr.relatedCollection;
527
+ if (!relNameOrId)
528
+ continue;
529
+ const relId = nameToIdMapping.get(relNameOrId) || relNameOrId;
530
+ if (relId) {
531
+ attr.relatedCollection = relId;
532
+ try {
533
+ await adapter.createAttribute({
534
+ databaseId,
535
+ tableId,
536
+ key: attr.key,
537
+ type: attr.type,
538
+ size: attr.size,
539
+ required: !!attr.required,
540
+ default: attr.xdefault,
541
+ array: !!attr.array,
542
+ min: attr.min,
543
+ max: attr.max,
544
+ elements: attr.elements,
545
+ relatedCollection: relId,
546
+ relationType: attr.relationType,
547
+ twoWay: attr.twoWay,
548
+ twoWayKey: attr.twoWayKey,
549
+ onDelete: attr.onDelete,
550
+ side: attr.side
551
+ });
552
+ await delay(150);
553
+ MessageFormatter.info(`✅ Successfully processed queued relationship: ${attr.key}`, { prefix: "Tables" });
554
+ }
555
+ catch (e) {
556
+ MessageFormatter.error(`Failed queued relationship ${attr.key}`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Attributes' });
557
+ }
558
+ }
559
+ else {
560
+ MessageFormatter.warning(`Could not resolve relationship ${attr.key} -> ${relNameOrId}`, { prefix: "Tables" });
561
+ }
562
+ }
563
+ }
564
+ // Process any remaining queued operations to complete relationship sync
565
+ try {
566
+ MessageFormatter.info(`🔄 Processing final operation queue for database ${databaseId}`, { prefix: "Tables" });
567
+ await processQueue(adapter, databaseId);
568
+ MessageFormatter.info(`✅ Operation queue processing completed`, { prefix: "Tables" });
569
+ }
570
+ catch (error) {
571
+ MessageFormatter.error(`Failed to process operation queue`, error instanceof Error ? error : new Error(String(error)), { prefix: 'Tables' });
572
+ }
573
+ };
574
+ export const generateMockData = async (database, databaseId, configCollections) => {
575
+ for (const { collection, mockFunction } of configCollections) {
576
+ if (mockFunction) {
577
+ MessageFormatter.progress(`Generating mock data for collection: ${collection.name}`, { prefix: "Mock Data" });
578
+ const mockData = mockFunction();
579
+ for (const data of mockData) {
580
+ await database.createDocument(databaseId, collection.$id, ID.unique(), data);
581
+ }
582
+ }
583
+ }
584
+ };
585
+ export const fetchAllCollections = async (dbId, database) => {
586
+ MessageFormatter.progress(`Fetching all collections for database ID: ${dbId}`, { prefix: "Collections" });
587
+ let collections = [];
588
+ let moreCollections = true;
589
+ let lastCollectionId;
590
+ while (moreCollections) {
591
+ const queries = [Query.limit(500)];
592
+ if (lastCollectionId) {
593
+ queries.push(Query.cursorAfter(lastCollectionId));
594
+ }
595
+ const response = await tryAwaitWithRetry(async () => await database.listCollections(dbId, queries));
596
+ collections = collections.concat(response.collections);
597
+ moreCollections = response.collections.length === 500;
598
+ if (moreCollections) {
599
+ lastCollectionId =
600
+ response.collections[response.collections.length - 1].$id;
601
+ }
602
+ }
603
+ MessageFormatter.success(`Fetched a total of ${collections.length} collections`, { prefix: "Collections" });
604
+ return collections;
605
+ };