appwrite-utils-cli 1.5.2 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +479 -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 +209 -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 +274 -1563
  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
package/dist/main.js CHANGED
@@ -14,6 +14,47 @@ import { listSpecifications } from "./functions/methods.js";
14
14
  import { MessageFormatter } from "./shared/messageFormatter.js";
15
15
  import { ConfirmationDialogs } from "./shared/confirmationDialogs.js";
16
16
  import path from "path";
17
+ import fs from "fs";
18
+ import { loadAppwriteProjectConfig, findAppwriteProjectConfig, projectConfigToAppwriteConfig } from "./utils/projectConfig.js";
19
+ import { hasSessionAuth, getAvailableSessions, getAuthenticationStatus } from "./utils/sessionAuth.js";
20
+ import { findYamlConfig, loadYamlConfigWithSession } from "./config/yamlConfig.js";
21
+ /**
22
+ * Checks if the migration from collections to tables should be allowed
23
+ * Returns an object with:
24
+ * - allowed: boolean indicating if migration should proceed
25
+ * - reason: string explaining why migration was blocked (if not allowed)
26
+ */
27
+ function checkMigrationConditions(configPath) {
28
+ const collectionsPath = path.join(configPath, "collections");
29
+ const tablesPath = path.join(configPath, "tables");
30
+ // Check if collections/ folder exists
31
+ if (!fs.existsSync(collectionsPath)) {
32
+ return {
33
+ allowed: false,
34
+ reason: "No collections/ folder found. Migration requires existing collections to migrate."
35
+ };
36
+ }
37
+ // Check if collections/ folder has YAML files
38
+ const collectionFiles = fs.readdirSync(collectionsPath).filter(file => file.endsWith(".yaml") || file.endsWith(".yml"));
39
+ if (collectionFiles.length === 0) {
40
+ return {
41
+ allowed: false,
42
+ reason: "No YAML files found in collections/ folder. Migration requires existing collection YAML files."
43
+ };
44
+ }
45
+ // Check if tables/ folder exists and has YAML files
46
+ if (fs.existsSync(tablesPath)) {
47
+ const tableFiles = fs.readdirSync(tablesPath).filter(file => file.endsWith(".yaml") || file.endsWith(".yml"));
48
+ if (tableFiles.length > 0) {
49
+ return {
50
+ allowed: false,
51
+ reason: `Tables folder already exists with ${tableFiles.length} YAML file(s). Migration appears to have already been completed.`
52
+ };
53
+ }
54
+ }
55
+ // All conditions met
56
+ return { allowed: true };
57
+ }
17
58
  const argv = yargs(hideBin(process.argv))
18
59
  .option("config", {
19
60
  type: "string",
@@ -29,9 +70,9 @@ const argv = yargs(hideBin(process.argv))
29
70
  description: "Comma-separated list of database IDs to target (e.g., 'db1,db2,db3')",
30
71
  })
31
72
  .option("collectionIds", {
32
- alias: ["collIds"],
73
+ alias: ["collIds", "tableIds", "tables"],
33
74
  type: "string",
34
- description: "Comma-separated list of collection IDs to target (e.g., 'users,posts')",
75
+ description: "Comma-separated list of collection/table IDs to target (e.g., 'users,posts')",
35
76
  })
36
77
  .option("bucketIds", {
37
78
  type: "string",
@@ -43,7 +84,7 @@ const argv = yargs(hideBin(process.argv))
43
84
  })
44
85
  .option("wipeCollections", {
45
86
  type: "boolean",
46
- description: "⚠️ DESTRUCTIVE: Wipe specific collections (requires --collectionIds)",
87
+ description: "⚠️ DESTRUCTIVE: Wipe specific collections/tables (requires --collectionIds or --tableIds)",
47
88
  })
48
89
  .option("transferUsers", {
49
90
  type: "boolean",
@@ -60,6 +101,31 @@ const argv = yargs(hideBin(process.argv))
60
101
  .option("backup", {
61
102
  type: "boolean",
62
103
  description: "Create a complete backup of your databases and collections",
104
+ })
105
+ .option("backupFormat", {
106
+ type: "string",
107
+ choices: ["json", "zip"],
108
+ default: "json",
109
+ description: "Backup file format (json or zip)",
110
+ })
111
+ .option("listBackups", {
112
+ type: "boolean",
113
+ description: "List all backups for databases",
114
+ })
115
+ .option("comprehensiveBackup", {
116
+ alias: ["comprehensive", "backup-all"],
117
+ type: "boolean",
118
+ description: "🚀 Create comprehensive backup of ALL databases and ALL storage buckets",
119
+ })
120
+ .option("trackingDatabaseId", {
121
+ alias: ["tracking-db"],
122
+ type: "string",
123
+ description: "Database ID to use for centralized backup tracking (interactive prompt if not specified)",
124
+ })
125
+ .option("parallelDownloads", {
126
+ type: "number",
127
+ default: 10,
128
+ description: "Number of parallel file downloads for bucket backups (default: 10)",
63
129
  })
64
130
  .option("writeData", {
65
131
  type: "boolean",
@@ -174,25 +240,130 @@ const argv = yargs(hideBin(process.argv))
174
240
  type: "string",
175
241
  description: "Output directory for generated constants files (default: config-folder/constants)",
176
242
  default: "auto",
243
+ })
244
+ .option("migrateCollectionsToTables", {
245
+ alias: ["migrate-collections"],
246
+ type: "boolean",
247
+ description: "Migrate collections to tables format for TablesDB API compatibility",
248
+ })
249
+ .option("useSession", {
250
+ alias: ["session"],
251
+ type: "boolean",
252
+ description: "Use Appwrite CLI session authentication instead of API key",
253
+ })
254
+ .option("sessionCookie", {
255
+ type: "string",
256
+ description: "Explicit session cookie to use for authentication",
177
257
  })
178
258
  .parse();
179
259
  async function main() {
180
260
  const startTime = Date.now();
181
261
  const operationStats = {};
262
+ // Early session detection for better user guidance
263
+ const availableSessions = getAvailableSessions();
264
+ let hasAnyValidSessions = availableSessions.length > 0;
182
265
  if (argv.it) {
183
266
  const cli = new InteractiveCLI(process.cwd());
184
267
  await cli.run();
185
268
  }
186
269
  else {
187
- const directConfig = argv.endpoint || argv.projectId || argv.apiKey
188
- ? {
189
- appwriteEndpoint: argv.endpoint,
190
- appwriteProject: argv.projectId,
191
- appwriteKey: argv.apiKey,
192
- }
193
- : undefined;
194
- const controller = new UtilsController(process.cwd(), directConfig);
195
- await controller.init();
270
+ // Enhanced config creation with session and project file support
271
+ let directConfig = undefined;
272
+ // Show authentication status on startup if no config provided
273
+ if (!argv.config && !argv.endpoint && !argv.projectId && !argv.apiKey && !argv.useSession && !argv.sessionCookie) {
274
+ if (hasAnyValidSessions) {
275
+ MessageFormatter.info(`Found ${availableSessions.length} available session(s)`, { prefix: "Auth" });
276
+ availableSessions.forEach(session => {
277
+ MessageFormatter.info(` \u2022 ${session.projectId} (${session.email || 'unknown'}) at ${session.endpoint}`, { prefix: "Auth" });
278
+ });
279
+ MessageFormatter.info("Use --session to enable session authentication", { prefix: "Auth" });
280
+ }
281
+ else {
282
+ MessageFormatter.info("No active Appwrite sessions found", { prefix: "Auth" });
283
+ MessageFormatter.info("\u2022 Run 'appwrite login' to authenticate with session", { prefix: "Auth" });
284
+ MessageFormatter.info("\u2022 Or provide --apiKey for API key authentication", { prefix: "Auth" });
285
+ }
286
+ }
287
+ // Priority 1: Check for appwrite.json project configuration
288
+ const projectConfigPath = findAppwriteProjectConfig(process.cwd());
289
+ if (projectConfigPath) {
290
+ const projectConfig = loadAppwriteProjectConfig(projectConfigPath);
291
+ if (projectConfig) {
292
+ directConfig = projectConfigToAppwriteConfig(projectConfig);
293
+ MessageFormatter.info(`Loaded project configuration from ${projectConfigPath}`, { prefix: "CLI" });
294
+ }
295
+ }
296
+ // Priority 2: CLI arguments override project config
297
+ if (argv.endpoint || argv.projectId || argv.apiKey || argv.useSession || argv.sessionCookie) {
298
+ directConfig = {
299
+ ...directConfig,
300
+ appwriteEndpoint: argv.endpoint || directConfig?.appwriteEndpoint,
301
+ appwriteProject: argv.projectId || directConfig?.appwriteProject,
302
+ appwriteKey: argv.apiKey || directConfig?.appwriteKey,
303
+ };
304
+ }
305
+ // Priority 3: Session authentication support with improved detection
306
+ let sessionAuthAvailable = false;
307
+ if (directConfig?.appwriteEndpoint && directConfig?.appwriteProject) {
308
+ sessionAuthAvailable = hasSessionAuth(directConfig.appwriteEndpoint, directConfig.appwriteProject);
309
+ }
310
+ if (argv.useSession || argv.sessionCookie) {
311
+ if (argv.sessionCookie) {
312
+ // Explicit session cookie provided
313
+ MessageFormatter.info("Using explicit session cookie for authentication", { prefix: "Auth" });
314
+ }
315
+ else if (sessionAuthAvailable) {
316
+ MessageFormatter.info("Session authentication detected and will be used", { prefix: "Auth" });
317
+ }
318
+ else {
319
+ MessageFormatter.warning("Session authentication requested but no valid session found", { prefix: "Auth" });
320
+ const availableSessions = getAvailableSessions();
321
+ if (availableSessions.length > 0) {
322
+ MessageFormatter.info(`Available sessions: ${availableSessions.map(s => `${s.projectId} (${s.email || 'unknown'})`).join(", ")}`, { prefix: "Auth" });
323
+ MessageFormatter.info("Use --session flag to enable session authentication", { prefix: "Auth" });
324
+ }
325
+ else {
326
+ MessageFormatter.warning("No Appwrite CLI sessions found. Please run 'appwrite login' first.", { prefix: "Auth" });
327
+ }
328
+ MessageFormatter.error("Session authentication requested but not available", undefined, { prefix: "Auth" });
329
+ return; // Exit early if session auth was requested but not available
330
+ }
331
+ }
332
+ else if (sessionAuthAvailable && !argv.apiKey) {
333
+ // Auto-detect session authentication when no API key is provided
334
+ MessageFormatter.info("Session authentication detected - no API key required", { prefix: "Auth" });
335
+ MessageFormatter.info("Use --session flag to explicitly enable session authentication", { prefix: "Auth" });
336
+ }
337
+ // Enhanced session authentication support:
338
+ // 1. If session auth is explicitly requested via flags, use it
339
+ // 2. If no API key is provided but sessions are available, offer to use session auth
340
+ // 3. Auto-detect session authentication when possible
341
+ let finalDirectConfig = directConfig;
342
+ if ((argv.useSession || argv.sessionCookie) &&
343
+ (!directConfig || !directConfig.appwriteEndpoint || !directConfig.appwriteProject)) {
344
+ // Don't pass incomplete directConfig - let UtilsController load YAML config normally
345
+ finalDirectConfig = null;
346
+ }
347
+ else if (finalDirectConfig && !finalDirectConfig.appwriteKey && !argv.useSession && !argv.sessionCookie) {
348
+ // Auto-detect session authentication when no API key provided
349
+ if (sessionAuthAvailable) {
350
+ MessageFormatter.info("No API key provided, but session authentication is available", { prefix: "Auth" });
351
+ MessageFormatter.info("Automatically using session authentication (add --session to suppress this message)", { prefix: "Auth" });
352
+ // Implicitly enable session authentication
353
+ argv.useSession = true;
354
+ }
355
+ }
356
+ // Create controller with session authentication support
357
+ const controller = new UtilsController(process.cwd(), finalDirectConfig);
358
+ // Pass session authentication options to the controller
359
+ const initOptions = {};
360
+ if (argv.useSession || argv.sessionCookie) {
361
+ initOptions.useSession = true;
362
+ if (argv.sessionCookie) {
363
+ initOptions.sessionCookie = argv.sessionCookie;
364
+ }
365
+ }
366
+ await controller.init(initOptions);
196
367
  if (argv.setup) {
197
368
  await setupDirsFiles(false, process.cwd());
198
369
  return;
@@ -229,11 +400,114 @@ async function main() {
229
400
  MessageFormatter.success(`Constants generated in ${outputDir}`, { prefix: "Constants" });
230
401
  return;
231
402
  }
403
+ if (argv.migrateCollectionsToTables) {
404
+ try {
405
+ if (!controller.config) {
406
+ MessageFormatter.error("No Appwrite configuration found", undefined, { prefix: "Migration" });
407
+ return;
408
+ }
409
+ // Get the config path from the controller or use .appwrite in current directory
410
+ let configPath = controller.getAppwriteFolderPath();
411
+ if (!configPath) {
412
+ // Try .appwrite in current directory
413
+ const defaultPath = path.join(process.cwd(), ".appwrite");
414
+ if (fs.existsSync(defaultPath)) {
415
+ configPath = defaultPath;
416
+ }
417
+ else {
418
+ MessageFormatter.error("Could not determine configuration folder path", undefined, { prefix: "Migration" });
419
+ MessageFormatter.info("Make sure you have a .appwrite/ folder in your current directory", { prefix: "Migration" });
420
+ return;
421
+ }
422
+ }
423
+ // Check if migration conditions are met
424
+ const migrationCheck = checkMigrationConditions(configPath);
425
+ if (!migrationCheck.allowed) {
426
+ MessageFormatter.error(`Migration not allowed: ${migrationCheck.reason}`, undefined, { prefix: "Migration" });
427
+ MessageFormatter.info("Migration requirements:", { prefix: "Migration" });
428
+ MessageFormatter.info(" • Configuration must be loaded (use --config or have .appwrite/ folder)", { prefix: "Migration" });
429
+ MessageFormatter.info(" • collections/ folder must exist with YAML files", { prefix: "Migration" });
430
+ MessageFormatter.info(" • tables/ folder must not exist or be empty", { prefix: "Migration" });
431
+ return;
432
+ }
433
+ const { migrateCollectionsToTables } = await import("./config/configMigration.js");
434
+ MessageFormatter.info("Starting collections to tables migration...", { prefix: "Migration" });
435
+ const result = migrateCollectionsToTables(controller.config, {
436
+ strategy: "full_migration",
437
+ validateResult: true,
438
+ dryRun: false
439
+ });
440
+ if (result.success) {
441
+ operationStats.migratedCollections = result.changes.length;
442
+ MessageFormatter.success("Collections migration completed successfully", { prefix: "Migration" });
443
+ }
444
+ else {
445
+ MessageFormatter.error(`Migration failed: ${result.errors.join(", ")}`, undefined, { prefix: "Migration" });
446
+ process.exit(1);
447
+ }
448
+ }
449
+ catch (error) {
450
+ MessageFormatter.error("Migration failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Migration" });
451
+ process.exit(1);
452
+ }
453
+ return;
454
+ }
232
455
  if (!controller.config) {
233
- MessageFormatter.error("No Appwrite connection found", undefined, { prefix: "CLI" });
456
+ // Provide better guidance based on available authentication methods
457
+ const availableSessions = getAvailableSessions();
458
+ if (availableSessions.length > 0) {
459
+ MessageFormatter.error("No Appwrite configuration found", undefined, { prefix: "CLI" });
460
+ MessageFormatter.info("Available authentication options:", { prefix: "Auth" });
461
+ MessageFormatter.info("• Session authentication: Add --session flag", { prefix: "Auth" });
462
+ MessageFormatter.info("• API key authentication: Add --apiKey YOUR_API_KEY", { prefix: "Auth" });
463
+ MessageFormatter.info(`• Available sessions: ${availableSessions.map(s => `${s.projectId} (${s.email || 'unknown'})`).join(", ")}`, { prefix: "Auth" });
464
+ }
465
+ else {
466
+ MessageFormatter.error("No Appwrite configuration found", undefined, { prefix: "CLI" });
467
+ MessageFormatter.info("Authentication options:", { prefix: "Auth" });
468
+ MessageFormatter.info("• Login with Appwrite CLI: Run 'appwrite login' then use --session flag", { prefix: "Auth" });
469
+ MessageFormatter.info("• Use API key: Add --apiKey YOUR_API_KEY", { prefix: "Auth" });
470
+ MessageFormatter.info("• Create config file: Run with --setup to initialize project configuration", { prefix: "Auth" });
471
+ }
234
472
  return;
235
473
  }
236
474
  const parsedArgv = argv;
475
+ // List backups if requested
476
+ if (parsedArgv.listBackups) {
477
+ const { AdapterFactory } = await import("./adapters/AdapterFactory.js");
478
+ const { listBackups } = await import("./shared/backupTracking.js");
479
+ if (!controller.config) {
480
+ MessageFormatter.error("No Appwrite configuration found", undefined, { prefix: "Backups" });
481
+ return;
482
+ }
483
+ const { adapter } = await AdapterFactory.create({
484
+ appwriteEndpoint: controller.config.appwriteEndpoint,
485
+ appwriteProject: controller.config.appwriteProject,
486
+ appwriteKey: controller.config.appwriteKey
487
+ });
488
+ const databases = parsedArgv.dbIds
489
+ ? await controller.getDatabasesByIds(parsedArgv.dbIds.split(","))
490
+ : await fetchAllDatabases(controller.database);
491
+ if (!databases || databases.length === 0) {
492
+ MessageFormatter.info("No databases found", { prefix: "Backups" });
493
+ return;
494
+ }
495
+ for (const db of databases) {
496
+ const backups = await listBackups(adapter, db.$id);
497
+ MessageFormatter.info(`\nBackups for database: ${db.name} (${db.$id})`, { prefix: "Backups" });
498
+ if (backups.length === 0) {
499
+ MessageFormatter.info(" No backups found", { prefix: "Backups" });
500
+ }
501
+ else {
502
+ backups.forEach((backup, index) => {
503
+ const date = new Date(backup.$createdAt).toLocaleString();
504
+ const size = MessageFormatter.formatBytes(backup.sizeBytes);
505
+ MessageFormatter.info(` ${index + 1}. ${date} - ${backup.format.toUpperCase()} - ${size} - ${backup.collections} collections, ${backup.documents} documents`, { prefix: "Backups" });
506
+ });
507
+ }
508
+ }
509
+ return;
510
+ }
237
511
  const options = {
238
512
  databases: parsedArgv.dbIds
239
513
  ? await controller.getDatabasesByIds(parsedArgv.dbIds.split(","))
@@ -261,13 +535,14 @@ async function main() {
261
535
  }
262
536
  await controller.updateFunctionSpecifications(parsedArgv.functionId, parsedArgv.specification);
263
537
  }
264
- // Add default databases if not specified
265
- if (!options.databases || options.databases.length === 0) {
538
+ // Add default databases if not specified (only if we need them for operations)
539
+ const needsDatabases = options.doBackup || options.wipeDatabase ||
540
+ options.wipeDocumentStorage || options.wipeUsers ||
541
+ options.wipeCollections || options.importData ||
542
+ parsedArgv.sync || parsedArgv.transfer;
543
+ if (needsDatabases && (!options.databases || options.databases.length === 0)) {
266
544
  const allDatabases = await fetchAllDatabases(controller.database);
267
- options.databases = allDatabases.filter((db) => {
268
- const useMigrations = controller.config?.useMigrations ?? true;
269
- return useMigrations || db.name.toLowerCase() !== "migrations";
270
- });
545
+ options.databases = allDatabases;
271
546
  }
272
547
  // Add default collections if not specified
273
548
  if (!options.collections || options.collections.length === 0) {
@@ -278,10 +553,78 @@ async function main() {
278
553
  options.collections = [];
279
554
  }
280
555
  }
556
+ // Comprehensive backup (all databases + all buckets)
557
+ if (parsedArgv.comprehensiveBackup) {
558
+ const { comprehensiveBackup } = await import("./backups/operations/comprehensiveBackup.js");
559
+ const { AdapterFactory } = await import("./adapters/AdapterFactory.js");
560
+ // Get tracking database ID (interactive prompt if not specified)
561
+ let trackingDatabaseId = parsedArgv.trackingDatabaseId;
562
+ if (!trackingDatabaseId) {
563
+ // Fetch all databases for selection
564
+ const allDatabases = await fetchAllDatabases(controller.database);
565
+ if (allDatabases.length === 0) {
566
+ MessageFormatter.error("No databases found. Cannot create comprehensive backup without a tracking database.", undefined, { prefix: "Backup" });
567
+ return;
568
+ }
569
+ if (allDatabases.length === 1) {
570
+ trackingDatabaseId = allDatabases[0].$id;
571
+ MessageFormatter.info(`Using only available database for tracking: ${allDatabases[0].name} (${trackingDatabaseId})`, { prefix: "Backup" });
572
+ }
573
+ else {
574
+ // Interactive selection
575
+ const inquirer = (await import("inquirer")).default;
576
+ const answer = await inquirer.prompt([{
577
+ type: 'list',
578
+ name: 'trackingDb',
579
+ message: 'Select database to store backup tracking metadata:',
580
+ choices: allDatabases.map(db => ({
581
+ name: `${db.name} (${db.$id})`,
582
+ value: db.$id
583
+ }))
584
+ }]);
585
+ trackingDatabaseId = answer.trackingDb;
586
+ }
587
+ }
588
+ // Ensure trackingDatabaseId is defined before proceeding
589
+ if (!trackingDatabaseId) {
590
+ throw new Error('Tracking database ID is required for comprehensive backup');
591
+ }
592
+ MessageFormatter.info(`Using tracking database: ${trackingDatabaseId}`, { prefix: "Backup" });
593
+ // Create adapter for backup tracking
594
+ const { adapter } = await AdapterFactory.create({
595
+ appwriteEndpoint: controller.config.appwriteEndpoint,
596
+ appwriteProject: controller.config.appwriteProject,
597
+ appwriteKey: controller.config.appwriteKey,
598
+ sessionCookie: controller.config.sessionCookie
599
+ });
600
+ const result = await comprehensiveBackup(controller.config, controller.database, controller.storage, adapter, {
601
+ trackingDatabaseId,
602
+ backupFormat: parsedArgv.backupFormat || 'zip',
603
+ parallelDownloads: parsedArgv.parallelDownloads || 10,
604
+ onProgress: (message) => {
605
+ MessageFormatter.info(message, { prefix: "Backup" });
606
+ }
607
+ });
608
+ operationStats.comprehensiveBackup = 1;
609
+ operationStats.databasesBackedUp = result.databaseBackups.length;
610
+ operationStats.bucketsBackedUp = result.bucketBackups.length;
611
+ operationStats.totalBackupSize = result.totalSizeBytes;
612
+ if (result.status === 'completed') {
613
+ MessageFormatter.success(`Comprehensive backup completed successfully (ID: ${result.backupId})`, { prefix: "Backup" });
614
+ }
615
+ else if (result.status === 'partial') {
616
+ MessageFormatter.warning(`Comprehensive backup completed with errors (ID: ${result.backupId})`, { prefix: "Backup" });
617
+ result.errors.forEach(err => MessageFormatter.warning(err, { prefix: "Backup" }));
618
+ }
619
+ else {
620
+ MessageFormatter.error(`Comprehensive backup failed (ID: ${result.backupId})`, undefined, { prefix: "Backup" });
621
+ result.errors.forEach(err => MessageFormatter.error(err, undefined, { prefix: "Backup" }));
622
+ }
623
+ }
281
624
  if (options.doBackup && options.databases) {
282
- MessageFormatter.info(`Creating backups for ${options.databases.length} database(s)`, { prefix: "Backup" });
625
+ MessageFormatter.info(`Creating backups for ${options.databases.length} database(s) in ${parsedArgv.backupFormat} format`, { prefix: "Backup" });
283
626
  for (const db of options.databases) {
284
- await controller.backupDatabase(db);
627
+ await controller.backupDatabase(db, parsedArgv.backupFormat || 'json');
285
628
  }
286
629
  operationStats.backups = options.databases.length;
287
630
  MessageFormatter.success(`Backup completed for ${options.databases.length} database(s)`, { prefix: "Backup" });
@@ -1,26 +1,18 @@
1
- import { Databases, Storage, Query, ID, Client, Compression, } from "node-appwrite";
1
+ import { Databases, Storage, Query, ID, Compression, } from "node-appwrite";
2
2
  import { InputFile } from "node-appwrite/file";
3
3
  import path from "path";
4
4
  import fs from "fs";
5
5
  import os from "os";
6
6
  import { logger } from "../shared/logging.js";
7
7
  import { tryAwaitWithRetry, } from "appwrite-utils";
8
+ import { getClientFromConfig } from "../utils/getClientFromConfig.js";
9
+ import { MessageFormatter } from "../shared/messageFormatter.js";
8
10
  export const getDatabaseFromConfig = (config) => {
9
- if (!config.appwriteClient) {
10
- config.appwriteClient = new Client()
11
- .setEndpoint(config.appwriteEndpoint)
12
- .setProject(config.appwriteProject)
13
- .setKey(config.appwriteKey);
14
- }
11
+ getClientFromConfig(config); // Sets config.appwriteClient if missing
15
12
  return new Databases(config.appwriteClient);
16
13
  };
17
14
  export const getStorageFromConfig = (config) => {
18
- if (!config.appwriteClient) {
19
- config.appwriteClient = new Client()
20
- .setEndpoint(config.appwriteEndpoint)
21
- .setProject(config.appwriteProject)
22
- .setKey(config.appwriteKey);
23
- }
15
+ getClientFromConfig(config); // Sets config.appwriteClient if missing
24
16
  return new Storage(config.appwriteClient);
25
17
  };
26
18
  export const afterImportActions = {
@@ -30,7 +22,7 @@ export const afterImportActions = {
30
22
  await tryAwaitWithRetry(async () => await db.updateDocument(dbId, collId, docId, data));
31
23
  }
32
24
  catch (error) {
33
- console.error("Error updating document: ", error);
25
+ MessageFormatter.error("Error updating document", error instanceof Error ? error : new Error(String(error)), { prefix: "Import" });
34
26
  }
35
27
  },
36
28
  checkAndUpdateFieldInDocument: async (config, dbId, collId, docId, fieldName, oldFieldValue, newFieldValue) => {
@@ -44,7 +36,7 @@ export const afterImportActions = {
44
36
  }
45
37
  }
46
38
  catch (error) {
47
- console.error("Error updating document: ", error);
39
+ MessageFormatter.error("Error updating document", error instanceof Error ? error : new Error(String(error)), { prefix: "Import" });
48
40
  }
49
41
  },
50
42
  setFieldFromOtherCollectionDocument: async (config, dbId, collIdOrName, docId, fieldName, otherCollIdOrName, otherDocId, otherFieldName) => {
@@ -76,10 +68,10 @@ export const afterImportActions = {
76
68
  [fieldName]: valueToSet,
77
69
  }));
78
70
  }
79
- console.log(`Field ${fieldName} updated successfully in document ${docId}.`);
71
+ MessageFormatter.success(`Field ${fieldName} updated successfully in document ${docId}`, { prefix: "Import" });
80
72
  }
81
73
  catch (error) {
82
- console.error("Error setting field from other collection document: ", error);
74
+ MessageFormatter.error("Error setting field from other collection document", error instanceof Error ? error : new Error(String(error)), { prefix: "Import" });
83
75
  }
84
76
  },
85
77
  /**
@@ -136,11 +128,11 @@ export const afterImportActions = {
136
128
  ? { [fieldName]: documentIds }
137
129
  : { [fieldName]: documentIds[0] };
138
130
  await tryAwaitWithRetry(async () => await db.updateDocument(dbId, targetCollectionId, docId, updatePayload));
139
- console.log(`Field ${fieldName} updated successfully in document ${docId} with ${documentIds.length} document IDs.`);
131
+ MessageFormatter.success(`Field ${fieldName} updated successfully in document ${docId} with ${documentIds.length} document IDs`, { prefix: "Import" });
140
132
  }
141
133
  }
142
134
  catch (error) {
143
- console.error("Error setting field from other collection documents: ", error);
135
+ MessageFormatter.error("Error setting field from other collection documents", error instanceof Error ? error : new Error(String(error)), { prefix: "Import" });
144
136
  }
145
137
  },
146
138
  setTargetFieldFromOtherCollectionDocumentsByMatchingField: async (config, dbId, collIdOrName, docId, fieldName, otherCollIdOrName, matchingFieldName, matchingFieldValue, targetField) => {
@@ -190,11 +182,11 @@ export const afterImportActions = {
190
182
  ? { [fieldName]: targetFieldValues }
191
183
  : { [fieldName]: targetFieldValues[0] };
192
184
  await tryAwaitWithRetry(async () => await db.updateDocument(dbId, targetCollectionId, docId, updatePayload));
193
- console.log(`Field ${fieldName} updated successfully in document ${docId} with values from field ${targetField}.`);
185
+ MessageFormatter.success(`Field ${fieldName} updated successfully in document ${docId} with values from field ${targetField}`, { prefix: "Import" });
194
186
  }
195
187
  }
196
188
  catch (error) {
197
- console.error("Error setting field from other collection documents: ", error);
189
+ MessageFormatter.error("Error setting field from other collection documents", error instanceof Error ? error : new Error(String(error)), { prefix: "Import" });
198
190
  }
199
191
  },
200
192
  createOrGetBucket: async (config, bucketName, bucketId, permissions, fileSecurity, enabled, maxFileSize, allowedExtensions, compression, encryption, antivirus) => {
@@ -217,7 +209,7 @@ export const afterImportActions = {
217
209
  }
218
210
  }
219
211
  catch (error) {
220
- console.error("Error creating or getting bucket: ", error);
212
+ MessageFormatter.error("Error creating or getting bucket", error instanceof Error ? error : new Error(String(error)), { prefix: "Import" });
221
213
  }
222
214
  },
223
215
  createFileAndUpdateField: async (config, dbId, collId, docId, fieldName, bucketId, filePath, fileName) => {
@@ -231,12 +223,12 @@ export const afterImportActions = {
231
223
  // `Processing field ${fieldName} in collection ${collId} for document ${docId} in database ${dbId} in bucket ${bucketId} with path ${filePath} and name ${fileName}...`
232
224
  // );
233
225
  if (filePath.length === 0 || fileName.length === 0) {
234
- console.error(`File path or name is empty for field ${fieldName} in collection ${collId}, skipping...`);
226
+ MessageFormatter.error(`File path or name is empty for field ${fieldName} in collection ${collId}, skipping...`, undefined, { prefix: "Import" });
235
227
  return;
236
228
  }
237
229
  let isArray = false;
238
230
  if (!attribute) {
239
- console.log(`Field ${fieldName} not found in collection ${collId}, weird, skipping...`);
231
+ MessageFormatter.warning(`Field ${fieldName} not found in collection ${collId}, weird, skipping...`, { prefix: "Import" });
240
232
  return;
241
233
  }
242
234
  else if (attribute.array === true) {
@@ -260,7 +252,7 @@ export const afterImportActions = {
260
252
  // Download the file using fetch
261
253
  const response = await tryAwaitWithRetry(async () => await fetch(filePath));
262
254
  if (!response.ok)
263
- console.error(`Failed to fetch ${filePath}: ${response.statusText} for document ${docId} with field ${fieldName}`);
255
+ MessageFormatter.error(`Failed to fetch ${filePath}: ${response.statusText} for document ${docId} with field ${fieldName}`, undefined, { prefix: "Import" });
264
256
  // Use arrayBuffer if buffer is not available
265
257
  const arrayBuffer = await response.arrayBuffer();
266
258
  const buffer = Buffer.from(arrayBuffer);
@@ -269,7 +261,7 @@ export const afterImportActions = {
269
261
  const inputFile = InputFile.fromPath(tempFilePath, fileName);
270
262
  // Use the full file name (with extension) for creating the file
271
263
  const file = await tryAwaitWithRetry(async () => await storage.createFile(bucketId, ID.unique(), inputFile));
272
- console.log("Created file from URL: ", file.$id);
264
+ MessageFormatter.success(`Created file from URL: ${file.$id}`, { prefix: "Import" });
273
265
  // After uploading, adjust the updateData based on whether the field is an array or not
274
266
  if (isArray) {
275
267
  updateData = [...updateData, file.$id]; // Append the new file ID
@@ -287,7 +279,7 @@ export const afterImportActions = {
287
279
  const files = fs.readdirSync(filePath);
288
280
  const fileFullName = files.find((file) => file.includes(fileName));
289
281
  if (!fileFullName) {
290
- console.error(`File starting with '${fileName}' not found in '${filePath}'`);
282
+ MessageFormatter.error(`File starting with '${fileName}' not found in '${filePath}'`, undefined, { prefix: "Import" });
291
283
  return;
292
284
  }
293
285
  const pathToFile = path.join(filePath, fileFullName);
@@ -302,13 +294,13 @@ export const afterImportActions = {
302
294
  tryAwaitWithRetry(async () => await db.updateDocument(dbId, collId, doc.$id, {
303
295
  [fieldName]: updateData,
304
296
  }));
305
- console.log("Created file from path: ", file.$id);
297
+ MessageFormatter.success(`Created file from path: ${file.$id}`, { prefix: "Import" });
306
298
  }
307
299
  }
308
300
  catch (error) {
309
301
  logger.error(`Error creating file and updating field, params were:\ndbId: ${dbId}, collId: ${collId}, docId: ${docId}, fieldName: ${fieldName}, filePath: ${filePath}, fileName: ${fileName}\n\nError: ${error}`);
310
- console.error("Error creating file and updating field: ", error);
311
- console.log(`Params were: dbId: ${dbId}, collId: ${collId}, docId: ${docId}, fieldName: ${fieldName}, filePath: ${filePath}, fileName: ${fileName}`);
302
+ MessageFormatter.error("Error creating file and updating field", error instanceof Error ? error : new Error(String(error)), { prefix: "Import" });
303
+ MessageFormatter.info(`Params were: dbId: ${dbId}, collId: ${collId}, docId: ${docId}, fieldName: ${fieldName}, filePath: ${filePath}, fileName: ${fileName}`, { prefix: "Import" });
312
304
  }
313
305
  },
314
306
  };