Skip to content

Commit 46375e8

Browse files
feat: notificationFlags migration (#2958)
1 parent 1a95169 commit 46375e8

File tree

5 files changed

+290
-1
lines changed

5 files changed

+290
-1
lines changed

.infra/application.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ debezium.source.database.password=%database_pass%
99
debezium.source.database.dbname=%database_dbname%
1010
debezium.source.database.server.name=api
1111
debezium.source.table.include.list=public.comment,public.user_comment,public.comment_mention,public.source_request,public.post,public.user,public.post_report,public.source_feed,public.settings,public.reputation_event,public.submission,public.user_state,public.notification_v2,public.source_member,public.feature,public.source,public.post_mention,public.content_image,public.comment_report,public.user_post,public.banner,public.post_relation,public.marketing_cta,public.squad_public_request,public.user_streak,public.bookmark,public.user_company,public.source_report,public.user_top_reader,public.source_post_moderation,public.user_report,public.user_transaction,public.content_preference
12-
debezium.source.column.exclude.list=public.post.tsv,public.post.placeholder,public.source.flags,public.user_top_reader.image,public.source_member.flags
12+
debezium.source.column.exclude.list=public.post.tsv,public.post.placeholder,public.source.flags,public.user_top_reader.image,public.source_member.flags,public.user.notificationFlags
1313
debezium.source.skip.messages.without.change=true
1414
debezium.source.plugin.name=pgoutput
1515
debezium.source.heartbeat.interval.ms=60000

bin/migrateNotificationFlags.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import '../src/config';
2+
import createOrGetConnection from '../src/db';
3+
import {
4+
DEFAULT_NOTIFICATION_SETTINGS,
5+
NotificationPreferenceStatus,
6+
NotificationType,
7+
} from '../src/notifications/common';
8+
import { User } from '../src/entity/user/User';
9+
import type { UserNotificationFlags } from '../src/entity/user/User';
10+
11+
interface UserData {
12+
id: string;
13+
notificationEmail: boolean;
14+
followingEmail: boolean;
15+
followNotifications: boolean;
16+
awardEmail: boolean;
17+
awardNotifications: boolean;
18+
notificationFlags: UserNotificationFlags;
19+
}
20+
21+
function buildNotificationFlags(user: UserData): UserNotificationFlags {
22+
const flags: UserNotificationFlags = { ...DEFAULT_NOTIFICATION_SETTINGS };
23+
24+
if (!user.notificationEmail) {
25+
Object.keys(flags).forEach((type) => {
26+
if (flags[type]) {
27+
flags[type]!.email = NotificationPreferenceStatus.Muted;
28+
}
29+
});
30+
}
31+
32+
if (!user.followingEmail) {
33+
flags[NotificationType.UserPostAdded]!.email =
34+
NotificationPreferenceStatus.Muted;
35+
}
36+
37+
if (!user.followNotifications) {
38+
flags[NotificationType.UserPostAdded]!.inApp =
39+
NotificationPreferenceStatus.Muted;
40+
}
41+
42+
if (!user.awardEmail) {
43+
flags[NotificationType.UserReceivedAward]!.email =
44+
NotificationPreferenceStatus.Muted;
45+
}
46+
47+
if (!user.awardNotifications) {
48+
flags[NotificationType.UserReceivedAward]!.inApp =
49+
NotificationPreferenceStatus.Muted;
50+
}
51+
52+
return flags;
53+
}
54+
55+
(async (): Promise<void> => {
56+
const limitArgument = process.argv[2];
57+
const offsetArgument = process.argv[3];
58+
59+
if (!limitArgument || !offsetArgument) {
60+
throw new Error('limit and offset arguments are required');
61+
}
62+
63+
const limit = +limitArgument;
64+
if (Number.isNaN(limit)) {
65+
throw new Error('limit argument is invalid, it should be a number');
66+
}
67+
68+
const offset = +offsetArgument;
69+
if (Number.isNaN(offset)) {
70+
throw new Error('offset argument is invalid, it should be a number');
71+
}
72+
73+
const con = await createOrGetConnection();
74+
75+
try {
76+
const userRepo = con.getRepository(User);
77+
78+
const users = await userRepo
79+
.createQueryBuilder('user')
80+
.select([
81+
'user.id',
82+
'user.notificationEmail',
83+
'user.followingEmail',
84+
'user.followNotifications',
85+
'user.awardEmail',
86+
'user.awardNotifications',
87+
'user.notificationFlags',
88+
])
89+
.orderBy('user.id')
90+
.limit(limit)
91+
.offset(offset)
92+
.getMany();
93+
94+
console.log(
95+
`Processing ${users.length} users (offset ${offset} to ${offset + users.length - 1})...`,
96+
);
97+
98+
await con.transaction(async (manager) => {
99+
for (const user of users) {
100+
const userData: UserData = {
101+
id: user.id,
102+
notificationEmail: user.notificationEmail,
103+
followingEmail: user.followingEmail,
104+
followNotifications: user.followNotifications,
105+
awardEmail: user.awardEmail,
106+
awardNotifications: user.awardNotifications,
107+
notificationFlags: user.notificationFlags,
108+
};
109+
110+
const newFlags = buildNotificationFlags(userData);
111+
112+
await manager
113+
.getRepository(User)
114+
.update({ id: user.id }, { notificationFlags: newFlags });
115+
}
116+
});
117+
118+
console.log(
119+
`Migration completed successfully. Updated ${users.length} users (offset ${offset} to ${offset + users.length - 1}).`,
120+
);
121+
} catch (error) {
122+
console.error('Migration failed:', error);
123+
throw error;
124+
}
125+
126+
process.exit(0);
127+
})();

src/entity/user/User.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type {
2525
} from '../../common/plus';
2626
import type { UserJobPreferences } from './UserJobPreferences';
2727
import type { UserExperience } from './experiences/UserExperience';
28+
import type { NotificationPreferenceStatus } from '../../notifications/common';
2829

2930
export type UserFlags = Partial<{
3031
vordr: boolean;
@@ -59,6 +60,16 @@ export type UserSubscriptionFlags = Partial<{
5960
PaddleUserSubscriptionFlags &
6061
StoreKitUserSubscriptionFlags;
6162

63+
export type UserNotificationFlags = Partial<
64+
Record<
65+
string,
66+
{
67+
email?: NotificationPreferenceStatus;
68+
inApp?: NotificationPreferenceStatus;
69+
}
70+
>
71+
>;
72+
6273
@Entity()
6374
@Index('IDX_user_lowerusername_username', { synchronize: false })
6475
@Index('IDX_user_lowertwitter', { synchronize: false })
@@ -303,4 +314,7 @@ export class User {
303314
},
304315
)
305316
experiences: Promise<UserExperience[]>;
317+
318+
@Column({ type: 'jsonb', default: {} })
319+
notificationFlags: UserNotificationFlags;
306320
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { MigrationInterface, QueryRunner } from "typeorm";
2+
3+
export class UserNotificationFlags1754302649060 implements MigrationInterface {
4+
name = 'UserNotificationFlags1754302649060'
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query(
8+
`ALTER TABLE "user" ADD "notificationFlags" jsonb NOT NULL DEFAULT '{}'`,
9+
);
10+
}
11+
12+
public async down(queryRunner: QueryRunner): Promise<void> {
13+
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "notificationFlags"`);
14+
}
15+
16+
}

src/notifications/common.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
import { UserNotification } from '../entity/notifications/UserNotification';
2222
import type { ConnectionManager } from '../entity/posts';
2323
import { Comment } from '../entity/Comment';
24+
import type { UserNotificationFlags } from '../entity/user/User';
2425

2526
export enum NotificationType {
2627
CommunityPicksFailed = 'community_picks_failed',
@@ -96,6 +97,137 @@ export const notificationPreferenceMap: Partial<
9697
[NotificationType.UserTopReaderBadge]: NotificationPreferenceType.User,
9798
};
9899

100+
export const DEFAULT_NOTIFICATION_SETTINGS: UserNotificationFlags = {
101+
[NotificationType.ArticleNewComment]: {
102+
email: NotificationPreferenceStatus.Subscribed,
103+
inApp: NotificationPreferenceStatus.Subscribed,
104+
},
105+
[NotificationType.CommentReply]: {
106+
email: NotificationPreferenceStatus.Subscribed,
107+
inApp: NotificationPreferenceStatus.Subscribed,
108+
},
109+
[NotificationType.ArticleUpvoteMilestone]: {
110+
email: NotificationPreferenceStatus.Subscribed,
111+
inApp: NotificationPreferenceStatus.Subscribed,
112+
},
113+
[NotificationType.CommentUpvoteMilestone]: {
114+
email: NotificationPreferenceStatus.Subscribed,
115+
inApp: NotificationPreferenceStatus.Subscribed,
116+
},
117+
[NotificationType.PostMention]: {
118+
email: NotificationPreferenceStatus.Subscribed,
119+
inApp: NotificationPreferenceStatus.Subscribed,
120+
},
121+
[NotificationType.CommentMention]: {
122+
email: NotificationPreferenceStatus.Subscribed,
123+
inApp: NotificationPreferenceStatus.Subscribed,
124+
},
125+
[NotificationType.SquadNewComment]: {
126+
email: NotificationPreferenceStatus.Subscribed,
127+
inApp: NotificationPreferenceStatus.Subscribed,
128+
},
129+
[NotificationType.UserReceivedAward]: {
130+
email: NotificationPreferenceStatus.Subscribed,
131+
inApp: NotificationPreferenceStatus.Subscribed,
132+
},
133+
[NotificationType.ArticleReportApproved]: {
134+
email: NotificationPreferenceStatus.Subscribed,
135+
inApp: NotificationPreferenceStatus.Subscribed,
136+
},
137+
[NotificationType.StreakResetRestore]: {
138+
email: NotificationPreferenceStatus.Subscribed,
139+
inApp: NotificationPreferenceStatus.Subscribed,
140+
},
141+
['streak_reminder']: {
142+
email: NotificationPreferenceStatus.Subscribed,
143+
inApp: NotificationPreferenceStatus.Subscribed,
144+
},
145+
[NotificationType.UserTopReaderBadge]: {
146+
email: NotificationPreferenceStatus.Subscribed,
147+
inApp: NotificationPreferenceStatus.Subscribed,
148+
},
149+
[NotificationType.DevCardUnlocked]: {
150+
email: NotificationPreferenceStatus.Subscribed,
151+
inApp: NotificationPreferenceStatus.Subscribed,
152+
},
153+
[NotificationType.SourcePostAdded]: {
154+
email: NotificationPreferenceStatus.Subscribed,
155+
inApp: NotificationPreferenceStatus.Subscribed,
156+
},
157+
[NotificationType.SquadPostAdded]: {
158+
email: NotificationPreferenceStatus.Subscribed,
159+
inApp: NotificationPreferenceStatus.Subscribed,
160+
},
161+
[NotificationType.UserPostAdded]: {
162+
email: NotificationPreferenceStatus.Subscribed,
163+
inApp: NotificationPreferenceStatus.Subscribed,
164+
},
165+
[NotificationType.CollectionUpdated]: {
166+
email: NotificationPreferenceStatus.Subscribed,
167+
inApp: NotificationPreferenceStatus.Subscribed,
168+
},
169+
[NotificationType.PostBookmarkReminder]: {
170+
email: NotificationPreferenceStatus.Subscribed,
171+
inApp: NotificationPreferenceStatus.Subscribed,
172+
},
173+
[NotificationType.PromotedToAdmin]: {
174+
email: NotificationPreferenceStatus.Subscribed,
175+
inApp: NotificationPreferenceStatus.Subscribed,
176+
},
177+
[NotificationType.PromotedToModerator]: {
178+
email: NotificationPreferenceStatus.Subscribed,
179+
inApp: NotificationPreferenceStatus.Subscribed,
180+
},
181+
[NotificationType.SourceApproved]: {
182+
email: NotificationPreferenceStatus.Subscribed,
183+
inApp: NotificationPreferenceStatus.Subscribed,
184+
},
185+
[NotificationType.SourceRejected]: {
186+
email: NotificationPreferenceStatus.Subscribed,
187+
inApp: NotificationPreferenceStatus.Subscribed,
188+
},
189+
[NotificationType.SourcePostSubmitted]: {
190+
email: NotificationPreferenceStatus.Subscribed,
191+
inApp: NotificationPreferenceStatus.Subscribed,
192+
},
193+
[NotificationType.SourcePostApproved]: {
194+
email: NotificationPreferenceStatus.Subscribed,
195+
inApp: NotificationPreferenceStatus.Subscribed,
196+
},
197+
[NotificationType.SourcePostRejected]: {
198+
email: NotificationPreferenceStatus.Subscribed,
199+
inApp: NotificationPreferenceStatus.Subscribed,
200+
},
201+
[NotificationType.BriefingReady]: {
202+
email: NotificationPreferenceStatus.Subscribed,
203+
inApp: NotificationPreferenceStatus.Subscribed,
204+
},
205+
[NotificationType.ArticlePicked]: {
206+
email: NotificationPreferenceStatus.Subscribed,
207+
inApp: NotificationPreferenceStatus.Subscribed,
208+
},
209+
[NotificationType.ArticleAnalytics]: {
210+
email: NotificationPreferenceStatus.Subscribed,
211+
inApp: NotificationPreferenceStatus.Subscribed,
212+
},
213+
[NotificationType.SquadMemberJoined]: {
214+
email: NotificationPreferenceStatus.Subscribed,
215+
inApp: NotificationPreferenceStatus.Subscribed,
216+
},
217+
[NotificationType.SquadReply]: {
218+
email: NotificationPreferenceStatus.Subscribed,
219+
inApp: NotificationPreferenceStatus.Subscribed,
220+
},
221+
[NotificationType.SquadBlocked]: {
222+
email: NotificationPreferenceStatus.Subscribed,
223+
inApp: NotificationPreferenceStatus.Subscribed,
224+
},
225+
[NotificationType.DemotedToMember]: {
226+
email: NotificationPreferenceStatus.Subscribed,
227+
inApp: NotificationPreferenceStatus.Subscribed,
228+
},
229+
};
230+
99231
export const commentReplyNotificationTypes = [
100232
NotificationType.CommentReply,
101233
NotificationType.SquadReply,

0 commit comments

Comments
 (0)