appwrite-utils-cli 0.0.46 → 0.0.48
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/README.md +53 -5
- package/dist/main.js +23 -0
- package/dist/migrations/collections.d.ts +6 -0
- package/dist/migrations/collections.js +138 -7
- package/dist/migrations/dataLoader.d.ts +11 -1
- package/dist/migrations/dataLoader.js +40 -29
- package/dist/migrations/databases.d.ts +10 -0
- package/dist/migrations/databases.js +114 -2
- package/dist/migrations/importController.d.ts +1 -0
- package/dist/migrations/importController.js +19 -6
- package/dist/migrations/storage.d.ts +2 -0
- package/dist/migrations/storage.js +68 -1
- package/dist/migrations/users.d.ts +1 -0
- package/dist/migrations/users.js +46 -1
- package/dist/utils/helperFunctions.d.ts +2 -1
- package/dist/utils/helperFunctions.js +7 -1
- package/dist/utilsController.d.ts +11 -0
- package/dist/utilsController.js +54 -0
- package/package.json +2 -2
- package/src/main.ts +53 -0
- package/src/migrations/collections.ts +226 -6
- package/src/migrations/dataLoader.ts +69 -33
- package/src/migrations/databases.ts +221 -2
- package/src/migrations/importController.ts +31 -6
- package/src/migrations/storage.ts +124 -1
- package/src/migrations/users.ts +66 -1
- package/src/utils/helperFunctions.ts +17 -1
- package/src/utilsController.ts +140 -0
|
@@ -7,6 +7,9 @@ import { logger } from "./logging.js";
|
|
|
7
7
|
import { updateOperation } from "./migrationHelper.js";
|
|
8
8
|
import { BatchSchema, OperationCreateSchema, OperationSchema, } from "./backup.js";
|
|
9
9
|
import { DataLoader } from "./dataLoader.js";
|
|
10
|
+
import { transferDocumentsBetweenDbsLocalToLocal } from "./collections.js";
|
|
11
|
+
import { transferDatabaseLocalToLocal } from "./databases.js";
|
|
12
|
+
import { transferStorageLocalToLocal } from "./storage.js";
|
|
10
13
|
export class ImportController {
|
|
11
14
|
config;
|
|
12
15
|
database;
|
|
@@ -37,6 +40,7 @@ export class ImportController {
|
|
|
37
40
|
this.setupOptions.runDev))
|
|
38
41
|
.map((db) => db.name);
|
|
39
42
|
let dataLoader;
|
|
43
|
+
let databaseRan;
|
|
40
44
|
for (let db of this.config.databases) {
|
|
41
45
|
if (db.name.toLowerCase().trim().replace(" ", "") === "migrations" ||
|
|
42
46
|
!databasesToRun.includes(db.name)) {
|
|
@@ -54,21 +58,30 @@ export class ImportController {
|
|
|
54
58
|
console.log(`Starting import data for database: ${db.name}`);
|
|
55
59
|
console.log(`---------------------------------`);
|
|
56
60
|
// await this.importCollections(db);
|
|
57
|
-
if (!
|
|
61
|
+
if (!databaseRan) {
|
|
62
|
+
databaseRan = db;
|
|
58
63
|
dataLoader = new DataLoader(this.appwriteFolderPath, this.importDataActions, this.database, this.config, this.setupOptions.shouldWriteFile);
|
|
59
64
|
await dataLoader.start(db.$id);
|
|
65
|
+
await this.importCollections(db, dataLoader);
|
|
66
|
+
await resolveAndUpdateRelationships(db.$id, this.database, this.config);
|
|
67
|
+
await this.executePostImportActions(db.$id, dataLoader);
|
|
60
68
|
}
|
|
61
|
-
else {
|
|
62
|
-
|
|
69
|
+
else if (databaseRan.$id !== db.$id) {
|
|
70
|
+
await this.updateOthersToFinalData(databaseRan, db);
|
|
63
71
|
}
|
|
64
|
-
await this.importCollections(db, dataLoader);
|
|
65
|
-
await resolveAndUpdateRelationships(db.$id, this.database, this.config);
|
|
66
|
-
await this.executePostImportActions(db.$id, dataLoader);
|
|
67
72
|
console.log(`---------------------------------`);
|
|
68
73
|
console.log(`Finished import data for database: ${db.name}`);
|
|
69
74
|
console.log(`---------------------------------`);
|
|
70
75
|
}
|
|
71
76
|
}
|
|
77
|
+
async updateOthersToFinalData(updatedDb, targetDb) {
|
|
78
|
+
await transferDatabaseLocalToLocal(this.database, updatedDb.$id, targetDb.$id);
|
|
79
|
+
await transferStorageLocalToLocal(this.storage, `${this.config.documentBucketId}_${updatedDb.name
|
|
80
|
+
.toLowerCase()
|
|
81
|
+
.replace(" ", "")}`, `${this.config.documentBucketId}_${targetDb.name
|
|
82
|
+
.toLowerCase()
|
|
83
|
+
.replace(" ", "")}`);
|
|
84
|
+
}
|
|
72
85
|
async importCollections(db, dataLoader) {
|
|
73
86
|
if (!this.config.collections) {
|
|
74
87
|
return;
|
|
@@ -6,3 +6,5 @@ export declare const initOrGetBackupStorage: (storage: Storage) => Promise<Model
|
|
|
6
6
|
export declare const initOrGetDocumentStorage: (storage: Storage, config: AppwriteConfig, dbName: string) => Promise<Models.Bucket | undefined>;
|
|
7
7
|
export declare const wipeDocumentStorage: (storage: Storage, config: AppwriteConfig, dbName: string) => Promise<void>;
|
|
8
8
|
export declare const backupDatabase: (database: Databases, databaseId: string, storage: Storage) => Promise<void>;
|
|
9
|
+
export declare const transferStorageLocalToLocal: (storage: Storage, fromBucketId: string, toBucketId: string) => Promise<void>;
|
|
10
|
+
export declare const transferStorageLocalToRemote: (localStorage: Storage, endpoint: string, projectId: string, apiKey: string, fromBucketId: string, toBucketId: string) => Promise<void>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Storage, Databases, Query, InputFile, ID, Permission, } from "node-appwrite";
|
|
2
2
|
import {} from "./backup.js";
|
|
3
3
|
import { splitIntoBatches } from "./migrationHelper.js";
|
|
4
|
-
import { tryAwaitWithRetry } from "../utils/helperFunctions.js";
|
|
4
|
+
import { getAppwriteClient, tryAwaitWithRetry, } from "../utils/helperFunctions.js";
|
|
5
5
|
export const logOperation = async (db, dbId, operationDetails, operationId) => {
|
|
6
6
|
try {
|
|
7
7
|
let operation;
|
|
@@ -245,3 +245,70 @@ export const backupDatabase = async (database, databaseId, storage) => {
|
|
|
245
245
|
console.log("Database Backup Complete");
|
|
246
246
|
console.log("---------------------------------");
|
|
247
247
|
};
|
|
248
|
+
export const transferStorageLocalToLocal = async (storage, fromBucketId, toBucketId) => {
|
|
249
|
+
console.log(`Transferring files from ${fromBucketId} to ${toBucketId}`);
|
|
250
|
+
let lastFileId;
|
|
251
|
+
let fromFiles = await tryAwaitWithRetry(async () => await storage.listFiles(fromBucketId, [Query.limit(100)]));
|
|
252
|
+
const allFromFiles = fromFiles.files;
|
|
253
|
+
let numberOfFiles = 0;
|
|
254
|
+
if (fromFiles.files.length < 100) {
|
|
255
|
+
for (const file of allFromFiles) {
|
|
256
|
+
const fileData = await storage.getFileDownload(file.bucketId, file.$id);
|
|
257
|
+
const fileToCreate = InputFile.fromBuffer(Buffer.from(fileData), file.name);
|
|
258
|
+
console.log(`Creating file: ${file.name}`);
|
|
259
|
+
tryAwaitWithRetry(async () => await storage.createFile(toBucketId, file.$id, fileToCreate, file.$permissions));
|
|
260
|
+
numberOfFiles++;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
lastFileId = fromFiles.files[fromFiles.files.length - 1].$id;
|
|
265
|
+
while (lastFileId) {
|
|
266
|
+
const files = await storage.listFiles(fromBucketId, [
|
|
267
|
+
Query.limit(100),
|
|
268
|
+
Query.cursorAfter(lastFileId),
|
|
269
|
+
]);
|
|
270
|
+
allFromFiles.push(...files.files);
|
|
271
|
+
if (files.files.length < 100) {
|
|
272
|
+
lastFileId = undefined;
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
lastFileId = files.files[files.files.length - 1].$id;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
for (const file of allFromFiles) {
|
|
279
|
+
const fileData = await storage.getFileDownload(file.bucketId, file.$id);
|
|
280
|
+
const fileToCreate = InputFile.fromBuffer(Buffer.from(fileData), file.name);
|
|
281
|
+
await tryAwaitWithRetry(async () => await storage.createFile(toBucketId, file.$id, fileToCreate, file.$permissions));
|
|
282
|
+
numberOfFiles++;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
console.log(`Transferred ${numberOfFiles} files from ${fromBucketId} to ${toBucketId}`);
|
|
286
|
+
};
|
|
287
|
+
export const transferStorageLocalToRemote = async (localStorage, endpoint, projectId, apiKey, fromBucketId, toBucketId) => {
|
|
288
|
+
console.log(`Transferring files from current storage ${fromBucketId} to ${endpoint} bucket ${toBucketId}`);
|
|
289
|
+
const client = getAppwriteClient(endpoint, apiKey, projectId);
|
|
290
|
+
const remoteStorage = new Storage(client);
|
|
291
|
+
let numberOfFiles = 0;
|
|
292
|
+
let lastFileId;
|
|
293
|
+
let fromFiles = await tryAwaitWithRetry(async () => await localStorage.listFiles(fromBucketId, [Query.limit(100)]));
|
|
294
|
+
const allFromFiles = fromFiles.files;
|
|
295
|
+
if (fromFiles.files.length === 100) {
|
|
296
|
+
lastFileId = fromFiles.files[fromFiles.files.length - 1].$id;
|
|
297
|
+
while (lastFileId) {
|
|
298
|
+
const files = await localStorage.listFiles(fromBucketId, [
|
|
299
|
+
Query.limit(100),
|
|
300
|
+
Query.cursorAfter(lastFileId),
|
|
301
|
+
]);
|
|
302
|
+
allFromFiles.push(...files.files);
|
|
303
|
+
if (files.files.length < 100) {
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
lastFileId = files.files[files.files.length - 1].$id;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
for (const file of allFromFiles) {
|
|
310
|
+
await tryAwaitWithRetry(async () => await remoteStorage.createFile(toBucketId, file.$id, file, file.$permissions));
|
|
311
|
+
numberOfFiles++;
|
|
312
|
+
}
|
|
313
|
+
console.log(`Transferred ${numberOfFiles} files from ${fromBucketId} to ${toBucketId}`);
|
|
314
|
+
};
|
|
@@ -12,4 +12,5 @@ export declare class UsersController {
|
|
|
12
12
|
createUserAndReturn(item: AuthUserCreate, numAttempts?: number): Promise<Models.User<Models.Preferences>>;
|
|
13
13
|
createAndCheckForUserAndReturn(item: AuthUserCreate): Promise<Models.User<Models.Preferences> | undefined>;
|
|
14
14
|
getUserIdByEmailOrPhone(email?: string, phone?: string): Promise<string | undefined>;
|
|
15
|
+
transferUsersBetweenDbsLocalToRemote: (endpoint: string, projectId: string, apiKey: string) => Promise<void>;
|
|
15
16
|
}
|
package/dist/migrations/users.js
CHANGED
|
@@ -3,7 +3,7 @@ import { AuthUserSchema, } from "../schemas/authUser.js";
|
|
|
3
3
|
import _ from "lodash";
|
|
4
4
|
import { logger } from "./logging.js";
|
|
5
5
|
import { splitIntoBatches } from "./migrationHelper.js";
|
|
6
|
-
import { tryAwaitWithRetry } from "../utils/helperFunctions.js";
|
|
6
|
+
import { getAppwriteClient, tryAwaitWithRetry, } from "../utils/helperFunctions.js";
|
|
7
7
|
export class UsersController {
|
|
8
8
|
config;
|
|
9
9
|
users;
|
|
@@ -232,4 +232,49 @@ export class UsersController {
|
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
234
|
}
|
|
235
|
+
transferUsersBetweenDbsLocalToRemote = async (endpoint, projectId, apiKey) => {
|
|
236
|
+
const localUsers = this.users;
|
|
237
|
+
const client = getAppwriteClient(endpoint, projectId, apiKey);
|
|
238
|
+
const remoteUsers = new Users(client);
|
|
239
|
+
let fromUsers = await localUsers.list([Query.limit(50)]);
|
|
240
|
+
if (fromUsers.users.length === 0) {
|
|
241
|
+
console.log(`No users found`);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
else if (fromUsers.users.length < 50) {
|
|
245
|
+
console.log(`Transferring ${fromUsers.users.length} users to remote`);
|
|
246
|
+
const batchedPromises = fromUsers.users.map((user) => {
|
|
247
|
+
return tryAwaitWithRetry(async () => {
|
|
248
|
+
const toCreateObject = {
|
|
249
|
+
...user,
|
|
250
|
+
};
|
|
251
|
+
delete toCreateObject.$id;
|
|
252
|
+
delete toCreateObject.$createdAt;
|
|
253
|
+
delete toCreateObject.$updatedAt;
|
|
254
|
+
await remoteUsers.create(user.$id, user.email, user.phone, user.password, user.name);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
await Promise.all(batchedPromises);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
while (fromUsers.users.length === 50) {
|
|
261
|
+
fromUsers = await localUsers.list([
|
|
262
|
+
Query.limit(50),
|
|
263
|
+
Query.cursorAfter(fromUsers.users[fromUsers.users.length - 1].$id),
|
|
264
|
+
]);
|
|
265
|
+
const batchedPromises = fromUsers.users.map((user) => {
|
|
266
|
+
return tryAwaitWithRetry(async () => {
|
|
267
|
+
const toCreateObject = {
|
|
268
|
+
...user,
|
|
269
|
+
};
|
|
270
|
+
delete toCreateObject.$id;
|
|
271
|
+
delete toCreateObject.$createdAt;
|
|
272
|
+
delete toCreateObject.$updatedAt;
|
|
273
|
+
await remoteUsers.create(user.$id, user.email, user.phone, user.password, user.name);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
await Promise.all(batchedPromises);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
};
|
|
235
280
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Models } from "node-appwrite";
|
|
1
|
+
import { Client, type Models } from "node-appwrite";
|
|
2
2
|
import type { CollectionImportData } from "../migrations/dataLoader.js";
|
|
3
3
|
import type { ConfigCollection } from "appwrite-utils";
|
|
4
4
|
export declare const toPascalCase: (str: string) => string;
|
|
@@ -44,3 +44,4 @@ export declare let numTimesFailedTotal: number;
|
|
|
44
44
|
* @return {Promise<any>} - A promise that resolves to the result of the createFunction or rejects with an error if it fails after 5 attempts.
|
|
45
45
|
*/
|
|
46
46
|
export declare const tryAwaitWithRetry: <T>(createFunction: () => Promise<T>, attemptNum?: number, throwError?: boolean) => Promise<T>;
|
|
47
|
+
export declare const getAppwriteClient: (endpoint: string, projectId: string, apiKey: string) => Client;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AppwriteException } from "node-appwrite";
|
|
1
|
+
import { AppwriteException, Client, } from "node-appwrite";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
export const toPascalCase = (str) => {
|
|
@@ -109,3 +109,9 @@ export const tryAwaitWithRetry = async (createFunction, attemptNum = 0, throwErr
|
|
|
109
109
|
return Promise.resolve();
|
|
110
110
|
}
|
|
111
111
|
};
|
|
112
|
+
export const getAppwriteClient = (endpoint, projectId, apiKey) => {
|
|
113
|
+
return new Client()
|
|
114
|
+
.setEndpoint(endpoint)
|
|
115
|
+
.setProject(projectId)
|
|
116
|
+
.setKey(apiKey);
|
|
117
|
+
};
|
|
@@ -16,6 +16,17 @@ export interface SetupOptions {
|
|
|
16
16
|
endpoint?: string;
|
|
17
17
|
project?: string;
|
|
18
18
|
key?: string;
|
|
19
|
+
transfer?: boolean;
|
|
20
|
+
transferEndpoint?: string;
|
|
21
|
+
transferProject?: string;
|
|
22
|
+
transferKey?: string;
|
|
23
|
+
fromDbId?: string;
|
|
24
|
+
targetDbId?: string;
|
|
25
|
+
fromCollection?: string;
|
|
26
|
+
collection?: string;
|
|
27
|
+
transferUsers?: boolean;
|
|
28
|
+
fromBucket?: string;
|
|
29
|
+
targetBucket?: string;
|
|
19
30
|
}
|
|
20
31
|
export declare class UtilsController {
|
|
21
32
|
private appwriteFolderPath;
|
package/dist/utilsController.js
CHANGED
|
@@ -12,6 +12,10 @@ import _ from "lodash";
|
|
|
12
12
|
import { AppwriteToX } from "./migrations/appwriteToX.js";
|
|
13
13
|
import { loadConfig as loadTsConfig } from "./utils/loadConfigs.js";
|
|
14
14
|
import { findAppwriteConfig } from "./utils/loadConfigs.js";
|
|
15
|
+
import { transferDocumentsBetweenDbsLocalToLocal, transferDocumentsBetweenDbsLocalToRemote, } from "./migrations/collections.js";
|
|
16
|
+
import { UsersController } from "./migrations/users.js";
|
|
17
|
+
import { transferDatabaseLocalToLocal, transferDatabaseLocalToRemote, } from "./migrations/databases.js";
|
|
18
|
+
import { transferStorageLocalToLocal, transferStorageLocalToRemote, } from "./migrations/storage.js";
|
|
15
19
|
export class UtilsController {
|
|
16
20
|
appwriteFolderPath;
|
|
17
21
|
appwriteConfigPath;
|
|
@@ -96,6 +100,56 @@ export class UtilsController {
|
|
|
96
100
|
if (!this.database || !this.storage || !this.config) {
|
|
97
101
|
throw new Error("Database or storage not initialized");
|
|
98
102
|
}
|
|
103
|
+
if (options.transfer) {
|
|
104
|
+
if (options.fromCollection) {
|
|
105
|
+
if (options.transferEndpoint &&
|
|
106
|
+
options.transferProject &&
|
|
107
|
+
options.transferKey) {
|
|
108
|
+
if (options.transferUsers) {
|
|
109
|
+
console.log(`Transferring users from local database ${options.fromDbId} to remote database ${options.targetDbId} on endpoint ${options.transferEndpoint}...`);
|
|
110
|
+
const usersController = new UsersController(this.config, this.database);
|
|
111
|
+
await usersController.transferUsersBetweenDbsLocalToRemote(options.transferEndpoint, options.transferProject, options.transferKey);
|
|
112
|
+
}
|
|
113
|
+
console.log("Transferring documents to remote database...");
|
|
114
|
+
await transferDocumentsBetweenDbsLocalToRemote(this.database, options.transferEndpoint, options.transferProject, options.transferKey, options.fromDbId, options.targetDbId, options.fromCollection, options.collection);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
console.log("Transferring documents between local databases...");
|
|
118
|
+
await transferDocumentsBetweenDbsLocalToLocal(this.database, options.fromDbId, options.targetDbId, options.fromCollection, options.collection);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else if (options.fromDbId && options.targetDbId) {
|
|
122
|
+
if (options.transferEndpoint &&
|
|
123
|
+
options.transferProject &&
|
|
124
|
+
options.transferKey) {
|
|
125
|
+
if (options.transferUsers) {
|
|
126
|
+
console.log(`Transferring users from local database ${options.fromDbId} to remote database ${options.targetDbId} on endpoint ${options.transferEndpoint}...`);
|
|
127
|
+
const usersController = new UsersController(this.config, this.database);
|
|
128
|
+
await usersController.transferUsersBetweenDbsLocalToRemote(options.transferEndpoint, options.transferProject, options.transferKey);
|
|
129
|
+
}
|
|
130
|
+
console.log(`Transferring databases from local database ${options.fromDbId} to remote database ${options.targetDbId} on endpoint ${options.transferEndpoint}...`);
|
|
131
|
+
await transferDatabaseLocalToRemote(this.database, options.transferEndpoint, options.transferProject, options.transferKey, options.fromDbId, options.targetDbId);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
console.log(`Transferring databases from local database ${options.fromDbId} to local database ${options.targetDbId}`);
|
|
135
|
+
await transferDatabaseLocalToLocal(this.database, options.fromDbId, options.targetDbId);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (options.fromBucket && options.targetBucket) {
|
|
139
|
+
if (options.transferEndpoint &&
|
|
140
|
+
options.transferProject &&
|
|
141
|
+
options.transferKey) {
|
|
142
|
+
console.log(`Transferring files from bucket ${options.fromBucket} to bucket ${options.targetBucket} on endpoint ${options.transferEndpoint}...`);
|
|
143
|
+
await transferStorageLocalToRemote(this.storage, options.transferEndpoint, options.transferProject, options.transferKey, options.fromBucket, options.targetBucket);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
console.log(`Transferring files from bucket ${options.fromBucket} to bucket ${options.targetBucket}...`);
|
|
147
|
+
await transferStorageLocalToLocal(this.storage, options.fromBucket, options.targetBucket);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
console.log("Transfer complete.");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
99
153
|
if (options.sync) {
|
|
100
154
|
console.log("Starting synchronization with server...");
|
|
101
155
|
const appwriteToX = new AppwriteToX(this.config, this.appwriteFolderPath);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appwrite-utils-cli",
|
|
3
3
|
"description": "Appwrite Utility Functions to help with database management, data conversion, data import, migrations, and much more. Meant to be used as a CLI tool, I do not recommend installing this in frontend environments.",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.48",
|
|
5
5
|
"main": "src/main.ts",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@types/inquirer": "^9.0.7",
|
|
36
|
-
"appwrite-utils": "^0.2.
|
|
36
|
+
"appwrite-utils": "^0.2.7",
|
|
37
37
|
"commander": "^12.0.0",
|
|
38
38
|
"inquirer": "^9.2.20",
|
|
39
39
|
"js-yaml": "^4.1.0",
|
package/src/main.ts
CHANGED
|
@@ -9,6 +9,45 @@ program
|
|
|
9
9
|
.option("--endpoint <endpoint>", "Set the Appwrite endpoint", undefined)
|
|
10
10
|
.option("--project <project>", "Set the Appwrite project ID", undefined)
|
|
11
11
|
.option("--key <key>", "Set the Appwrite API key", undefined)
|
|
12
|
+
.option("--transfer", "Transfer documents between databases", false)
|
|
13
|
+
.option("--transfer-users", "Transfer users between local and remote", false)
|
|
14
|
+
.option(
|
|
15
|
+
"--transferendpoint <transferEndpoint>",
|
|
16
|
+
"Set the transfer endpoint for remote transfers",
|
|
17
|
+
undefined
|
|
18
|
+
)
|
|
19
|
+
.option(
|
|
20
|
+
"--transferproject <transferProject>",
|
|
21
|
+
"Set the transfer project ID for remote transfers",
|
|
22
|
+
undefined
|
|
23
|
+
)
|
|
24
|
+
.option(
|
|
25
|
+
"--transferkey <transferKey>",
|
|
26
|
+
"Set the transfer key for remote transfers",
|
|
27
|
+
undefined
|
|
28
|
+
)
|
|
29
|
+
.option("--fromdb <fromDbId>", "Set the source database ID", undefined)
|
|
30
|
+
.option(
|
|
31
|
+
"--targetdb <targetDbId>",
|
|
32
|
+
"Set the destination database ID",
|
|
33
|
+
undefined
|
|
34
|
+
)
|
|
35
|
+
.option(
|
|
36
|
+
"--fromcoll <collectionId>",
|
|
37
|
+
"Set the source collection ID for transfer, only used for transfer",
|
|
38
|
+
undefined
|
|
39
|
+
)
|
|
40
|
+
.option(
|
|
41
|
+
"--targetcoll <collectionId>",
|
|
42
|
+
"Set the collection ID to import data into",
|
|
43
|
+
undefined
|
|
44
|
+
)
|
|
45
|
+
.option("--frombucket <bucketId>", "Set the source bucket ID", undefined)
|
|
46
|
+
.option(
|
|
47
|
+
"--targetbucket <bucketId>",
|
|
48
|
+
"Set the destination bucket ID",
|
|
49
|
+
undefined
|
|
50
|
+
)
|
|
12
51
|
.option("--backup", "Perform a backup before executing the command", false)
|
|
13
52
|
.option("--dev", "Run in development environment", false)
|
|
14
53
|
.option("--prod", "Run in production environment", false)
|
|
@@ -28,6 +67,9 @@ program.on("--help", () => {
|
|
|
28
67
|
console.log(
|
|
29
68
|
" $ npx appwrite-utils-cli appwrite-migrate --sync --endpoint https://appwrite.example.com --project 123456 --key 7890"
|
|
30
69
|
);
|
|
70
|
+
console.log(
|
|
71
|
+
" $ npx appwrite-utils-cli appwrite-migrate --transfer --fromdb fromDbId --targetdb toDbId --transferendpoint https://appwrite.otherserver.com --transferproject yourProjectId --transferkey yourApiKey"
|
|
72
|
+
);
|
|
31
73
|
console.log(
|
|
32
74
|
" $ npx appwrite-utils-cli appwrite-migrate --sync --dev --backup"
|
|
33
75
|
);
|
|
@@ -70,6 +112,17 @@ program.action(async (options) => {
|
|
|
70
112
|
endpoint: options.endpoint,
|
|
71
113
|
project: options.project,
|
|
72
114
|
key: options.key,
|
|
115
|
+
transfer: options.transfer,
|
|
116
|
+
transferEndpoint: options.transferEndpoint,
|
|
117
|
+
transferProject: options.transferProject,
|
|
118
|
+
transferKey: options.transferKey,
|
|
119
|
+
fromDbId: options.fromdb,
|
|
120
|
+
targetDbId: options.targetdb,
|
|
121
|
+
fromCollection: options.fromcoll,
|
|
122
|
+
collection: options.targetcoll, // Add this line
|
|
123
|
+
transferUsers: options.transferUsers,
|
|
124
|
+
fromBucket: options.frombucket,
|
|
125
|
+
targetBucket: options.targetbucket,
|
|
73
126
|
};
|
|
74
127
|
console.log("Running operation...", setupOptions);
|
|
75
128
|
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Client,
|
|
3
|
+
Databases,
|
|
4
|
+
ID,
|
|
5
|
+
Permission,
|
|
6
|
+
Query,
|
|
7
|
+
type Models,
|
|
8
|
+
} from "node-appwrite";
|
|
2
9
|
import type { AppwriteConfig, CollectionCreate } from "appwrite-utils";
|
|
3
10
|
import { nameToIdMapping, processQueue } from "./queue.js";
|
|
4
11
|
import { createUpdateCollectionAttributes } from "./attributes.js";
|
|
@@ -133,7 +140,9 @@ export const wipeDatabase = async (
|
|
|
133
140
|
collectionId: collectionId,
|
|
134
141
|
collectionName: name,
|
|
135
142
|
});
|
|
136
|
-
|
|
143
|
+
tryAwaitWithRetry(
|
|
144
|
+
async () => await database.deleteCollection(databaseId, collectionId)
|
|
145
|
+
); // Try to delete the collection and ignore errors if it doesn't exist or if it's already being deleted
|
|
137
146
|
}
|
|
138
147
|
return collectionsDeleted;
|
|
139
148
|
};
|
|
@@ -201,16 +210,16 @@ export const createOrUpdateCollections = async (
|
|
|
201
210
|
let collectionId: string;
|
|
202
211
|
if (!collectionToUse) {
|
|
203
212
|
console.log(`Creating collection: ${collection.name}`);
|
|
204
|
-
|
|
213
|
+
let foundColl = deletedCollections?.find(
|
|
205
214
|
(coll) =>
|
|
206
215
|
coll.collectionName.toLowerCase().trim().replace(" ", "") ===
|
|
207
216
|
collection.name.toLowerCase().trim().replace(" ", "")
|
|
208
217
|
);
|
|
209
218
|
|
|
210
|
-
if (
|
|
219
|
+
if (collection.$id) {
|
|
220
|
+
collectionId = collection.$id; // Always use the provided $id if present
|
|
221
|
+
} else if (foundColl && !usedIds.has(foundColl.collectionId)) {
|
|
211
222
|
collectionId = foundColl.collectionId; // Use ID from deleted collection if not already used
|
|
212
|
-
} else if (collection.$id && !usedIds.has(collection.$id)) {
|
|
213
|
-
collectionId = collection.$id; // Use the provided $id if not already used
|
|
214
223
|
} else {
|
|
215
224
|
collectionId = ID.unique(); // Generate a new unique ID
|
|
216
225
|
}
|
|
@@ -322,3 +331,214 @@ export const fetchAllCollections = async (
|
|
|
322
331
|
console.log(`Fetched a total of ${collections.length} collections.`);
|
|
323
332
|
return collections;
|
|
324
333
|
};
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Transfers all documents from one collection to another in a different database
|
|
337
|
+
* within the same Appwrite Project
|
|
338
|
+
*/
|
|
339
|
+
export const transferDocumentsBetweenDbsLocalToLocal = async (
|
|
340
|
+
db: Databases,
|
|
341
|
+
fromDbId: string,
|
|
342
|
+
toDbId: string,
|
|
343
|
+
fromCollId: string,
|
|
344
|
+
toCollId: string
|
|
345
|
+
) => {
|
|
346
|
+
let fromCollDocs = await tryAwaitWithRetry(async () =>
|
|
347
|
+
db.listDocuments(fromDbId, fromCollId, [Query.limit(50)])
|
|
348
|
+
);
|
|
349
|
+
let totalDocumentsTransferred = 0;
|
|
350
|
+
|
|
351
|
+
if (fromCollDocs.documents.length === 0) {
|
|
352
|
+
console.log(`No documents found in collection ${fromCollId}`);
|
|
353
|
+
return;
|
|
354
|
+
} else if (fromCollDocs.documents.length < 50) {
|
|
355
|
+
const batchedPromises = fromCollDocs.documents.map((doc) => {
|
|
356
|
+
const toCreateObject: Partial<typeof doc> = {
|
|
357
|
+
...doc,
|
|
358
|
+
};
|
|
359
|
+
delete toCreateObject.$databaseId;
|
|
360
|
+
delete toCreateObject.$collectionId;
|
|
361
|
+
delete toCreateObject.$createdAt;
|
|
362
|
+
delete toCreateObject.$updatedAt;
|
|
363
|
+
delete toCreateObject.$id;
|
|
364
|
+
delete toCreateObject.$permissions;
|
|
365
|
+
return tryAwaitWithRetry(
|
|
366
|
+
async () =>
|
|
367
|
+
await db.createDocument(
|
|
368
|
+
toDbId,
|
|
369
|
+
toCollId,
|
|
370
|
+
doc.$id,
|
|
371
|
+
toCreateObject,
|
|
372
|
+
doc.$permissions
|
|
373
|
+
)
|
|
374
|
+
);
|
|
375
|
+
});
|
|
376
|
+
await Promise.all(batchedPromises);
|
|
377
|
+
totalDocumentsTransferred += fromCollDocs.documents.length;
|
|
378
|
+
} else {
|
|
379
|
+
const batchedPromises = fromCollDocs.documents.map((doc) => {
|
|
380
|
+
const toCreateObject: Partial<typeof doc> = {
|
|
381
|
+
...doc,
|
|
382
|
+
};
|
|
383
|
+
delete toCreateObject.$databaseId;
|
|
384
|
+
delete toCreateObject.$collectionId;
|
|
385
|
+
delete toCreateObject.$createdAt;
|
|
386
|
+
delete toCreateObject.$updatedAt;
|
|
387
|
+
delete toCreateObject.$id;
|
|
388
|
+
delete toCreateObject.$permissions;
|
|
389
|
+
return tryAwaitWithRetry(async () =>
|
|
390
|
+
db.createDocument(
|
|
391
|
+
toDbId,
|
|
392
|
+
toCollId,
|
|
393
|
+
doc.$id,
|
|
394
|
+
toCreateObject,
|
|
395
|
+
doc.$permissions
|
|
396
|
+
)
|
|
397
|
+
);
|
|
398
|
+
});
|
|
399
|
+
await Promise.all(batchedPromises);
|
|
400
|
+
totalDocumentsTransferred += fromCollDocs.documents.length;
|
|
401
|
+
while (fromCollDocs.documents.length === 50) {
|
|
402
|
+
fromCollDocs = await db.listDocuments(fromDbId, fromCollId, [
|
|
403
|
+
Query.limit(50),
|
|
404
|
+
Query.cursorAfter(
|
|
405
|
+
fromCollDocs.documents[fromCollDocs.documents.length - 1].$id
|
|
406
|
+
),
|
|
407
|
+
]);
|
|
408
|
+
const batchedPromises = fromCollDocs.documents.map((doc) => {
|
|
409
|
+
const toCreateObject: Partial<typeof doc> = {
|
|
410
|
+
...doc,
|
|
411
|
+
};
|
|
412
|
+
delete toCreateObject.$databaseId;
|
|
413
|
+
delete toCreateObject.$collectionId;
|
|
414
|
+
delete toCreateObject.$createdAt;
|
|
415
|
+
delete toCreateObject.$updatedAt;
|
|
416
|
+
delete toCreateObject.$id;
|
|
417
|
+
delete toCreateObject.$permissions;
|
|
418
|
+
return tryAwaitWithRetry(
|
|
419
|
+
async () =>
|
|
420
|
+
await db.createDocument(
|
|
421
|
+
toDbId,
|
|
422
|
+
toCollId,
|
|
423
|
+
doc.$id,
|
|
424
|
+
toCreateObject,
|
|
425
|
+
doc.$permissions
|
|
426
|
+
)
|
|
427
|
+
);
|
|
428
|
+
});
|
|
429
|
+
await Promise.all(batchedPromises);
|
|
430
|
+
totalDocumentsTransferred += fromCollDocs.documents.length;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
console.log(
|
|
435
|
+
`Transferred ${totalDocumentsTransferred} documents from database ${fromDbId} to database ${toDbId} -- collection ${fromCollId} to collection ${toCollId}`
|
|
436
|
+
);
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
export const transferDocumentsBetweenDbsLocalToRemote = async (
|
|
440
|
+
localDb: Databases,
|
|
441
|
+
endpoint: string,
|
|
442
|
+
projectId: string,
|
|
443
|
+
apiKey: string,
|
|
444
|
+
fromDbId: string,
|
|
445
|
+
toDbId: string,
|
|
446
|
+
fromCollId: string,
|
|
447
|
+
toCollId: string
|
|
448
|
+
) => {
|
|
449
|
+
const client = new Client()
|
|
450
|
+
.setEndpoint(endpoint)
|
|
451
|
+
.setProject(projectId)
|
|
452
|
+
.setKey(apiKey);
|
|
453
|
+
let totalDocumentsTransferred = 0;
|
|
454
|
+
const remoteDb = new Databases(client);
|
|
455
|
+
let fromCollDocs = await tryAwaitWithRetry(async () =>
|
|
456
|
+
localDb.listDocuments(fromDbId, fromCollId, [Query.limit(50)])
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
if (fromCollDocs.documents.length === 0) {
|
|
460
|
+
console.log(`No documents found in collection ${fromCollId}`);
|
|
461
|
+
return;
|
|
462
|
+
} else if (fromCollDocs.documents.length < 50) {
|
|
463
|
+
const batchedPromises = fromCollDocs.documents.map((doc) => {
|
|
464
|
+
const toCreateObject: Partial<typeof doc> = {
|
|
465
|
+
...doc,
|
|
466
|
+
};
|
|
467
|
+
delete toCreateObject.$databaseId;
|
|
468
|
+
delete toCreateObject.$collectionId;
|
|
469
|
+
delete toCreateObject.$createdAt;
|
|
470
|
+
delete toCreateObject.$updatedAt;
|
|
471
|
+
delete toCreateObject.$id;
|
|
472
|
+
delete toCreateObject.$permissions;
|
|
473
|
+
return tryAwaitWithRetry(async () =>
|
|
474
|
+
remoteDb.createDocument(
|
|
475
|
+
toDbId,
|
|
476
|
+
toCollId,
|
|
477
|
+
doc.$id,
|
|
478
|
+
toCreateObject,
|
|
479
|
+
doc.$permissions
|
|
480
|
+
)
|
|
481
|
+
);
|
|
482
|
+
});
|
|
483
|
+
await Promise.all(batchedPromises);
|
|
484
|
+
totalDocumentsTransferred += fromCollDocs.documents.length;
|
|
485
|
+
} else {
|
|
486
|
+
const batchedPromises = fromCollDocs.documents.map((doc) => {
|
|
487
|
+
const toCreateObject: Partial<typeof doc> = {
|
|
488
|
+
...doc,
|
|
489
|
+
};
|
|
490
|
+
delete toCreateObject.$databaseId;
|
|
491
|
+
delete toCreateObject.$collectionId;
|
|
492
|
+
delete toCreateObject.$createdAt;
|
|
493
|
+
delete toCreateObject.$updatedAt;
|
|
494
|
+
delete toCreateObject.$id;
|
|
495
|
+
delete toCreateObject.$permissions;
|
|
496
|
+
return tryAwaitWithRetry(async () =>
|
|
497
|
+
remoteDb.createDocument(
|
|
498
|
+
toDbId,
|
|
499
|
+
toCollId,
|
|
500
|
+
doc.$id,
|
|
501
|
+
toCreateObject,
|
|
502
|
+
doc.$permissions
|
|
503
|
+
)
|
|
504
|
+
);
|
|
505
|
+
});
|
|
506
|
+
await Promise.all(batchedPromises);
|
|
507
|
+
totalDocumentsTransferred += fromCollDocs.documents.length;
|
|
508
|
+
while (fromCollDocs.documents.length === 50) {
|
|
509
|
+
fromCollDocs = await tryAwaitWithRetry(async () =>
|
|
510
|
+
localDb.listDocuments(fromDbId, fromCollId, [
|
|
511
|
+
Query.limit(50),
|
|
512
|
+
Query.cursorAfter(
|
|
513
|
+
fromCollDocs.documents[fromCollDocs.documents.length - 1].$id
|
|
514
|
+
),
|
|
515
|
+
])
|
|
516
|
+
);
|
|
517
|
+
const batchedPromises = fromCollDocs.documents.map((doc) => {
|
|
518
|
+
const toCreateObject: Partial<typeof doc> = {
|
|
519
|
+
...doc,
|
|
520
|
+
};
|
|
521
|
+
delete toCreateObject.$databaseId;
|
|
522
|
+
delete toCreateObject.$collectionId;
|
|
523
|
+
delete toCreateObject.$createdAt;
|
|
524
|
+
delete toCreateObject.$updatedAt;
|
|
525
|
+
delete toCreateObject.$id;
|
|
526
|
+
delete toCreateObject.$permissions;
|
|
527
|
+
return tryAwaitWithRetry(async () =>
|
|
528
|
+
remoteDb.createDocument(
|
|
529
|
+
toDbId,
|
|
530
|
+
toCollId,
|
|
531
|
+
doc.$id,
|
|
532
|
+
toCreateObject,
|
|
533
|
+
doc.$permissions
|
|
534
|
+
)
|
|
535
|
+
);
|
|
536
|
+
});
|
|
537
|
+
await Promise.all(batchedPromises);
|
|
538
|
+
totalDocumentsTransferred += fromCollDocs.documents.length;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
console.log(
|
|
542
|
+
`Total documents transferred from database ${fromDbId} to database ${toDbId} -- collection ${fromCollId} to collection ${toCollId}: ${totalDocumentsTransferred}`
|
|
543
|
+
);
|
|
544
|
+
};
|