manifold/functions/src/change-user-info.ts
Marshall Polaris 7144e57c93
Denormalize user display fields onto bets (#853)
* Denormalize user display fields onto bets

* Make bet denormalization script fast enough to run it on prod

* Make `placeBet`/`sellShares` immediately post denormalized info
2022-09-14 01:33:59 -07:00

117 lines
3.5 KiB
TypeScript

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<Contract> = removeUndefinedProps({
creatorName: update.name,
creatorUsername: update.username,
creatorAvatarUrl: update.avatarUrl,
})
const commentSnap = await firestore
.collectionGroup('comments')
.where('userUsername', '==', user.username)
.get()
const commentUpdate: Partial<Comment> = removeUndefinedProps({
userName: update.name,
userUsername: update.username,
userAvatarUrl: update.avatarUrl,
})
const answerSnap = await firestore
.collectionGroup('answers')
.where('username', '==', user.username)
.get()
const answerUpdate: Partial<Answer> = removeUndefinedProps(update)
const betsSnap = await firestore
.collectionGroup('bets')
.where('userId', '==', user.id)
.get()
const betsUpdate: Partial<Bet> = 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<User> = removeUndefinedProps(update)
transaction.update(userRef, userUpdate)
})
}
const firestore = admin.firestore()