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
@@ -0,0 +1,310 @@
1
+ import type { Storage, Databases, Models } from "node-appwrite";
2
+ import { ID, Query } from "node-appwrite";
3
+ import { InputFile } from "node-appwrite/file";
4
+ import { ulid } from "ulidx";
5
+ import { MessageFormatter } from "../../shared/messageFormatter.js";
6
+ import { logger } from "../../shared/logging.js";
7
+ import type { DatabaseAdapter } from "../../adapters/DatabaseAdapter.js";
8
+ import { tryAwaitWithRetry } from "appwrite-utils";
9
+ import { splitIntoBatches } from "../../shared/migrationHelpers.js";
10
+ import { retryFailedPromises } from "../../utils/retryFailedPromises.js";
11
+ import { ProgressManager } from "../../shared/progressManager.js";
12
+ import { createBackupZip } from "../../storage/backupCompression.js";
13
+ import {
14
+ recordCentralizedBackup,
15
+ createCentralizedBackupTrackingTable
16
+ } from "../tracking/centralizedTracking.js";
17
+ import type { AppwriteConfig } from "appwrite-utils";
18
+
19
+ export interface CollectionBackupOptions {
20
+ trackingDatabaseId: string;
21
+ databaseId: string;
22
+ collectionIds: string[];
23
+ backupFormat?: 'json' | 'zip';
24
+ onProgress?: (message: string) => void;
25
+ }
26
+
27
+ export interface CollectionBackupResult {
28
+ backupId: string;
29
+ manifestFileId: string;
30
+ databaseId: string;
31
+ collections: Array<{
32
+ collectionId: string;
33
+ collectionName: string;
34
+ documentCount: number;
35
+ status: 'completed' | 'failed';
36
+ error?: string;
37
+ }>;
38
+ totalDocuments: number;
39
+ sizeBytes: number;
40
+ status: 'completed' | 'partial' | 'failed';
41
+ errors: string[];
42
+ }
43
+
44
+ interface BackupData {
45
+ database: string;
46
+ collections: string[];
47
+ documents: Array<{
48
+ collectionId: string;
49
+ data: string;
50
+ }>;
51
+ }
52
+
53
+ /**
54
+ * Backup specific collections from a database
55
+ */
56
+ export async function backupCollections(
57
+ config: AppwriteConfig,
58
+ databases: Databases,
59
+ storage: Storage,
60
+ adapter: DatabaseAdapter,
61
+ options: CollectionBackupOptions
62
+ ): Promise<CollectionBackupResult> {
63
+ const startTime = Date.now();
64
+ const backupId = ulid();
65
+ const errors: string[] = [];
66
+ const collections: CollectionBackupResult['collections'] = [];
67
+ let totalDocuments = 0;
68
+ let totalSizeBytes = 0;
69
+
70
+ try {
71
+ // Ensure tracking table exists
72
+ await createCentralizedBackupTrackingTable(adapter, options.trackingDatabaseId);
73
+
74
+ const backupBucketId = "appwrite-backups";
75
+ MessageFormatter.info(`Starting collection backup ${backupId}`, { prefix: "Backup" });
76
+ MessageFormatter.info(`Database: ${options.databaseId}, Collections: ${options.collectionIds.length}`, { prefix: "Backup" });
77
+
78
+ // Get database info
79
+ const db = await tryAwaitWithRetry(
80
+ async () => await databases.get(options.databaseId)
81
+ );
82
+
83
+ const backupData: BackupData = {
84
+ database: JSON.stringify(db),
85
+ collections: [],
86
+ documents: []
87
+ };
88
+
89
+ // Phase 1: Count documents for progress tracking
90
+ MessageFormatter.step(1, 3, "Analyzing collections");
91
+ let totalItems = options.collectionIds.length; // Start with collection count
92
+
93
+ for (const collectionId of options.collectionIds) {
94
+ try {
95
+ const documentCount = await tryAwaitWithRetry(
96
+ async () => (await databases.listDocuments(options.databaseId, collectionId, [Query.limit(1)])).total
97
+ );
98
+ totalDocuments += documentCount;
99
+ totalItems += documentCount;
100
+ } catch (error) {
101
+ MessageFormatter.warning(`Could not count documents in collection ${collectionId}`);
102
+ }
103
+ }
104
+
105
+ const progress = ProgressManager.create(`backup-collections-${backupId}`, totalItems, {
106
+ title: `Backing up ${options.collectionIds.length} collections`,
107
+ });
108
+
109
+ // Phase 2: Backup selected collections
110
+ MessageFormatter.step(2, 3, `Processing ${options.collectionIds.length} collections and ${totalDocuments} documents`);
111
+
112
+ let processedDocuments = 0;
113
+
114
+ for (const collectionId of options.collectionIds) {
115
+ try {
116
+ if (options.onProgress) {
117
+ options.onProgress(`Backing up collection: ${collectionId}`);
118
+ }
119
+
120
+ // Get collection metadata
121
+ const collection = await tryAwaitWithRetry(
122
+ async () => await databases.getCollection(options.databaseId, collectionId)
123
+ );
124
+
125
+ backupData.collections.push(JSON.stringify(collection));
126
+ progress.increment(1, `Processing collection: ${collection.name}`);
127
+
128
+ // Backup all documents in this collection
129
+ let lastDocumentId = "";
130
+ let moreDocuments = true;
131
+ let collectionDocumentCount = 0;
132
+
133
+ while (moreDocuments) {
134
+ const documentResponse = await tryAwaitWithRetry(
135
+ async () =>
136
+ await databases.listDocuments(options.databaseId, collectionId, [
137
+ Query.limit(500),
138
+ ...(lastDocumentId ? [Query.cursorAfter(lastDocumentId)] : []),
139
+ ])
140
+ );
141
+
142
+ collectionDocumentCount += documentResponse.documents.length;
143
+
144
+ const documentPromises = documentResponse.documents.map(
145
+ ({ $id: documentId }) =>
146
+ databases.getDocument(options.databaseId, collectionId, documentId)
147
+ );
148
+
149
+ const promiseBatches = splitIntoBatches(documentPromises);
150
+ const documentsPulled = [];
151
+
152
+ for (const batch of promiseBatches) {
153
+ const successfulDocuments = await retryFailedPromises(batch);
154
+ documentsPulled.push(...successfulDocuments);
155
+
156
+ // Update progress for each batch
157
+ progress.increment(successfulDocuments.length,
158
+ `Processing ${collection.name}: ${processedDocuments + successfulDocuments.length}/${totalDocuments} documents`
159
+ );
160
+ processedDocuments += successfulDocuments.length;
161
+ }
162
+
163
+ backupData.documents.push({
164
+ collectionId: collectionId,
165
+ data: JSON.stringify(documentsPulled),
166
+ });
167
+
168
+ moreDocuments = documentResponse.documents.length === 500;
169
+ if (moreDocuments) {
170
+ lastDocumentId = documentResponse.documents[documentResponse.documents.length - 1].$id;
171
+ }
172
+ }
173
+
174
+ collections.push({
175
+ collectionId,
176
+ collectionName: collection.name,
177
+ documentCount: collectionDocumentCount,
178
+ status: 'completed'
179
+ });
180
+
181
+ MessageFormatter.success(
182
+ `Collection ${collection.name} backed up with ${MessageFormatter.formatNumber(collectionDocumentCount)} documents`
183
+ );
184
+ } catch (error) {
185
+ const errorMsg = `Failed to backup collection ${collectionId}: ${error instanceof Error ? error.message : String(error)}`;
186
+ errors.push(errorMsg);
187
+ logger.error(errorMsg);
188
+
189
+ collections.push({
190
+ collectionId,
191
+ collectionName: collectionId,
192
+ documentCount: 0,
193
+ status: 'failed',
194
+ error: errorMsg
195
+ });
196
+ }
197
+ }
198
+
199
+ // Phase 3: Create backup file
200
+ MessageFormatter.step(3, 3, "Creating backup file");
201
+
202
+ let inputFile: any;
203
+ let fileName: string;
204
+ let backupSize: number;
205
+
206
+ if (options.backupFormat === 'zip') {
207
+ // Create compressed backup
208
+ const zipBuffer = await createBackupZip(backupData);
209
+ fileName = `${new Date().toISOString()}-${options.databaseId}-collections.zip`;
210
+ backupSize = zipBuffer.length;
211
+ inputFile = InputFile.fromBuffer(new Uint8Array(zipBuffer), fileName);
212
+ } else {
213
+ // Use JSON format
214
+ const backupDataString = JSON.stringify(backupData, null, 2);
215
+ fileName = `${new Date().toISOString()}-${options.databaseId}-collections.json`;
216
+ backupSize = Buffer.byteLength(backupDataString, 'utf8');
217
+ inputFile = InputFile.fromPlainText(backupDataString, fileName);
218
+ }
219
+
220
+ const fileCreated = await storage.createFile(
221
+ backupBucketId,
222
+ ulid(),
223
+ inputFile
224
+ );
225
+
226
+ totalSizeBytes = backupSize;
227
+
228
+ // Create manifest
229
+ const manifestData = {
230
+ version: "1.0",
231
+ backupId,
232
+ databaseId: options.databaseId,
233
+ collectionIds: options.collectionIds,
234
+ collections: collections,
235
+ format: options.backupFormat || 'json',
236
+ createdAt: new Date().toISOString(),
237
+ totalDocuments: processedDocuments,
238
+ totalSizeBytes: backupSize
239
+ };
240
+
241
+ const manifestBuffer = Buffer.from(JSON.stringify(manifestData, null, 2), 'utf-8');
242
+ const manifestFile = await storage.createFile(
243
+ backupBucketId,
244
+ ID.unique(),
245
+ InputFile.fromBuffer(new Uint8Array(manifestBuffer), `${backupId}-manifest.json`)
246
+ );
247
+
248
+ // Record in centralized tracking
249
+ await recordCentralizedBackup(adapter, options.trackingDatabaseId, {
250
+ backupType: 'database',
251
+ backupId: fileCreated.$id,
252
+ manifestFileId: manifestFile.$id,
253
+ format: options.backupFormat || 'json',
254
+ sizeBytes: backupSize,
255
+ databaseId: options.databaseId,
256
+ collections: backupData.collections.length,
257
+ documents: processedDocuments,
258
+ status: errors.length === 0 ? 'completed' : 'partial',
259
+ error: errors.length > 0 ? errors.join('; ') : undefined,
260
+ restorationStatus: 'not_restored'
261
+ });
262
+
263
+ progress.stop();
264
+
265
+ const duration = Date.now() - startTime;
266
+ const status: 'completed' | 'partial' | 'failed' =
267
+ errors.length === 0 ? 'completed' :
268
+ collections.some(c => c.status === 'completed') ? 'partial' :
269
+ 'failed';
270
+
271
+ MessageFormatter.success(
272
+ `Collection backup ${status} in ${(duration / 1000).toFixed(2)}s`,
273
+ { prefix: "Backup" }
274
+ );
275
+
276
+ MessageFormatter.operationSummary("Collection Backup", {
277
+ database: options.databaseId,
278
+ collections: backupData.collections.length,
279
+ documents: processedDocuments,
280
+ fileSize: MessageFormatter.formatBytes(backupSize),
281
+ backupFile: fileName,
282
+ bucket: backupBucketId,
283
+ }, duration);
284
+
285
+ return {
286
+ backupId,
287
+ manifestFileId: manifestFile.$id,
288
+ databaseId: options.databaseId,
289
+ collections,
290
+ totalDocuments: processedDocuments,
291
+ sizeBytes: totalSizeBytes,
292
+ status,
293
+ errors
294
+ };
295
+ } catch (error) {
296
+ const errorMsg = `Collection backup failed: ${error instanceof Error ? error.message : String(error)}`;
297
+ MessageFormatter.error(errorMsg, error instanceof Error ? error : new Error(errorMsg), { prefix: "Backup" });
298
+
299
+ return {
300
+ backupId,
301
+ manifestFileId: '',
302
+ databaseId: options.databaseId,
303
+ collections: [],
304
+ totalDocuments: 0,
305
+ sizeBytes: 0,
306
+ status: 'failed',
307
+ errors: [errorMsg, ...errors]
308
+ };
309
+ }
310
+ }
@@ -0,0 +1,342 @@
1
+ import type { Storage, Databases, Models } from "node-appwrite";
2
+ import { ID } from "node-appwrite";
3
+ import { InputFile } from "node-appwrite/file";
4
+ import { ulid } from "ulidx";
5
+ import { MessageFormatter } from "../../shared/messageFormatter.js";
6
+ import { logger } from "../../shared/logging.js";
7
+ import type { DatabaseAdapter } from "../../adapters/DatabaseAdapter.js";
8
+ import { backupDatabase } from "../../storage/methods.js";
9
+ import { backupBucket } from "./bucketBackup.js";
10
+ import {
11
+ recordCentralizedBackup,
12
+ createCentralizedBackupTrackingTable
13
+ } from "../tracking/centralizedTracking.js";
14
+ import type {
15
+ ComprehensiveManifest,
16
+ DatabaseBackupReference,
17
+ BucketBackupReference
18
+ } from "../schemas/comprehensiveManifest.js";
19
+ import type { AppwriteConfig } from "appwrite-utils";
20
+ import { fetchAllDatabases } from "../../databases/methods.js";
21
+
22
+ export interface ComprehensiveBackupOptions {
23
+ trackingDatabaseId: string; // Database to store backup tracking
24
+ backupFormat?: 'json' | 'zip';
25
+ skipDatabases?: boolean;
26
+ skipBuckets?: boolean;
27
+ parallelDownloads?: number;
28
+ onProgress?: (message: string) => void;
29
+ }
30
+
31
+ export interface ComprehensiveBackupResult {
32
+ backupId: string;
33
+ manifestFileId: string;
34
+ databaseBackups: DatabaseBackupReference[];
35
+ bucketBackups: BucketBackupReference[];
36
+ totalSizeBytes: number;
37
+ status: 'completed' | 'partial' | 'failed';
38
+ errors: string[];
39
+ }
40
+
41
+ /**
42
+ * Orchestrates comprehensive backup of ALL databases and ALL storage buckets
43
+ */
44
+ export async function comprehensiveBackup(
45
+ config: AppwriteConfig,
46
+ databases: Databases,
47
+ storage: Storage,
48
+ adapter: DatabaseAdapter,
49
+ options: ComprehensiveBackupOptions
50
+ ): Promise<ComprehensiveBackupResult> {
51
+ const startTime = Date.now();
52
+ const backupId = ulid();
53
+ const errors: string[] = [];
54
+ const databaseBackups: DatabaseBackupReference[] = [];
55
+ const bucketBackups: BucketBackupReference[] = [];
56
+ let totalSizeBytes = 0;
57
+
58
+ try {
59
+ // Ensure tracking table exists
60
+ await createCentralizedBackupTrackingTable(adapter, options.trackingDatabaseId);
61
+
62
+ // Initialize backup bucket
63
+ const backupBucketId = "appwrite-backups";
64
+ MessageFormatter.info(`Starting comprehensive backup ${backupId}`, { prefix: "Backup" });
65
+
66
+ // Phase 1: Backup ALL databases
67
+ if (!options.skipDatabases) {
68
+ MessageFormatter.info("Phase 1: Backing up ALL databases", { prefix: "Backup" });
69
+
70
+ const allDatabases = await fetchAllDatabases(databases);
71
+
72
+ // Validate each database exists before attempting backup
73
+ const validDatabases: Models.Database[] = [];
74
+ const skippedDatabases: string[] = [];
75
+
76
+ MessageFormatter.info(`Validating ${allDatabases.length} databases...`, { prefix: "Backup" });
77
+
78
+ for (const db of allDatabases) {
79
+ try {
80
+ await databases.get(db.$id); // Validate existence
81
+ validDatabases.push(db);
82
+ } catch (error) {
83
+ skippedDatabases.push(`${db.name} (${db.$id})`);
84
+ MessageFormatter.warning(
85
+ `Database ${db.name} not found - skipping`,
86
+ { prefix: "Backup" }
87
+ );
88
+ logger.warn('Database validation failed', {
89
+ databaseId: db.$id,
90
+ databaseName: db.name,
91
+ error: error instanceof Error ? error.message : String(error)
92
+ });
93
+ }
94
+ }
95
+
96
+ if (skippedDatabases.length > 0) {
97
+ MessageFormatter.info(
98
+ `Skipped ${skippedDatabases.length} invalid databases: ${skippedDatabases.join(', ')}`,
99
+ { prefix: "Backup" }
100
+ );
101
+ }
102
+
103
+ MessageFormatter.info(`Found ${validDatabases.length} valid databases to backup`, { prefix: "Backup" });
104
+
105
+ for (const db of validDatabases) {
106
+ try {
107
+ if (options.onProgress) {
108
+ options.onProgress(`Backing up database: ${db.name}`);
109
+ }
110
+
111
+ MessageFormatter.info(`Backing up database: ${db.name} (${db.$id})`, { prefix: "Backup" });
112
+
113
+ // Use existing backupDatabase function
114
+ const dbBackupResult = await backupDatabase(
115
+ config,
116
+ databases,
117
+ db.$id,
118
+ storage,
119
+ options.backupFormat || 'zip'
120
+ );
121
+
122
+ // Create database manifest with complete data
123
+ const manifestData = {
124
+ version: "1.0",
125
+ databaseId: dbBackupResult.databaseId,
126
+ databaseName: dbBackupResult.databaseName,
127
+ format: dbBackupResult.format,
128
+ collectionCount: dbBackupResult.collectionCount,
129
+ documentCount: dbBackupResult.documentCount,
130
+ backupFileId: dbBackupResult.backupFileId,
131
+ createdAt: new Date().toISOString()
132
+ };
133
+
134
+ const manifestBuffer = Buffer.from(JSON.stringify(manifestData, null, 2), 'utf-8');
135
+ const manifestFile = await storage.createFile(
136
+ backupBucketId,
137
+ ID.unique(),
138
+ InputFile.fromBuffer(new Uint8Array(manifestBuffer), `${db.$id}-manifest.json`)
139
+ );
140
+
141
+ databaseBackups.push({
142
+ databaseId: dbBackupResult.databaseId,
143
+ databaseName: dbBackupResult.databaseName,
144
+ backupFileId: dbBackupResult.backupFileId,
145
+ manifestFileId: manifestFile.$id,
146
+ collectionCount: dbBackupResult.collectionCount,
147
+ documentCount: dbBackupResult.documentCount,
148
+ sizeBytes: dbBackupResult.backupSizeBytes,
149
+ status: 'completed'
150
+ });
151
+
152
+ totalSizeBytes += dbBackupResult.backupSizeBytes;
153
+
154
+ // Record individual database backup in tracking
155
+ await recordCentralizedBackup(adapter, options.trackingDatabaseId, {
156
+ backupType: 'database',
157
+ backupId: dbBackupResult.backupFileId,
158
+ manifestFileId: manifestFile.$id,
159
+ format: dbBackupResult.format,
160
+ sizeBytes: dbBackupResult.backupSizeBytes,
161
+ databaseId: dbBackupResult.databaseId,
162
+ collections: dbBackupResult.collectionCount,
163
+ documents: dbBackupResult.documentCount,
164
+ status: 'completed',
165
+ restorationStatus: 'not_restored'
166
+ });
167
+
168
+ MessageFormatter.success(`Database ${db.name} backed up successfully`, { prefix: "Backup" });
169
+ } catch (error) {
170
+ const errorMsg = `Failed to backup database ${db.name}: ${error instanceof Error ? error.message : String(error)}`;
171
+ errors.push(errorMsg);
172
+ logger.error(errorMsg);
173
+
174
+ databaseBackups.push({
175
+ databaseId: db.$id,
176
+ databaseName: db.name,
177
+ backupFileId: '',
178
+ manifestFileId: '',
179
+ collectionCount: 0,
180
+ documentCount: 0,
181
+ sizeBytes: 0,
182
+ status: 'failed',
183
+ error: errorMsg
184
+ });
185
+ }
186
+ }
187
+ }
188
+
189
+ // Phase 2: Backup ALL storage buckets
190
+ if (!options.skipBuckets) {
191
+ MessageFormatter.info("Phase 2: Backing up ALL storage buckets", { prefix: "Backup" });
192
+
193
+ const allBuckets = await storage.listBuckets();
194
+ const bucketsToBackup = allBuckets.buckets.filter(b => b.$id !== backupBucketId);
195
+
196
+ MessageFormatter.info(`Found ${bucketsToBackup.length} buckets to backup`, { prefix: "Backup" });
197
+
198
+ for (const bucket of bucketsToBackup) {
199
+ try {
200
+ if (options.onProgress) {
201
+ options.onProgress(`Backing up bucket: ${bucket.name}`);
202
+ }
203
+
204
+ MessageFormatter.info(`Backing up bucket: ${bucket.name} (${bucket.$id})`, { prefix: "Backup" });
205
+
206
+ const bucketBackupResult = await backupBucket(
207
+ storage,
208
+ bucket.$id,
209
+ backupBucketId,
210
+ {
211
+ parallelDownloads: options.parallelDownloads || 10,
212
+ onProgress: (current, total, fileName) => {
213
+ if (options.onProgress) {
214
+ options.onProgress(`Downloading ${fileName} (${current}/${total})`);
215
+ }
216
+ }
217
+ }
218
+ );
219
+
220
+ bucketBackups.push({
221
+ bucketId: bucket.$id,
222
+ bucketName: bucket.name,
223
+ backupFileId: bucketBackupResult.backupFileId,
224
+ manifestFileId: bucketBackupResult.manifestFileId,
225
+ fileCount: bucketBackupResult.fileCount,
226
+ sizeBytes: bucketBackupResult.totalSizeBytes,
227
+ status: bucketBackupResult.status,
228
+ error: bucketBackupResult.errors?.join('; ')
229
+ });
230
+
231
+ totalSizeBytes += bucketBackupResult.zipSizeBytes;
232
+
233
+ // Record individual bucket backup in tracking
234
+ await recordCentralizedBackup(adapter, options.trackingDatabaseId, {
235
+ backupType: 'bucket',
236
+ backupId: bucketBackupResult.backupFileId,
237
+ manifestFileId: bucketBackupResult.manifestFileId,
238
+ format: 'zip',
239
+ sizeBytes: bucketBackupResult.zipSizeBytes,
240
+ bucketId: bucket.$id,
241
+ fileCount: bucketBackupResult.fileCount,
242
+ status: bucketBackupResult.status,
243
+ error: bucketBackupResult.errors?.join('; '),
244
+ restorationStatus: 'not_restored'
245
+ });
246
+
247
+ MessageFormatter.success(`Bucket ${bucket.name} backed up successfully`, { prefix: "Backup" });
248
+ } catch (error) {
249
+ const errorMsg = `Failed to backup bucket ${bucket.name}: ${error instanceof Error ? error.message : String(error)}`;
250
+ errors.push(errorMsg);
251
+ logger.error(errorMsg);
252
+
253
+ bucketBackups.push({
254
+ bucketId: bucket.$id,
255
+ bucketName: bucket.name,
256
+ backupFileId: '',
257
+ manifestFileId: '',
258
+ fileCount: 0,
259
+ sizeBytes: 0,
260
+ status: 'failed',
261
+ error: errorMsg
262
+ });
263
+ }
264
+ }
265
+ }
266
+
267
+ // Phase 3: Create comprehensive manifest
268
+ MessageFormatter.info("Creating comprehensive backup manifest", { prefix: "Backup" });
269
+
270
+ const comprehensiveStatus: 'completed' | 'partial' | 'failed' =
271
+ errors.length === 0 ? 'completed' :
272
+ (databaseBackups.length > 0 || bucketBackups.length > 0) ? 'partial' :
273
+ 'failed';
274
+
275
+ const manifest: ComprehensiveManifest = {
276
+ version: "1.0",
277
+ backupId,
278
+ createdAt: new Date().toISOString(),
279
+ databases: databaseBackups,
280
+ buckets: bucketBackups,
281
+ totalSizeBytes,
282
+ status: comprehensiveStatus,
283
+ errors: errors.length > 0 ? errors : undefined
284
+ };
285
+
286
+ // Upload comprehensive manifest
287
+ const manifestFileName = `comprehensive-${backupId}.json`;
288
+ const manifestBuffer = Buffer.from(JSON.stringify(manifest, null, 2), 'utf-8');
289
+ const manifestFile = await storage.createFile(
290
+ backupBucketId,
291
+ ID.unique(),
292
+ InputFile.fromBuffer(new Uint8Array(manifestBuffer), manifestFileName)
293
+ );
294
+
295
+ // Record comprehensive backup in tracking
296
+ await recordCentralizedBackup(adapter, options.trackingDatabaseId, {
297
+ backupType: 'comprehensive',
298
+ backupId,
299
+ manifestFileId: manifestFile.$id,
300
+ format: 'zip',
301
+ sizeBytes: totalSizeBytes,
302
+ comprehensiveBackupId: backupId,
303
+ status: comprehensiveStatus,
304
+ error: errors.length > 0 ? errors.join('; ') : undefined,
305
+ restorationStatus: 'not_restored'
306
+ });
307
+
308
+ const duration = Date.now() - startTime;
309
+ MessageFormatter.success(
310
+ `Comprehensive backup ${comprehensiveStatus} in ${(duration / 1000).toFixed(2)}s`,
311
+ { prefix: "Backup" }
312
+ );
313
+
314
+ MessageFormatter.info(
315
+ `Backed up ${databaseBackups.length} databases and ${bucketBackups.length} buckets (${MessageFormatter.formatBytes(totalSizeBytes)})`,
316
+ { prefix: "Backup" }
317
+ );
318
+
319
+ return {
320
+ backupId,
321
+ manifestFileId: manifestFile.$id,
322
+ databaseBackups,
323
+ bucketBackups,
324
+ totalSizeBytes,
325
+ status: comprehensiveStatus,
326
+ errors
327
+ };
328
+ } catch (error) {
329
+ const errorMsg = `Comprehensive backup failed: ${error instanceof Error ? error.message : String(error)}`;
330
+ MessageFormatter.error(errorMsg, error instanceof Error ? error : new Error(errorMsg), { prefix: "Backup" });
331
+
332
+ return {
333
+ backupId,
334
+ manifestFileId: '',
335
+ databaseBackups,
336
+ bucketBackups,
337
+ totalSizeBytes,
338
+ status: 'failed',
339
+ errors: [errorMsg, ...errors]
340
+ };
341
+ }
342
+ }