import * as admin from 'firebase-admin' import { z } from 'zod' import { getUser } from './utils' import { Bet } from '../../common/bet' import { Contract } from '../../common/contract' import { Comment } from '../../common/comment' import { User } from '../../common/user' import { cleanUsername, cleanDisplayName, } from '../../common/util/clean-username' import { removeUndefinedProps } from '../../common/util/object' import { Answer } from '../../common/answer' import { APIError, newEndpoint, validate } from './api' const bodySchema = z.object({ username: z.string().optional(), name: z.string().optional(), avatarUrl: z.string().optional(), }) export const changeuserinfo = newEndpoint({}, async (req, auth) => { const { username, name, avatarUrl } = validate(bodySchema, req.body) const user = await getUser(auth.uid) if (!user) throw new APIError(400, 'User not found') await changeUser(user, { username, name, avatarUrl }) return { message: 'Successfully changed user info.' } }) export const changeUser = async ( user: User, update: { username?: string name?: string avatarUrl?: string } ) => { // Update contracts, comments, and answers outside of a transaction to avoid contention. // Using bulkWriter to supports >500 writes at a time const contractsRef = firestore .collection('contracts') .where('creatorId', '==', user.id) const contracts = await contractsRef.get() const contractUpdate: Partial = removeUndefinedProps({ creatorName: update.name, creatorUsername: update.username, creatorAvatarUrl: update.avatarUrl, }) const commentSnap = await firestore .collectionGroup('comments') .where('userUsername', '==', user.username) .get() const commentUpdate: Partial = removeUndefinedProps({ userName: update.name, userUsername: update.username, userAvatarUrl: update.avatarUrl, }) const answerSnap = await firestore .collectionGroup('answers') .where('username', '==', user.username) .get() const answerUpdate: Partial = removeUndefinedProps(update) const betsSnap = await firestore .collectionGroup('bets') .where('userId', '==', user.id) .get() const betsUpdate: Partial = removeUndefinedProps({ userName: update.name, userUsername: update.username, userAvatarUrl: update.avatarUrl, }) const bulkWriter = firestore.bulkWriter() commentSnap.docs.forEach((d) => bulkWriter.update(d.ref, commentUpdate)) answerSnap.docs.forEach((d) => bulkWriter.update(d.ref, answerUpdate)) contracts.docs.forEach((d) => bulkWriter.update(d.ref, contractUpdate)) betsSnap.docs.forEach((d) => bulkWriter.update(d.ref, betsUpdate)) await bulkWriter.flush() console.log('Done writing!') // Update the username inside a transaction return await firestore.runTransaction(async (transaction) => { if (update.username) { update.username = cleanUsername(update.username) if (!update.username) { throw new APIError(400, 'Invalid username') } const sameNameUser = await transaction.get( firestore.collection('users').where('username', '==', update.username) ) if (!sameNameUser.empty) { throw new APIError(400, 'Username already exists') } } if (update.name) { update.name = cleanDisplayName(update.name) } const userRef = firestore.collection('users').doc(user.id) const userUpdate: Partial = removeUndefinedProps(update) transaction.update(userRef, userUpdate) }) } const firestore = admin.firestore()