Skip to content

Commit d19b4ab

Browse files
committed
refactor: Makes profiles collection the canon source for notificationFrequency and nextDigestAt. Moves updates to these fields to point to the profiles collection. Deprecates old scripts that reference these fields in users. Includes backfill script for nextDigestAt that will need to be run to fully enable the email notification sending flow with these changes.
1 parent eb8e5d4 commit d19b4ab

File tree

7 files changed

+80
-26
lines changed

7 files changed

+80
-26
lines changed

components/db/profile/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const SOCIAL_NETWORKS = [
2323
] as const
2424

2525
export type SocialLinks = Partial<
26-
Record<(typeof SOCIAL_NETWORKS)[number], string>
26+
Record<typeof SOCIAL_NETWORKS[number], string>
2727
>
2828

2929
export type Profile = {
@@ -35,6 +35,7 @@ export type Profile = {
3535
senator?: ProfileMember
3636
public?: boolean
3737
notificationFrequency?: Frequency
38+
nextDigestAt?: FirebaseFirestore.Timestamp
3839
about?: string
3940
social?: SocialLinks
4041
profileImage?: string

functions/src/notifications/deliverNotifications.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as fs from "fs"
55
import { Timestamp } from "../firebase"
66
import { getNextDigestAt, getNotificationStartDate } from "./helpers"
77
import { startOfDay } from "date-fns"
8-
import { TestimonySubmissionNotificationFields, User } from "./types"
8+
import { TestimonySubmissionNotificationFields } from "./types"
99
import {
1010
BillDigest,
1111
NotificationEmailDigest,
@@ -15,6 +15,7 @@ import {
1515
import { prepareHandlebars } from "../email/handlebarsHelpers"
1616
import { getAuth } from "firebase-admin/auth"
1717
import { Frequency } from "../auth/types"
18+
import { Profile } from "../../../components/db/profile/types"
1819

1920
const NUM_BILLS_TO_DISPLAY = 4
2021
const NUM_USERS_TO_DISPLAY = 4
@@ -44,28 +45,30 @@ const deliverEmailNotifications = async () => {
4445
prepareHandlebars()
4546
console.log("Handlebars helpers and partials prepared")
4647

47-
const usersSnapshot = await db
48-
.collection("users")
48+
const profilesSnapshot = await db
49+
.collection("profiles")
4950
.where("nextDigestAt", "<=", now)
5051
.get()
5152

52-
const emailPromises = usersSnapshot.docs.map(async userDoc => {
53-
const user = userDoc.data() as User
53+
const emailPromises = profilesSnapshot.docs.map(async profileDoc => {
54+
const user = profileDoc.data() as Profile
5455
if (!user || !user.notificationFrequency) {
55-
console.log(`User ${userDoc.id} has no notificationFrequency - skipping`)
56+
console.log(
57+
`User ${profileDoc.id} has no notificationFrequency - skipping`
58+
)
5659
return
5760
}
5861

59-
const verifiedEmail = await getVerifiedUserEmail(userDoc.id)
62+
const verifiedEmail = await getVerifiedUserEmail(profileDoc.id)
6063
if (!verifiedEmail) {
6164
console.log(
62-
`Skipping user ${userDoc.id} because they have no verified email address`
65+
`Skipping user ${profileDoc.id} because they have no verified email address`
6366
)
6467
return
6568
}
6669

6770
const digestData = await buildDigestData(
68-
userDoc.id,
71+
profileDoc.id,
6972
now,
7073
user.notificationFrequency
7174
)
@@ -75,7 +78,9 @@ const deliverEmailNotifications = async () => {
7578
digestData.numBillsWithNewTestimony === 0 &&
7679
digestData.numUsersWithNewTestimony === 0
7780
) {
78-
console.log(`No new notifications for ${userDoc.id} - not sending email`)
81+
console.log(
82+
`No new notifications for ${profileDoc.id} - not sending email`
83+
)
7984
} else {
8085
const htmlString = renderToHtmlString(digestData)
8186

@@ -90,13 +95,13 @@ const deliverEmailNotifications = async () => {
9095
createdAt: Timestamp.now()
9196
})
9297

93-
console.log(`Saved email message to user ${userDoc.id}`)
98+
console.log(`Saved email message to user ${profileDoc.id}`)
9499
}
95100

96101
const nextDigestAt = getNextDigestAt(user.notificationFrequency)
97-
await userDoc.ref.update({ nextDigestAt })
102+
await profileDoc.ref.update({ nextDigestAt })
98103

99-
console.log(`Updated nextDigestAt for ${userDoc.id} to ${nextDigestAt}`)
104+
console.log(`Updated nextDigestAt for ${profileDoc.id} to ${nextDigestAt}`)
100105
})
101106

102107
// Wait for all email documents to be created

functions/src/notifications/types.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
1-
import { Frequency } from "../auth/types"
21
import { BillHistory } from "../bills/types"
32
import { Timestamp } from "../firebase"
43

5-
// This should probably live somewhere else once other code starts caring about this
6-
export interface User {
7-
email?: string
8-
notificationFrequency?: Frequency
9-
nextDigestAt?: Timestamp
10-
}
11-
124
export interface Notification {
135
type: string
146
updateTime: Timestamp

functions/src/notifications/updateUserNotificationFrequency.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,13 @@ export const updateUserNotificationFrequency = functions.firestore
3535
return null
3636
}
3737

38-
// Update user document in the 'users' collection
38+
// Update the profile document to include the computed `nextDigestAt`
3939
await admin
4040
.firestore()
41-
.collection("users")
41+
.collection("profiles")
4242
.doc(userId)
4343
.set(
4444
{
45-
notificationFrequency: notificationFrequency,
4645
nextDigestAt: getNextDigestAt(notificationFrequency)
4746
},
4847
{ merge: true }
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
This script is intended to backfill the `nextDigestAt` field
3+
into the `profiles` collection. This field is used to determine
4+
when the next notifications digest email should be sent to a user
5+
based on their set `notificationFrequency`.
6+
7+
This script can be safely removed after the `nextDigestAt` field
8+
has been added to the profiles collection.
9+
*/
10+
11+
import { getNextDigestAt } from "../../functions/src/notifications/helpers"
12+
import { Profile } from "../../components/db/profile/types"
13+
import { Script } from "./types"
14+
import { Boolean, Record } from "runtypes"
15+
16+
// todo - actually move nextDigestAt into profiles
17+
// - no reason not to do it now, we have the backfill and the rules
18+
// don't protect any collection
19+
// I'll need to fix the rules so that writes to the `profiles::nextDigestAt` field can only be made by admins
20+
// - e.g. this script, the deliverNotifcaitons script, or theupdateNotifiaciotn script
21+
const Args = Record({
22+
dryRun: Boolean
23+
})
24+
25+
export const script: Script = async ({ db, auth, args }) => {
26+
const profilesSnapshot = await db.collection("profiles").get()
27+
28+
const updatePromises = profilesSnapshot.docs.map(async profileDoc => {
29+
const profile = profileDoc.data() as Profile
30+
31+
if (profile.notificationFrequency) {
32+
const nextDigestAt = getNextDigestAt(profile.notificationFrequency)
33+
34+
if (!args.dryRun) {
35+
await profileDoc.ref.update({ nextDigestAt })
36+
}
37+
console.log(
38+
`Updated nextDigestAt for ${profileDoc.id} to ${nextDigestAt}`
39+
)
40+
} else {
41+
console.log(
42+
`Profile ${profileDoc.id} does not have notificationFrequency - skipping`
43+
)
44+
}
45+
})
46+
47+
await Promise.all(updatePromises)
48+
console.log("Backfill complete")
49+
}

scripts/firebase-admin/backfillNotificationFrequency.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// DEPRECATED - This will no longer be useful after we've moved notificationFrequency to profiles
2+
// This should no longer be run and will be removed in a future update
3+
14
import { Script } from "./types"
25
import { listAllUsers } from "./list-all-users"
36

@@ -9,7 +12,7 @@ export const script: Script = async ({ db, auth }) => {
912

1013
for (const user of allUsers) {
1114
// Get user document from Firestore
12-
const userDoc = db.collection("users").doc(user.uid)
15+
const userDoc = db.collection("profiles").doc(user.uid)
1316
const doc = await userDoc.get()
1417

1518
// If the user document exists in Firestore

scripts/firebase-admin/backfillUserEmails.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
// DEPRECATED - This will no longer be useful going forward
2+
// We plan to rely on the `profiles` collection for casual email use
3+
// and the firebase-auth API where we need verified email addresses
4+
// This should no longer be run and will be removed in a future update
5+
16
import { UserRecord } from "firebase-admin/auth"
27
import { Auth } from "functions/src/types"
38
import { Script } from "./types"

0 commit comments

Comments
 (0)