appwrite-utils-cli
Version:
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.
278 lines (277 loc) • 11.8 kB
JavaScript
import { AppwriteException, Databases, ID, Query, Users, } from "node-appwrite";
import { AuthUserSchema, } from "../schemas/authUser.js";
import { logger } from "../shared/logging.js";
import { splitIntoBatches } from "../shared/migrationHelpers.js";
import { getAppwriteClient, tryAwaitWithRetry, } from "../utils/helperFunctions.js";
import { isUndefined } from "es-toolkit/compat";
import { isEmpty } from "es-toolkit/compat";
import { MessageFormatter } from "../shared/messageFormatter.js";
export class UsersController {
config;
users;
static userFields = [
"email",
"name",
"password",
"phone",
"labels",
"prefs",
"userId",
"$createdAt",
"$updatedAt",
];
constructor(config, db) {
this.config = config;
this.users = new Users(this.config.appwriteClient);
}
async wipeUsers() {
const allUsers = await this.getAllUsers();
MessageFormatter.progress("Deleting all users...", { prefix: "Users" });
const createBatches = (finalData, batchSize) => {
const finalBatches = [];
for (let i = 0; i < finalData.length; i += batchSize) {
finalBatches.push(finalData.slice(i, i + batchSize));
}
return finalBatches;
};
let usersDeleted = 0;
if (allUsers.length > 0) {
const batchedUserPromises = createBatches(allUsers, 25); // Batch size of 25
for (const batch of batchedUserPromises) {
MessageFormatter.progress(`Deleting ${batch.length} users...`, { prefix: "Users" });
await Promise.all(batch.map((user) => tryAwaitWithRetry(async () => await this.users.delete(user.$id))));
usersDeleted += batch.length;
if (usersDeleted % 100 === 0) {
MessageFormatter.progress(`Deleted ${usersDeleted} users...`, { prefix: "Users" });
}
}
}
else {
MessageFormatter.info("No users to delete", { prefix: "Users" });
}
}
async getAllUsers() {
const allUsers = [];
const users = await tryAwaitWithRetry(async () => await this.users.list([Query.limit(200)]));
if (users.users.length === 0) {
return [];
}
if (users.users.length === 200) {
let lastDocumentId = users.users[users.users.length - 1].$id;
allUsers.push(...users.users);
while (lastDocumentId) {
const moreUsers = await tryAwaitWithRetry(async () => await this.users.list([
Query.limit(200),
Query.cursorAfter(lastDocumentId),
]));
allUsers.push(...moreUsers.users);
if (moreUsers.users.length < 200) {
break;
}
lastDocumentId = moreUsers.users[moreUsers.users.length - 1].$id;
}
}
else {
allUsers.push(...users.users);
}
return allUsers;
}
async createUsersAndReturn(items) {
const users = await Promise.all(items.map((item) => this.createUserAndReturn(item)));
return users;
}
async createUserAndReturn(item) {
try {
const user = await tryAwaitWithRetry(async () => {
const createdUser = await this.users.create(item.userId || ID.unique(), item.email || undefined, item.phone && item.phone.length < 15 && item.phone.startsWith("+")
? item.phone
: undefined, `changeMe${item.email?.toLowerCase()}` || `changeMePlease`, item.name || undefined);
if (item.labels) {
await this.users.updateLabels(createdUser.$id, item.labels);
}
if (item.prefs) {
await this.users.updatePrefs(createdUser.$id, item.prefs);
}
return createdUser;
}); // Set throwError to true since we want to handle errors
return user;
}
catch (e) {
if (e instanceof Error) {
logger.error("FAILED CREATING USER: ", e.message, item);
}
}
}
async createAndCheckForUserAndReturn(item) {
let userToReturn = undefined;
try {
// Attempt to find an existing user by email or phone.
let foundUsers = [];
if (item.email) {
const foundUsersByEmail = await this.users.list([
Query.equal("email", item.email),
]);
foundUsers = foundUsersByEmail.users;
}
if (item.phone) {
const foundUsersByPhone = await this.users.list([
Query.equal("phone", item.phone),
]);
foundUsers = foundUsers.length
? foundUsers.concat(foundUsersByPhone.users)
: foundUsersByPhone.users;
}
userToReturn = foundUsers[0] || undefined;
if (!userToReturn) {
userToReturn = await this.users.create(item.userId || ID.unique(), item.email || undefined, item.phone && item.phone.length < 15 && item.phone.startsWith("+")
? item.phone
: undefined, item.password?.toLowerCase() ||
`changeMe${item.email?.toLowerCase()}` ||
`changeMePlease`, item.name || undefined);
}
else {
// Update user details as necessary, ensuring email uniqueness if attempting an update.
if (item.email &&
item.email !== userToReturn.email &&
!isEmpty(item.email) &&
!isUndefined(item.email)) {
const emailExists = await this.users.list([
Query.equal("email", item.email),
]);
if (emailExists.users.length === 0) {
userToReturn = await this.users.updateEmail(userToReturn.$id, item.email);
}
else {
MessageFormatter.warning("Email update skipped: Email already exists.", { prefix: "Users" });
}
}
if (item.password) {
userToReturn = await this.users.updatePassword(userToReturn.$id, item.password.toLowerCase());
}
if (item.name && item.name !== userToReturn.name) {
userToReturn = await this.users.updateName(userToReturn.$id, item.name);
}
if (item.phone &&
item.phone !== userToReturn.phone &&
item.phone.length < 15 &&
item.phone.startsWith("+") &&
(isUndefined(userToReturn.phone) || isEmpty(userToReturn.phone))) {
const userFoundWithPhone = await this.users.list([
Query.equal("phone", item.phone),
]);
if (userFoundWithPhone.total === 0) {
userToReturn = await this.users.updatePhone(userToReturn.$id, item.phone);
}
}
}
if (item.$createdAt && item.$updatedAt) {
MessageFormatter.warning("$createdAt and $updatedAt are not yet supported, sorry about that!", { prefix: "Users" });
}
if (item.labels && item.labels.length) {
userToReturn = await this.users.updateLabels(userToReturn.$id, item.labels);
}
if (item.prefs && Object.keys(item.prefs).length) {
await this.users.updatePrefs(userToReturn.$id, item.prefs);
userToReturn.prefs = item.prefs;
}
return userToReturn;
}
catch (error) {
return userToReturn;
}
}
async getUserIdByEmailOrPhone(email, phone) {
if (!email && !phone) {
return undefined;
}
if (email && phone) {
const foundUsersByEmail = await this.users.list([
// @ts-ignore
Query.or([Query.equal("email", email), Query.equal("phone", phone)]),
]);
if (foundUsersByEmail.users.length > 0) {
return foundUsersByEmail.users[0]?.$id;
}
}
else if (email) {
const foundUsersByEmail = await this.users.list([
Query.equal("email", email),
]);
if (foundUsersByEmail.users.length > 0) {
return foundUsersByEmail.users[0]?.$id;
}
else {
if (!phone) {
return undefined;
}
else {
const foundUsersByPhone = await this.users.list([
Query.equal("phone", phone),
]);
if (foundUsersByPhone.users.length > 0) {
return foundUsersByPhone.users[0]?.$id;
}
else {
return undefined;
}
}
}
}
if (phone) {
const foundUsersByPhone = await this.users.list([
Query.equal("phone", phone),
]);
if (foundUsersByPhone.users.length > 0) {
return foundUsersByPhone.users[0]?.$id;
}
else {
return undefined;
}
}
}
transferUsersBetweenDbsLocalToRemote = async (endpoint, projectId, apiKey) => {
const localUsers = this.users;
const client = getAppwriteClient(endpoint, projectId, apiKey);
const remoteUsers = new Users(client);
let fromUsers = await localUsers.list([Query.limit(50)]);
if (fromUsers.users.length === 0) {
MessageFormatter.info("No users found", { prefix: "Users" });
return;
}
else if (fromUsers.users.length < 50) {
MessageFormatter.progress(`Transferring ${fromUsers.users.length} users to remote`, { prefix: "Users" });
const batchedPromises = fromUsers.users.map((user) => {
return tryAwaitWithRetry(async () => {
const toCreateObject = {
...user,
};
delete toCreateObject.$id;
delete toCreateObject.$createdAt;
delete toCreateObject.$updatedAt;
await remoteUsers.create(user.$id, user.email, user.phone, user.password, user.name);
});
});
await Promise.all(batchedPromises);
}
else {
while (fromUsers.users.length === 50) {
fromUsers = await localUsers.list([
Query.limit(50),
Query.cursorAfter(fromUsers.users[fromUsers.users.length - 1].$id),
]);
const batchedPromises = fromUsers.users.map((user) => {
return tryAwaitWithRetry(async () => {
const toCreateObject = {
...user,
};
delete toCreateObject.$id;
delete toCreateObject.$createdAt;
delete toCreateObject.$updatedAt;
await remoteUsers.create(user.$id, user.email, user.phone, user.password, user.name);
});
});
await Promise.all(batchedPromises);
}
}
};
}