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.
- package/CHANGELOG.md +199 -0
- package/README.md +251 -29
- package/dist/adapters/AdapterFactory.d.ts +10 -3
- package/dist/adapters/AdapterFactory.js +213 -17
- package/dist/adapters/TablesDBAdapter.js +60 -17
- package/dist/backups/operations/bucketBackup.d.ts +19 -0
- package/dist/backups/operations/bucketBackup.js +197 -0
- package/dist/backups/operations/collectionBackup.d.ts +30 -0
- package/dist/backups/operations/collectionBackup.js +201 -0
- package/dist/backups/operations/comprehensiveBackup.d.ts +25 -0
- package/dist/backups/operations/comprehensiveBackup.js +238 -0
- package/dist/backups/schemas/bucketManifest.d.ts +93 -0
- package/dist/backups/schemas/bucketManifest.js +33 -0
- package/dist/backups/schemas/comprehensiveManifest.d.ts +108 -0
- package/dist/backups/schemas/comprehensiveManifest.js +32 -0
- package/dist/backups/tracking/centralizedTracking.d.ts +34 -0
- package/dist/backups/tracking/centralizedTracking.js +274 -0
- package/dist/cli/commands/configCommands.d.ts +8 -0
- package/dist/cli/commands/configCommands.js +160 -0
- package/dist/cli/commands/databaseCommands.d.ts +13 -0
- package/dist/cli/commands/databaseCommands.js +479 -0
- package/dist/cli/commands/functionCommands.d.ts +7 -0
- package/dist/cli/commands/functionCommands.js +289 -0
- package/dist/cli/commands/schemaCommands.d.ts +7 -0
- package/dist/cli/commands/schemaCommands.js +134 -0
- package/dist/cli/commands/transferCommands.d.ts +5 -0
- package/dist/cli/commands/transferCommands.js +384 -0
- package/dist/collections/attributes.d.ts +5 -4
- package/dist/collections/attributes.js +539 -246
- package/dist/collections/indexes.js +39 -37
- package/dist/collections/methods.d.ts +2 -16
- package/dist/collections/methods.js +90 -538
- package/dist/collections/transferOperations.d.ts +7 -0
- package/dist/collections/transferOperations.js +331 -0
- package/dist/collections/wipeOperations.d.ts +16 -0
- package/dist/collections/wipeOperations.js +328 -0
- package/dist/config/configMigration.d.ts +87 -0
- package/dist/config/configMigration.js +390 -0
- package/dist/config/configValidation.d.ts +66 -0
- package/dist/config/configValidation.js +358 -0
- package/dist/config/yamlConfig.d.ts +455 -1
- package/dist/config/yamlConfig.js +145 -52
- package/dist/databases/methods.js +3 -2
- package/dist/databases/setup.d.ts +1 -2
- package/dist/databases/setup.js +9 -87
- package/dist/examples/yamlTerminologyExample.d.ts +42 -0
- package/dist/examples/yamlTerminologyExample.js +269 -0
- package/dist/functions/deployments.js +11 -10
- package/dist/functions/methods.d.ts +1 -1
- package/dist/functions/methods.js +5 -4
- package/dist/init.js +9 -9
- package/dist/interactiveCLI.d.ts +8 -17
- package/dist/interactiveCLI.js +209 -1172
- package/dist/main.js +364 -21
- package/dist/migrations/afterImportActions.js +22 -30
- package/dist/migrations/appwriteToX.js +71 -25
- package/dist/migrations/dataLoader.js +35 -26
- package/dist/migrations/importController.js +29 -30
- package/dist/migrations/relationships.js +13 -12
- package/dist/migrations/services/ImportOrchestrator.js +16 -19
- package/dist/migrations/transfer.js +46 -46
- package/dist/migrations/yaml/YamlImportConfigLoader.d.ts +3 -1
- package/dist/migrations/yaml/YamlImportConfigLoader.js +6 -3
- package/dist/migrations/yaml/YamlImportIntegration.d.ts +9 -3
- package/dist/migrations/yaml/YamlImportIntegration.js +22 -11
- package/dist/migrations/yaml/generateImportSchemas.d.ts +14 -1
- package/dist/migrations/yaml/generateImportSchemas.js +736 -7
- package/dist/schemas/authUser.d.ts +1 -1
- package/dist/setupController.js +3 -2
- package/dist/shared/backupMetadataSchema.d.ts +94 -0
- package/dist/shared/backupMetadataSchema.js +38 -0
- package/dist/shared/backupTracking.d.ts +18 -0
- package/dist/shared/backupTracking.js +176 -0
- package/dist/shared/confirmationDialogs.js +15 -15
- package/dist/shared/errorUtils.d.ts +54 -0
- package/dist/shared/errorUtils.js +95 -0
- package/dist/shared/functionManager.js +20 -19
- package/dist/shared/indexManager.js +12 -11
- package/dist/shared/jsonSchemaGenerator.js +10 -26
- package/dist/shared/logging.d.ts +51 -0
- package/dist/shared/logging.js +70 -0
- package/dist/shared/messageFormatter.d.ts +2 -0
- package/dist/shared/messageFormatter.js +10 -0
- package/dist/shared/migrationHelpers.d.ts +6 -16
- package/dist/shared/migrationHelpers.js +24 -21
- package/dist/shared/operationLogger.d.ts +8 -1
- package/dist/shared/operationLogger.js +11 -24
- package/dist/shared/operationQueue.d.ts +28 -1
- package/dist/shared/operationQueue.js +268 -66
- package/dist/shared/operationsTable.d.ts +26 -0
- package/dist/shared/operationsTable.js +286 -0
- package/dist/shared/operationsTableSchema.d.ts +48 -0
- package/dist/shared/operationsTableSchema.js +35 -0
- package/dist/shared/relationshipExtractor.d.ts +56 -0
- package/dist/shared/relationshipExtractor.js +138 -0
- package/dist/shared/schemaGenerator.d.ts +19 -1
- package/dist/shared/schemaGenerator.js +56 -75
- package/dist/storage/backupCompression.d.ts +20 -0
- package/dist/storage/backupCompression.js +67 -0
- package/dist/storage/methods.d.ts +16 -2
- package/dist/storage/methods.js +98 -14
- package/dist/users/methods.js +9 -8
- package/dist/utils/configDiscovery.d.ts +78 -0
- package/dist/utils/configDiscovery.js +430 -0
- package/dist/utils/directoryUtils.d.ts +22 -0
- package/dist/utils/directoryUtils.js +59 -0
- package/dist/utils/getClientFromConfig.d.ts +17 -8
- package/dist/utils/getClientFromConfig.js +162 -17
- package/dist/utils/helperFunctions.d.ts +16 -2
- package/dist/utils/helperFunctions.js +19 -5
- package/dist/utils/loadConfigs.d.ts +34 -9
- package/dist/utils/loadConfigs.js +236 -316
- package/dist/utils/pathResolvers.d.ts +53 -0
- package/dist/utils/pathResolvers.js +72 -0
- package/dist/utils/projectConfig.d.ts +119 -0
- package/dist/utils/projectConfig.js +171 -0
- package/dist/utils/retryFailedPromises.js +4 -2
- package/dist/utils/sessionAuth.d.ts +48 -0
- package/dist/utils/sessionAuth.js +164 -0
- package/dist/utils/sessionPreservationExample.d.ts +1666 -0
- package/dist/utils/sessionPreservationExample.js +101 -0
- package/dist/utils/setupFiles.js +301 -41
- package/dist/utils/typeGuards.d.ts +35 -0
- package/dist/utils/typeGuards.js +57 -0
- package/dist/utils/versionDetection.js +145 -9
- package/dist/utils/yamlConverter.d.ts +53 -3
- package/dist/utils/yamlConverter.js +232 -13
- package/dist/utils/yamlLoader.d.ts +70 -0
- package/dist/utils/yamlLoader.js +263 -0
- package/dist/utilsController.d.ts +36 -3
- package/dist/utilsController.js +186 -56
- package/package.json +12 -2
- package/src/adapters/AdapterFactory.ts +263 -35
- package/src/adapters/TablesDBAdapter.ts +225 -36
- package/src/backups/operations/bucketBackup.ts +277 -0
- package/src/backups/operations/collectionBackup.ts +310 -0
- package/src/backups/operations/comprehensiveBackup.ts +342 -0
- package/src/backups/schemas/bucketManifest.ts +78 -0
- package/src/backups/schemas/comprehensiveManifest.ts +76 -0
- package/src/backups/tracking/centralizedTracking.ts +352 -0
- package/src/cli/commands/configCommands.ts +194 -0
- package/src/cli/commands/databaseCommands.ts +635 -0
- package/src/cli/commands/functionCommands.ts +379 -0
- package/src/cli/commands/schemaCommands.ts +163 -0
- package/src/cli/commands/transferCommands.ts +457 -0
- package/src/collections/attributes.ts +900 -621
- package/src/collections/attributes.ts.backup +1555 -0
- package/src/collections/indexes.ts +116 -114
- package/src/collections/methods.ts +295 -968
- package/src/collections/transferOperations.ts +516 -0
- package/src/collections/wipeOperations.ts +501 -0
- package/src/config/README.md +274 -0
- package/src/config/configMigration.ts +575 -0
- package/src/config/configValidation.ts +445 -0
- package/src/config/yamlConfig.ts +168 -55
- package/src/databases/methods.ts +3 -2
- package/src/databases/setup.ts +11 -138
- package/src/examples/yamlTerminologyExample.ts +341 -0
- package/src/functions/deployments.ts +14 -12
- package/src/functions/methods.ts +11 -11
- package/src/functions/templates/hono-typescript/README.md +286 -0
- package/src/functions/templates/hono-typescript/package.json +26 -0
- package/src/functions/templates/hono-typescript/src/adapters/request.ts +74 -0
- package/src/functions/templates/hono-typescript/src/adapters/response.ts +106 -0
- package/src/functions/templates/hono-typescript/src/app.ts +180 -0
- package/src/functions/templates/hono-typescript/src/context.ts +103 -0
- package/src/functions/templates/hono-typescript/src/index.ts +54 -0
- package/src/functions/templates/hono-typescript/src/middleware/appwrite.ts +119 -0
- package/src/functions/templates/hono-typescript/tsconfig.json +20 -0
- package/src/functions/templates/typescript-node/package.json +2 -1
- package/src/functions/templates/typescript-node/src/context.ts +103 -0
- package/src/functions/templates/typescript-node/src/index.ts +18 -12
- package/src/functions/templates/uv/pyproject.toml +1 -0
- package/src/functions/templates/uv/src/context.py +125 -0
- package/src/functions/templates/uv/src/index.py +35 -5
- package/src/init.ts +9 -11
- package/src/interactiveCLI.ts +274 -1563
- package/src/main.ts +418 -24
- package/src/migrations/afterImportActions.ts +71 -44
- package/src/migrations/appwriteToX.ts +100 -34
- package/src/migrations/dataLoader.ts +48 -34
- package/src/migrations/importController.ts +44 -39
- package/src/migrations/relationships.ts +28 -18
- package/src/migrations/services/ImportOrchestrator.ts +24 -27
- package/src/migrations/transfer.ts +159 -121
- package/src/migrations/yaml/YamlImportConfigLoader.ts +11 -4
- package/src/migrations/yaml/YamlImportIntegration.ts +47 -20
- package/src/migrations/yaml/generateImportSchemas.ts +751 -12
- package/src/setupController.ts +3 -2
- package/src/shared/backupMetadataSchema.ts +93 -0
- package/src/shared/backupTracking.ts +211 -0
- package/src/shared/confirmationDialogs.ts +19 -19
- package/src/shared/errorUtils.ts +110 -0
- package/src/shared/functionManager.ts +21 -20
- package/src/shared/indexManager.ts +12 -11
- package/src/shared/jsonSchemaGenerator.ts +38 -52
- package/src/shared/logging.ts +75 -0
- package/src/shared/messageFormatter.ts +14 -1
- package/src/shared/migrationHelpers.ts +45 -38
- package/src/shared/operationLogger.ts +11 -36
- package/src/shared/operationQueue.ts +322 -93
- package/src/shared/operationsTable.ts +338 -0
- package/src/shared/operationsTableSchema.ts +60 -0
- package/src/shared/relationshipExtractor.ts +214 -0
- package/src/shared/schemaGenerator.ts +179 -219
- package/src/storage/backupCompression.ts +88 -0
- package/src/storage/methods.ts +131 -34
- package/src/users/methods.ts +11 -9
- package/src/utils/configDiscovery.ts +502 -0
- package/src/utils/directoryUtils.ts +61 -0
- package/src/utils/getClientFromConfig.ts +205 -22
- package/src/utils/helperFunctions.ts +23 -5
- package/src/utils/loadConfigs.ts +313 -345
- package/src/utils/pathResolvers.ts +81 -0
- package/src/utils/projectConfig.ts +299 -0
- package/src/utils/retryFailedPromises.ts +4 -2
- package/src/utils/sessionAuth.ts +230 -0
- package/src/utils/setupFiles.ts +322 -54
- package/src/utils/typeGuards.ts +65 -0
- package/src/utils/versionDetection.ts +218 -64
- package/src/utils/yamlConverter.ts +296 -13
- package/src/utils/yamlLoader.ts +364 -0
- package/src/utilsController.ts +314 -110
- package/tests/README.md +497 -0
- package/tests/adapters/AdapterFactory.test.ts +277 -0
- package/tests/integration/syncOperations.test.ts +463 -0
- package/tests/jest.config.js +25 -0
- package/tests/migration/configMigration.test.ts +546 -0
- package/tests/setup.ts +62 -0
- package/tests/testUtils.ts +340 -0
- package/tests/utils/loadConfigs.test.ts +350 -0
- package/tests/validation/configValidation.test.ts +412 -0
- 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
|
+
}
|