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.
360 lines (340 loc) • 11.5 kB
text/typescript
import type { AppwriteConfig, ConfigCollection } from "appwrite-utils";
import {
AppwriteException,
Databases,
ID,
Query,
Users,
type Models,
} from "node-appwrite";
import {
AuthUserSchema,
type AuthUser,
type AuthUserCreate,
} 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 {
private config: AppwriteConfig;
private users: Users;
static userFields = [
"email",
"name",
"password",
"phone",
"labels",
"prefs",
"userId",
"$createdAt",
"$updatedAt",
];
constructor(config: AppwriteConfig, db: Databases) {
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: any[], batchSize: number) => {
const finalBatches: any[][] = [];
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: Models.User<Models.Preferences>[] = [];
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: AuthUserCreate[]): Promise<any[]> {
const users = await Promise.all(
items.map((item) => this.createUserAndReturn(item))
);
return users;
}
async createUserAndReturn(item: AuthUserCreate): Promise<any> {
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: AuthUserCreate) {
let userToReturn: Models.User<Models.Preferences> | undefined = undefined;
try {
// Attempt to find an existing user by email or phone.
let foundUsers: Models.User<Models.Preferences>[] = [];
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?: string, phone?: string) {
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: string,
projectId: string,
apiKey: string
) => {
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: Partial<typeof user> = {
...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: Partial<typeof user> = {
...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);
}
}
};
}