UNPKG

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
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); } } }; }