From 899c6ab0e05442f02c60140bb17f355a4ee6d575 Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Wed, 4 May 2022 11:07:00 -0700 Subject: [PATCH] Add script to denormalize avatars into other docs (#127) * Add script to denormalize avatars into contracts/comments * Also handle denormalizing answer avatar URLs * Small fixups --- .../src/scripts/denormalize-avatar-urls.ts | 125 ++++++++++++++++++ functions/src/scripts/denormalize.ts | 48 +++++++ 2 files changed, 173 insertions(+) create mode 100644 functions/src/scripts/denormalize-avatar-urls.ts create mode 100644 functions/src/scripts/denormalize.ts diff --git a/functions/src/scripts/denormalize-avatar-urls.ts b/functions/src/scripts/denormalize-avatar-urls.ts new file mode 100644 index 00000000..23b7dfc9 --- /dev/null +++ b/functions/src/scripts/denormalize-avatar-urls.ts @@ -0,0 +1,125 @@ +// Script for lining up users and contracts/comments to make sure the denormalized avatar URLs in the contracts and +// comments match the user avatar URLs. + +import * as admin from 'firebase-admin' +import { initAdmin } from './script-init' +import { + DocumentCorrespondence, + findDiffs, + describeDiff, + applyDiff, +} from './denormalize' +import { DocumentSnapshot, Transaction } from 'firebase-admin/firestore' + +initAdmin() +const firestore = admin.firestore() + +async function getUsersById(transaction: Transaction) { + const results = new Map() + const users = await transaction.get(firestore.collection('users')) + users.forEach((doc) => { + results.set(doc.get('id'), doc) + }) + console.log(`Found ${results.size} unique users.`) + return results +} + +async function getContractsByUserId(transaction: Transaction) { + let n = 0 + const results = new Map() + const contracts = await transaction.get(firestore.collection('contracts')) + contracts.forEach((doc) => { + const creatorId = doc.get('creatorId') + const creatorContracts = results.get(creatorId) || [] + creatorContracts.push(doc) + results.set(creatorId, creatorContracts) + n++ + }) + console.log(`Found ${n} contracts from ${results.size} unique users.`) + return results +} + +async function getCommentsByUserId(transaction: Transaction) { + let n = 0 + const results = new Map() + const comments = await transaction.get(firestore.collectionGroup('comments')) + comments.forEach((doc) => { + const userId = doc.get('userId') + const userComments = results.get(userId) || [] + userComments.push(doc) + results.set(userId, userComments) + n++ + }) + console.log(`Found ${n} comments from ${results.size} unique users.`) + return results +} + +async function getAnswersByUserId(transaction: Transaction) { + let n = 0 + const results = new Map() + const answers = await transaction.get(firestore.collectionGroup('answers')) + answers.forEach((doc) => { + const userId = doc.get('userId') + const userAnswers = results.get(userId) || [] + userAnswers.push(doc) + results.set(userId, userAnswers) + n++ + }) + console.log(`Found ${n} answers from ${results.size} unique users.`) + return results +} + +if (require.main === module) { + admin.firestore().runTransaction(async (transaction) => { + const [usersById, contractsByUserId, commentsByUserId, answersByUserId] = + await Promise.all([ + getUsersById(transaction), + getContractsByUserId(transaction), + getCommentsByUserId(transaction), + getAnswersByUserId(transaction), + ]) + + const usersContracts = Array.from( + usersById.entries(), + ([id, doc]): DocumentCorrespondence => { + return [doc, contractsByUserId.get(id) || []] + } + ) + const contractDiffs = findDiffs( + usersContracts, + 'avatarUrl', + 'creatorAvatarUrl' + ) + console.log(`Found ${contractDiffs.length} contracts with mismatches.`) + contractDiffs.forEach((d) => { + console.log(describeDiff(d)) + applyDiff(transaction, d) + }) + + const usersComments = Array.from( + usersById.entries(), + ([id, doc]): DocumentCorrespondence => { + return [doc, commentsByUserId.get(id) || []] + } + ) + const commentDiffs = findDiffs(usersComments, 'avatarUrl', 'userAvatarUrl') + console.log(`Found ${commentDiffs.length} comments with mismatches.`) + commentDiffs.forEach((d) => { + console.log(describeDiff(d)) + applyDiff(transaction, d) + }) + + const usersAnswers = Array.from( + usersById.entries(), + ([id, doc]): DocumentCorrespondence => { + return [doc, answersByUserId.get(id) || []] + } + ) + const answerDiffs = findDiffs(usersAnswers, 'avatarUrl', 'avatarUrl') + console.log(`Found ${answerDiffs.length} answers with mismatches.`) + answerDiffs.forEach((d) => { + console.log(describeDiff(d)) + applyDiff(transaction, d) + }) + }) +} diff --git a/functions/src/scripts/denormalize.ts b/functions/src/scripts/denormalize.ts new file mode 100644 index 00000000..ca4111b0 --- /dev/null +++ b/functions/src/scripts/denormalize.ts @@ -0,0 +1,48 @@ +// Helper functions for maintaining the relationship between fields in one set of documents and denormalized copies in +// another set of documents. + +import { DocumentSnapshot, Transaction } from 'firebase-admin/firestore' + +export type DocumentValue = { + doc: DocumentSnapshot + field: string + val: any +} +export type DocumentCorrespondence = [DocumentSnapshot, DocumentSnapshot[]] +export type DocumentDiff = { + src: DocumentValue + dest: DocumentValue +} + +export function findDiffs( + docs: DocumentCorrespondence[], + srcPath: string, + destPath: string +) { + const diffs: DocumentDiff[] = [] + for (let [srcDoc, destDocs] of docs) { + const srcVal = srcDoc.get(srcPath) + for (let destDoc of destDocs) { + const destVal = destDoc.get(destPath) + if (destVal !== srcVal) { + diffs.push({ + src: { doc: srcDoc, field: srcPath, val: srcVal }, + dest: { doc: destDoc, field: destPath, val: destVal }, + }) + } + } + } + return diffs +} + +export function describeDiff(diff: DocumentDiff) { + function describeDocVal(x: DocumentValue): string { + return `${x.doc.ref.path}.${x.field}: ${x.val}` + } + return `${describeDocVal(diff.src)} -> ${describeDocVal(diff.dest)}` +} + +export function applyDiff(transaction: Transaction, diff: DocumentDiff) { + const { src, dest } = diff + transaction.update(dest.doc.ref, dest.field, src.val) +}